mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-20 11:55:32 +00:00
fix: add Mieru subscription URL
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- Added public site manager for port 80 with install, remove, and custom HTML upload.
|
- Added public site manager for port 80 with install, remove, and custom HTML upload.
|
||||||
- Added README docs for custom domain and public site flow.
|
- Added README docs for custom domain and public site flow.
|
||||||
- Added Mieru / mita management in web-admin with install, port validation, client JSON, mihomo YAML, logs, services, and backup support.
|
- Added Mieru / mita management in web-admin with install, port validation, client JSON, mihomo YAML, logs, services, and backup support.
|
||||||
|
- Added tokenized Mieru mihomo subscription URL and Clash import URL for iOS clients.
|
||||||
|
|
||||||
## 2.5.0
|
## 2.5.0
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ Per-client runtime routing в текущем telemt не включается а
|
|||||||
- `Install / save Mieru` — если `mita` ещё нет, web-admin скачивает свежий пакет `mita` из GitHub release `enfein/mieru`, ставит его, применяет JSON через `mita apply config`, затем запускает `mita start`.
|
- `Install / save Mieru` — если `mita` ещё нет, web-admin скачивает свежий пакет `mita` из GitHub release `enfein/mieru`, ставит его, применяет JSON через `mita apply config`, затем запускает `mita start`.
|
||||||
- `Client JSON` — готовый конфиг для официального `mieru` client.
|
- `Client JSON` — готовый конфиг для официального `mieru` client.
|
||||||
- `mihomo YAML` — proxy block для клиентов с поддержкой `type: mieru`.
|
- `mihomo YAML` — proxy block для клиентов с поддержкой `type: mieru`.
|
||||||
|
- `mihomo subscription URL` — ссылка вида `http://SERVER:1984/sub/mieru/<token>.yaml`. Её нужно вставлять в iOS Clash/mihomo apps как subscription/profile URL.
|
||||||
|
- `Clash import URL` — deep link `clash://install-config?...` для клиентов, которые умеют открывать Clash import links.
|
||||||
|
|
||||||
|
Не вставляйте сырой `mihomo YAML` в поле URL: iOS clients ожидают ссылку, иначе будет ошибка `No host specified in URI proxies:`. YAML нужен только для ручной вставки в config file.
|
||||||
|
|
||||||
Mieru не меняет `telemt`, nginx, WARP и сайт на 80. Перед сохранением web-admin проверяет выбранный порт через `ss`; чужой listener блокирует сохранение. Файлы `mieru.json` и `mieru_server_config.json` хранятся с правами `0600` и входят в backup.
|
Mieru не меняет `telemt`, nginx, WARP и сайт на 80. Перед сохранением web-admin проверяет выбранный порт через `ss`; чужой listener блокирует сохранение. Файлы `mieru.json` и `mieru_server_config.json` хранятся с правами `0600` и входят в backup.
|
||||||
|
|
||||||
|
|||||||
@@ -789,6 +789,7 @@ def read_mieru_config() -> dict[str, Any]:
|
|||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
"user": user,
|
"user": user,
|
||||||
"password": str(raw.get("password") or "").strip(),
|
"password": str(raw.get("password") or "").strip(),
|
||||||
|
"subscription_token": str(raw.get("subscription_token") or "").strip(),
|
||||||
"updated_at": str(raw.get("updated_at") or ""),
|
"updated_at": str(raw.get("updated_at") or ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,8 +897,41 @@ def mieru_mihomo_proxy(cfg: dict[str, Any]) -> str:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_mieru_subscription_token(cfg: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
||||||
|
token = str(cfg.get("subscription_token") or "").strip()
|
||||||
|
if len(token) >= 24 and re.match(r"^[A-Za-z0-9_-]+$", token):
|
||||||
|
return cfg, False
|
||||||
|
next_cfg = dict(cfg)
|
||||||
|
next_cfg["subscription_token"] = secrets.token_urlsafe(24)
|
||||||
|
write_mieru_config(next_cfg)
|
||||||
|
return read_mieru_config(), True
|
||||||
|
|
||||||
|
|
||||||
|
def admin_public_base_url() -> str:
|
||||||
|
host = public_host_for_notes()
|
||||||
|
scheme = "http"
|
||||||
|
return f"{scheme}://{host}:{PORT}"
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_subscription_path(cfg: dict[str, Any]) -> str:
|
||||||
|
token = str(cfg.get("subscription_token") or "").strip()
|
||||||
|
return f"/sub/mieru/{urllib.parse.quote(token, safe='')}.yaml" if token else ""
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_subscription_url(cfg: dict[str, Any]) -> str:
|
||||||
|
path = mieru_subscription_path(cfg)
|
||||||
|
return f"{admin_public_base_url()}{path}" if path else ""
|
||||||
|
|
||||||
|
|
||||||
|
def mieru_clash_import_url(cfg: dict[str, Any]) -> str:
|
||||||
|
url = mieru_subscription_url(cfg)
|
||||||
|
return f"clash://install-config?url={urllib.parse.quote(url, safe='')}" if url else ""
|
||||||
|
|
||||||
|
|
||||||
def public_mieru_config() -> dict[str, Any]:
|
def public_mieru_config() -> dict[str, Any]:
|
||||||
cfg = read_mieru_config()
|
cfg = read_mieru_config()
|
||||||
|
if cfg["password"]:
|
||||||
|
cfg, _ = ensure_mieru_subscription_token(cfg)
|
||||||
listeners, errors = collect_port_listeners(cfg["port"])
|
listeners, errors = collect_port_listeners(cfg["port"])
|
||||||
conflicts = mieru_port_conflicts(cfg["port"], cfg["protocol"])
|
conflicts = mieru_port_conflicts(cfg["port"], cfg["protocol"])
|
||||||
status_text = mieru_status_text()
|
status_text = mieru_status_text()
|
||||||
@@ -925,6 +959,8 @@ def public_mieru_config() -> dict[str, Any]:
|
|||||||
"error": "; ".join(errors[:2]),
|
"error": "; ".join(errors[:2]),
|
||||||
"client_config": mieru_client_config(cfg) if cfg["password"] else {},
|
"client_config": mieru_client_config(cfg) if cfg["password"] else {},
|
||||||
"mihomo_yaml": mieru_mihomo_proxy(cfg) if cfg["password"] else "",
|
"mihomo_yaml": mieru_mihomo_proxy(cfg) if cfg["password"] else "",
|
||||||
|
"subscription_url": mieru_subscription_url(cfg) if cfg["password"] else "",
|
||||||
|
"clash_import_url": mieru_clash_import_url(cfg) if cfg["password"] else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1037,6 +1073,7 @@ def save_mieru_settings(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
"user": user,
|
"user": user,
|
||||||
"password": password,
|
"password": password,
|
||||||
|
"subscription_token": current.get("subscription_token") or secrets.token_urlsafe(24),
|
||||||
}
|
}
|
||||||
apply_result = apply_mieru_config(cfg)
|
apply_result = apply_mieru_config(cfg)
|
||||||
write_mieru_config(cfg)
|
write_mieru_config(cfg)
|
||||||
@@ -2619,6 +2656,26 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def send_mieru_subscription(self, parsed: urllib.parse.ParseResult) -> None:
|
||||||
|
match = re.fullmatch(r"/sub/mieru/([A-Za-z0-9_-]+)\.yaml", parsed.path)
|
||||||
|
if not match:
|
||||||
|
self.send_error(404)
|
||||||
|
return
|
||||||
|
cfg = read_mieru_config()
|
||||||
|
token = str(cfg.get("subscription_token") or "")
|
||||||
|
if not token or not hmac.compare_digest(match.group(1), token) or not cfg.get("password"):
|
||||||
|
self.send_error(404)
|
||||||
|
return
|
||||||
|
body = mieru_mihomo_proxy(cfg).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_security_headers()
|
||||||
|
self.send_header("Content-Type", "text/yaml; charset=utf-8")
|
||||||
|
self.send_header("Cache-Control", "no-store")
|
||||||
|
self.send_header("Content-Disposition", 'inline; filename="pcatelegram_web_mieru.yaml"')
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
def send_error_json(self, status: int, message: str) -> None:
|
def send_error_json(self, status: int, message: str) -> None:
|
||||||
self.send_json({"ok": False, "error": message}, status)
|
self.send_json({"ok": False, "error": message}, status)
|
||||||
|
|
||||||
@@ -3105,6 +3162,9 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
if parsed.path.startswith("/sub/mieru/"):
|
||||||
|
self.send_mieru_subscription(parsed)
|
||||||
|
return
|
||||||
if not self.is_authorized():
|
if not self.is_authorized():
|
||||||
if parsed.path.startswith("/api/"):
|
if parsed.path.startswith("/api/"):
|
||||||
self.send_error_json(401, "unauthorized")
|
self.send_error_json(401, "unauthorized")
|
||||||
|
|||||||
@@ -148,7 +148,11 @@ const i18n = {
|
|||||||
mieruSaved: "Mieru settings saved",
|
mieruSaved: "Mieru settings saved",
|
||||||
mieruClientConfig: "Client JSON",
|
mieruClientConfig: "Client JSON",
|
||||||
mieruMihomoYaml: "mihomo YAML",
|
mieruMihomoYaml: "mihomo YAML",
|
||||||
mieruNote: "Mieru server uses mita. Port must be free and in range 1025-65535. Client JSON is for mieru app; YAML is for clients with mieru support.",
|
mieruSubscriptionUrl: "mihomo subscription URL",
|
||||||
|
mieruClashImportUrl: "Clash import URL",
|
||||||
|
copySubscriptionUrl: "Copy subscription URL",
|
||||||
|
copyYaml: "Copy YAML",
|
||||||
|
mieruNote: "Mieru server uses mita. Paste subscription URL into iOS Clash/mihomo apps, not raw YAML. Client JSON is for mieru app.",
|
||||||
copyConfig: "Copy config",
|
copyConfig: "Copy config",
|
||||||
savedKey: "saved key",
|
savedKey: "saved key",
|
||||||
siteMaskEyebrow: "Public site",
|
siteMaskEyebrow: "Public site",
|
||||||
@@ -437,7 +441,11 @@ const i18n = {
|
|||||||
mieruSaved: "Настройки Mieru сохранены",
|
mieruSaved: "Настройки Mieru сохранены",
|
||||||
mieruClientConfig: "Client JSON",
|
mieruClientConfig: "Client JSON",
|
||||||
mieruMihomoYaml: "mihomo YAML",
|
mieruMihomoYaml: "mihomo YAML",
|
||||||
mieruNote: "Mieru server работает через mita. Порт должен быть свободен и в диапазоне 1025-65535. Client JSON для mieru app; YAML для клиентов с поддержкой mieru.",
|
mieruSubscriptionUrl: "URL подписки mihomo",
|
||||||
|
mieruClashImportUrl: "URL импорта Clash",
|
||||||
|
copySubscriptionUrl: "Копировать URL подписки",
|
||||||
|
copyYaml: "Копировать YAML",
|
||||||
|
mieruNote: "Mieru server работает через mita. В iOS Clash/mihomo вставляйте URL подписки, не сырой YAML. Client JSON для mieru app.",
|
||||||
copyConfig: "Копировать конфиг",
|
copyConfig: "Копировать конфиг",
|
||||||
savedKey: "ключ сохранён",
|
savedKey: "ключ сохранён",
|
||||||
siteMaskEyebrow: "Публичный сайт",
|
siteMaskEyebrow: "Публичный сайт",
|
||||||
@@ -1485,6 +1493,8 @@ function renderMieruSettings(payload = null) {
|
|||||||
const noteEl = $("#mieruRuntimeNote");
|
const noteEl = $("#mieruRuntimeNote");
|
||||||
const clientEl = $("#mieruClientConfig");
|
const clientEl = $("#mieruClientConfig");
|
||||||
const yamlEl = $("#mieruMihomoYaml");
|
const yamlEl = $("#mieruMihomoYaml");
|
||||||
|
const subUrlEl = $("#mieruSubscriptionUrl");
|
||||||
|
const clashUrlEl = $("#mieruClashImportUrl");
|
||||||
if (!portEl || !protocolEl || !userEl || !passwordEl || !statusEl || !noteEl) return;
|
if (!portEl || !protocolEl || !userEl || !passwordEl || !statusEl || !noteEl) return;
|
||||||
if (document.activeElement !== portEl) portEl.value = cfg.port || 2999;
|
if (document.activeElement !== portEl) portEl.value = cfg.port || 2999;
|
||||||
if (document.activeElement !== protocolEl) protocolEl.value = cfg.protocol || "TCP";
|
if (document.activeElement !== protocolEl) protocolEl.value = cfg.protocol || "TCP";
|
||||||
@@ -1506,6 +1516,12 @@ function renderMieruSettings(payload = null) {
|
|||||||
if (yamlEl && document.activeElement !== yamlEl) {
|
if (yamlEl && document.activeElement !== yamlEl) {
|
||||||
yamlEl.value = cfg.mihomo_yaml || "";
|
yamlEl.value = cfg.mihomo_yaml || "";
|
||||||
}
|
}
|
||||||
|
if (subUrlEl && document.activeElement !== subUrlEl) {
|
||||||
|
subUrlEl.value = cfg.subscription_url || "";
|
||||||
|
}
|
||||||
|
if (clashUrlEl && document.activeElement !== clashUrlEl) {
|
||||||
|
clashUrlEl.value = cfg.clash_import_url || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function siteMaskSummary(payload = {}) {
|
function siteMaskSummary(payload = {}) {
|
||||||
@@ -2353,7 +2369,8 @@ $("#mieruSettingsForm").addEventListener("submit", saveMieruSettings);
|
|||||||
$("#mieruStartBtn").addEventListener("click", () => controlMieru("start"));
|
$("#mieruStartBtn").addEventListener("click", () => controlMieru("start"));
|
||||||
$("#mieruRestartBtn").addEventListener("click", () => controlMieru("restart"));
|
$("#mieruRestartBtn").addEventListener("click", () => controlMieru("restart"));
|
||||||
$("#mieruStopBtn").addEventListener("click", () => controlMieru("stop"));
|
$("#mieruStopBtn").addEventListener("click", () => controlMieru("stop"));
|
||||||
$("#mieruCopyBtn").addEventListener("click", () => copyText($("#mieruClientConfig").value || $("#mieruMihomoYaml").value || ""));
|
$("#mieruCopyUrlBtn").addEventListener("click", () => copyText($("#mieruSubscriptionUrl").value || ""));
|
||||||
|
$("#mieruCopyYamlBtn").addEventListener("click", () => copyText($("#mieruMihomoYaml").value || ""));
|
||||||
$("#siteMaskForm").addEventListener("submit", saveSiteMask);
|
$("#siteMaskForm").addEventListener("submit", saveSiteMask);
|
||||||
$("#siteMaskRemoveBtn").addEventListener("click", removeSiteMask);
|
$("#siteMaskRemoveBtn").addEventListener("click", removeSiteMask);
|
||||||
$("#siteMaskUploadBtn").addEventListener("click", uploadSiteMaskHtml);
|
$("#siteMaskUploadBtn").addEventListener("click", uploadSiteMaskHtml);
|
||||||
|
|||||||
@@ -501,6 +501,18 @@
|
|||||||
<button id="mieruRestartBtn" class="soft" type="button" data-i18n="restart">Restart</button>
|
<button id="mieruRestartBtn" class="soft" type="button" data-i18n="restart">Restart</button>
|
||||||
<button id="mieruStopBtn" class="danger" type="button" data-i18n="mieruStop">Stop</button>
|
<button id="mieruStopBtn" class="danger" type="button" data-i18n="mieruStop">Stop</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruSubscriptionUrl">mihomo subscription URL</span>
|
||||||
|
<input id="mieruSubscriptionUrl" readonly spellcheck="false">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="mieruClashImportUrl">Clash import URL</span>
|
||||||
|
<input id="mieruClashImportUrl" readonly spellcheck="false">
|
||||||
|
</label>
|
||||||
|
<div class="inline-form">
|
||||||
|
<button id="mieruCopyUrlBtn" class="soft" type="button" data-i18n="copySubscriptionUrl">Copy subscription URL</button>
|
||||||
|
<button id="mieruCopyYamlBtn" class="soft" type="button" data-i18n="copyYaml">Copy YAML</button>
|
||||||
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n="mieruClientConfig">Client JSON</span>
|
<span data-i18n="mieruClientConfig">Client JSON</span>
|
||||||
<textarea id="mieruClientConfig" rows="8" readonly spellcheck="false"></textarea>
|
<textarea id="mieruClientConfig" rows="8" readonly spellcheck="false"></textarea>
|
||||||
@@ -509,7 +521,6 @@
|
|||||||
<span data-i18n="mieruMihomoYaml">mihomo YAML</span>
|
<span data-i18n="mieruMihomoYaml">mihomo YAML</span>
|
||||||
<textarea id="mieruMihomoYaml" rows="8" readonly spellcheck="false"></textarea>
|
<textarea id="mieruMihomoYaml" rows="8" readonly spellcheck="false"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<button id="mieruCopyBtn" class="soft" type="button" data-i18n="copyConfig">Copy config</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user