cloudflared-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -0
- package/README.md +217 -0
- package/cloudflared_manager.mjs +2080 -0
- package/cloudflared_manager.sh +2727 -0
- package/package.json +26 -0
|
@@ -0,0 +1,2727 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_NAME="$(basename "$0")"
|
|
5
|
+
SETTINGS_FILE="${CLOUDFLARED_MANAGER_SETTINGS:-$HOME/.cloudflared-manager.env}"
|
|
6
|
+
DEFAULT_LOGIN_CERT="$HOME/.cloudflared/cert.pem"
|
|
7
|
+
DEFAULT_MANAGER_HOME="$HOME/.cloudflared-manager"
|
|
8
|
+
|
|
9
|
+
CLOUDFLARED_BIN="${CLOUDFLARED_BIN:-$(command -v cloudflared || true)}"
|
|
10
|
+
|
|
11
|
+
CLI_PROFILE_DIR=""
|
|
12
|
+
CLI_DEFAULT_CONFIG_FILE=""
|
|
13
|
+
CLI_ORIGIN_CERT=""
|
|
14
|
+
CLI_MANAGER_ROOT=""
|
|
15
|
+
SAVE_PROFILE=0
|
|
16
|
+
|
|
17
|
+
SETTINGS_PROFILE_DIR=""
|
|
18
|
+
SETTINGS_DEFAULT_CONFIG_FILE=""
|
|
19
|
+
SETTINGS_ORIGIN_CERT=""
|
|
20
|
+
SETTINGS_MANAGER_ROOT=""
|
|
21
|
+
|
|
22
|
+
PROFILE_DIR=""
|
|
23
|
+
DEFAULT_CONFIG_FILE=""
|
|
24
|
+
ORIGIN_CERT=""
|
|
25
|
+
MANAGER_ROOT=""
|
|
26
|
+
APPS_DIR=""
|
|
27
|
+
ARCHIVE_DIR=""
|
|
28
|
+
|
|
29
|
+
META_NAME=""
|
|
30
|
+
META_TUNNEL_NAME=""
|
|
31
|
+
META_TUNNEL_ID=""
|
|
32
|
+
META_HOSTNAME=""
|
|
33
|
+
META_SERVICE=""
|
|
34
|
+
META_APP_DIR=""
|
|
35
|
+
META_CONFIG_FILE=""
|
|
36
|
+
META_CREDENTIALS_FILE=""
|
|
37
|
+
META_INGRESS_FILE=""
|
|
38
|
+
META_PID_FILE=""
|
|
39
|
+
META_LOG_FILE=""
|
|
40
|
+
META_META_FILE=""
|
|
41
|
+
META_CREATED_AT=""
|
|
42
|
+
META_UPDATED_AT=""
|
|
43
|
+
|
|
44
|
+
INGRESS_HOSTS=()
|
|
45
|
+
INGRESS_SERVICES=()
|
|
46
|
+
PARSED_INGRESS_HOSTNAME=""
|
|
47
|
+
PARSED_INGRESS_SERVICE=""
|
|
48
|
+
PARSED_MODIFY_INDEX=""
|
|
49
|
+
PARSED_MODIFY_HOSTNAME=""
|
|
50
|
+
PARSED_MODIFY_SERVICE=""
|
|
51
|
+
|
|
52
|
+
REMOTE_FOUND=0
|
|
53
|
+
REMOTE_TUNNEL_ID=""
|
|
54
|
+
REMOTE_TUNNEL_NAME=""
|
|
55
|
+
REMOTE_TUNNEL_CREATED_AT=""
|
|
56
|
+
REMOTE_TUNNEL_CONNECTIONS="0"
|
|
57
|
+
|
|
58
|
+
# 返回当前 UTC 时间,统一用于元数据时间戳。
|
|
59
|
+
utc_now() {
|
|
60
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# 向标准错误输出文本。
|
|
64
|
+
eprint() {
|
|
65
|
+
printf '%s\n' "$*" >&2
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# 向标准输出打印普通信息。
|
|
69
|
+
info() {
|
|
70
|
+
printf '%s\n' "$*"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# 输出警告信息。
|
|
74
|
+
warn() {
|
|
75
|
+
eprint "警告: $*"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 输出错误信息并退出。
|
|
79
|
+
die() {
|
|
80
|
+
eprint "错误: $*"
|
|
81
|
+
exit 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# 展开形如 ~ 或 ~/path 的用户目录写法。
|
|
85
|
+
expand_tilde() {
|
|
86
|
+
local value="$1"
|
|
87
|
+
if [[ "$value" == "~" ]]; then
|
|
88
|
+
printf '%s\n' "$HOME"
|
|
89
|
+
elif [[ "$value" == "~/"* ]]; then
|
|
90
|
+
printf '%s/%s\n' "$HOME" "${value#~/}"
|
|
91
|
+
else
|
|
92
|
+
printf '%s\n' "$value"
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# 将相对路径转换为绝对路径,但不要求目标必须存在。
|
|
97
|
+
normalize_path() {
|
|
98
|
+
local value
|
|
99
|
+
value="$(expand_tilde "$1")"
|
|
100
|
+
if [[ -z "$value" ]]; then
|
|
101
|
+
printf '\n'
|
|
102
|
+
elif [[ "$value" == /* ]]; then
|
|
103
|
+
printf '%s\n' "$value"
|
|
104
|
+
else
|
|
105
|
+
printf '%s/%s\n' "$PWD" "$value"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 在目标存在时尽量返回规范路径,否则返回归一化后的绝对路径。
|
|
110
|
+
canonical_path() {
|
|
111
|
+
local value dir base
|
|
112
|
+
value="$(expand_tilde "$1")"
|
|
113
|
+
if [[ -d "$value" ]]; then
|
|
114
|
+
(
|
|
115
|
+
cd "$value"
|
|
116
|
+
pwd -P
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
fi
|
|
120
|
+
if [[ -e "$value" ]]; then
|
|
121
|
+
dir="$(dirname "$value")"
|
|
122
|
+
base="$(basename "$value")"
|
|
123
|
+
(
|
|
124
|
+
cd "$dir"
|
|
125
|
+
printf '%s/%s\n' "$(pwd -P)" "$base"
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
fi
|
|
129
|
+
normalize_path "$value"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# 确保目录存在。
|
|
133
|
+
ensure_dir() {
|
|
134
|
+
mkdir -p "$1"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# 生成兼容 macOS 的临时文件路径。
|
|
138
|
+
portable_mktemp() {
|
|
139
|
+
local prefix="$1"
|
|
140
|
+
mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# 返回路径的父目录。
|
|
144
|
+
path_parent() {
|
|
145
|
+
dirname "$1"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# 返回路径的文件名部分。
|
|
149
|
+
path_basename() {
|
|
150
|
+
basename "$1"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# 判断文件是否存在。
|
|
154
|
+
file_exists() {
|
|
155
|
+
[[ -f "$1" ]]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# 判断目录是否存在。
|
|
159
|
+
dir_exists() {
|
|
160
|
+
[[ -d "$1" ]]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# 判断命令是否可用。
|
|
164
|
+
command_exists() {
|
|
165
|
+
command -v "$1" >/dev/null 2>&1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 生成备份文件路径。
|
|
169
|
+
backup_path() {
|
|
170
|
+
local original="$1"
|
|
171
|
+
local stamp
|
|
172
|
+
stamp="$(date +"%Y%m%d-%H%M%S")"
|
|
173
|
+
printf '%s/%s.bak-%s\n' "$(dirname "$original")" "$(basename "$original")" "$stamp"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# 备份文件,若源文件不存在则直接返回。
|
|
177
|
+
backup_file() {
|
|
178
|
+
local original="$1"
|
|
179
|
+
local backup=""
|
|
180
|
+
if ! file_exists "$original"; then
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
backup="$(backup_path "$original")"
|
|
184
|
+
cp "$original" "$backup"
|
|
185
|
+
printf '%s\n' "$backup"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# 将键值对写入制表符分隔的简单配置文件。
|
|
189
|
+
write_kv_file() {
|
|
190
|
+
local file="$1"
|
|
191
|
+
shift
|
|
192
|
+
ensure_dir "$(dirname "$file")"
|
|
193
|
+
: >"$file"
|
|
194
|
+
while [[ $# -gt 1 ]]; do
|
|
195
|
+
printf '%s\t%s\n' "$1" "$2" >>"$file"
|
|
196
|
+
shift 2
|
|
197
|
+
done
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# 读取用户保存的默认路径设置。
|
|
201
|
+
load_settings() {
|
|
202
|
+
SETTINGS_PROFILE_DIR=""
|
|
203
|
+
SETTINGS_DEFAULT_CONFIG_FILE=""
|
|
204
|
+
SETTINGS_ORIGIN_CERT=""
|
|
205
|
+
SETTINGS_MANAGER_ROOT=""
|
|
206
|
+
if ! file_exists "$SETTINGS_FILE"; then
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
while IFS=$'\t' read -r key value; do
|
|
210
|
+
case "$key" in
|
|
211
|
+
profile_dir) SETTINGS_PROFILE_DIR="$value" ;;
|
|
212
|
+
default_config_file) SETTINGS_DEFAULT_CONFIG_FILE="$value" ;;
|
|
213
|
+
origin_cert) SETTINGS_ORIGIN_CERT="$value" ;;
|
|
214
|
+
manager_root) SETTINGS_MANAGER_ROOT="$value" ;;
|
|
215
|
+
esac
|
|
216
|
+
done <"$SETTINGS_FILE"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# 保存当前解析出的路径设置。
|
|
220
|
+
save_settings() {
|
|
221
|
+
write_kv_file "$SETTINGS_FILE" \
|
|
222
|
+
profile_dir "${PROFILE_DIR:-}" \
|
|
223
|
+
default_config_file "${DEFAULT_CONFIG_FILE:-}" \
|
|
224
|
+
origin_cert "${ORIGIN_CERT:-}" \
|
|
225
|
+
manager_root "${MANAGER_ROOT:-}"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# 在常见目录中自动发现一个可用的 profile 目录。
|
|
229
|
+
discover_profile_dir() {
|
|
230
|
+
local candidates=()
|
|
231
|
+
local candidate=""
|
|
232
|
+
if [[ -n "${SETTINGS_PROFILE_DIR:-}" ]]; then
|
|
233
|
+
candidates+=("${SETTINGS_PROFILE_DIR}")
|
|
234
|
+
fi
|
|
235
|
+
if [[ -n "${CLOUDFLARED_PROFILE_DIR:-}" ]]; then
|
|
236
|
+
candidates+=("${CLOUDFLARED_PROFILE_DIR}")
|
|
237
|
+
fi
|
|
238
|
+
if [[ -n "${CLOUDFLARED_DIR:-}" ]]; then
|
|
239
|
+
candidates+=("${CLOUDFLARED_DIR}")
|
|
240
|
+
fi
|
|
241
|
+
candidates+=(
|
|
242
|
+
"$PWD/.cloudflared"
|
|
243
|
+
"$PWD/cloudflared"
|
|
244
|
+
"$HOME/.cloudflared"
|
|
245
|
+
"$HOME/cloudflared"
|
|
246
|
+
)
|
|
247
|
+
for candidate in "${candidates[@]}"; do
|
|
248
|
+
[[ -z "$candidate" ]] && continue
|
|
249
|
+
candidate="$(canonical_path "$candidate")"
|
|
250
|
+
if dir_exists "$candidate" && { file_exists "$candidate/config.yml" || file_exists "$candidate/cert.pem"; }; then
|
|
251
|
+
printf '%s\n' "$candidate"
|
|
252
|
+
return 0
|
|
253
|
+
fi
|
|
254
|
+
done
|
|
255
|
+
return 1
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# 解析 profile、config、证书和管理目录的最终路径。
|
|
259
|
+
resolve_paths() {
|
|
260
|
+
local anchor_dir=""
|
|
261
|
+
local discovered=""
|
|
262
|
+
|
|
263
|
+
load_settings
|
|
264
|
+
|
|
265
|
+
PROFILE_DIR="${CLI_PROFILE_DIR:-${CLOUDFLARED_PROFILE_DIR:-${CLOUDFLARED_DIR:-${SETTINGS_PROFILE_DIR:-}}}}"
|
|
266
|
+
DEFAULT_CONFIG_FILE="${CLI_DEFAULT_CONFIG_FILE:-${CLOUDFLARED_CONFIG_FILE:-${SETTINGS_DEFAULT_CONFIG_FILE:-}}}"
|
|
267
|
+
ORIGIN_CERT="${CLI_ORIGIN_CERT:-${CLOUDFLARED_ORIGIN_CERT:-${TUNNEL_ORIGIN_CERT:-${SETTINGS_ORIGIN_CERT:-}}}}"
|
|
268
|
+
MANAGER_ROOT="${CLI_MANAGER_ROOT:-${CLOUDFLARED_MANAGER_ROOT:-${SETTINGS_MANAGER_ROOT:-}}}"
|
|
269
|
+
|
|
270
|
+
if [[ -z "$PROFILE_DIR" && -z "$DEFAULT_CONFIG_FILE" && -z "$ORIGIN_CERT" ]]; then
|
|
271
|
+
discovered="$(discover_profile_dir || true)"
|
|
272
|
+
PROFILE_DIR="$discovered"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
if [[ -n "$PROFILE_DIR" ]]; then
|
|
276
|
+
PROFILE_DIR="$(canonical_path "$PROFILE_DIR")"
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
if [[ -z "$DEFAULT_CONFIG_FILE" && -n "$PROFILE_DIR" ]]; then
|
|
280
|
+
DEFAULT_CONFIG_FILE="$PROFILE_DIR/config.yml"
|
|
281
|
+
fi
|
|
282
|
+
if [[ -n "$DEFAULT_CONFIG_FILE" ]]; then
|
|
283
|
+
DEFAULT_CONFIG_FILE="$(canonical_path "$DEFAULT_CONFIG_FILE")"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
if [[ -z "$ORIGIN_CERT" ]]; then
|
|
287
|
+
if [[ -n "$PROFILE_DIR" ]]; then
|
|
288
|
+
ORIGIN_CERT="$PROFILE_DIR/cert.pem"
|
|
289
|
+
elif file_exists "$DEFAULT_LOGIN_CERT"; then
|
|
290
|
+
ORIGIN_CERT="$DEFAULT_LOGIN_CERT"
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
if [[ -n "$ORIGIN_CERT" ]]; then
|
|
294
|
+
ORIGIN_CERT="$(canonical_path "$ORIGIN_CERT")"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
if [[ -z "$MANAGER_ROOT" ]]; then
|
|
298
|
+
if [[ -n "$PROFILE_DIR" ]]; then
|
|
299
|
+
anchor_dir="$PROFILE_DIR"
|
|
300
|
+
elif [[ -n "$DEFAULT_CONFIG_FILE" ]]; then
|
|
301
|
+
anchor_dir="$(dirname "$DEFAULT_CONFIG_FILE")"
|
|
302
|
+
elif [[ -n "$ORIGIN_CERT" ]]; then
|
|
303
|
+
anchor_dir="$(dirname "$ORIGIN_CERT")"
|
|
304
|
+
else
|
|
305
|
+
anchor_dir="$DEFAULT_MANAGER_HOME"
|
|
306
|
+
fi
|
|
307
|
+
MANAGER_ROOT="$anchor_dir/manager"
|
|
308
|
+
if [[ "$anchor_dir" == "$DEFAULT_MANAGER_HOME" ]]; then
|
|
309
|
+
MANAGER_ROOT="$DEFAULT_MANAGER_HOME"
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
MANAGER_ROOT="$(canonical_path "$MANAGER_ROOT")"
|
|
313
|
+
APPS_DIR="$MANAGER_ROOT/apps"
|
|
314
|
+
ARCHIVE_DIR="$MANAGER_ROOT/archive"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# 确保管理目录结构存在。
|
|
318
|
+
ensure_manager_dirs() {
|
|
319
|
+
ensure_dir "$APPS_DIR"
|
|
320
|
+
ensure_dir "$ARCHIVE_DIR"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# 确保系统已经安装 cloudflared。
|
|
324
|
+
ensure_cloudflared() {
|
|
325
|
+
if [[ -z "$CLOUDFLARED_BIN" ]]; then
|
|
326
|
+
die "cloudflared 未安装,请先执行 install。"
|
|
327
|
+
fi
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# 如目标证书不存在,则尝试从默认登录位置自动导入。
|
|
331
|
+
maybe_import_default_login_cert() {
|
|
332
|
+
if [[ -z "${ORIGIN_CERT:-}" ]]; then
|
|
333
|
+
return 0
|
|
334
|
+
fi
|
|
335
|
+
if file_exists "$ORIGIN_CERT"; then
|
|
336
|
+
return 0
|
|
337
|
+
fi
|
|
338
|
+
if ! file_exists "$DEFAULT_LOGIN_CERT"; then
|
|
339
|
+
return 0
|
|
340
|
+
fi
|
|
341
|
+
if [[ "$ORIGIN_CERT" == "$DEFAULT_LOGIN_CERT" ]]; then
|
|
342
|
+
return 0
|
|
343
|
+
fi
|
|
344
|
+
ensure_dir "$(dirname "$ORIGIN_CERT")"
|
|
345
|
+
cp "$DEFAULT_LOGIN_CERT" "$ORIGIN_CERT"
|
|
346
|
+
info "已自动导入登录证书到: $ORIGIN_CERT"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# 确保当前环境拥有可用于远端管理的 Origin 证书。
|
|
350
|
+
ensure_logged_in() {
|
|
351
|
+
maybe_import_default_login_cert
|
|
352
|
+
if [[ -z "${ORIGIN_CERT:-}" ]]; then
|
|
353
|
+
die "未配置 Origin 证书路径,请使用 --origincert、--profile-dir 或执行 init/use。"
|
|
354
|
+
fi
|
|
355
|
+
if ! file_exists "$ORIGIN_CERT"; then
|
|
356
|
+
die "找不到 Origin 证书: $ORIGIN_CERT。请先执行 login 或 import-cert。"
|
|
357
|
+
fi
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# 统一封装依赖 Origin 证书的 cloudflared tunnel 命令。
|
|
361
|
+
remote_cmd() {
|
|
362
|
+
ensure_cloudflared
|
|
363
|
+
ensure_logged_in
|
|
364
|
+
"$CLOUDFLARED_BIN" tunnel --origincert "$ORIGIN_CERT" "$@"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# 校验应用名称是否符合目录和标识要求。
|
|
368
|
+
validate_app_name() {
|
|
369
|
+
local name="$1"
|
|
370
|
+
if [[ ! "$name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]]; then
|
|
371
|
+
die "应用名非法。只允许字母、数字、点、下划线、短横线,且必须以字母或数字开头。"
|
|
372
|
+
fi
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# 规范化并校验公网 hostname。
|
|
376
|
+
normalize_hostname() {
|
|
377
|
+
local value="$1"
|
|
378
|
+
value="${value%%.}"
|
|
379
|
+
value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
|
|
380
|
+
if [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]; then
|
|
381
|
+
die "主机名非法: $1"
|
|
382
|
+
fi
|
|
383
|
+
printf '%s\n' "$value"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# 规范化 service 地址,兼容 host:port 与多种协议写法。
|
|
387
|
+
normalize_service() {
|
|
388
|
+
local value="$1"
|
|
389
|
+
local scheme=""
|
|
390
|
+
local host=""
|
|
391
|
+
|
|
392
|
+
if [[ "$value" == "hello_world" || "$value" == http_status:* ]]; then
|
|
393
|
+
printf '%s\n' "$value"
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if [[ "$value" == unix:* ]]; then
|
|
398
|
+
printf '%s\n' "$value"
|
|
399
|
+
return 0
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
if [[ "$value" != *"://"* ]]; then
|
|
403
|
+
if [[ "$value" == *:* ]]; then
|
|
404
|
+
value="http://$value"
|
|
405
|
+
else
|
|
406
|
+
die "服务地址必须包含协议,或使用 host:port 形式,例如 http://localhost:5173"
|
|
407
|
+
fi
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
if [[ "$value" =~ ^([A-Za-z][A-Za-z0-9+.-]*)://(\[[^]]+\]|[^/:?#]+) ]]; then
|
|
411
|
+
scheme="${BASH_REMATCH[1]}"
|
|
412
|
+
host="${BASH_REMATCH[2]}"
|
|
413
|
+
else
|
|
414
|
+
die "服务地址格式非法: $1"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
case "$scheme" in
|
|
418
|
+
http|https|tcp|ssh|rdp) ;;
|
|
419
|
+
*) die "不支持的服务协议: $scheme。支持 http、https、tcp、ssh、rdp、unix:、hello_world、http_status:*" ;;
|
|
420
|
+
esac
|
|
421
|
+
|
|
422
|
+
if [[ -z "$host" ]]; then
|
|
423
|
+
die "服务地址缺少主机名: $1"
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
printf '%s\n' "$value"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# 从 service URL 中提取协议、主机和端口,供连通性检查复用。
|
|
430
|
+
parse_service_url() {
|
|
431
|
+
local service="$1"
|
|
432
|
+
PARSED_SCHEME=""
|
|
433
|
+
PARSED_HOST=""
|
|
434
|
+
PARSED_PORT=""
|
|
435
|
+
|
|
436
|
+
if [[ "$service" =~ ^([A-Za-z][A-Za-z0-9+.-]*)://(\[[^]]+\]|[^/:?#]+)(:([0-9]+))? ]]; then
|
|
437
|
+
PARSED_SCHEME="${BASH_REMATCH[1]}"
|
|
438
|
+
PARSED_HOST="${BASH_REMATCH[2]}"
|
|
439
|
+
PARSED_PORT="${BASH_REMATCH[4]}"
|
|
440
|
+
PARSED_HOST="${PARSED_HOST#[}"
|
|
441
|
+
PARSED_HOST="${PARSED_HOST%]}"
|
|
442
|
+
fi
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# 校验本地 service 是否可达,避免 tunnel 启动后立刻失败。
|
|
446
|
+
local_target_reachable() {
|
|
447
|
+
local service="$1"
|
|
448
|
+
local timeout="${2:-2}"
|
|
449
|
+
local host=""
|
|
450
|
+
local port=""
|
|
451
|
+
|
|
452
|
+
if [[ "$service" == "hello_world" || "$service" == http_status:* ]]; then
|
|
453
|
+
return 0
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
if [[ "$service" == unix:* ]]; then
|
|
457
|
+
local socket_path="${service#unix:}"
|
|
458
|
+
if ! file_exists "$socket_path"; then
|
|
459
|
+
die "Unix socket 不存在: $socket_path"
|
|
460
|
+
fi
|
|
461
|
+
return 0
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
parse_service_url "$service"
|
|
465
|
+
host="$PARSED_HOST"
|
|
466
|
+
port="$PARSED_PORT"
|
|
467
|
+
if [[ -z "$host" ]]; then
|
|
468
|
+
die "服务地址格式非法: $service"
|
|
469
|
+
fi
|
|
470
|
+
case "$host" in
|
|
471
|
+
localhost|127.0.0.1|::1) ;;
|
|
472
|
+
*) return 0 ;;
|
|
473
|
+
esac
|
|
474
|
+
|
|
475
|
+
if [[ -z "$port" ]]; then
|
|
476
|
+
case "$PARSED_SCHEME" in
|
|
477
|
+
http) port="80" ;;
|
|
478
|
+
https) port="443" ;;
|
|
479
|
+
ssh) port="22" ;;
|
|
480
|
+
rdp) port="3389" ;;
|
|
481
|
+
*) die "服务地址缺少端口: $service" ;;
|
|
482
|
+
esac
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
if ! command_exists nc; then
|
|
486
|
+
warn "系统没有 nc,跳过本地服务连通性检查。"
|
|
487
|
+
return 0
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
if ! nc -z -G "$timeout" "$host" "$port" >/dev/null 2>&1; then
|
|
491
|
+
die "本地服务无法连接: $service"
|
|
492
|
+
fi
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# 返回指定应用的根目录。
|
|
496
|
+
app_dir() {
|
|
497
|
+
printf '%s/%s\n' "$APPS_DIR" "$1"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# 返回指定应用的元数据文件路径。
|
|
501
|
+
meta_path() {
|
|
502
|
+
printf '%s/meta.env\n' "$(app_dir "$1")"
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# 返回指定应用的配置文件路径。
|
|
506
|
+
config_path() {
|
|
507
|
+
printf '%s/config.yml\n' "$(app_dir "$1")"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# 返回指定应用的凭据文件路径。
|
|
511
|
+
credentials_path() {
|
|
512
|
+
printf '%s/credentials.json\n' "$(app_dir "$1")"
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# 返回指定应用的 ingress 规则文件路径。
|
|
516
|
+
ingress_path() {
|
|
517
|
+
printf '%s/ingress.tsv\n' "$(app_dir "$1")"
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
# 返回指定应用的 PID 文件路径。
|
|
521
|
+
pid_path() {
|
|
522
|
+
printf '%s/run/cloudflared.pid\n' "$(app_dir "$1")"
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# 返回指定应用的日志文件路径。
|
|
526
|
+
log_path() {
|
|
527
|
+
printf '%s/logs/cloudflared.log\n' "$(app_dir "$1")"
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# 返回指定应用的备份目录路径。
|
|
531
|
+
backups_dir() {
|
|
532
|
+
printf '%s/backups\n' "$(app_dir "$1")"
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# 从 config.yml 读取顶层字段值。
|
|
536
|
+
read_top_level_config_value() {
|
|
537
|
+
local config_file="$1"
|
|
538
|
+
local key="$2"
|
|
539
|
+
awk -F': ' -v wanted="$key" '
|
|
540
|
+
$1 == wanted {
|
|
541
|
+
sub(/^[^:]+:[[:space:]]*/, "", $0)
|
|
542
|
+
print
|
|
543
|
+
exit
|
|
544
|
+
}
|
|
545
|
+
' "$config_file"
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
# 从 config.yml 中读取第一个 ingress hostname。
|
|
549
|
+
read_first_ingress_hostname() {
|
|
550
|
+
local config_file="$1"
|
|
551
|
+
awk '
|
|
552
|
+
/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*/ {
|
|
553
|
+
sub(/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*/, "", $0)
|
|
554
|
+
print
|
|
555
|
+
exit
|
|
556
|
+
}
|
|
557
|
+
' "$config_file"
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# 从 config.yml 中读取第一个 ingress service。
|
|
561
|
+
read_first_ingress_service() {
|
|
562
|
+
local config_file="$1"
|
|
563
|
+
awk '
|
|
564
|
+
/^[[:space:]]*service:[[:space:]]*/ {
|
|
565
|
+
sub(/^[[:space:]]*service:[[:space:]]*/, "", $0)
|
|
566
|
+
print
|
|
567
|
+
exit
|
|
568
|
+
}
|
|
569
|
+
' "$config_file"
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# 从 credentials JSON 中提取 TunnelID。
|
|
573
|
+
read_tunnel_id_from_credentials() {
|
|
574
|
+
local credentials_file="$1"
|
|
575
|
+
plutil -extract TunnelID raw -o - "$credentials_file" 2>/dev/null || true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# 清空当前进程内缓存的 ingress 规则。
|
|
579
|
+
reset_ingress_rules() {
|
|
580
|
+
INGRESS_HOSTS=()
|
|
581
|
+
INGRESS_SERVICES=()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# 返回当前缓存中的 ingress 规则数量。
|
|
585
|
+
ingress_rule_count() {
|
|
586
|
+
printf '%s\n' "${#INGRESS_HOSTS[@]}"
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# 查找某个 hostname 在 ingress 规则中的位置,可选跳过指定序号。
|
|
590
|
+
find_ingress_rule_index_by_hostname() {
|
|
591
|
+
local wanted="$1"
|
|
592
|
+
local skip_index="${2:--1}"
|
|
593
|
+
local i=0
|
|
594
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
595
|
+
if [[ "$i" != "$skip_index" && "${INGRESS_HOSTS[$i]}" == "$wanted" ]]; then
|
|
596
|
+
printf '%s\n' "$i"
|
|
597
|
+
return 0
|
|
598
|
+
fi
|
|
599
|
+
i=$((i + 1))
|
|
600
|
+
done
|
|
601
|
+
return 1
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
# 解析形如 hostname=service 的 ingress 简写。
|
|
605
|
+
parse_ingress_spec() {
|
|
606
|
+
local spec="$1"
|
|
607
|
+
local hostname=""
|
|
608
|
+
local service=""
|
|
609
|
+
|
|
610
|
+
if [[ "$spec" != *=* ]]; then
|
|
611
|
+
die "ingress 规则格式非法: $spec。请使用 --ingress hostname=service"
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
hostname="${spec%%=*}"
|
|
615
|
+
service="${spec#*=}"
|
|
616
|
+
[[ -n "$hostname" ]] || die "ingress 规则缺少 hostname: $spec"
|
|
617
|
+
[[ -n "$service" ]] || die "ingress 规则缺少 service: $spec"
|
|
618
|
+
|
|
619
|
+
PARSED_INGRESS_HOSTNAME="$(normalize_hostname "$hostname")"
|
|
620
|
+
PARSED_INGRESS_SERVICE="$(normalize_service "$service")"
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# 解析形如 2:hostname=service 的批量修改简写。
|
|
624
|
+
parse_modify_spec() {
|
|
625
|
+
local spec="$1"
|
|
626
|
+
local index_part=""
|
|
627
|
+
local ingress_part=""
|
|
628
|
+
|
|
629
|
+
if [[ "$spec" != *:* || "$spec" != *=* ]]; then
|
|
630
|
+
die "批量修改格式非法: $spec。请使用 --set N:hostname=service"
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
index_part="${spec%%:*}"
|
|
634
|
+
ingress_part="${spec#*:}"
|
|
635
|
+
[[ "$index_part" =~ ^[1-9][0-9]*$ ]] || die "批量修改序号非法: $spec"
|
|
636
|
+
|
|
637
|
+
parse_ingress_spec "$ingress_part"
|
|
638
|
+
PARSED_MODIFY_INDEX="$index_part"
|
|
639
|
+
PARSED_MODIFY_HOSTNAME="$PARSED_INGRESS_HOSTNAME"
|
|
640
|
+
PARSED_MODIFY_SERVICE="$PARSED_INGRESS_SERVICE"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# 向当前缓存追加一条 ingress 规则,并确保 hostname 不重复。
|
|
644
|
+
append_ingress_rule() {
|
|
645
|
+
local hostname="$1"
|
|
646
|
+
local service="$2"
|
|
647
|
+
|
|
648
|
+
if find_ingress_rule_index_by_hostname "$hostname" >/dev/null 2>&1; then
|
|
649
|
+
die "同一个 Tunnel 下不能重复使用主机名: $hostname"
|
|
650
|
+
fi
|
|
651
|
+
|
|
652
|
+
INGRESS_HOSTS+=("$hostname")
|
|
653
|
+
INGRESS_SERVICES+=("$service")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# 根据命令行参数组装本次要写入的 ingress 规则集合。
|
|
657
|
+
build_requested_ingress_rules() {
|
|
658
|
+
local hostname="${1:-}"
|
|
659
|
+
local service="${2:-}"
|
|
660
|
+
shift 2 || true
|
|
661
|
+
local spec=""
|
|
662
|
+
|
|
663
|
+
reset_ingress_rules
|
|
664
|
+
|
|
665
|
+
if [[ -n "$hostname" || -n "$service" ]]; then
|
|
666
|
+
[[ -n "$hostname" ]] || die "缺少 --hostname"
|
|
667
|
+
[[ -n "$service" ]] || die "缺少 --service"
|
|
668
|
+
append_ingress_rule "$(normalize_hostname "$hostname")" "$(normalize_service "$service")"
|
|
669
|
+
fi
|
|
670
|
+
|
|
671
|
+
for spec in "$@"; do
|
|
672
|
+
parse_ingress_spec "$spec"
|
|
673
|
+
append_ingress_rule "$PARSED_INGRESS_HOSTNAME" "$PARSED_INGRESS_SERVICE"
|
|
674
|
+
done
|
|
675
|
+
|
|
676
|
+
if [[ "$(ingress_rule_count)" == "0" ]]; then
|
|
677
|
+
die "至少需要提供一条 ingress 规则。可使用 --hostname/--service,或重复传 --ingress hostname=service"
|
|
678
|
+
fi
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
# 从 ingress.tsv 读取全部规则到当前缓存。
|
|
682
|
+
load_ingress_rules_file() {
|
|
683
|
+
local file="$1"
|
|
684
|
+
local hostname=""
|
|
685
|
+
local service=""
|
|
686
|
+
|
|
687
|
+
reset_ingress_rules
|
|
688
|
+
while IFS=$'\t' read -r hostname service; do
|
|
689
|
+
[[ -z "$hostname" ]] && continue
|
|
690
|
+
[[ -z "$service" ]] && continue
|
|
691
|
+
append_ingress_rule "$hostname" "$service"
|
|
692
|
+
done <"$file"
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# 从当前缓存同步主规则,兼容旧版单规则字段。
|
|
696
|
+
sync_primary_ingress_to_meta() {
|
|
697
|
+
if [[ "${#INGRESS_HOSTS[@]}" -gt 0 ]]; then
|
|
698
|
+
META_HOSTNAME="${INGRESS_HOSTS[0]}"
|
|
699
|
+
META_SERVICE="${INGRESS_SERVICES[0]}"
|
|
700
|
+
else
|
|
701
|
+
META_HOSTNAME=""
|
|
702
|
+
META_SERVICE=""
|
|
703
|
+
fi
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# 将当前缓存中的 ingress 规则写回 ingress.tsv。
|
|
707
|
+
save_ingress_rules() {
|
|
708
|
+
local i=0
|
|
709
|
+
ensure_dir "$(dirname "$META_INGRESS_FILE")"
|
|
710
|
+
: >"$META_INGRESS_FILE"
|
|
711
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
712
|
+
printf '%s\t%s\n' "${INGRESS_HOSTS[$i]}" "${INGRESS_SERVICES[$i]}" >>"$META_INGRESS_FILE"
|
|
713
|
+
i=$((i + 1))
|
|
714
|
+
done
|
|
715
|
+
sync_primary_ingress_to_meta
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# 确保当前应用拥有 ingress 规则文件,旧数据会自动迁移。
|
|
719
|
+
ensure_ingress_rules_loaded() {
|
|
720
|
+
if [[ -z "$META_INGRESS_FILE" && -n "$META_NAME" ]]; then
|
|
721
|
+
META_INGRESS_FILE="$(canonical_path "$(ingress_path "$META_NAME")")"
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
if [[ -n "$META_INGRESS_FILE" && -f "$META_INGRESS_FILE" ]]; then
|
|
725
|
+
load_ingress_rules_file "$META_INGRESS_FILE"
|
|
726
|
+
sync_primary_ingress_to_meta
|
|
727
|
+
return 0
|
|
728
|
+
fi
|
|
729
|
+
|
|
730
|
+
reset_ingress_rules
|
|
731
|
+
if [[ -n "$META_HOSTNAME" && -n "$META_SERVICE" ]]; then
|
|
732
|
+
append_ingress_rule "$(normalize_hostname "$META_HOSTNAME")" "$(normalize_service "$META_SERVICE")"
|
|
733
|
+
sync_primary_ingress_to_meta
|
|
734
|
+
return 0
|
|
735
|
+
fi
|
|
736
|
+
|
|
737
|
+
die "找不到 ingress 规则文件: $META_INGRESS_FILE"
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
# 从现有 config.yml 中读取全部 hostname/service ingress 规则。
|
|
741
|
+
load_ingress_rules_from_config() {
|
|
742
|
+
local config_file="$1"
|
|
743
|
+
local hostname=""
|
|
744
|
+
local service=""
|
|
745
|
+
|
|
746
|
+
reset_ingress_rules
|
|
747
|
+
while IFS=$'\t' read -r hostname service; do
|
|
748
|
+
[[ -z "$hostname" ]] && continue
|
|
749
|
+
[[ -z "$service" ]] && continue
|
|
750
|
+
append_ingress_rule "$(normalize_hostname "$hostname")" "$(normalize_service "$service")"
|
|
751
|
+
done < <(
|
|
752
|
+
awk '
|
|
753
|
+
/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*/ {
|
|
754
|
+
line = $0
|
|
755
|
+
sub(/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*/, "", line)
|
|
756
|
+
host = line
|
|
757
|
+
next
|
|
758
|
+
}
|
|
759
|
+
/^[[:space:]]*hostname:[[:space:]]*/ {
|
|
760
|
+
line = $0
|
|
761
|
+
sub(/^[[:space:]]*hostname:[[:space:]]*/, "", line)
|
|
762
|
+
host = line
|
|
763
|
+
next
|
|
764
|
+
}
|
|
765
|
+
/^[[:space:]]*service:[[:space:]]*/ {
|
|
766
|
+
line = $0
|
|
767
|
+
sub(/^[[:space:]]*service:[[:space:]]*/, "", line)
|
|
768
|
+
if (host != "") {
|
|
769
|
+
print host "\t" line
|
|
770
|
+
host = ""
|
|
771
|
+
}
|
|
772
|
+
next
|
|
773
|
+
}
|
|
774
|
+
/^[[:space:]]*-[[:space:]]*service:[[:space:]]*/ {
|
|
775
|
+
host = ""
|
|
776
|
+
next
|
|
777
|
+
}
|
|
778
|
+
' "$config_file"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
[[ "$(ingress_rule_count)" != "0" ]]
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
# 校验当前缓存中的全部本地服务是否可达。
|
|
785
|
+
check_all_ingress_targets() {
|
|
786
|
+
local i=0
|
|
787
|
+
while [[ "$i" -lt "${#INGRESS_SERVICES[@]}" ]]; do
|
|
788
|
+
local_target_reachable "${INGRESS_SERVICES[$i]}"
|
|
789
|
+
i=$((i + 1))
|
|
790
|
+
done
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# 为当前缓存中的全部 hostname 建立 DNS 路由。
|
|
794
|
+
route_all_dns() {
|
|
795
|
+
local tunnel_name="$1"
|
|
796
|
+
local overwrite="${2:-0}"
|
|
797
|
+
local i=0
|
|
798
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
799
|
+
route_dns "$tunnel_name" "${INGRESS_HOSTS[$i]}" "$overwrite"
|
|
800
|
+
i=$((i + 1))
|
|
801
|
+
done
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# 复制现有 config.yml,并把 credentials-file 改写到受管目录。
|
|
805
|
+
copy_config_with_credentials_file() {
|
|
806
|
+
local source_config="$1"
|
|
807
|
+
local target_config="$2"
|
|
808
|
+
local target_credentials="$3"
|
|
809
|
+
ensure_dir "$(dirname "$target_config")"
|
|
810
|
+
awk -v credentials="$target_credentials" '
|
|
811
|
+
BEGIN { replaced = 0 }
|
|
812
|
+
/^credentials-file:[[:space:]]*/ {
|
|
813
|
+
print "credentials-file: " credentials
|
|
814
|
+
replaced = 1
|
|
815
|
+
next
|
|
816
|
+
}
|
|
817
|
+
{ print }
|
|
818
|
+
END {
|
|
819
|
+
if (!replaced) {
|
|
820
|
+
print "credentials-file: " credentials
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
' "$source_config" >"$target_config"
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
# 初始化某个应用的目录结构。
|
|
827
|
+
init_app_dirs() {
|
|
828
|
+
local name="$1"
|
|
829
|
+
ensure_manager_dirs
|
|
830
|
+
ensure_dir "$(app_dir "$name")"
|
|
831
|
+
ensure_dir "$(dirname "$(pid_path "$name")")"
|
|
832
|
+
ensure_dir "$(dirname "$(log_path "$name")")"
|
|
833
|
+
ensure_dir "$(backups_dir "$name")"
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
# 判断某个应用是否已被接管。
|
|
837
|
+
app_exists() {
|
|
838
|
+
file_exists "$(meta_path "$1")"
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
# 清空内存中的元数据变量,避免旧数据串用。
|
|
842
|
+
reset_meta() {
|
|
843
|
+
META_NAME=""
|
|
844
|
+
META_TUNNEL_NAME=""
|
|
845
|
+
META_TUNNEL_ID=""
|
|
846
|
+
META_HOSTNAME=""
|
|
847
|
+
META_SERVICE=""
|
|
848
|
+
META_APP_DIR=""
|
|
849
|
+
META_CONFIG_FILE=""
|
|
850
|
+
META_CREDENTIALS_FILE=""
|
|
851
|
+
META_INGRESS_FILE=""
|
|
852
|
+
META_PID_FILE=""
|
|
853
|
+
META_LOG_FILE=""
|
|
854
|
+
META_META_FILE=""
|
|
855
|
+
META_CREATED_AT=""
|
|
856
|
+
META_UPDATED_AT=""
|
|
857
|
+
reset_ingress_rules
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
# 尝试把当前默认 config.yml 和现有凭据自动接管成受管应用。
|
|
861
|
+
try_import_current_default_app() {
|
|
862
|
+
local name="$1"
|
|
863
|
+
local tunnel_name=""
|
|
864
|
+
local source_credentials=""
|
|
865
|
+
local tunnel_id=""
|
|
866
|
+
|
|
867
|
+
if [[ -z "${DEFAULT_CONFIG_FILE:-}" ]] || ! file_exists "$DEFAULT_CONFIG_FILE"; then
|
|
868
|
+
return 1
|
|
869
|
+
fi
|
|
870
|
+
|
|
871
|
+
tunnel_name="$(read_top_level_config_value "$DEFAULT_CONFIG_FILE" "tunnel")"
|
|
872
|
+
if [[ -z "$tunnel_name" || "$tunnel_name" != "$name" ]]; then
|
|
873
|
+
return 1
|
|
874
|
+
fi
|
|
875
|
+
|
|
876
|
+
source_credentials="$(read_top_level_config_value "$DEFAULT_CONFIG_FILE" "credentials-file")"
|
|
877
|
+
|
|
878
|
+
if [[ -z "$source_credentials" ]]; then
|
|
879
|
+
return 1
|
|
880
|
+
fi
|
|
881
|
+
|
|
882
|
+
if ! load_ingress_rules_from_config "$DEFAULT_CONFIG_FILE"; then
|
|
883
|
+
return 1
|
|
884
|
+
fi
|
|
885
|
+
if [[ "$(ingress_rule_count)" == "0" ]]; then
|
|
886
|
+
return 1
|
|
887
|
+
fi
|
|
888
|
+
|
|
889
|
+
source_credentials="$(canonical_path "$source_credentials")"
|
|
890
|
+
if ! file_exists "$source_credentials"; then
|
|
891
|
+
return 1
|
|
892
|
+
fi
|
|
893
|
+
|
|
894
|
+
tunnel_id="$(read_tunnel_id_from_credentials "$source_credentials")"
|
|
895
|
+
if [[ -z "$tunnel_id" && -n "${ORIGIN_CERT:-}" ]] && file_exists "$ORIGIN_CERT"; then
|
|
896
|
+
remote_lookup_by_name "$tunnel_name"
|
|
897
|
+
if [[ "$REMOTE_FOUND" == "1" ]]; then
|
|
898
|
+
tunnel_id="$REMOTE_TUNNEL_ID"
|
|
899
|
+
fi
|
|
900
|
+
fi
|
|
901
|
+
|
|
902
|
+
build_meta "$name" "$tunnel_name" "$tunnel_id" "${INGRESS_HOSTS[0]}" "${INGRESS_SERVICES[0]}"
|
|
903
|
+
cp "$source_credentials" "$META_CREDENTIALS_FILE"
|
|
904
|
+
save_ingress_rules
|
|
905
|
+
copy_config_with_credentials_file "$DEFAULT_CONFIG_FILE" "$META_CONFIG_FILE" "$META_CREDENTIALS_FILE"
|
|
906
|
+
save_meta
|
|
907
|
+
info "已自动接管现有默认配置为受管应用: $name"
|
|
908
|
+
return 0
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
# 从 meta.env 读取应用元数据到当前进程变量。
|
|
912
|
+
load_meta() {
|
|
913
|
+
local name="$1"
|
|
914
|
+
local file
|
|
915
|
+
file="$(meta_path "$name")"
|
|
916
|
+
reset_meta
|
|
917
|
+
if ! file_exists "$file"; then
|
|
918
|
+
if ! try_import_current_default_app "$name"; then
|
|
919
|
+
die "未找到受管应用: $name。远端 Tunnel 存在,不代表已经被本脚本接管。请执行 adopt,或让默认 config.yml 指向该 Tunnel 后重试。"
|
|
920
|
+
fi
|
|
921
|
+
file="$(meta_path "$name")"
|
|
922
|
+
fi
|
|
923
|
+
while IFS=$'\t' read -r key value; do
|
|
924
|
+
case "$key" in
|
|
925
|
+
name) META_NAME="$value" ;;
|
|
926
|
+
tunnel_name) META_TUNNEL_NAME="$value" ;;
|
|
927
|
+
tunnel_id) META_TUNNEL_ID="$value" ;;
|
|
928
|
+
hostname) META_HOSTNAME="$value" ;;
|
|
929
|
+
service) META_SERVICE="$value" ;;
|
|
930
|
+
app_dir) META_APP_DIR="$value" ;;
|
|
931
|
+
config_file) META_CONFIG_FILE="$value" ;;
|
|
932
|
+
credentials_file) META_CREDENTIALS_FILE="$value" ;;
|
|
933
|
+
ingress_file) META_INGRESS_FILE="$value" ;;
|
|
934
|
+
pid_file) META_PID_FILE="$value" ;;
|
|
935
|
+
log_file) META_LOG_FILE="$value" ;;
|
|
936
|
+
meta_file) META_META_FILE="$value" ;;
|
|
937
|
+
created_at) META_CREATED_AT="$value" ;;
|
|
938
|
+
updated_at) META_UPDATED_AT="$value" ;;
|
|
939
|
+
esac
|
|
940
|
+
done <"$file"
|
|
941
|
+
if [[ -z "$META_INGRESS_FILE" ]]; then
|
|
942
|
+
META_INGRESS_FILE="$(canonical_path "$(ingress_path "$name")")"
|
|
943
|
+
fi
|
|
944
|
+
ensure_ingress_rules_loaded
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
# 将当前元数据变量写回 meta.env。
|
|
948
|
+
save_meta() {
|
|
949
|
+
sync_primary_ingress_to_meta
|
|
950
|
+
META_UPDATED_AT="$(utc_now)"
|
|
951
|
+
write_kv_file "$META_META_FILE" \
|
|
952
|
+
name "$META_NAME" \
|
|
953
|
+
tunnel_name "$META_TUNNEL_NAME" \
|
|
954
|
+
tunnel_id "$META_TUNNEL_ID" \
|
|
955
|
+
hostname "$META_HOSTNAME" \
|
|
956
|
+
service "$META_SERVICE" \
|
|
957
|
+
app_dir "$META_APP_DIR" \
|
|
958
|
+
config_file "$META_CONFIG_FILE" \
|
|
959
|
+
credentials_file "$META_CREDENTIALS_FILE" \
|
|
960
|
+
ingress_file "$META_INGRESS_FILE" \
|
|
961
|
+
pid_file "$META_PID_FILE" \
|
|
962
|
+
log_file "$META_LOG_FILE" \
|
|
963
|
+
meta_file "$META_META_FILE" \
|
|
964
|
+
created_at "$META_CREATED_AT" \
|
|
965
|
+
updated_at "$META_UPDATED_AT"
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
# 根据输入参数构造一份完整的应用元数据。
|
|
969
|
+
build_meta() {
|
|
970
|
+
local name="$1"
|
|
971
|
+
local tunnel_name="$2"
|
|
972
|
+
local tunnel_id="$3"
|
|
973
|
+
local hostname="$4"
|
|
974
|
+
local service="$5"
|
|
975
|
+
init_app_dirs "$name"
|
|
976
|
+
META_NAME="$name"
|
|
977
|
+
META_TUNNEL_NAME="$tunnel_name"
|
|
978
|
+
META_TUNNEL_ID="$tunnel_id"
|
|
979
|
+
META_HOSTNAME="$hostname"
|
|
980
|
+
META_SERVICE="$service"
|
|
981
|
+
META_APP_DIR="$(canonical_path "$(app_dir "$name")")"
|
|
982
|
+
META_CONFIG_FILE="$(canonical_path "$(config_path "$name")")"
|
|
983
|
+
META_CREDENTIALS_FILE="$(canonical_path "$(credentials_path "$name")")"
|
|
984
|
+
META_INGRESS_FILE="$(canonical_path "$(ingress_path "$name")")"
|
|
985
|
+
META_PID_FILE="$(canonical_path "$(pid_path "$name")")"
|
|
986
|
+
META_LOG_FILE="$(canonical_path "$(log_path "$name")")"
|
|
987
|
+
META_META_FILE="$(canonical_path "$(meta_path "$name")")"
|
|
988
|
+
META_CREATED_AT="$(utc_now)"
|
|
989
|
+
META_UPDATED_AT="$META_CREATED_AT"
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
# 渲染 cloudflared 所需的 config.yml 内容。
|
|
993
|
+
render_config() {
|
|
994
|
+
local i=0
|
|
995
|
+
|
|
996
|
+
if [[ "$(ingress_rule_count)" == "0" ]]; then
|
|
997
|
+
die "当前应用没有可用的 ingress 规则,无法生成 config.yml"
|
|
998
|
+
fi
|
|
999
|
+
|
|
1000
|
+
cat <<EOF
|
|
1001
|
+
tunnel: $META_TUNNEL_NAME
|
|
1002
|
+
credentials-file: $META_CREDENTIALS_FILE
|
|
1003
|
+
|
|
1004
|
+
ingress:
|
|
1005
|
+
EOF
|
|
1006
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
1007
|
+
cat <<EOF
|
|
1008
|
+
- hostname: ${INGRESS_HOSTS[$i]}
|
|
1009
|
+
service: ${INGRESS_SERVICES[$i]}
|
|
1010
|
+
EOF
|
|
1011
|
+
i=$((i + 1))
|
|
1012
|
+
done
|
|
1013
|
+
cat <<EOF
|
|
1014
|
+
- service: http_status:404
|
|
1015
|
+
EOF
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
# 写入应用配置,可选先备份旧文件。
|
|
1019
|
+
write_config() {
|
|
1020
|
+
local do_backup="${1:-1}"
|
|
1021
|
+
if [[ "$do_backup" == "1" ]]; then
|
|
1022
|
+
backup_file "$META_CONFIG_FILE" >/dev/null || true
|
|
1023
|
+
fi
|
|
1024
|
+
ensure_dir "$(dirname "$META_CONFIG_FILE")"
|
|
1025
|
+
render_config >"$META_CONFIG_FILE"
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
# 使用 cloudflared 自带校验能力检查 config.yml 是否有效。
|
|
1029
|
+
validate_config() {
|
|
1030
|
+
ensure_cloudflared
|
|
1031
|
+
"$CLOUDFLARED_BIN" tunnel --config "$1" ingress validate >/dev/null
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
# 读取 PID 文件中的进程号。
|
|
1035
|
+
read_pid() {
|
|
1036
|
+
local pid_file="$1"
|
|
1037
|
+
if ! file_exists "$pid_file"; then
|
|
1038
|
+
return 1
|
|
1039
|
+
fi
|
|
1040
|
+
tr -d '[:space:]' <"$pid_file"
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
# 查询指定 PID 对应的命令行。
|
|
1044
|
+
process_command() {
|
|
1045
|
+
local pid="$1"
|
|
1046
|
+
ps -p "$pid" -o command= 2>/dev/null || true
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
# 判断某个 PID 是否仍在运行,并可选校验其命令行特征。
|
|
1050
|
+
is_pid_running() {
|
|
1051
|
+
local pid="$1"
|
|
1052
|
+
local expected_hint="${2:-}"
|
|
1053
|
+
local cmdline=""
|
|
1054
|
+
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
|
1055
|
+
return 1
|
|
1056
|
+
fi
|
|
1057
|
+
if [[ -z "$expected_hint" ]]; then
|
|
1058
|
+
return 0
|
|
1059
|
+
fi
|
|
1060
|
+
cmdline="$(process_command "$pid")"
|
|
1061
|
+
if [[ -z "$cmdline" ]]; then
|
|
1062
|
+
return 0
|
|
1063
|
+
fi
|
|
1064
|
+
[[ "$cmdline" == *"$expected_hint"* ]]
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
# 通过 pidfile 和 pgrep 双重方式定位当前应用的运行进程。
|
|
1068
|
+
find_running_pid() {
|
|
1069
|
+
local pid=""
|
|
1070
|
+
if pid="$(read_pid "$META_PID_FILE" 2>/dev/null || true)"; then
|
|
1071
|
+
if [[ -n "$pid" ]] && is_pid_running "$pid" "$META_CONFIG_FILE"; then
|
|
1072
|
+
printf '%s\n' "$pid"
|
|
1073
|
+
return 0
|
|
1074
|
+
fi
|
|
1075
|
+
fi
|
|
1076
|
+
rm -f "$META_PID_FILE"
|
|
1077
|
+
while IFS= read -r pid; do
|
|
1078
|
+
[[ -z "$pid" ]] && continue
|
|
1079
|
+
if is_pid_running "$pid" "$META_CONFIG_FILE"; then
|
|
1080
|
+
ensure_dir "$(dirname "$META_PID_FILE")"
|
|
1081
|
+
printf '%s\n' "$pid" >"$META_PID_FILE"
|
|
1082
|
+
printf '%s\n' "$pid"
|
|
1083
|
+
return 0
|
|
1084
|
+
fi
|
|
1085
|
+
done < <(pgrep -f "$META_CONFIG_FILE" 2>/dev/null || true)
|
|
1086
|
+
return 1
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
# 读取日志文件尾部若干行。
|
|
1090
|
+
read_log_tail() {
|
|
1091
|
+
local log_file="$1"
|
|
1092
|
+
local lines="$2"
|
|
1093
|
+
if ! file_exists "$log_file"; then
|
|
1094
|
+
return 0
|
|
1095
|
+
fi
|
|
1096
|
+
tail -n "$lines" "$log_file"
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
# 为指定 tunnel 拉取并写入凭据文件。
|
|
1100
|
+
issue_credentials() {
|
|
1101
|
+
local tunnel_name="$1"
|
|
1102
|
+
local output_file="$2"
|
|
1103
|
+
ensure_dir "$(dirname "$output_file")"
|
|
1104
|
+
remote_cmd token --cred-file "$output_file" "$tunnel_name" >/dev/null
|
|
1105
|
+
if ! file_exists "$output_file"; then
|
|
1106
|
+
die "写入凭据文件失败: $output_file"
|
|
1107
|
+
fi
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
# 创建远端 tunnel,并尽量把凭据直接落到受管目录。
|
|
1111
|
+
create_remote_tunnel() {
|
|
1112
|
+
local tunnel_name="$1"
|
|
1113
|
+
local output_file="$2"
|
|
1114
|
+
remote_lookup_by_name "$tunnel_name"
|
|
1115
|
+
if [[ "$REMOTE_FOUND" == "1" ]]; then
|
|
1116
|
+
die "远端 tunnel 已存在: $tunnel_name。请使用 adopt 或 add --use-existing。"
|
|
1117
|
+
fi
|
|
1118
|
+
ensure_dir "$(dirname "$output_file")"
|
|
1119
|
+
remote_cmd create --cred-file "$output_file" "$tunnel_name" >/dev/null
|
|
1120
|
+
if ! file_exists "$output_file"; then
|
|
1121
|
+
warn "创建 tunnel 时未直接生成凭据文件,将尝试补发 token。"
|
|
1122
|
+
issue_credentials "$tunnel_name" "$output_file"
|
|
1123
|
+
fi
|
|
1124
|
+
remote_lookup_by_name "$tunnel_name"
|
|
1125
|
+
if [[ "$REMOTE_FOUND" != "1" ]]; then
|
|
1126
|
+
die "Tunnel 已创建,但无法再次查询到: $tunnel_name"
|
|
1127
|
+
fi
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
# 为 tunnel 绑定或覆盖 DNS 记录。
|
|
1131
|
+
route_dns() {
|
|
1132
|
+
local tunnel_name="$1"
|
|
1133
|
+
local hostname="$2"
|
|
1134
|
+
local overwrite="${3:-0}"
|
|
1135
|
+
if [[ "$overwrite" == "1" ]]; then
|
|
1136
|
+
remote_cmd route dns --overwrite-dns "$tunnel_name" "$hostname" >/dev/null
|
|
1137
|
+
else
|
|
1138
|
+
remote_cmd route dns "$tunnel_name" "$hostname" >/dev/null
|
|
1139
|
+
fi
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
# 将当前应用配置复制为默认 config.yml。
|
|
1143
|
+
activate_default_config() {
|
|
1144
|
+
local backup=""
|
|
1145
|
+
if [[ -z "${DEFAULT_CONFIG_FILE:-}" ]]; then
|
|
1146
|
+
die "未配置默认配置文件路径,请使用 --config-file、--profile-dir 或 init/use。"
|
|
1147
|
+
fi
|
|
1148
|
+
ensure_dir "$(dirname "$DEFAULT_CONFIG_FILE")"
|
|
1149
|
+
backup="$(backup_file "$DEFAULT_CONFIG_FILE" || true)"
|
|
1150
|
+
cp "$META_CONFIG_FILE" "$DEFAULT_CONFIG_FILE"
|
|
1151
|
+
if [[ -n "$backup" ]]; then
|
|
1152
|
+
info "已备份默认配置: $backup"
|
|
1153
|
+
fi
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
# 将应用目录归档到 archive 目录,便于后续追溯。
|
|
1157
|
+
archive_app_dir() {
|
|
1158
|
+
local src="$1"
|
|
1159
|
+
local stamp
|
|
1160
|
+
stamp="$(date +"%Y%m%d-%H%M%S")"
|
|
1161
|
+
ensure_manager_dirs
|
|
1162
|
+
local dest="$ARCHIVE_DIR/$(basename "$src")-$stamp"
|
|
1163
|
+
mv "$src" "$dest"
|
|
1164
|
+
printf '%s\n' "$dest"
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
# 列出当前管理目录下的全部受管应用名称。
|
|
1168
|
+
managed_apps() {
|
|
1169
|
+
if ! dir_exists "$APPS_DIR"; then
|
|
1170
|
+
return 0
|
|
1171
|
+
fi
|
|
1172
|
+
local found=0
|
|
1173
|
+
local entry=""
|
|
1174
|
+
for entry in "$APPS_DIR"/*; do
|
|
1175
|
+
[[ ! -d "$entry" ]] && continue
|
|
1176
|
+
if file_exists "$entry/meta.env"; then
|
|
1177
|
+
basename "$entry"
|
|
1178
|
+
found=1
|
|
1179
|
+
fi
|
|
1180
|
+
done | sort
|
|
1181
|
+
if [[ "$found" == "0" ]]; then
|
|
1182
|
+
return 0
|
|
1183
|
+
fi
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
# 统计 JSON 数组的元素个数。
|
|
1187
|
+
json_index_count() {
|
|
1188
|
+
local file="$1"
|
|
1189
|
+
local i=0
|
|
1190
|
+
while plutil -type "$i" "$file" >/dev/null 2>&1; do
|
|
1191
|
+
i=$((i + 1))
|
|
1192
|
+
done
|
|
1193
|
+
printf '%s\n' "$i"
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
# 从 JSON 中提取原始值。
|
|
1197
|
+
json_raw() {
|
|
1198
|
+
plutil -extract "$2" raw -o - "$1" 2>/dev/null
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
# 按 tunnel 名称查询远端 tunnel 信息并写入缓存变量。
|
|
1202
|
+
remote_lookup_by_name() {
|
|
1203
|
+
local tunnel_name="$1"
|
|
1204
|
+
local tmp_json
|
|
1205
|
+
local count=0
|
|
1206
|
+
local i=0
|
|
1207
|
+
local current_name=""
|
|
1208
|
+
REMOTE_FOUND=0
|
|
1209
|
+
REMOTE_TUNNEL_ID=""
|
|
1210
|
+
REMOTE_TUNNEL_NAME=""
|
|
1211
|
+
REMOTE_TUNNEL_CREATED_AT=""
|
|
1212
|
+
REMOTE_TUNNEL_CONNECTIONS="0"
|
|
1213
|
+
|
|
1214
|
+
tmp_json="$(portable_mktemp "cloudflared-manager-remote")"
|
|
1215
|
+
remote_cmd list --name "$tunnel_name" -o json >"$tmp_json"
|
|
1216
|
+
count="$(json_index_count "$tmp_json")"
|
|
1217
|
+
while [[ "$i" -lt "$count" ]]; do
|
|
1218
|
+
current_name="$(json_raw "$tmp_json" "$i.name" || true)"
|
|
1219
|
+
if [[ "$current_name" == "$tunnel_name" ]]; then
|
|
1220
|
+
REMOTE_FOUND=1
|
|
1221
|
+
REMOTE_TUNNEL_NAME="$current_name"
|
|
1222
|
+
REMOTE_TUNNEL_ID="$(json_raw "$tmp_json" "$i.id" || true)"
|
|
1223
|
+
REMOTE_TUNNEL_CREATED_AT="$(json_raw "$tmp_json" "$i.created_at" || true)"
|
|
1224
|
+
REMOTE_TUNNEL_CONNECTIONS="$(json_raw "$tmp_json" "$i.connections" || printf '0')"
|
|
1225
|
+
rm -f "$tmp_json"
|
|
1226
|
+
return 0
|
|
1227
|
+
fi
|
|
1228
|
+
i=$((i + 1))
|
|
1229
|
+
done
|
|
1230
|
+
rm -f "$tmp_json"
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
# 生成单个应用的一行摘要信息。
|
|
1234
|
+
app_summary() {
|
|
1235
|
+
local pid=""
|
|
1236
|
+
local state="未运行"
|
|
1237
|
+
local connections="0"
|
|
1238
|
+
local count="0"
|
|
1239
|
+
|
|
1240
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
1241
|
+
[[ -n "$pid" ]] && state="运行中"
|
|
1242
|
+
fi
|
|
1243
|
+
|
|
1244
|
+
if [[ -n "$CLOUDFLARED_BIN" && -n "${ORIGIN_CERT:-}" ]] && file_exists "$ORIGIN_CERT"; then
|
|
1245
|
+
remote_lookup_by_name "$META_TUNNEL_NAME"
|
|
1246
|
+
if [[ "$REMOTE_FOUND" == "1" ]]; then
|
|
1247
|
+
connections="$REMOTE_TUNNEL_CONNECTIONS"
|
|
1248
|
+
fi
|
|
1249
|
+
fi
|
|
1250
|
+
|
|
1251
|
+
count="$(ingress_rule_count)"
|
|
1252
|
+
printf '%s: 状态=%s,Tunnel=%s,主入口=%s -> %s,ingress=%s 条,连接数=%s\n' \
|
|
1253
|
+
"$META_NAME" "$state" "$META_TUNNEL_NAME" "$META_HOSTNAME" "$META_SERVICE" "$count" "$connections"
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
# 确保运行所需的凭据文件存在,缺失时自动补发。
|
|
1257
|
+
ensure_credentials_present() {
|
|
1258
|
+
if file_exists "$META_CREDENTIALS_FILE"; then
|
|
1259
|
+
return 0
|
|
1260
|
+
fi
|
|
1261
|
+
if [[ -n "$CLOUDFLARED_BIN" && -n "${ORIGIN_CERT:-}" ]] && file_exists "$ORIGIN_CERT"; then
|
|
1262
|
+
info "凭据文件不存在,尝试自动补发 token: $META_TUNNEL_NAME"
|
|
1263
|
+
issue_credentials "$META_TUNNEL_NAME" "$META_CREDENTIALS_FILE"
|
|
1264
|
+
return 0
|
|
1265
|
+
fi
|
|
1266
|
+
die "找不到凭据文件: $META_CREDENTIALS_FILE"
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
# 启动当前应用对应的 cloudflared 进程,支持前台和后台模式。
|
|
1270
|
+
start_app() {
|
|
1271
|
+
local foreground="${1:-0}"
|
|
1272
|
+
local skip_check="${2:-0}"
|
|
1273
|
+
local pid=""
|
|
1274
|
+
local cmd=()
|
|
1275
|
+
|
|
1276
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
1277
|
+
if [[ -n "$pid" ]]; then
|
|
1278
|
+
die "应用已在运行: $META_NAME"
|
|
1279
|
+
fi
|
|
1280
|
+
fi
|
|
1281
|
+
|
|
1282
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
1283
|
+
check_all_ingress_targets
|
|
1284
|
+
fi
|
|
1285
|
+
|
|
1286
|
+
if ! file_exists "$META_CONFIG_FILE"; then
|
|
1287
|
+
die "找不到配置文件: $META_CONFIG_FILE"
|
|
1288
|
+
fi
|
|
1289
|
+
|
|
1290
|
+
ensure_credentials_present
|
|
1291
|
+
validate_config "$META_CONFIG_FILE"
|
|
1292
|
+
|
|
1293
|
+
cmd=(
|
|
1294
|
+
"$CLOUDFLARED_BIN"
|
|
1295
|
+
tunnel
|
|
1296
|
+
--config "$META_CONFIG_FILE"
|
|
1297
|
+
--no-autoupdate
|
|
1298
|
+
--pidfile "$META_PID_FILE"
|
|
1299
|
+
run "$META_TUNNEL_NAME"
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
if [[ "$foreground" == "1" ]]; then
|
|
1303
|
+
"${cmd[@]}"
|
|
1304
|
+
return 0
|
|
1305
|
+
fi
|
|
1306
|
+
|
|
1307
|
+
ensure_dir "$(dirname "$META_LOG_FILE")"
|
|
1308
|
+
nohup "${cmd[@]}" >>"$META_LOG_FILE" 2>&1 &
|
|
1309
|
+
printf '%s\n' "$!" >"$META_PID_FILE"
|
|
1310
|
+
sleep 2
|
|
1311
|
+
if ! pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
1312
|
+
local tail_output=""
|
|
1313
|
+
tail_output="$(read_log_tail "$META_LOG_FILE" 50 || true)"
|
|
1314
|
+
die "cloudflared 启动后立即退出。最近日志如下:\n${tail_output:-"(暂无日志输出)"}"
|
|
1315
|
+
fi
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
# 停止当前应用对应的 cloudflared 进程,可选强制结束。
|
|
1319
|
+
stop_app() {
|
|
1320
|
+
local force="${1:-0}"
|
|
1321
|
+
local pid=""
|
|
1322
|
+
local deadline=0
|
|
1323
|
+
|
|
1324
|
+
if ! pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
1325
|
+
info "应用未运行: $META_NAME"
|
|
1326
|
+
return 0
|
|
1327
|
+
fi
|
|
1328
|
+
if [[ -z "$pid" ]]; then
|
|
1329
|
+
info "应用未运行: $META_NAME"
|
|
1330
|
+
return 0
|
|
1331
|
+
fi
|
|
1332
|
+
|
|
1333
|
+
kill "$pid" >/dev/null 2>&1 || true
|
|
1334
|
+
deadline=$((SECONDS + 20))
|
|
1335
|
+
while [[ "$SECONDS" -lt "$deadline" ]]; do
|
|
1336
|
+
if ! is_pid_running "$pid" "$META_CONFIG_FILE"; then
|
|
1337
|
+
rm -f "$META_PID_FILE"
|
|
1338
|
+
info "已停止: $META_NAME"
|
|
1339
|
+
return 0
|
|
1340
|
+
fi
|
|
1341
|
+
sleep 1
|
|
1342
|
+
done
|
|
1343
|
+
|
|
1344
|
+
if [[ "$force" == "1" ]]; then
|
|
1345
|
+
kill -9 "$pid" >/dev/null 2>&1 || true
|
|
1346
|
+
rm -f "$META_PID_FILE"
|
|
1347
|
+
info "已强制停止: $META_NAME"
|
|
1348
|
+
return 0
|
|
1349
|
+
fi
|
|
1350
|
+
|
|
1351
|
+
die "停止超时: $META_NAME。请重试 stop --force。"
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
# 打印命令行帮助。
|
|
1355
|
+
print_usage() {
|
|
1356
|
+
cat <<EOF
|
|
1357
|
+
用法:
|
|
1358
|
+
$SCRIPT_NAME [全局选项] <command> [command options]
|
|
1359
|
+
|
|
1360
|
+
全局选项:
|
|
1361
|
+
--profile-dir DIR
|
|
1362
|
+
指定配置档目录。目录名不要求叫 .cloudflared。
|
|
1363
|
+
如果同时不显式传 --config-file 和 --origincert,则默认会推导为:
|
|
1364
|
+
DIR/config.yml 和 DIR/cert.pem
|
|
1365
|
+
|
|
1366
|
+
--cloudflared-dir DIR
|
|
1367
|
+
--profile-dir 的兼容别名
|
|
1368
|
+
|
|
1369
|
+
--config-file FILE
|
|
1370
|
+
指定默认 config.yml 的目标路径
|
|
1371
|
+
|
|
1372
|
+
--origincert FILE
|
|
1373
|
+
指定 Origin 证书路径
|
|
1374
|
+
|
|
1375
|
+
--manager-root DIR
|
|
1376
|
+
指定受管应用元数据目录
|
|
1377
|
+
默认会落在 <profile-dir>/manager 或推导出的锚点目录下
|
|
1378
|
+
|
|
1379
|
+
--save-profile
|
|
1380
|
+
将当前解析出来的路径保存到:
|
|
1381
|
+
$SETTINGS_FILE
|
|
1382
|
+
|
|
1383
|
+
命令:
|
|
1384
|
+
doctor
|
|
1385
|
+
查看当前脚本解析到的配置档目录、默认配置文件、Origin 证书、
|
|
1386
|
+
管理目录、cloudflared 版本,以及远端 Tunnel 数量
|
|
1387
|
+
|
|
1388
|
+
init [--install] [--login]
|
|
1389
|
+
初始化管理目录并保存当前路径设置
|
|
1390
|
+
可选顺带安装 cloudflared、执行 tunnel login
|
|
1391
|
+
|
|
1392
|
+
use
|
|
1393
|
+
仅保存当前路径设置,不做远端操作
|
|
1394
|
+
|
|
1395
|
+
install [--sudo-install]
|
|
1396
|
+
在 macOS 上下载安装 cloudflared
|
|
1397
|
+
不带 --sudo-install 时会直接打开 pkg 安装包
|
|
1398
|
+
|
|
1399
|
+
login
|
|
1400
|
+
执行 cloudflared tunnel login
|
|
1401
|
+
登录后会尝试把默认证书同步到当前 Origin 证书目标路径
|
|
1402
|
+
|
|
1403
|
+
import-cert [FILE]
|
|
1404
|
+
把已有证书导入到当前 Origin 证书路径
|
|
1405
|
+
默认来源为: $DEFAULT_LOGIN_CERT
|
|
1406
|
+
|
|
1407
|
+
tunnels
|
|
1408
|
+
列出当前 Cloudflare 账号下的远端 Tunnel
|
|
1409
|
+
这里只是远端列表,不代表已经被本脚本接管
|
|
1410
|
+
|
|
1411
|
+
list
|
|
1412
|
+
列出已经被本脚本接管的本地受管应用
|
|
1413
|
+
|
|
1414
|
+
show NAME
|
|
1415
|
+
查看某个受管应用的元数据和 config.yml 内容
|
|
1416
|
+
|
|
1417
|
+
add NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [其他参数]
|
|
1418
|
+
创建新的远端 Tunnel,并在本地生成受管目录、配置、凭据和元数据
|
|
1419
|
+
支持“一个 Tunnel + 多条 ingress 规则”
|
|
1420
|
+
适合“远端还没创建”的场景
|
|
1421
|
+
|
|
1422
|
+
adopt NAME --tunnel-name NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [其他参数]
|
|
1423
|
+
接管一个已经存在的远端 Tunnel
|
|
1424
|
+
支持把多个 hostname/service 一次性纳入同一个 Tunnel
|
|
1425
|
+
适合“脚本外已经创建过 Tunnel”的场景
|
|
1426
|
+
|
|
1427
|
+
modify NAME [--index N] [--hostname HOST] [--service URL] [--set N:HOST=URL ...] [其他参数]
|
|
1428
|
+
修改某个受管应用的一条或多条 ingress 规则
|
|
1429
|
+
如果应用当前在运行,默认会自动重启
|
|
1430
|
+
|
|
1431
|
+
ingress-list NAME
|
|
1432
|
+
列出某个受管应用下的全部 ingress 规则
|
|
1433
|
+
|
|
1434
|
+
ingress-add NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [其他参数]
|
|
1435
|
+
为某个 Tunnel 追加一条或多条 ingress 规则,并自动写入 DNS 路由
|
|
1436
|
+
|
|
1437
|
+
ingress-remove NAME [--hostname HOST ...] [--index N ...] [其他参数]
|
|
1438
|
+
从某个 Tunnel 中移除一条或多条 ingress 规则
|
|
1439
|
+
只会改本地配置,不会自动删除 Cloudflare 上旧 DNS 记录
|
|
1440
|
+
|
|
1441
|
+
start NAME
|
|
1442
|
+
启动某个已经接管的应用
|
|
1443
|
+
|
|
1444
|
+
stop NAME
|
|
1445
|
+
停止某个已经接管的应用
|
|
1446
|
+
|
|
1447
|
+
restart NAME
|
|
1448
|
+
重启某个已经接管的应用
|
|
1449
|
+
|
|
1450
|
+
status [NAME]
|
|
1451
|
+
查看一个或全部受管应用的状态、PID、配置文件、日志文件
|
|
1452
|
+
|
|
1453
|
+
logs NAME [-f]
|
|
1454
|
+
查看日志,或持续跟随日志输出
|
|
1455
|
+
|
|
1456
|
+
activate NAME
|
|
1457
|
+
将某个受管应用的 config.yml 复制为默认 config.yml
|
|
1458
|
+
|
|
1459
|
+
delete NAME [--delete-tunnel]
|
|
1460
|
+
归档本地受管目录
|
|
1461
|
+
可选顺带删除远端 Tunnel
|
|
1462
|
+
|
|
1463
|
+
help
|
|
1464
|
+
显示本帮助
|
|
1465
|
+
|
|
1466
|
+
常见场景示例:
|
|
1467
|
+
|
|
1468
|
+
1. 初始化一个新的配置档目录,并完成登录
|
|
1469
|
+
$SCRIPT_NAME --profile-dir /opt/cf-profile init --login
|
|
1470
|
+
|
|
1471
|
+
2. 只检查当前脚本实际使用了哪些路径
|
|
1472
|
+
$SCRIPT_NAME doctor
|
|
1473
|
+
|
|
1474
|
+
3. 使用完全拆开的路径,而不是依赖某个 .cloudflared 目录名
|
|
1475
|
+
$SCRIPT_NAME \\
|
|
1476
|
+
--config-file /srv/cloudflared/config.yml \\
|
|
1477
|
+
--origincert /srv/cloudflared/cert.pem \\
|
|
1478
|
+
--manager-root /srv/cloudflared/manager \\
|
|
1479
|
+
doctor
|
|
1480
|
+
|
|
1481
|
+
4. 创建一个新的远端 Tunnel,并立即后台启动
|
|
1482
|
+
$SCRIPT_NAME add admin-dev \\
|
|
1483
|
+
--hostname admin.example.com \\
|
|
1484
|
+
--service http://localhost:5173 \\
|
|
1485
|
+
--start \\
|
|
1486
|
+
--activate
|
|
1487
|
+
|
|
1488
|
+
5. 创建一个 Tunnel,同时挂多个域名/服务
|
|
1489
|
+
$SCRIPT_NAME add wchros-main \\
|
|
1490
|
+
--ingress admin.wchros.cn=http://localhost:8080 \\
|
|
1491
|
+
--ingress api.wchros.cn=http://localhost:9000 \\
|
|
1492
|
+
--ingress ssh.wchros.cn=ssh://localhost:22 \\
|
|
1493
|
+
--start
|
|
1494
|
+
|
|
1495
|
+
6. 接管一个已经存在的远端 Tunnel
|
|
1496
|
+
$SCRIPT_NAME adopt admin-dev \\
|
|
1497
|
+
--tunnel-name wchros-admin-dev \\
|
|
1498
|
+
--hostname admin.wchros.cn \\
|
|
1499
|
+
--service http://localhost:8080 \\
|
|
1500
|
+
--activate
|
|
1501
|
+
|
|
1502
|
+
7. 给现有 Tunnel 继续追加入口规则
|
|
1503
|
+
$SCRIPT_NAME ingress-add wchros-main \\
|
|
1504
|
+
--ingress static.wchros.cn=http://localhost:7000 \\
|
|
1505
|
+
--ingress blog.wchros.cn=http://localhost:7100
|
|
1506
|
+
|
|
1507
|
+
8. 查看某个 Tunnel 下面有哪些入口规则
|
|
1508
|
+
$SCRIPT_NAME ingress-list wchros-main
|
|
1509
|
+
|
|
1510
|
+
9. 查看远端 Tunnel 列表
|
|
1511
|
+
$SCRIPT_NAME tunnels
|
|
1512
|
+
|
|
1513
|
+
10. 查看已经被脚本接管的本地应用
|
|
1514
|
+
$SCRIPT_NAME list
|
|
1515
|
+
|
|
1516
|
+
11. 启动、查看状态和日志
|
|
1517
|
+
$SCRIPT_NAME start admin-dev
|
|
1518
|
+
$SCRIPT_NAME status admin-dev
|
|
1519
|
+
$SCRIPT_NAME logs admin-dev -f
|
|
1520
|
+
|
|
1521
|
+
12. 一次修改多条 ingress 规则并自动重启
|
|
1522
|
+
$SCRIPT_NAME modify wchros-main \\
|
|
1523
|
+
--set 2:api.wchros.cn=http://localhost:9001 \\
|
|
1524
|
+
--set 3:ssh.wchros.cn=ssh://localhost:2222
|
|
1525
|
+
|
|
1526
|
+
13. 一次删除多条 ingress 规则
|
|
1527
|
+
$SCRIPT_NAME ingress-remove wchros-main --index 3 --hostname blog.wchros.cn
|
|
1528
|
+
|
|
1529
|
+
14. 删除本地接管记录,并顺带删除远端 Tunnel
|
|
1530
|
+
$SCRIPT_NAME delete admin-dev --delete-tunnel
|
|
1531
|
+
|
|
1532
|
+
说明:
|
|
1533
|
+
- tunnels 看到的是“远端存在”
|
|
1534
|
+
- list / start / stop / status 操作的是“本地已接管”
|
|
1535
|
+
- 一个受管应用默认对应一个 Tunnel,但这个 Tunnel 下面可以有多条 ingress
|
|
1536
|
+
- 如果远端 Tunnel 已存在,但本地未接管,请先用 adopt
|
|
1537
|
+
EOF
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
# 输出当前环境、路径解析和远端连接情况。
|
|
1541
|
+
cmd_doctor() {
|
|
1542
|
+
local remote_count="未知"
|
|
1543
|
+
printf '设置文件: %s\n' "$SETTINGS_FILE"
|
|
1544
|
+
printf 'Profile 目录: %s\n' "${PROFILE_DIR:-"(未设置)"}"
|
|
1545
|
+
printf '默认配置文件: %s\n' "${DEFAULT_CONFIG_FILE:-"(未设置)"}"
|
|
1546
|
+
printf 'Origin 证书: %s\n' "${ORIGIN_CERT:-"(未设置)"}"
|
|
1547
|
+
printf '管理根目录: %s\n' "$MANAGER_ROOT"
|
|
1548
|
+
printf '管理根目录存在: %s\n' "$(dir_exists "$MANAGER_ROOT" && printf 是 || printf 否)"
|
|
1549
|
+
printf '应用目录: %s\n' "$APPS_DIR"
|
|
1550
|
+
printf '归档目录: %s\n' "$ARCHIVE_DIR"
|
|
1551
|
+
if [[ -n "$CLOUDFLARED_BIN" ]]; then
|
|
1552
|
+
printf 'cloudflared 路径: %s\n' "$CLOUDFLARED_BIN"
|
|
1553
|
+
printf 'cloudflared 版本: %s\n' "$("$CLOUDFLARED_BIN" --version 2>/dev/null || true)"
|
|
1554
|
+
else
|
|
1555
|
+
printf 'cloudflared: 未安装\n'
|
|
1556
|
+
fi
|
|
1557
|
+
printf 'Origin 证书存在: %s\n' "$(file_exists "${ORIGIN_CERT:-/nonexistent}" && printf 是 || printf 否)"
|
|
1558
|
+
printf '默认登录证书存在: %s\n' "$(file_exists "$DEFAULT_LOGIN_CERT" && printf 是 || printf 否)"
|
|
1559
|
+
printf '受管应用数量: %s\n' "$(managed_apps | wc -l | tr -d '[:space:]')"
|
|
1560
|
+
if [[ -n "$CLOUDFLARED_BIN" && -n "${ORIGIN_CERT:-}" ]] && file_exists "$ORIGIN_CERT"; then
|
|
1561
|
+
local tmp_json
|
|
1562
|
+
tmp_json="$(portable_mktemp "cloudflared-manager-doctor")"
|
|
1563
|
+
if remote_cmd list -o json >"$tmp_json" 2>/dev/null; then
|
|
1564
|
+
remote_count="$(json_index_count "$tmp_json")"
|
|
1565
|
+
fi
|
|
1566
|
+
rm -f "$tmp_json"
|
|
1567
|
+
fi
|
|
1568
|
+
printf '远端 Tunnel 数量: %s\n' "$remote_count"
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
# 保存当前 profile 到设置文件。
|
|
1572
|
+
cmd_use() {
|
|
1573
|
+
ensure_manager_dirs
|
|
1574
|
+
save_settings
|
|
1575
|
+
info "已保存当前配置档。"
|
|
1576
|
+
info "配置档目录: ${PROFILE_DIR:-"(未设置)"}"
|
|
1577
|
+
info "默认配置文件: ${DEFAULT_CONFIG_FILE:-"(未设置)"}"
|
|
1578
|
+
info "Origin 证书: ${ORIGIN_CERT:-"(未设置)"}"
|
|
1579
|
+
info "管理根目录: $MANAGER_ROOT"
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
# 初始化管理目录,可选顺带安装和登录。
|
|
1583
|
+
cmd_init() {
|
|
1584
|
+
local do_install=0
|
|
1585
|
+
local do_login=0
|
|
1586
|
+
while [[ $# -gt 0 ]]; do
|
|
1587
|
+
case "$1" in
|
|
1588
|
+
--install) do_install=1; shift ;;
|
|
1589
|
+
--login) do_login=1; shift ;;
|
|
1590
|
+
-h|--help)
|
|
1591
|
+
cat <<EOF
|
|
1592
|
+
用法:
|
|
1593
|
+
$SCRIPT_NAME [全局选项] init [--install] [--login]
|
|
1594
|
+
EOF
|
|
1595
|
+
return 0
|
|
1596
|
+
;;
|
|
1597
|
+
*)
|
|
1598
|
+
die "init 不支持的参数: $1"
|
|
1599
|
+
;;
|
|
1600
|
+
esac
|
|
1601
|
+
done
|
|
1602
|
+
|
|
1603
|
+
ensure_manager_dirs
|
|
1604
|
+
if [[ -n "${DEFAULT_CONFIG_FILE:-}" ]]; then
|
|
1605
|
+
ensure_dir "$(dirname "$DEFAULT_CONFIG_FILE")"
|
|
1606
|
+
fi
|
|
1607
|
+
if [[ -n "${ORIGIN_CERT:-}" ]]; then
|
|
1608
|
+
ensure_dir "$(dirname "$ORIGIN_CERT")"
|
|
1609
|
+
fi
|
|
1610
|
+
if [[ "$do_install" == "1" ]]; then
|
|
1611
|
+
cmd_install --sudo-install
|
|
1612
|
+
fi
|
|
1613
|
+
save_settings
|
|
1614
|
+
if [[ "$do_login" == "1" ]]; then
|
|
1615
|
+
cmd_login
|
|
1616
|
+
fi
|
|
1617
|
+
info "初始化完成。"
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
# 在 macOS 上下载安装 cloudflared。
|
|
1621
|
+
cmd_install() {
|
|
1622
|
+
local sudo_install=0
|
|
1623
|
+
while [[ $# -gt 0 ]]; do
|
|
1624
|
+
case "$1" in
|
|
1625
|
+
--sudo-install) sudo_install=1; shift ;;
|
|
1626
|
+
-h|--help)
|
|
1627
|
+
cat <<EOF
|
|
1628
|
+
用法:
|
|
1629
|
+
$SCRIPT_NAME install [--sudo-install]
|
|
1630
|
+
EOF
|
|
1631
|
+
return 0
|
|
1632
|
+
;;
|
|
1633
|
+
*)
|
|
1634
|
+
die "install 不支持的参数: $1"
|
|
1635
|
+
;;
|
|
1636
|
+
esac
|
|
1637
|
+
done
|
|
1638
|
+
|
|
1639
|
+
if [[ -n "$CLOUDFLARED_BIN" ]]; then
|
|
1640
|
+
info "cloudflared 已安装:$("$CLOUDFLARED_BIN" --version)"
|
|
1641
|
+
return 0
|
|
1642
|
+
fi
|
|
1643
|
+
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
1644
|
+
die "自动安装目前只支持 macOS。"
|
|
1645
|
+
fi
|
|
1646
|
+
|
|
1647
|
+
local arch pkg_name url pkg_path
|
|
1648
|
+
arch="$(uname -m)"
|
|
1649
|
+
case "$arch" in
|
|
1650
|
+
x86_64) pkg_name="cloudflared-amd64.pkg" ;;
|
|
1651
|
+
arm64) pkg_name="cloudflared-arm64.pkg" ;;
|
|
1652
|
+
*) die "不支持的 macOS 架构: $arch" ;;
|
|
1653
|
+
esac
|
|
1654
|
+
url="https://github.com/cloudflare/cloudflared/releases/latest/download/$pkg_name"
|
|
1655
|
+
pkg_path="/tmp/$pkg_name"
|
|
1656
|
+
curl -L --fail -o "$pkg_path" "$url"
|
|
1657
|
+
if [[ "$sudo_install" == "1" ]]; then
|
|
1658
|
+
sudo installer -pkg "$pkg_path" -target /
|
|
1659
|
+
else
|
|
1660
|
+
open "$pkg_path"
|
|
1661
|
+
info "已打开安装包: $pkg_path"
|
|
1662
|
+
fi
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
# 执行 cloudflared tunnel login,并同步默认证书到目标位置。
|
|
1666
|
+
cmd_login() {
|
|
1667
|
+
ensure_cloudflared
|
|
1668
|
+
"$CLOUDFLARED_BIN" tunnel login
|
|
1669
|
+
maybe_import_default_login_cert
|
|
1670
|
+
if [[ -n "${ORIGIN_CERT:-}" ]] && file_exists "$ORIGIN_CERT"; then
|
|
1671
|
+
info "当前 Origin 证书: $ORIGIN_CERT"
|
|
1672
|
+
else
|
|
1673
|
+
warn "登录完成,但尚未在目标位置发现证书。默认登录证书可能仍在: $DEFAULT_LOGIN_CERT"
|
|
1674
|
+
fi
|
|
1675
|
+
save_settings
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
# 从现有证书文件导入一份 Origin 证书到当前目标位置。
|
|
1679
|
+
cmd_import_cert() {
|
|
1680
|
+
local source="${1:-$DEFAULT_LOGIN_CERT}"
|
|
1681
|
+
source="$(canonical_path "$source")"
|
|
1682
|
+
if [[ -z "${ORIGIN_CERT:-}" ]]; then
|
|
1683
|
+
die "当前没有目标 Origin 证书路径,请先通过 --origincert、--profile-dir 或 init/use 指定。"
|
|
1684
|
+
fi
|
|
1685
|
+
if ! file_exists "$source"; then
|
|
1686
|
+
die "来源证书不存在: $source"
|
|
1687
|
+
fi
|
|
1688
|
+
ensure_dir "$(dirname "$ORIGIN_CERT")"
|
|
1689
|
+
cp "$source" "$ORIGIN_CERT"
|
|
1690
|
+
info "已导入证书: $ORIGIN_CERT"
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
# 列出全部受管应用。
|
|
1694
|
+
cmd_list() {
|
|
1695
|
+
local name=""
|
|
1696
|
+
local any=0
|
|
1697
|
+
while IFS= read -r name; do
|
|
1698
|
+
[[ -z "$name" ]] && continue
|
|
1699
|
+
any=1
|
|
1700
|
+
load_meta "$name"
|
|
1701
|
+
app_summary
|
|
1702
|
+
done < <(managed_apps)
|
|
1703
|
+
if [[ "$any" == "0" ]]; then
|
|
1704
|
+
info "没有受管应用。"
|
|
1705
|
+
fi
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
# 展示单个应用的元数据和配置内容。
|
|
1709
|
+
cmd_show() {
|
|
1710
|
+
local name="${1:-}"
|
|
1711
|
+
local i=0
|
|
1712
|
+
[[ -z "$name" ]] && die "show 需要 NAME"
|
|
1713
|
+
load_meta "$name"
|
|
1714
|
+
cat <<EOF
|
|
1715
|
+
应用名称: $META_NAME
|
|
1716
|
+
Tunnel 名称: $META_TUNNEL_NAME
|
|
1717
|
+
Tunnel ID: $META_TUNNEL_ID
|
|
1718
|
+
主入口主机名: $META_HOSTNAME
|
|
1719
|
+
主入口服务地址: $META_SERVICE
|
|
1720
|
+
ingress 规则数: $(ingress_rule_count)
|
|
1721
|
+
应用目录: $META_APP_DIR
|
|
1722
|
+
配置文件: $META_CONFIG_FILE
|
|
1723
|
+
凭据文件: $META_CREDENTIALS_FILE
|
|
1724
|
+
ingress 文件: $META_INGRESS_FILE
|
|
1725
|
+
PID 文件: $META_PID_FILE
|
|
1726
|
+
日志文件: $META_LOG_FILE
|
|
1727
|
+
元数据文件: $META_META_FILE
|
|
1728
|
+
创建时间: $META_CREATED_AT
|
|
1729
|
+
更新时间: $META_UPDATED_AT
|
|
1730
|
+
|
|
1731
|
+
# ingress 规则
|
|
1732
|
+
EOF
|
|
1733
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
1734
|
+
printf ' %s. %s -> %s\n' "$((i + 1))" "${INGRESS_HOSTS[$i]}" "${INGRESS_SERVICES[$i]}"
|
|
1735
|
+
i=$((i + 1))
|
|
1736
|
+
done
|
|
1737
|
+
cat <<EOF
|
|
1738
|
+
|
|
1739
|
+
# config.yml
|
|
1740
|
+
EOF
|
|
1741
|
+
cat "$META_CONFIG_FILE"
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
# 列出当前账号下的远端 tunnels。
|
|
1745
|
+
cmd_tunnels() {
|
|
1746
|
+
local tmp_json count=0 i=0 name="" id="" created_at="" connections=""
|
|
1747
|
+
tmp_json="$(portable_mktemp "cloudflared-manager-tunnels")"
|
|
1748
|
+
remote_cmd list -o json >"$tmp_json"
|
|
1749
|
+
count="$(json_index_count "$tmp_json")"
|
|
1750
|
+
if [[ "$count" == "0" ]]; then
|
|
1751
|
+
info "没有远端 tunnels。"
|
|
1752
|
+
rm -f "$tmp_json"
|
|
1753
|
+
return 0
|
|
1754
|
+
fi
|
|
1755
|
+
while [[ "$i" -lt "$count" ]]; do
|
|
1756
|
+
name="$(json_raw "$tmp_json" "$i.name" || true)"
|
|
1757
|
+
id="$(json_raw "$tmp_json" "$i.id" || true)"
|
|
1758
|
+
created_at="$(json_raw "$tmp_json" "$i.created_at" || true)"
|
|
1759
|
+
connections="$(json_raw "$tmp_json" "$i.connections" || printf '0')"
|
|
1760
|
+
printf '%s | ID=%s | 连接数=%s | 创建时间=%s\n' "$name" "$id" "$connections" "$created_at"
|
|
1761
|
+
i=$((i + 1))
|
|
1762
|
+
done
|
|
1763
|
+
rm -f "$tmp_json"
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
# 创建新的远端 tunnel,并接管到本地管理目录。
|
|
1767
|
+
cmd_add() {
|
|
1768
|
+
local name="${1:-}"
|
|
1769
|
+
local hostname=""
|
|
1770
|
+
local service=""
|
|
1771
|
+
local tunnel_name=""
|
|
1772
|
+
local ingress_specs=()
|
|
1773
|
+
local use_existing=0
|
|
1774
|
+
local overwrite_dns=0
|
|
1775
|
+
local skip_check=0
|
|
1776
|
+
local do_start=0
|
|
1777
|
+
local foreground=0
|
|
1778
|
+
local activate=0
|
|
1779
|
+
|
|
1780
|
+
[[ -z "$name" ]] && die "add 需要 NAME"
|
|
1781
|
+
shift
|
|
1782
|
+
|
|
1783
|
+
while [[ $# -gt 0 ]]; do
|
|
1784
|
+
case "$1" in
|
|
1785
|
+
--hostname) hostname="${2:-}"; shift 2 ;;
|
|
1786
|
+
--service) service="${2:-}"; shift 2 ;;
|
|
1787
|
+
--ingress) ingress_specs+=("${2:-}"); shift 2 ;;
|
|
1788
|
+
--tunnel-name) tunnel_name="${2:-}"; shift 2 ;;
|
|
1789
|
+
--use-existing) use_existing=1; shift ;;
|
|
1790
|
+
--overwrite-dns) overwrite_dns=1; shift ;;
|
|
1791
|
+
--skip-check) skip_check=1; shift ;;
|
|
1792
|
+
--start) do_start=1; shift ;;
|
|
1793
|
+
--foreground) foreground=1; shift ;;
|
|
1794
|
+
--activate) activate=1; shift ;;
|
|
1795
|
+
-h|--help)
|
|
1796
|
+
cat <<EOF
|
|
1797
|
+
用法:
|
|
1798
|
+
$SCRIPT_NAME add NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [--tunnel-name NAME] [--use-existing] [--overwrite-dns] [--skip-check] [--start] [--foreground] [--activate]
|
|
1799
|
+
|
|
1800
|
+
说明:
|
|
1801
|
+
- 兼容旧写法:使用一组 --hostname 和 --service
|
|
1802
|
+
- 多 ingress 写法:重复传 --ingress hostname=service
|
|
1803
|
+
- 可以混用,首条规则会作为“主入口”展示
|
|
1804
|
+
EOF
|
|
1805
|
+
return 0
|
|
1806
|
+
;;
|
|
1807
|
+
*)
|
|
1808
|
+
die "add 不支持的参数: $1"
|
|
1809
|
+
;;
|
|
1810
|
+
esac
|
|
1811
|
+
done
|
|
1812
|
+
|
|
1813
|
+
validate_app_name "$name"
|
|
1814
|
+
ensure_cloudflared
|
|
1815
|
+
ensure_logged_in
|
|
1816
|
+
|
|
1817
|
+
if app_exists "$name"; then
|
|
1818
|
+
die "受管应用已存在: $name"
|
|
1819
|
+
fi
|
|
1820
|
+
|
|
1821
|
+
build_requested_ingress_rules "$hostname" "$service" "${ingress_specs[@]}"
|
|
1822
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
1823
|
+
check_all_ingress_targets
|
|
1824
|
+
fi
|
|
1825
|
+
[[ -z "$tunnel_name" ]] && tunnel_name="$name"
|
|
1826
|
+
|
|
1827
|
+
build_meta "$name" "$tunnel_name" "" "${INGRESS_HOSTS[0]}" "${INGRESS_SERVICES[0]}"
|
|
1828
|
+
remote_lookup_by_name "$tunnel_name"
|
|
1829
|
+
if [[ "$REMOTE_FOUND" == "1" ]]; then
|
|
1830
|
+
if [[ "$use_existing" != "1" ]]; then
|
|
1831
|
+
die "远端 tunnel 已存在: $tunnel_name。请使用 adopt 或 add --use-existing。"
|
|
1832
|
+
fi
|
|
1833
|
+
META_TUNNEL_ID="$REMOTE_TUNNEL_ID"
|
|
1834
|
+
issue_credentials "$tunnel_name" "$META_CREDENTIALS_FILE"
|
|
1835
|
+
else
|
|
1836
|
+
create_remote_tunnel "$tunnel_name" "$META_CREDENTIALS_FILE"
|
|
1837
|
+
META_TUNNEL_ID="$REMOTE_TUNNEL_ID"
|
|
1838
|
+
fi
|
|
1839
|
+
|
|
1840
|
+
route_all_dns "$tunnel_name" "$overwrite_dns"
|
|
1841
|
+
save_ingress_rules
|
|
1842
|
+
write_config 0
|
|
1843
|
+
save_meta
|
|
1844
|
+
|
|
1845
|
+
if [[ "$activate" == "1" ]]; then
|
|
1846
|
+
activate_default_config
|
|
1847
|
+
fi
|
|
1848
|
+
if [[ "$do_start" == "1" ]]; then
|
|
1849
|
+
start_app "$foreground" "$skip_check"
|
|
1850
|
+
fi
|
|
1851
|
+
info "已添加受管应用: $name"
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
# 接管已有的远端 tunnel。
|
|
1855
|
+
cmd_adopt() {
|
|
1856
|
+
local name="${1:-}"
|
|
1857
|
+
local tunnel_name=""
|
|
1858
|
+
local hostname=""
|
|
1859
|
+
local service=""
|
|
1860
|
+
local ingress_specs=()
|
|
1861
|
+
local ensure_dns=0
|
|
1862
|
+
local overwrite_dns=0
|
|
1863
|
+
local skip_check=0
|
|
1864
|
+
local activate=0
|
|
1865
|
+
|
|
1866
|
+
[[ -z "$name" ]] && die "adopt 需要 NAME"
|
|
1867
|
+
shift
|
|
1868
|
+
|
|
1869
|
+
while [[ $# -gt 0 ]]; do
|
|
1870
|
+
case "$1" in
|
|
1871
|
+
--tunnel-name) tunnel_name="${2:-}"; shift 2 ;;
|
|
1872
|
+
--hostname) hostname="${2:-}"; shift 2 ;;
|
|
1873
|
+
--service) service="${2:-}"; shift 2 ;;
|
|
1874
|
+
--ingress) ingress_specs+=("${2:-}"); shift 2 ;;
|
|
1875
|
+
--ensure-dns) ensure_dns=1; shift ;;
|
|
1876
|
+
--overwrite-dns) overwrite_dns=1; shift ;;
|
|
1877
|
+
--skip-check) skip_check=1; shift ;;
|
|
1878
|
+
--activate) activate=1; shift ;;
|
|
1879
|
+
-h|--help)
|
|
1880
|
+
cat <<EOF
|
|
1881
|
+
用法:
|
|
1882
|
+
$SCRIPT_NAME adopt NAME --tunnel-name NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [--ensure-dns] [--overwrite-dns] [--skip-check] [--activate]
|
|
1883
|
+
|
|
1884
|
+
说明:
|
|
1885
|
+
- 至少要提供一条 ingress 规则
|
|
1886
|
+
- 如果远端 DNS 已经配好,可不传 --ensure-dns
|
|
1887
|
+
EOF
|
|
1888
|
+
return 0
|
|
1889
|
+
;;
|
|
1890
|
+
*)
|
|
1891
|
+
die "adopt 不支持的参数: $1"
|
|
1892
|
+
;;
|
|
1893
|
+
esac
|
|
1894
|
+
done
|
|
1895
|
+
|
|
1896
|
+
[[ -z "$tunnel_name" ]] && die "adopt 缺少 --tunnel-name"
|
|
1897
|
+
|
|
1898
|
+
validate_app_name "$name"
|
|
1899
|
+
ensure_cloudflared
|
|
1900
|
+
ensure_logged_in
|
|
1901
|
+
|
|
1902
|
+
if app_exists "$name"; then
|
|
1903
|
+
die "受管应用已存在: $name"
|
|
1904
|
+
fi
|
|
1905
|
+
|
|
1906
|
+
build_requested_ingress_rules "$hostname" "$service" "${ingress_specs[@]}"
|
|
1907
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
1908
|
+
check_all_ingress_targets
|
|
1909
|
+
fi
|
|
1910
|
+
|
|
1911
|
+
remote_lookup_by_name "$tunnel_name"
|
|
1912
|
+
if [[ "$REMOTE_FOUND" != "1" ]]; then
|
|
1913
|
+
die "远端 tunnel 不存在: $tunnel_name"
|
|
1914
|
+
fi
|
|
1915
|
+
|
|
1916
|
+
build_meta "$name" "$tunnel_name" "$REMOTE_TUNNEL_ID" "${INGRESS_HOSTS[0]}" "${INGRESS_SERVICES[0]}"
|
|
1917
|
+
issue_credentials "$tunnel_name" "$META_CREDENTIALS_FILE"
|
|
1918
|
+
if [[ "$ensure_dns" == "1" ]]; then
|
|
1919
|
+
route_all_dns "$tunnel_name" "$overwrite_dns"
|
|
1920
|
+
fi
|
|
1921
|
+
save_ingress_rules
|
|
1922
|
+
write_config 0
|
|
1923
|
+
save_meta
|
|
1924
|
+
|
|
1925
|
+
if [[ "$activate" == "1" ]]; then
|
|
1926
|
+
activate_default_config
|
|
1927
|
+
fi
|
|
1928
|
+
info "已接管远端 tunnel: $name"
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
# 修改既有应用的主机名或服务地址,并按需自动重启。
|
|
1932
|
+
cmd_modify() {
|
|
1933
|
+
local name="${1:-}"
|
|
1934
|
+
local new_hostname=""
|
|
1935
|
+
local new_service=""
|
|
1936
|
+
local index="1"
|
|
1937
|
+
local modify_specs=()
|
|
1938
|
+
local overwrite_dns=0
|
|
1939
|
+
local skip_check=0
|
|
1940
|
+
local no_restart=0
|
|
1941
|
+
local activate=0
|
|
1942
|
+
local was_running=0
|
|
1943
|
+
local pid=""
|
|
1944
|
+
local current_index=0
|
|
1945
|
+
local current_hostname=""
|
|
1946
|
+
local current_service=""
|
|
1947
|
+
local help_text=""
|
|
1948
|
+
local pending_hosts=()
|
|
1949
|
+
local pending_services=()
|
|
1950
|
+
local changed_indices=()
|
|
1951
|
+
local changed_count=0
|
|
1952
|
+
local spec=""
|
|
1953
|
+
local changed_index=""
|
|
1954
|
+
local candidate_index=0
|
|
1955
|
+
local candidate_hostname=""
|
|
1956
|
+
local candidate_service=""
|
|
1957
|
+
local planned_modify_flags=()
|
|
1958
|
+
local i=0
|
|
1959
|
+
local j=0
|
|
1960
|
+
local has_hostname_change=0
|
|
1961
|
+
|
|
1962
|
+
help_text="$(cat <<EOF
|
|
1963
|
+
用法:
|
|
1964
|
+
$SCRIPT_NAME modify NAME [--index N] [--hostname HOST] [--service URL] [--set N:HOST=URL ...] [--overwrite-dns] [--skip-check] [--no-restart] [--activate]
|
|
1965
|
+
|
|
1966
|
+
说明:
|
|
1967
|
+
- 旧写法仍可用:默认修改第 1 条,或配合 --index 修改单条
|
|
1968
|
+
- 批量写法:重复传 --set N:hostname=service
|
|
1969
|
+
- 使用 --set 时,不要再混用 --index / --hostname / --service
|
|
1970
|
+
EOF
|
|
1971
|
+
)"
|
|
1972
|
+
|
|
1973
|
+
if [[ "$name" == "-h" || "$name" == "--help" ]]; then
|
|
1974
|
+
printf '%s\n' "$help_text"
|
|
1975
|
+
return 0
|
|
1976
|
+
fi
|
|
1977
|
+
[[ -z "$name" ]] && die "modify 需要 NAME"
|
|
1978
|
+
shift
|
|
1979
|
+
|
|
1980
|
+
while [[ $# -gt 0 ]]; do
|
|
1981
|
+
case "$1" in
|
|
1982
|
+
--hostname) new_hostname="${2:-}"; shift 2 ;;
|
|
1983
|
+
--service) new_service="${2:-}"; shift 2 ;;
|
|
1984
|
+
--index) index="${2:-}"; shift 2 ;;
|
|
1985
|
+
--set) modify_specs+=("${2:-}"); shift 2 ;;
|
|
1986
|
+
--overwrite-dns) overwrite_dns=1; shift ;;
|
|
1987
|
+
--skip-check) skip_check=1; shift ;;
|
|
1988
|
+
--no-restart) no_restart=1; shift ;;
|
|
1989
|
+
--activate) activate=1; shift ;;
|
|
1990
|
+
-h|--help)
|
|
1991
|
+
printf '%s\n' "$help_text"
|
|
1992
|
+
return 0
|
|
1993
|
+
;;
|
|
1994
|
+
*)
|
|
1995
|
+
die "modify 不支持的参数: $1"
|
|
1996
|
+
;;
|
|
1997
|
+
esac
|
|
1998
|
+
done
|
|
1999
|
+
|
|
2000
|
+
load_meta "$name"
|
|
2001
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
2002
|
+
[[ -n "$pid" ]] && was_running=1
|
|
2003
|
+
fi
|
|
2004
|
+
|
|
2005
|
+
if [[ "${#modify_specs[@]}" -gt 0 ]]; then
|
|
2006
|
+
if [[ -n "$new_hostname" || -n "$new_service" || "$index" != "1" ]]; then
|
|
2007
|
+
die "使用 --set 批量修改时,不要再混用 --index / --hostname / --service"
|
|
2008
|
+
fi
|
|
2009
|
+
|
|
2010
|
+
pending_hosts=("${INGRESS_HOSTS[@]}")
|
|
2011
|
+
pending_services=("${INGRESS_SERVICES[@]}")
|
|
2012
|
+
i=0
|
|
2013
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
2014
|
+
planned_modify_flags+=(0)
|
|
2015
|
+
i=$((i + 1))
|
|
2016
|
+
done
|
|
2017
|
+
|
|
2018
|
+
for spec in "${modify_specs[@]}"; do
|
|
2019
|
+
parse_modify_spec "$spec"
|
|
2020
|
+
candidate_index=$((PARSED_MODIFY_INDEX - 1))
|
|
2021
|
+
if [[ "$candidate_index" -lt 0 || "$candidate_index" -ge "${#INGRESS_HOSTS[@]}" ]]; then
|
|
2022
|
+
die "批量修改序号超出范围: $PARSED_MODIFY_INDEX。当前共有 $(ingress_rule_count) 条规则。"
|
|
2023
|
+
fi
|
|
2024
|
+
if [[ "${planned_modify_flags[$candidate_index]}" == "1" ]]; then
|
|
2025
|
+
die "同一条 ingress 不能在一次批量修改里重复指定: $PARSED_MODIFY_INDEX"
|
|
2026
|
+
fi
|
|
2027
|
+
planned_modify_flags[$candidate_index]=1
|
|
2028
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
2029
|
+
local_target_reachable "$PARSED_MODIFY_SERVICE"
|
|
2030
|
+
fi
|
|
2031
|
+
pending_hosts[$candidate_index]="$PARSED_MODIFY_HOSTNAME"
|
|
2032
|
+
pending_services[$candidate_index]="$PARSED_MODIFY_SERVICE"
|
|
2033
|
+
done
|
|
2034
|
+
|
|
2035
|
+
i=0
|
|
2036
|
+
while [[ "$i" -lt "${#pending_hosts[@]}" ]]; do
|
|
2037
|
+
j=$((i + 1))
|
|
2038
|
+
while [[ "$j" -lt "${#pending_hosts[@]}" ]]; do
|
|
2039
|
+
if [[ "${pending_hosts[$i]}" == "${pending_hosts[$j]}" ]]; then
|
|
2040
|
+
die "批量修改后出现重复主机名: ${pending_hosts[$i]}"
|
|
2041
|
+
fi
|
|
2042
|
+
j=$((j + 1))
|
|
2043
|
+
done
|
|
2044
|
+
i=$((i + 1))
|
|
2045
|
+
done
|
|
2046
|
+
|
|
2047
|
+
i=0
|
|
2048
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
2049
|
+
if [[ "${INGRESS_HOSTS[$i]}" != "${pending_hosts[$i]}" || "${INGRESS_SERVICES[$i]}" != "${pending_services[$i]}" ]]; then
|
|
2050
|
+
changed_indices+=("$i")
|
|
2051
|
+
fi
|
|
2052
|
+
i=$((i + 1))
|
|
2053
|
+
done
|
|
2054
|
+
|
|
2055
|
+
changed_count="${#changed_indices[@]}"
|
|
2056
|
+
if [[ "$changed_count" == "0" ]]; then
|
|
2057
|
+
info "没有变更。"
|
|
2058
|
+
return 0
|
|
2059
|
+
fi
|
|
2060
|
+
|
|
2061
|
+
for changed_index in "${changed_indices[@]}"; do
|
|
2062
|
+
candidate_hostname="${pending_hosts[$changed_index]}"
|
|
2063
|
+
current_hostname="${INGRESS_HOSTS[$changed_index]}"
|
|
2064
|
+
if [[ "$candidate_hostname" != "$current_hostname" ]]; then
|
|
2065
|
+
route_dns "$META_TUNNEL_NAME" "$candidate_hostname" "$overwrite_dns"
|
|
2066
|
+
has_hostname_change=1
|
|
2067
|
+
fi
|
|
2068
|
+
done
|
|
2069
|
+
|
|
2070
|
+
backup_file "$META_META_FILE" >/dev/null || true
|
|
2071
|
+
backup_file "$META_CONFIG_FILE" >/dev/null || true
|
|
2072
|
+
backup_file "$META_INGRESS_FILE" >/dev/null || true
|
|
2073
|
+
INGRESS_HOSTS=("${pending_hosts[@]}")
|
|
2074
|
+
INGRESS_SERVICES=("${pending_services[@]}")
|
|
2075
|
+
else
|
|
2076
|
+
[[ "$index" =~ ^[1-9][0-9]*$ ]] || die "--index 必须是大于等于 1 的整数"
|
|
2077
|
+
current_index=$((index - 1))
|
|
2078
|
+
if [[ "$current_index" -ge "${#INGRESS_HOSTS[@]}" ]]; then
|
|
2079
|
+
die "ingress 序号超出范围: $index。当前共有 $(ingress_rule_count) 条规则。"
|
|
2080
|
+
fi
|
|
2081
|
+
current_hostname="${INGRESS_HOSTS[$current_index]}"
|
|
2082
|
+
current_service="${INGRESS_SERVICES[$current_index]}"
|
|
2083
|
+
|
|
2084
|
+
if [[ -n "$new_hostname" ]]; then
|
|
2085
|
+
new_hostname="$(normalize_hostname "$new_hostname")"
|
|
2086
|
+
else
|
|
2087
|
+
new_hostname="$current_hostname"
|
|
2088
|
+
fi
|
|
2089
|
+
if [[ -n "$new_service" ]]; then
|
|
2090
|
+
new_service="$(normalize_service "$new_service")"
|
|
2091
|
+
else
|
|
2092
|
+
new_service="$current_service"
|
|
2093
|
+
fi
|
|
2094
|
+
|
|
2095
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
2096
|
+
local_target_reachable "$new_service"
|
|
2097
|
+
fi
|
|
2098
|
+
|
|
2099
|
+
if [[ "$new_hostname" == "$current_hostname" && "$new_service" == "$current_service" ]]; then
|
|
2100
|
+
info "没有变更。"
|
|
2101
|
+
return 0
|
|
2102
|
+
fi
|
|
2103
|
+
|
|
2104
|
+
if [[ "$new_hostname" != "$current_hostname" ]] && find_ingress_rule_index_by_hostname "$new_hostname" "$current_index" >/dev/null 2>&1; then
|
|
2105
|
+
die "同一个 Tunnel 下已存在该主机名: $new_hostname"
|
|
2106
|
+
fi
|
|
2107
|
+
|
|
2108
|
+
if [[ "$new_hostname" != "$current_hostname" ]]; then
|
|
2109
|
+
route_dns "$META_TUNNEL_NAME" "$new_hostname" "$overwrite_dns"
|
|
2110
|
+
has_hostname_change=1
|
|
2111
|
+
fi
|
|
2112
|
+
|
|
2113
|
+
backup_file "$META_META_FILE" >/dev/null || true
|
|
2114
|
+
backup_file "$META_CONFIG_FILE" >/dev/null || true
|
|
2115
|
+
backup_file "$META_INGRESS_FILE" >/dev/null || true
|
|
2116
|
+
INGRESS_HOSTS[$current_index]="$new_hostname"
|
|
2117
|
+
INGRESS_SERVICES[$current_index]="$new_service"
|
|
2118
|
+
changed_count=1
|
|
2119
|
+
fi
|
|
2120
|
+
|
|
2121
|
+
if [[ "$has_hostname_change" == "1" ]]; then
|
|
2122
|
+
warn "cloudflared CLI 不会删除旧 DNS 记录。如果旧 hostname 不再使用,请到 Cloudflare 手工清理。"
|
|
2123
|
+
fi
|
|
2124
|
+
|
|
2125
|
+
save_ingress_rules
|
|
2126
|
+
write_config 0
|
|
2127
|
+
save_meta
|
|
2128
|
+
|
|
2129
|
+
if [[ "$activate" == "1" ]]; then
|
|
2130
|
+
activate_default_config
|
|
2131
|
+
fi
|
|
2132
|
+
if [[ "$was_running" == "1" && "$no_restart" == "0" ]]; then
|
|
2133
|
+
stop_app 0
|
|
2134
|
+
start_app 0 "$skip_check"
|
|
2135
|
+
if [[ "${#modify_specs[@]}" -gt 0 ]]; then
|
|
2136
|
+
info "已批量修改 ${changed_count} 条 ingress 并重启: $META_NAME"
|
|
2137
|
+
else
|
|
2138
|
+
info "已修改并重启: $META_NAME"
|
|
2139
|
+
fi
|
|
2140
|
+
else
|
|
2141
|
+
if [[ "${#modify_specs[@]}" -gt 0 ]]; then
|
|
2142
|
+
info "已批量修改 ingress 规则数量: ${changed_count}"
|
|
2143
|
+
else
|
|
2144
|
+
info "已修改: $META_NAME"
|
|
2145
|
+
fi
|
|
2146
|
+
fi
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
# 列出指定应用的全部 ingress 规则。
|
|
2150
|
+
cmd_ingress_list() {
|
|
2151
|
+
local name="${1:-}"
|
|
2152
|
+
local i=0
|
|
2153
|
+
[[ -z "$name" ]] && die "ingress-list 需要 NAME"
|
|
2154
|
+
|
|
2155
|
+
load_meta "$name"
|
|
2156
|
+
printf '应用名称: %s\n' "$META_NAME"
|
|
2157
|
+
printf 'Tunnel 名称: %s\n' "$META_TUNNEL_NAME"
|
|
2158
|
+
printf 'ingress 规则数: %s\n' "$(ingress_rule_count)"
|
|
2159
|
+
while [[ "$i" -lt "${#INGRESS_HOSTS[@]}" ]]; do
|
|
2160
|
+
printf ' %s. %s -> %s\n' "$((i + 1))" "${INGRESS_HOSTS[$i]}" "${INGRESS_SERVICES[$i]}"
|
|
2161
|
+
i=$((i + 1))
|
|
2162
|
+
done
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
# 为指定应用追加一批新的 ingress 规则。
|
|
2166
|
+
cmd_ingress_add() {
|
|
2167
|
+
local name="${1:-}"
|
|
2168
|
+
local hostname=""
|
|
2169
|
+
local service=""
|
|
2170
|
+
local ingress_specs=()
|
|
2171
|
+
local overwrite_dns=0
|
|
2172
|
+
local skip_check=0
|
|
2173
|
+
local no_restart=0
|
|
2174
|
+
local activate=0
|
|
2175
|
+
local was_running=0
|
|
2176
|
+
local pid=""
|
|
2177
|
+
local requested_hosts=()
|
|
2178
|
+
local requested_services=()
|
|
2179
|
+
local i=0
|
|
2180
|
+
local candidate_hostname=""
|
|
2181
|
+
local candidate_service=""
|
|
2182
|
+
|
|
2183
|
+
if [[ "$name" == "-h" || "$name" == "--help" ]]; then
|
|
2184
|
+
cat <<EOF
|
|
2185
|
+
用法:
|
|
2186
|
+
$SCRIPT_NAME ingress-add NAME [--hostname HOST --service URL] [--ingress HOST=URL ...] [--overwrite-dns] [--skip-check] [--no-restart] [--activate]
|
|
2187
|
+
|
|
2188
|
+
说明:
|
|
2189
|
+
- 兼容旧写法:使用一组 --hostname 和 --service
|
|
2190
|
+
- 批量写法:重复传 --ingress hostname=service
|
|
2191
|
+
- 可以混用,但同一个 hostname 不能重复
|
|
2192
|
+
EOF
|
|
2193
|
+
return 0
|
|
2194
|
+
fi
|
|
2195
|
+
[[ -z "$name" ]] && die "ingress-add 需要 NAME"
|
|
2196
|
+
shift
|
|
2197
|
+
|
|
2198
|
+
while [[ $# -gt 0 ]]; do
|
|
2199
|
+
case "$1" in
|
|
2200
|
+
--hostname) hostname="${2:-}"; shift 2 ;;
|
|
2201
|
+
--service) service="${2:-}"; shift 2 ;;
|
|
2202
|
+
--ingress) ingress_specs+=("${2:-}"); shift 2 ;;
|
|
2203
|
+
--overwrite-dns) overwrite_dns=1; shift ;;
|
|
2204
|
+
--skip-check) skip_check=1; shift ;;
|
|
2205
|
+
--no-restart) no_restart=1; shift ;;
|
|
2206
|
+
--activate) activate=1; shift ;;
|
|
2207
|
+
-h|--help)
|
|
2208
|
+
cmd_ingress_add --help
|
|
2209
|
+
return 0
|
|
2210
|
+
;;
|
|
2211
|
+
*)
|
|
2212
|
+
die "ingress-add 不支持的参数: $1"
|
|
2213
|
+
;;
|
|
2214
|
+
esac
|
|
2215
|
+
done
|
|
2216
|
+
|
|
2217
|
+
load_meta "$name"
|
|
2218
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
2219
|
+
[[ -n "$pid" ]] && was_running=1
|
|
2220
|
+
fi
|
|
2221
|
+
|
|
2222
|
+
if [[ -n "$hostname" || -n "$service" ]]; then
|
|
2223
|
+
[[ -n "$hostname" ]] || die "ingress-add 缺少 --hostname"
|
|
2224
|
+
[[ -n "$service" ]] || die "ingress-add 缺少 --service"
|
|
2225
|
+
requested_hosts+=("$(normalize_hostname "$hostname")")
|
|
2226
|
+
requested_services+=("$(normalize_service "$service")")
|
|
2227
|
+
fi
|
|
2228
|
+
|
|
2229
|
+
for candidate_service in "${ingress_specs[@]}"; do
|
|
2230
|
+
parse_ingress_spec "$candidate_service"
|
|
2231
|
+
requested_hosts+=("$PARSED_INGRESS_HOSTNAME")
|
|
2232
|
+
requested_services+=("$PARSED_INGRESS_SERVICE")
|
|
2233
|
+
done
|
|
2234
|
+
|
|
2235
|
+
if [[ "${#requested_hosts[@]}" == "0" ]]; then
|
|
2236
|
+
die "至少需要提供一条 ingress 规则。可使用 --hostname/--service,或重复传 --ingress hostname=service"
|
|
2237
|
+
fi
|
|
2238
|
+
|
|
2239
|
+
i=0
|
|
2240
|
+
while [[ "$i" -lt "${#requested_hosts[@]}" ]]; do
|
|
2241
|
+
candidate_hostname="${requested_hosts[$i]}"
|
|
2242
|
+
candidate_service="${requested_services[$i]}"
|
|
2243
|
+
|
|
2244
|
+
if find_ingress_rule_index_by_hostname "$candidate_hostname" >/dev/null 2>&1; then
|
|
2245
|
+
die "同一个 Tunnel 下已存在该主机名: $candidate_hostname"
|
|
2246
|
+
fi
|
|
2247
|
+
if [[ "$i" -gt 0 ]]; then
|
|
2248
|
+
local prior=0
|
|
2249
|
+
while [[ "$prior" -lt "$i" ]]; do
|
|
2250
|
+
if [[ "${requested_hosts[$prior]}" == "$candidate_hostname" ]]; then
|
|
2251
|
+
die "本次批量新增中出现重复主机名: $candidate_hostname"
|
|
2252
|
+
fi
|
|
2253
|
+
prior=$((prior + 1))
|
|
2254
|
+
done
|
|
2255
|
+
fi
|
|
2256
|
+
if [[ "$skip_check" == "0" ]]; then
|
|
2257
|
+
local_target_reachable "$candidate_service"
|
|
2258
|
+
fi
|
|
2259
|
+
i=$((i + 1))
|
|
2260
|
+
done
|
|
2261
|
+
|
|
2262
|
+
i=0
|
|
2263
|
+
while [[ "$i" -lt "${#requested_hosts[@]}" ]]; do
|
|
2264
|
+
route_dns "$META_TUNNEL_NAME" "${requested_hosts[$i]}" "$overwrite_dns"
|
|
2265
|
+
i=$((i + 1))
|
|
2266
|
+
done
|
|
2267
|
+
|
|
2268
|
+
backup_file "$META_META_FILE" >/dev/null || true
|
|
2269
|
+
backup_file "$META_CONFIG_FILE" >/dev/null || true
|
|
2270
|
+
backup_file "$META_INGRESS_FILE" >/dev/null || true
|
|
2271
|
+
i=0
|
|
2272
|
+
while [[ "$i" -lt "${#requested_hosts[@]}" ]]; do
|
|
2273
|
+
append_ingress_rule "${requested_hosts[$i]}" "${requested_services[$i]}"
|
|
2274
|
+
i=$((i + 1))
|
|
2275
|
+
done
|
|
2276
|
+
save_ingress_rules
|
|
2277
|
+
write_config 0
|
|
2278
|
+
save_meta
|
|
2279
|
+
|
|
2280
|
+
if [[ "$activate" == "1" ]]; then
|
|
2281
|
+
activate_default_config
|
|
2282
|
+
fi
|
|
2283
|
+
if [[ "$was_running" == "1" && "$no_restart" == "0" ]]; then
|
|
2284
|
+
stop_app 0
|
|
2285
|
+
start_app 0 "$skip_check"
|
|
2286
|
+
info "已新增 ingress 并重启: $META_NAME"
|
|
2287
|
+
else
|
|
2288
|
+
info "已新增 ingress 规则数量: ${#requested_hosts[@]}"
|
|
2289
|
+
fi
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
# 从指定应用批量移除 ingress 规则。
|
|
2293
|
+
cmd_ingress_remove() {
|
|
2294
|
+
local name="${1:-}"
|
|
2295
|
+
local hostnames=()
|
|
2296
|
+
local indices=()
|
|
2297
|
+
local no_restart=0
|
|
2298
|
+
local activate=0
|
|
2299
|
+
local was_running=0
|
|
2300
|
+
local pid=""
|
|
2301
|
+
local remove_flags=()
|
|
2302
|
+
local remaining_hosts=()
|
|
2303
|
+
local remaining_services=()
|
|
2304
|
+
local removed_hosts=()
|
|
2305
|
+
local total_rules=0
|
|
2306
|
+
local remove_count=0
|
|
2307
|
+
local i=0
|
|
2308
|
+
local current_index=0
|
|
2309
|
+
local hostname=""
|
|
2310
|
+
local index=""
|
|
2311
|
+
|
|
2312
|
+
if [[ "$name" == "-h" || "$name" == "--help" ]]; then
|
|
2313
|
+
cat <<EOF
|
|
2314
|
+
用法:
|
|
2315
|
+
$SCRIPT_NAME ingress-remove NAME [--hostname HOST ...] [--index N ...] [--no-restart] [--activate]
|
|
2316
|
+
|
|
2317
|
+
说明:
|
|
2318
|
+
- 可重复传 --hostname
|
|
2319
|
+
- 可重复传 --index
|
|
2320
|
+
- 两种写法可以混用,脚本会自动去重
|
|
2321
|
+
EOF
|
|
2322
|
+
return 0
|
|
2323
|
+
fi
|
|
2324
|
+
[[ -z "$name" ]] && die "ingress-remove 需要 NAME"
|
|
2325
|
+
shift
|
|
2326
|
+
|
|
2327
|
+
while [[ $# -gt 0 ]]; do
|
|
2328
|
+
case "$1" in
|
|
2329
|
+
--hostname) hostnames+=("${2:-}"); shift 2 ;;
|
|
2330
|
+
--index) indices+=("${2:-}"); shift 2 ;;
|
|
2331
|
+
--no-restart) no_restart=1; shift ;;
|
|
2332
|
+
--activate) activate=1; shift ;;
|
|
2333
|
+
-h|--help)
|
|
2334
|
+
cmd_ingress_remove --help
|
|
2335
|
+
return 0
|
|
2336
|
+
;;
|
|
2337
|
+
*)
|
|
2338
|
+
die "ingress-remove 不支持的参数: $1"
|
|
2339
|
+
;;
|
|
2340
|
+
esac
|
|
2341
|
+
done
|
|
2342
|
+
|
|
2343
|
+
if [[ "${#hostnames[@]}" == "0" && "${#indices[@]}" == "0" ]]; then
|
|
2344
|
+
die "ingress-remove 至少需要一个 --hostname 或 --index"
|
|
2345
|
+
fi
|
|
2346
|
+
|
|
2347
|
+
load_meta "$name"
|
|
2348
|
+
if [[ "$(ingress_rule_count)" == "1" ]]; then
|
|
2349
|
+
die "至少需要保留一条 ingress 规则。请直接删除整个应用,或先新增其他规则。"
|
|
2350
|
+
fi
|
|
2351
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
2352
|
+
[[ -n "$pid" ]] && was_running=1
|
|
2353
|
+
fi
|
|
2354
|
+
|
|
2355
|
+
total_rules="${#INGRESS_HOSTS[@]}"
|
|
2356
|
+
i=0
|
|
2357
|
+
while [[ "$i" -lt "$total_rules" ]]; do
|
|
2358
|
+
remove_flags+=(0)
|
|
2359
|
+
i=$((i + 1))
|
|
2360
|
+
done
|
|
2361
|
+
|
|
2362
|
+
for hostname in "${hostnames[@]}"; do
|
|
2363
|
+
hostname="$(normalize_hostname "$hostname")"
|
|
2364
|
+
current_index="$(find_ingress_rule_index_by_hostname "$hostname" 2>/dev/null || true)"
|
|
2365
|
+
[[ -n "$current_index" ]] || die "未找到要删除的 ingress 主机名: $hostname"
|
|
2366
|
+
remove_flags[$current_index]=1
|
|
2367
|
+
done
|
|
2368
|
+
|
|
2369
|
+
for index in "${indices[@]}"; do
|
|
2370
|
+
[[ "$index" =~ ^[1-9][0-9]*$ ]] || die "--index 必须是大于等于 1 的整数"
|
|
2371
|
+
current_index=$((index - 1))
|
|
2372
|
+
if [[ "$current_index" -lt 0 || "$current_index" -ge "$total_rules" ]]; then
|
|
2373
|
+
die "要删除的 ingress 序号超出范围: $index"
|
|
2374
|
+
fi
|
|
2375
|
+
remove_flags[$current_index]=1
|
|
2376
|
+
done
|
|
2377
|
+
|
|
2378
|
+
i=0
|
|
2379
|
+
while [[ "$i" -lt "$total_rules" ]]; do
|
|
2380
|
+
if [[ "${remove_flags[$i]}" == "1" ]]; then
|
|
2381
|
+
removed_hosts+=("${INGRESS_HOSTS[$i]}")
|
|
2382
|
+
remove_count=$((remove_count + 1))
|
|
2383
|
+
else
|
|
2384
|
+
remaining_hosts+=("${INGRESS_HOSTS[$i]}")
|
|
2385
|
+
remaining_services+=("${INGRESS_SERVICES[$i]}")
|
|
2386
|
+
fi
|
|
2387
|
+
i=$((i + 1))
|
|
2388
|
+
done
|
|
2389
|
+
|
|
2390
|
+
if [[ "$remove_count" == "0" ]]; then
|
|
2391
|
+
die "未找到要删除的 ingress 规则。"
|
|
2392
|
+
fi
|
|
2393
|
+
if [[ "${#remaining_hosts[@]}" == "0" ]]; then
|
|
2394
|
+
die "至少需要保留一条 ingress 规则。当前删除范围会把全部规则删空。"
|
|
2395
|
+
fi
|
|
2396
|
+
|
|
2397
|
+
backup_file "$META_META_FILE" >/dev/null || true
|
|
2398
|
+
backup_file "$META_CONFIG_FILE" >/dev/null || true
|
|
2399
|
+
backup_file "$META_INGRESS_FILE" >/dev/null || true
|
|
2400
|
+
INGRESS_HOSTS=("${remaining_hosts[@]}")
|
|
2401
|
+
INGRESS_SERVICES=("${remaining_services[@]}")
|
|
2402
|
+
save_ingress_rules
|
|
2403
|
+
write_config 0
|
|
2404
|
+
save_meta
|
|
2405
|
+
warn "已从配置中移除 ${#removed_hosts[@]} 条 ingress。Cloudflare 上旧 DNS 记录不会自动删除,如已弃用请手工清理。"
|
|
2406
|
+
|
|
2407
|
+
if [[ "$activate" == "1" ]]; then
|
|
2408
|
+
activate_default_config
|
|
2409
|
+
fi
|
|
2410
|
+
if [[ "$was_running" == "1" && "$no_restart" == "0" ]]; then
|
|
2411
|
+
stop_app 0
|
|
2412
|
+
start_app 0 0
|
|
2413
|
+
info "已移除 ingress 并重启: $META_NAME"
|
|
2414
|
+
else
|
|
2415
|
+
info "已移除 ingress 规则数量: ${#removed_hosts[@]}"
|
|
2416
|
+
fi
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
# 删除本地受管应用,可选同步删除远端 tunnel。
|
|
2420
|
+
cmd_delete() {
|
|
2421
|
+
local name="${1:-}"
|
|
2422
|
+
local delete_tunnel=0
|
|
2423
|
+
local force=0
|
|
2424
|
+
local archived=""
|
|
2425
|
+
|
|
2426
|
+
[[ -z "$name" ]] && die "delete 需要 NAME"
|
|
2427
|
+
shift
|
|
2428
|
+
|
|
2429
|
+
while [[ $# -gt 0 ]]; do
|
|
2430
|
+
case "$1" in
|
|
2431
|
+
--delete-tunnel) delete_tunnel=1; shift ;;
|
|
2432
|
+
--force) force=1; shift ;;
|
|
2433
|
+
-h|--help)
|
|
2434
|
+
cat <<EOF
|
|
2435
|
+
用法:
|
|
2436
|
+
$SCRIPT_NAME delete NAME [--delete-tunnel] [--force]
|
|
2437
|
+
EOF
|
|
2438
|
+
return 0
|
|
2439
|
+
;;
|
|
2440
|
+
*)
|
|
2441
|
+
die "delete 不支持的参数: $1"
|
|
2442
|
+
;;
|
|
2443
|
+
esac
|
|
2444
|
+
done
|
|
2445
|
+
|
|
2446
|
+
load_meta "$name"
|
|
2447
|
+
if find_running_pid >/dev/null 2>&1; then
|
|
2448
|
+
stop_app "$force"
|
|
2449
|
+
fi
|
|
2450
|
+
|
|
2451
|
+
if [[ "$delete_tunnel" == "1" ]]; then
|
|
2452
|
+
if [[ "$force" == "1" ]]; then
|
|
2453
|
+
remote_cmd delete --force "$META_TUNNEL_NAME" >/dev/null
|
|
2454
|
+
else
|
|
2455
|
+
remote_cmd delete "$META_TUNNEL_NAME" >/dev/null
|
|
2456
|
+
fi
|
|
2457
|
+
warn "cloudflared tunnel delete 不会清理历史 DNS 记录。如果旧主机名已弃用,请到 Cloudflare 手工删除。"
|
|
2458
|
+
fi
|
|
2459
|
+
|
|
2460
|
+
archived="$(archive_app_dir "$META_APP_DIR")"
|
|
2461
|
+
info "已归档本地应用目录: $archived"
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
# 启动指定应用。
|
|
2465
|
+
cmd_start() {
|
|
2466
|
+
local name="${1:-}"
|
|
2467
|
+
local foreground=0
|
|
2468
|
+
local skip_check=0
|
|
2469
|
+
[[ -z "$name" ]] && die "start 需要 NAME"
|
|
2470
|
+
shift
|
|
2471
|
+
|
|
2472
|
+
while [[ $# -gt 0 ]]; do
|
|
2473
|
+
case "$1" in
|
|
2474
|
+
--foreground) foreground=1; shift ;;
|
|
2475
|
+
--skip-check) skip_check=1; shift ;;
|
|
2476
|
+
-h|--help)
|
|
2477
|
+
cat <<EOF
|
|
2478
|
+
用法:
|
|
2479
|
+
$SCRIPT_NAME start NAME [--foreground] [--skip-check]
|
|
2480
|
+
EOF
|
|
2481
|
+
return 0
|
|
2482
|
+
;;
|
|
2483
|
+
*)
|
|
2484
|
+
die "start 不支持的参数: $1"
|
|
2485
|
+
;;
|
|
2486
|
+
esac
|
|
2487
|
+
done
|
|
2488
|
+
|
|
2489
|
+
load_meta "$name"
|
|
2490
|
+
start_app "$foreground" "$skip_check"
|
|
2491
|
+
if [[ "$foreground" == "0" ]]; then
|
|
2492
|
+
info "已启动: $name"
|
|
2493
|
+
info "日志文件: $META_LOG_FILE"
|
|
2494
|
+
fi
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
# 停止指定应用。
|
|
2498
|
+
cmd_stop() {
|
|
2499
|
+
local name="${1:-}"
|
|
2500
|
+
local force=0
|
|
2501
|
+
[[ -z "$name" ]] && die "stop 需要 NAME"
|
|
2502
|
+
shift
|
|
2503
|
+
|
|
2504
|
+
while [[ $# -gt 0 ]]; do
|
|
2505
|
+
case "$1" in
|
|
2506
|
+
--force) force=1; shift ;;
|
|
2507
|
+
-h|--help)
|
|
2508
|
+
cat <<EOF
|
|
2509
|
+
用法:
|
|
2510
|
+
$SCRIPT_NAME stop NAME [--force]
|
|
2511
|
+
EOF
|
|
2512
|
+
return 0
|
|
2513
|
+
;;
|
|
2514
|
+
*)
|
|
2515
|
+
die "stop 不支持的参数: $1"
|
|
2516
|
+
;;
|
|
2517
|
+
esac
|
|
2518
|
+
done
|
|
2519
|
+
|
|
2520
|
+
load_meta "$name"
|
|
2521
|
+
stop_app "$force"
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
# 重启指定应用。
|
|
2525
|
+
cmd_restart() {
|
|
2526
|
+
local name="${1:-}"
|
|
2527
|
+
local force=0
|
|
2528
|
+
local skip_check=0
|
|
2529
|
+
[[ -z "$name" ]] && die "restart 需要 NAME"
|
|
2530
|
+
shift
|
|
2531
|
+
|
|
2532
|
+
while [[ $# -gt 0 ]]; do
|
|
2533
|
+
case "$1" in
|
|
2534
|
+
--force) force=1; shift ;;
|
|
2535
|
+
--skip-check) skip_check=1; shift ;;
|
|
2536
|
+
-h|--help)
|
|
2537
|
+
cat <<EOF
|
|
2538
|
+
用法:
|
|
2539
|
+
$SCRIPT_NAME restart NAME [--force] [--skip-check]
|
|
2540
|
+
EOF
|
|
2541
|
+
return 0
|
|
2542
|
+
;;
|
|
2543
|
+
*)
|
|
2544
|
+
die "restart 不支持的参数: $1"
|
|
2545
|
+
;;
|
|
2546
|
+
esac
|
|
2547
|
+
done
|
|
2548
|
+
|
|
2549
|
+
load_meta "$name"
|
|
2550
|
+
if find_running_pid >/dev/null 2>&1; then
|
|
2551
|
+
stop_app "$force"
|
|
2552
|
+
fi
|
|
2553
|
+
start_app 0 "$skip_check"
|
|
2554
|
+
info "已重启: $name"
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
# 查看单个或全部应用状态。
|
|
2558
|
+
cmd_status() {
|
|
2559
|
+
local name="${1:-}"
|
|
2560
|
+
local pid=""
|
|
2561
|
+
if [[ -n "$name" ]]; then
|
|
2562
|
+
load_meta "$name"
|
|
2563
|
+
app_summary
|
|
2564
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
2565
|
+
[[ -n "$pid" ]] && printf ' 进程 PID=%s\n' "$pid"
|
|
2566
|
+
fi
|
|
2567
|
+
printf ' 配置文件=%s\n' "$META_CONFIG_FILE"
|
|
2568
|
+
printf ' 日志文件=%s\n' "$META_LOG_FILE"
|
|
2569
|
+
return 0
|
|
2570
|
+
fi
|
|
2571
|
+
|
|
2572
|
+
local any=0
|
|
2573
|
+
while IFS= read -r name; do
|
|
2574
|
+
[[ -z "$name" ]] && continue
|
|
2575
|
+
any=1
|
|
2576
|
+
load_meta "$name"
|
|
2577
|
+
app_summary
|
|
2578
|
+
if pid="$(find_running_pid 2>/dev/null || true)"; then
|
|
2579
|
+
[[ -n "$pid" ]] && printf ' 进程 PID=%s\n' "$pid"
|
|
2580
|
+
fi
|
|
2581
|
+
printf ' 配置文件=%s\n' "$META_CONFIG_FILE"
|
|
2582
|
+
printf ' 日志文件=%s\n' "$META_LOG_FILE"
|
|
2583
|
+
done < <(managed_apps)
|
|
2584
|
+
if [[ "$any" == "0" ]]; then
|
|
2585
|
+
info "没有受管应用。"
|
|
2586
|
+
fi
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
# 查看或持续跟随应用日志。
|
|
2590
|
+
cmd_logs() {
|
|
2591
|
+
local name="${1:-}"
|
|
2592
|
+
local follow=0
|
|
2593
|
+
local lines=100
|
|
2594
|
+
[[ -z "$name" ]] && die "logs 需要 NAME"
|
|
2595
|
+
shift
|
|
2596
|
+
|
|
2597
|
+
while [[ $# -gt 0 ]]; do
|
|
2598
|
+
case "$1" in
|
|
2599
|
+
-f|--follow) follow=1; shift ;;
|
|
2600
|
+
-n|--lines) lines="${2:-}"; shift 2 ;;
|
|
2601
|
+
-h|--help)
|
|
2602
|
+
cat <<EOF
|
|
2603
|
+
用法:
|
|
2604
|
+
$SCRIPT_NAME logs NAME [-f|--follow] [-n|--lines N]
|
|
2605
|
+
EOF
|
|
2606
|
+
return 0
|
|
2607
|
+
;;
|
|
2608
|
+
*)
|
|
2609
|
+
die "logs 不支持的参数: $1"
|
|
2610
|
+
;;
|
|
2611
|
+
esac
|
|
2612
|
+
done
|
|
2613
|
+
|
|
2614
|
+
load_meta "$name"
|
|
2615
|
+
if [[ "$follow" == "1" ]]; then
|
|
2616
|
+
if ! file_exists "$META_LOG_FILE"; then
|
|
2617
|
+
die "日志文件不存在: $META_LOG_FILE"
|
|
2618
|
+
fi
|
|
2619
|
+
tail -f -n "$lines" "$META_LOG_FILE"
|
|
2620
|
+
return 0
|
|
2621
|
+
fi
|
|
2622
|
+
if ! file_exists "$META_LOG_FILE"; then
|
|
2623
|
+
info "(日志为空)"
|
|
2624
|
+
return 0
|
|
2625
|
+
fi
|
|
2626
|
+
tail -n "$lines" "$META_LOG_FILE"
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
# 将某个应用配置复制到默认 config.yml。
|
|
2630
|
+
cmd_activate() {
|
|
2631
|
+
local name="${1:-}"
|
|
2632
|
+
[[ -z "$name" ]] && die "activate 需要 NAME"
|
|
2633
|
+
load_meta "$name"
|
|
2634
|
+
activate_default_config
|
|
2635
|
+
info "已将受管应用配置激活为默认配置: $META_NAME"
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
# 解析全局命令行参数。
|
|
2639
|
+
parse_global_options() {
|
|
2640
|
+
while [[ $# -gt 0 ]]; do
|
|
2641
|
+
case "$1" in
|
|
2642
|
+
--profile-dir|--cloudflared-dir)
|
|
2643
|
+
CLI_PROFILE_DIR="${2:-}"
|
|
2644
|
+
[[ -z "$CLI_PROFILE_DIR" ]] && die "$1 需要参数"
|
|
2645
|
+
shift 2
|
|
2646
|
+
;;
|
|
2647
|
+
--config-file|--default-config)
|
|
2648
|
+
CLI_DEFAULT_CONFIG_FILE="${2:-}"
|
|
2649
|
+
[[ -z "$CLI_DEFAULT_CONFIG_FILE" ]] && die "$1 需要参数"
|
|
2650
|
+
shift 2
|
|
2651
|
+
;;
|
|
2652
|
+
--origincert)
|
|
2653
|
+
CLI_ORIGIN_CERT="${2:-}"
|
|
2654
|
+
[[ -z "$CLI_ORIGIN_CERT" ]] && die "$1 需要参数"
|
|
2655
|
+
shift 2
|
|
2656
|
+
;;
|
|
2657
|
+
--manager-root)
|
|
2658
|
+
CLI_MANAGER_ROOT="${2:-}"
|
|
2659
|
+
[[ -z "$CLI_MANAGER_ROOT" ]] && die "$1 需要参数"
|
|
2660
|
+
shift 2
|
|
2661
|
+
;;
|
|
2662
|
+
--save-profile)
|
|
2663
|
+
SAVE_PROFILE=1
|
|
2664
|
+
shift
|
|
2665
|
+
;;
|
|
2666
|
+
-h|--help)
|
|
2667
|
+
print_usage
|
|
2668
|
+
exit 0
|
|
2669
|
+
;;
|
|
2670
|
+
--)
|
|
2671
|
+
shift
|
|
2672
|
+
break
|
|
2673
|
+
;;
|
|
2674
|
+
-*)
|
|
2675
|
+
die "未知全局参数: $1"
|
|
2676
|
+
;;
|
|
2677
|
+
*)
|
|
2678
|
+
break
|
|
2679
|
+
;;
|
|
2680
|
+
esac
|
|
2681
|
+
done
|
|
2682
|
+
REMAINING_ARGS=("$@")
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
# 主入口:解析全局参数并分发到具体命令。
|
|
2686
|
+
main() {
|
|
2687
|
+
local cmd=""
|
|
2688
|
+
parse_global_options "$@"
|
|
2689
|
+
set -- "${REMAINING_ARGS[@]}"
|
|
2690
|
+
resolve_paths
|
|
2691
|
+
if [[ "$SAVE_PROFILE" == "1" ]]; then
|
|
2692
|
+
save_settings
|
|
2693
|
+
fi
|
|
2694
|
+
|
|
2695
|
+
cmd="${1:-help}"
|
|
2696
|
+
shift || true
|
|
2697
|
+
case "$cmd" in
|
|
2698
|
+
doctor) cmd_doctor "$@" ;;
|
|
2699
|
+
init) cmd_init "$@" ;;
|
|
2700
|
+
use) cmd_use "$@" ;;
|
|
2701
|
+
install) cmd_install "$@" ;;
|
|
2702
|
+
login) cmd_login "$@" ;;
|
|
2703
|
+
import-cert) cmd_import_cert "$@" ;;
|
|
2704
|
+
list) cmd_list "$@" ;;
|
|
2705
|
+
show) cmd_show "$@" ;;
|
|
2706
|
+
tunnels) cmd_tunnels "$@" ;;
|
|
2707
|
+
add) cmd_add "$@" ;;
|
|
2708
|
+
adopt) cmd_adopt "$@" ;;
|
|
2709
|
+
modify) cmd_modify "$@" ;;
|
|
2710
|
+
ingress-list) cmd_ingress_list "$@" ;;
|
|
2711
|
+
ingress-add) cmd_ingress_add "$@" ;;
|
|
2712
|
+
ingress-remove) cmd_ingress_remove "$@" ;;
|
|
2713
|
+
delete) cmd_delete "$@" ;;
|
|
2714
|
+
start) cmd_start "$@" ;;
|
|
2715
|
+
stop) cmd_stop "$@" ;;
|
|
2716
|
+
restart) cmd_restart "$@" ;;
|
|
2717
|
+
status) cmd_status "$@" ;;
|
|
2718
|
+
logs) cmd_logs "$@" ;;
|
|
2719
|
+
activate) cmd_activate "$@" ;;
|
|
2720
|
+
help) print_usage ;;
|
|
2721
|
+
*)
|
|
2722
|
+
die "未知命令: $cmd。可用 help 查看帮助。"
|
|
2723
|
+
;;
|
|
2724
|
+
esac
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
main "$@"
|