Skip to content

Commit f4c9eec

Browse files
committed
chore: quickstart startup flow
1 parent 083f178 commit f4c9eec

File tree

7 files changed

+204
-18
lines changed

7 files changed

+204
-18
lines changed

docs/getting-started.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,25 @@ Release 中的主要二进制如下:
3535

3636
### 直接启动
3737

38-
已从 `server/config.example.yaml` 复制出本地 `config.yaml`,可直接启动:
38+
已有 `config.yaml`,或者让程序自动生成默认配置后,可直接启动:
3939

4040
```bash
4141
./malice-network_linux_amd64 -i <public-ip>
4242
```
4343

44+
默认启动时的初始化行为:
45+
46+
- 如果配置文件不存在,且当前是交互终端,程序会先询问是否进入 quickstart 向导
47+
- 如果配置文件不存在,且当前不是交互终端,程序会直接写入默认 `config.yaml` 并继续正常启动
48+
- 如果显式传入 `--quickstart`,则直接进入向导模式,而不是等默认启动时再判断
49+
4450
常用参数:
4551

4652
- `-c, --config`: 指定配置文件路径,默认是 `config.yaml`
4753
- `-i, --ip`: 覆盖配置里的外网 IP
4854
- `--server-only`: 只启动 server
4955
- `--listener-only`: 只启动 listener
50-
- `--quickstart`: 仅在配置文件不存在时运行初始化向导
56+
- `--quickstart`: 显式进入交互式初始化向导
5157
- `--debug`: 打开 debug 日志
5258

5359
### 首次启动生成的文件

docs/server/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Server 是 malice-network 的控制核心,职责包括:
3232
| `./malice-network --server-only` | 仅启动 Server,不启动 Listener |
3333
| `./malice-network --listener-only` | 仅启动 Listener,独立部署时使用 |
3434
| `./malice-network --daemon` | 以守护进程模式运行 |
35-
| `./malice-network --quickstart` | 交互式配置向导,引导完成初始配置 |
35+
| `./malice-network --quickstart` | 显式进入交互式配置向导,引导完成初始配置 |
3636

3737
| 参数 | 说明 |
3838
|------|------|
@@ -41,6 +41,12 @@ Server 是 malice-network 的控制核心,职责包括:
4141
| `--debug` | 开启 debug 日志 |
4242
| `--opsec` | 启用 OPSEC 模式 |
4343

44+
当默认启动时,如果配置文件不存在:
45+
46+
- 交互终端会先提示是否进入 quickstart
47+
- 非交互环境会直接生成默认配置并继续启动
48+
- 只有显式传入 `--quickstart` 时,才会强制进入向导
49+
4450
!!! tip "部署指南"
4551
完整的部署流程见 [部署操作指南](../operations/deployment.md)
4652

docs/server/quickstart.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
!!! important "IP 设置"
3131
`-i` 参数需要设置为 Client 可访问到的 IP 地址。公网服务器设置为公网 IP,内网环境设置为内网 IP。
3232

33+
如果当前还没有配置文件,默认启动会按下面的规则处理:
34+
35+
- **交互终端**:先提示你是否进入 quickstart 向导
36+
- **非交互环境**(比如 systemd、无 PTY 的脚本环境):直接生成默认 `config.yaml`,然后继续正常启动
37+
- **显式传入 `--quickstart`**:直接进入向导模式
38+
3339
首次启动后,Server 会自动完成:
3440

3541
1. 生成默认配置文件 `config.yaml`
@@ -53,6 +59,12 @@
5359

5460
向导会引导完成 IP、端口、构建源等基础配置。
5561

62+
注意:
63+
64+
- `--quickstart` 是显式入口,不依赖“配置文件是否存在”来触发
65+
- quickstart 基于 TUI,适合交互式终端,不适合 systemd 这类无 PTY 环境
66+
- 如果目标配置文件已经存在,quickstart 不会覆盖原文件
67+
5668
## 使用安装脚本(Linux)
5769

5870
!!! info "安装脚本会自动完成 Docker 安装、Server/Client 下载、malefic.zip 解压,并可选配置 systemd"
@@ -66,7 +78,7 @@ curl -L "https://raw.githubusercontent.com/chainreactors/malice-network/master/i
6678
- **安装路径**:默认 `/opt/iom`
6779
- **IP 地址**:自动检测,可手动修改
6880

69-
安装完成后,脚本会询问是否安装并启动 systemd 服务;如果跳过,会直接输出手动启动命令。
81+
安装完成后,脚本会询问是否安装并启动 systemd 服务;如果跳过,会直接输出手动启动命令。使用 systemd 时,服务端会走默认启动路径,不会自动进入 quickstart 向导。
7082

7183
## 防火墙配置
7284

