summarylogtreecommitdiffstats
path: root/mihomo-webui-setup
blob: e5f4f0ba69ed542c3e0b62eb2ff783c2f85a0387 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#!/usr/bin/env bash

set -Eeuo pipefail

PROGRAM=mihomo-webui-setup
SUBSCRIPTION_COMMAND=/usr/bin/mihomo-subscription
CONTROL_FILE=/etc/mihomo-subscription/subscriptions.yaml
SUBCONVERTER_UNIT=subconverter.service
SUBCONVERTER_DIR=/opt/subconverter

log() {
  printf '[%s] %s\n' "$PROGRAM" "$*" >&2
}

die() {
  log "错误:$*"
  exit 1
}

show_help() {
  cat <<'EOF'
用法:
  mihomo-webui-setup [setup] [mihomo-subscription init 参数]
  mihomo-webui-setup subscription [mihomo-subscription subscription 参数]
  mihomo-webui-setup update [mihomo-subscription update 参数]
  mihomo-webui-setup reset [--yes] [--dry-run]

一次性初始化订阅、准备本地 subconverter、执行首次严格更新,
并启用 mihomo.service 与 mihomo-subscription-update.timer。

推荐不带参数运行,按隐藏式交互提示输入订阅 URL:
  mihomo-webui-setup

也可传递 init 参数,例如:
  mihomo-webui-setup --subscription-url URL --converter direct
  mihomo-webui-setup --controller-listen 192.168.1.10:9090
  mihomo-webui-setup --controller-listen 0.0.0.0:9090 --allow-proxy-lan

日常手工更新:
  mihomo-webui-setup update
  mihomo-webui-setup update --strict

安全修改 main 订阅(隐藏输入 URL,默认保留现有订阅格式):
  mihomo-webui-setup subscription
  mihomo-webui-setup subscription --converter subconverter
  mihomo-webui-setup subscription --dry-run

软件包升级后按最新模板重置主配置:
  mihomo-webui-setup reset            # 显示保留与重置的项目并确认
  mihomo-webui-setup reset --yes      # 跳过确认,用于自动化
  mihomo-webui-setup reset --dry-run  # 只预览和校验,不修改文件

reset 会保留订阅 URL、subscriptions.yaml、API secret、external-controller、
allow-lan 及 bind-address/lan-allowed-ips/lan-disallowed-ips、认证与
skip-auth-prefixes、代理监听端口和 tun 配置;DNS、代理组、规则和 provider
定义按软件包最新模板重建,校验或替换失败时保留原配置。

注意:在命令行传 URL 可能被 shell 历史和进程列表记录。
controller 与 allow-proxy-lan 参数只在新建 config.yaml 时应用。
EOF
}

source_uses_local_subconverter() {
  python - "$CONTROL_FILE" <<'PY'
import ipaddress
import sys
import urllib.parse

import yaml

with open(sys.argv[1], encoding="utf-8") as file:
    control = yaml.safe_load(file)

if not isinstance(control, dict):
    raise SystemExit(1)

for source in control.get("sources", []):
    if not isinstance(source, dict) or not source.get("enabled", False):
        continue
    converter = source.get("converter") or {}
    if converter.get("type", "direct") != "subconverter":
        continue
    endpoint = urllib.parse.urlparse(
        str(converter.get("endpoint", "http://127.0.0.1:25500/sub"))
    )
    try:
        is_loopback = endpoint.hostname == "localhost" or ipaddress.ip_address(
            endpoint.hostname or ""
        ).is_loopback
    except ValueError:
        is_loopback = False
    if is_loopback and (endpoint.port or 80) == 25500:
        raise SystemExit(0)

raise SystemExit(1)
PY
}

has_enabled_source() {
  [[ -f "$CONTROL_FILE" ]] || return 1

  python - "$CONTROL_FILE" <<'PY'
import sys

import yaml

with open(sys.argv[1], encoding="utf-8") as file:
    control = yaml.safe_load(file)

if isinstance(control, dict) and any(
    isinstance(source, dict) and source.get("enabled", False)
    for source in control.get("sources", [])
):
    raise SystemExit(0)

raise SystemExit(1)
PY
}

subconverter_port_open() {
  python - <<'PY'
import socket

try:
    connection = socket.create_connection(("127.0.0.1", 25500), timeout=0.25)
except OSError:
    raise SystemExit(1)
else:
    connection.close()
PY
}

wait_for_subconverter() {
  local attempt=0

  for (( attempt = 0; attempt < 40; attempt++ )); do
    if subconverter_port_open; then
      return 0
    fi
    sleep 0.25
  done

  systemctl status --no-pager --full "$SUBCONVERTER_UNIT" >&2 || true
  die "subconverter 未在 127.0.0.1:25500 就绪"
}

prepare_subconverter() {
  if ! systemctl cat "$SUBCONVERTER_UNIT" >/dev/null 2>&1; then
    die "当前订阅需要本地 subconverter;请先安装可提供 subconverter.service 的 subconverter-bin"
  fi

  if [[ ! -e "$SUBCONVERTER_DIR/pref.ini" ]]; then
    [[ -f "$SUBCONVERTER_DIR/pref.example.ini" ]] || \
      die "缺少 $SUBCONVERTER_DIR/pref.example.ini"
    install -m 0644 \
      "$SUBCONVERTER_DIR/pref.example.ini" \
      "$SUBCONVERTER_DIR/pref.ini"
    sed -i \
      -e 's/^listen=0\.0\.0\.0$/listen=127.0.0.1/' \
      -e 's/^log_level=info$/log_level=warn/' \
      "$SUBCONVERTER_DIR/pref.ini"
    grep -Fqx 'listen=127.0.0.1' "$SUBCONVERTER_DIR/pref.ini" || \
      die "无法将 subconverter 限制到 127.0.0.1"
    log "已从示例创建 subconverter 配置:$SUBCONVERTER_DIR/pref.ini"
  else
    log "保留已有 subconverter 配置:$SUBCONVERTER_DIR/pref.ini"
    if grep -Fqx 'listen=0.0.0.0' "$SUBCONVERTER_DIR/pref.ini"; then
      log "警告:已有 subconverter 配置监听 0.0.0.0,请确认防火墙或改为 127.0.0.1"
    fi
  fi

  systemctl enable "$SUBCONVERTER_UNIT"
  # 即使服务此前已启动,也要重启以确保新创建的 pref.ini 被加载。
  systemctl restart "$SUBCONVERTER_UNIT"
  wait_for_subconverter
  log "本地 subconverter 已就绪"
}

# 只读探活,供 reset --dry-run 与确认前流程使用:不创建配置、不 enable/restart 服务。
check_subconverter_running() {
  if ! systemctl cat "$SUBCONVERTER_UNIT" >/dev/null 2>&1; then
    die "当前订阅需要本地 subconverter;请先安装可提供 subconverter.service 的 subconverter-bin"
  fi

  if systemctl is-active --quiet "$SUBCONVERTER_UNIT" && subconverter_port_open; then
    return 0
  fi

  die "本地 subconverter 未在 127.0.0.1:25500 运行;当前流程不会修改系统,请先运行 mihomo-webui-setup setup 或 systemctl start $SUBCONVERTER_UNIT"
}

main() {
  if [[ ${1-} == --help || ${1-} == -h ]]; then
    show_help
    return 0
  fi

  local command=setup
  if [[ ${1-} == setup || ${1-} == subscription || ${1-} == update || ${1-} == reset ]]; then
    command=$1
    shift
  elif [[ -n ${1-} && ${1-} != -* ]]; then
    die "未知子命令:$1(支持 setup、subscription、update、reset)"
  fi

  if (( EUID != 0 )); then
    exec sudo -- "$0" "$command" "$@"
  fi

  [[ -x "$SUBSCRIPTION_COMMAND" ]] || die "找不到 $SUBSCRIPTION_COMMAND"

  if [[ $command == subscription ]]; then
    log "安全修改 main 订阅"
    "$SUBSCRIPTION_COMMAND" subscription "$@"
    return 0
  fi

  if [[ $command == update ]]; then
    log "更新订阅"
    "$SUBSCRIPTION_COMMAND" update "$@"
    return 0
  fi

  if [[ $command == reset ]]; then
    log "按最新模板重置 Mihomo 主配置"

    local dry_run=false
    local assume_yes=false
    local arg
    for arg in "$@"; do
      case $arg in
        --dry-run) dry_run=true ;;
        --yes) assume_yes=true ;;
      esac
    done

    if ! $dry_run && ! $assume_yes; then
      # 确认必须发生在任何副作用(prepare_subconverter、下载、写文件)之前,
      # 保证取消时系统零改动。
      if [[ ! -t 0 ]]; then
        die "非交互环境必须使用 --yes 确认重置"
      fi
      local reply
      read -r -p "[mihomo-webui-setup] 确认重置 Mihomo 主配置?[y/N] " reply
      case $reply in
        y | Y | yes | YES | Yes) ;;
        *)
          log "已取消,未修改任何文件"
          return 0
          ;;
      esac
      set -- "$@" --yes
    fi

    if has_enabled_source && source_uses_local_subconverter; then
      if $dry_run; then
        check_subconverter_running
      else
        prepare_subconverter
      fi
    fi
    "$SUBSCRIPTION_COMMAND" reset "$@"
    return 0
  fi

  log "初始化 Mihomo 与订阅配置"
  if (( $# == 0 )) && has_enabled_source; then
    "$SUBSCRIPTION_COMMAND" init --non-interactive
    log "检测到已有启用的订阅来源,保留现有来源配置"
  else
    log "首次运行时请按提示配置 main 订阅 URL 与格式"
    "$SUBSCRIPTION_COMMAND" init --require-subscription "$@"
  fi

  if source_uses_local_subconverter; then
    prepare_subconverter
  fi

  log "执行首次严格订阅更新与配置校验"
  "$SUBSCRIPTION_COMMAND" update --strict --no-reload

  systemctl enable --now mihomo.service
  systemctl enable --now mihomo-subscription-update.timer

  log "安装完成"
  python - <<'PY'
from pathlib import Path

import yaml

config = yaml.safe_load(Path("/etc/mihomo/config.yaml").read_text(encoding="utf-8"))
controller = str(config.get("external-controller", "127.0.0.1:9090"))
print(f"[mihomo-webui-setup] WebUI/API 监听:{controller}")
if not controller.startswith(("0.0.0.0:", "[::]:")):
    print(f"[mihomo-webui-setup] WebUI:http://{controller}/ui/")
else:
    print("[mihomo-webui-setup] WebUI:请用服务器实际 IP 与上述端口访问 /ui/")
PY
  log "API secret:/etc/mihomo/config.yaml"
}

main "$@"