server/cmd/server/options.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,16 @@ func (opt *Options) Save() error {
210210
func (opt *Options) PrepareConfig(defaultConfig []byte) error {
211211
filename := configs.FindConfig(opt.Config)
212212
if filename == "" {
213-
err := os.WriteFile(configs.ServerConfigFileName, defaultConfig, 0644)
213+
target := opt.Config
214+
if target == "" {
215+
target = configs.ServerConfigFileName
216+
}
217+
err := os.WriteFile(target, defaultConfig, 0644)
214218
if err != nil {
215219
return err
216220
}
217-
logs.Log.Warnf("config file not found, created default config %s", configs.ServerConfigFileName)
218-
filename = configs.ServerConfigFileName
221+
logs.Log.Warnf("config file not found, created default config %s", target)
222+
filename = target
219223
}
220224

221225
config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) {

server/cmd/server/quickstart.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"net"
88
"net/url"
9-
"os"
109
"strconv"
1110
"strings"
1211

@@ -19,9 +18,19 @@ import (
1918
// RunQuickstart runs the interactive quickstart wizard using a single-page
2019
// tabbed form so that users can review/edit all configuration at once.
2120
func RunQuickstart(opt *Options) error {
21+
if opt == nil {
22+
return fmt.Errorf("options is required")
23+
}
24+
25+
configPath := opt.Config
26+
if configPath == "" {
27+
configPath = configs.ServerConfigFileName
28+
opt.Config = configPath
29+
}
30+
2231
// skip if config already exists
23-
if _, err := os.Stat(opt.Config); err == nil {
24-
logs.Log.Warnf("config %s already exists, skipping quickstart", opt.Config)
32+
if existing := configs.FindConfig(configPath); existing != "" {
33+
logs.Log.Warnf("config %s already exists, skipping quickstart", existing)
2534
return nil
2635
}
2736

@@ -85,7 +94,7 @@ func RunQuickstart(opt *Options) error {
8594
Name: "encryption_key", Title: "Encryption Key",
8695
Kind: wizard.KindInput, InputValue: encryptionKey,
8796
Required: true,
88-
Value: &encryptionKey,
97+
Value: &encryptionKey,
8998
},
9099
},
91100
},
@@ -120,21 +129,21 @@ func RunQuickstart(opt *Options) error {
120129
{
121130
Name: "tcp_port", Title: "TCP Port",
122131
Description: "ignored if tcp not selected",
123-
Kind: wizard.KindInput, InputValue: tcpPort,
132+
Kind: wizard.KindInput, InputValue: tcpPort,
124133
Validate: validatePort,
125134
Value: &tcpPort,
126135
},
127136
{
128137
Name: "http_port", Title: "HTTP Port",
129138
Description: "ignored if http not selected",
130-
Kind: wizard.KindInput, InputValue: httpPort,
139+
Kind: wizard.KindInput, InputValue: httpPort,
131140
Validate: validatePort,
132141
Value: &httpPort,
133142
},
134143
{
135144
Name: "rem_name", Title: "REM Pipeline Name",
136145
Description: "ignored if rem not selected",
137-
Kind: wizard.KindInput, InputValue: remName,
146+
Kind: wizard.KindInput, InputValue: remName,
138147
Value: &remName,
139148
},
140149
},
@@ -201,14 +210,14 @@ func RunQuickstart(opt *Options) error {
201210
{
202211
Name: "notify_param1", Title: "Webhook/Token/APIKey",
203212
Description: "main credential for the service",
204-
Kind: wizard.KindInput, InputValue: notifyParam1,
213+
Kind: wizard.KindInput, InputValue: notifyParam1,
205214
Required: true,
206215
Value: &notifyParam1,
207216
},
208217
{
209218
Name: "notify_param2", Title: "Secret/ChatID (optional)",
210219
Description: "dingtalk secret or telegram chat ID",
211-
Kind: wizard.KindInput, InputValue: notifyParam2,
220+
Kind: wizard.KindInput, InputValue: notifyParam2,
212221
Value: &notifyParam2,
213222
},
214223
},
@@ -326,7 +335,7 @@ func RunQuickstart(opt *Options) error {
326335
return fmt.Errorf("failed to save config: %w", err)
327336
}
328337

329-
logs.Log.Importantf("quickstart config saved to %s", opt.Config)
338+
logs.Log.Importantf("quickstart config saved to %s", configPath)
330339
return nil
331340
}
332341

server/cmd/server/quickstart_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,99 @@ func TestValidateNotEmpty(t *testing.T) {
8686
}
8787
}
8888

89+
func TestShouldOfferQuickstart(t *testing.T) {
90+
tests := []struct {
91+
name string
92+
opt *Options
93+
configMissing bool
94+
interactive bool
95+
hasActiveCommand bool
96+
want bool
97+
}{
98+
{
99+
name: "default startup offers quickstart on interactive terminal",
100+
opt: &Options{},
101+
configMissing: true,
102+
interactive: true,
103+
hasActiveCommand: false,
104+
want: true,
105+
},
106+
{
107+
name: "nil options never offer quickstart",
108+
opt: nil,
109+
configMissing: true,
110+
interactive: true,
111+
hasActiveCommand: false,
112+
want: false,
113+
},
114+
{
115+
name: "explicit quickstart skips prompt",
116+
opt: &Options{Quickstart: true},
117+
configMissing: true,
118+
interactive: true,
119+
hasActiveCommand: false,
120+
want: false,
121+
},
122+
{
123+
name: "existing config skips prompt",
124+
opt: &Options{},
125+
configMissing: false,
126+
interactive: true,
127+
hasActiveCommand: false,
128+
want: false,
129+
},
130+
{
131+
name: "non interactive terminal skips prompt",
132+
opt: &Options{},
133+
configMissing: true,
134+
interactive: false,
135+
hasActiveCommand: false,
136+
want: false,
137+
},
138+
{
139+
name: "active subcommand skips prompt",
140+
opt: &Options{},
141+
configMissing: true,
142+
interactive: true,
143+
hasActiveCommand: true,
144+
want: false,
145+
},
146+
{
147+
name: "daemon mode skips prompt",
148+
opt: &Options{Daemon: true},
149+
configMissing: true,
150+
interactive: true,
151+
hasActiveCommand: false,
152+
want: false,
153+
},
154+
{
155+
name: "server only skips prompt",
156+
opt: &Options{ServerOnly: true},
157+
configMissing: true,
158+
interactive: true,
159+
hasActiveCommand: false,
160+
want: false,
161+
},
162+
{
163+
name: "listener only skips prompt",
164+
opt: &Options{ListenerOnly: true},
165+
configMissing: true,
166+
interactive: true,
167+
hasActiveCommand: false,
168+
want: false,
169+
},
170+
}
171+
172+
for _, tt := range tests {
173+
t.Run(tt.name, func(t *testing.T) {
174+
got := shouldOfferQuickstart(tt.opt, tt.configMissing, tt.interactive, tt.hasActiveCommand)
175+
if got != tt.want {
176+
t.Fatalf("shouldOfferQuickstart() = %v, want %v", got, tt.want)
177+
}
178+
})
179+
}
180+
}
181+
89182
func TestRandomHex(t *testing.T) {
90183
h := randomHex(16)
91184
if len(h) != 32 {

server/cmd/server/server.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package server
22

33
import (
4+
"bufio"
45
"fmt"
6+
"io"
57
"os"
8+
"strings"
69

710
"github.com/chainreactors/logs"
811
"github.com/chainreactors/malice-network/helper/codenames"
@@ -28,6 +31,47 @@ func init() {
2831
assets.SetupGithubFile()
2932
}
3033

34+
func isInteractiveTerminal() bool {
35+
stdinInfo, err := os.Stdin.Stat()
36+
if err != nil {
37+
return false
38+
}
39+
stdoutInfo, err := os.Stdout.Stat()
40+
if err != nil {
41+
return false
42+
}
43+
return stdinInfo.Mode()&os.ModeCharDevice != 0 && stdoutInfo.Mode()&os.ModeCharDevice != 0
44+
}
45+
46+
func shouldOfferQuickstart(opt *Options, configMissing bool, interactive bool, hasActiveCommand bool) bool {
47+
if opt == nil {
48+
return false
49+
}
50+
if opt.Quickstart || !configMissing || !interactive || hasActiveCommand {
51+
return false
52+
}
53+
if opt.ServerOnly || opt.ListenerOnly || opt.Daemon {
54+
return false
55+
}
56+
return true
57+
}
58+
59+
func promptQuickstart(configPath string) (bool, error) {
60+
if configPath == "" {
61+
configPath = configs.ServerConfigFileName
62+
}
63+
64+
fmt.Printf("config %s not found. Run quickstart wizard now? [y/N] ", configPath)
65+
reader := bufio.NewReader(os.Stdin)
66+
line, err := reader.ReadString('\n')
67+
if err != nil && err != io.EOF {
68+
return false, err
69+
}
70+
71+
answer := strings.ToLower(strings.TrimSpace(line))
72+
return answer == "y" || answer == "yes", nil
73+
}
74+
3175
func Start(defaultConfig []byte) error {
3276
var opt Options
3377
var err error
@@ -40,10 +84,22 @@ func Start(defaultConfig []byte) error {
4084
}
4185
return nil
4286
}
43-
if _, statErr := os.Stat(opt.Config); opt.Quickstart || os.IsNotExist(statErr) {
87+
88+
configMissing := configs.FindConfig(opt.Config) == ""
89+
if opt.Quickstart {
4490
if err := RunQuickstart(&opt); err != nil {
4591
return fmt.Errorf("quickstart failed: %w", err)
4692
}
93+
} else if shouldOfferQuickstart(&opt, configMissing, isInteractiveTerminal(), parser.Active != nil) {
94+
runWizard, promptErr := promptQuickstart(opt.Config)
95+
if promptErr != nil {
96+
return fmt.Errorf("prompt quickstart: %w", promptErr)
97+
}
98+
if runWizard {
99+
if err := RunQuickstart(&opt); err != nil {
100+
return fmt.Errorf("quickstart failed: %w", err)
101+
}
102+
}
47103
}
48104

49105
err = opt.PrepareConfig(defaultConfig)

0 commit comments

Comments
 (0)