mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-20 11:55:32 +00:00
feat: add routing settings
This commit is contained in:
11
README.md
11
README.md
@@ -103,6 +103,17 @@ WARP+ key не отдается в API целиком: web показывает
|
|||||||
|
|
||||||
Per-client runtime routing в текущем telemt не включается автоматически: публичные параметры telemt дают users/limits/quotas/ad tags, но не документируют привязку upstream к конкретному user. Для настоящего WARP только одному клиенту нужен отдельный telemt route/service или upstream-схема.
|
Per-client runtime routing в текущем telemt не включается автоматически: публичные параметры telemt дают users/limits/quotas/ad tags, но не документируют привязку upstream к конкретному user. Для настоящего WARP только одному клиенту нужен отдельный telemt route/service или upstream-схема.
|
||||||
|
|
||||||
|
## Порт и маскировка
|
||||||
|
|
||||||
|
В web-admin Settings есть блок `Port and mask site`:
|
||||||
|
|
||||||
|
- `Public port` — реальный порт `telemt` (`[server] port`) и порт в tg-ссылках (`[general.links] public_port`).
|
||||||
|
- `Mask site` — сайт маскировки FakeTLS (`[censorship] tls_domain`), который вшивается в `ee` secret.
|
||||||
|
- Перед сохранением web-admin проверяет порт через `ss`. Если порт занят не `telemt` (например, Xray / 3x-ui на 443), сохранение блокируется.
|
||||||
|
- После сохранения обновляются `/etc/telemt/config.toml`, `/opt/pcatelegram_web/config.json`, ссылки клиентов и перезапускается `telemt`.
|
||||||
|
|
||||||
|
Один `telemt`-инстанс имеет один публичный порт и один сайт маскировки для всех клиентов. Разные порты или разные сайты маскировки на разных клиентов требуют отдельные `telemt` services/configs.
|
||||||
|
|
||||||
## Проверки
|
## Проверки
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ SESSIONS: dict[str, float] = {}
|
|||||||
VERSION = "2.5.0"
|
VERSION = "2.5.0"
|
||||||
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
||||||
LANG_RE = re.compile(r"^(en|ru)$")
|
LANG_RE = re.compile(r"^(en|ru)$")
|
||||||
|
HOST_RE = re.compile(r"^(?=.{1,253}$)([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$")
|
||||||
SENSITIVE_CONFIG_KEYS = {"secret"}
|
SENSITIVE_CONFIG_KEYS = {"secret"}
|
||||||
BACKUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+\.tar\.gz(\.enc)?$")
|
BACKUP_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+\.tar\.gz(\.enc)?$")
|
||||||
MAX_UNIQUE_IP_LIMIT = 1000000
|
MAX_UNIQUE_IP_LIMIT = 1000000
|
||||||
@@ -1029,6 +1030,118 @@ def read_telemt_edge_settings() -> dict[str, Any]:
|
|||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_port(value: Any) -> int:
|
||||||
|
try:
|
||||||
|
port = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("port must be a number") from None
|
||||||
|
if port < 1 or port > 65535:
|
||||||
|
raise ValueError("port must be between 1 and 65535")
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_mask_host(value: Any) -> str:
|
||||||
|
host = str(value or "").strip().lower().rstrip(".")
|
||||||
|
if not HOST_RE.match(host):
|
||||||
|
raise ValueError("mask site must be a valid domain")
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
|
def update_toml_scalar(section: str, key: str, literal: str) -> None:
|
||||||
|
TELEMT_CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
lines = TELEMT_CONFIG.read_text(encoding="utf-8", errors="ignore").splitlines() if TELEMT_CONFIG.exists() else []
|
||||||
|
header = f"[{section}]"
|
||||||
|
out: list[str] = []
|
||||||
|
current = ""
|
||||||
|
found_section = False
|
||||||
|
wrote_key = False
|
||||||
|
|
||||||
|
for raw in lines:
|
||||||
|
stripped = raw.strip()
|
||||||
|
if stripped.startswith("[") and stripped.endswith("]"):
|
||||||
|
if current == section and not wrote_key:
|
||||||
|
out.append(f"{key} = {literal}")
|
||||||
|
wrote_key = True
|
||||||
|
current = stripped.strip("[]")
|
||||||
|
if stripped == header:
|
||||||
|
found_section = True
|
||||||
|
out.append(raw)
|
||||||
|
continue
|
||||||
|
if current == section and stripped.startswith(key) and "=" in stripped:
|
||||||
|
out.append(f"{key} = {literal}")
|
||||||
|
wrote_key = True
|
||||||
|
continue
|
||||||
|
out.append(raw)
|
||||||
|
|
||||||
|
if not found_section:
|
||||||
|
if out and out[-1].strip():
|
||||||
|
out.append("")
|
||||||
|
out.append(header)
|
||||||
|
out.append(f"{key} = {literal}")
|
||||||
|
elif current == section and not wrote_key:
|
||||||
|
out.append(f"{key} = {literal}")
|
||||||
|
|
||||||
|
tmp = TELEMT_CONFIG.with_name(TELEMT_CONFIG.name + ".tmp")
|
||||||
|
tmp.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
tmp.replace(TELEMT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def routing_payload(port: int | None = None) -> dict[str, Any]:
|
||||||
|
config = load_json(PCATELEGRAM_WEB_CONFIG, {}) or {}
|
||||||
|
settings = read_telemt_edge_settings()
|
||||||
|
current_port = int(port or config.get("port") or read_telemt_port() or 443)
|
||||||
|
mask_host = str(config.get("mask_host") or settings.get("tls_domain") or "google.com")
|
||||||
|
listeners, errors = collect_port_listeners(current_port)
|
||||||
|
conflicts = [
|
||||||
|
item for item in listeners
|
||||||
|
if item.get("role") != "mtproxy" and "telemt" not in str(item.get("process", "")).lower()
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"port": current_port,
|
||||||
|
"mask_host": mask_host,
|
||||||
|
"mask_port": int(settings.get("mask_port") or 443),
|
||||||
|
"mode": str(config.get("mode") or "lite"),
|
||||||
|
"domain": str(config.get("domain") or ""),
|
||||||
|
"listeners": listeners,
|
||||||
|
"conflicts": conflicts,
|
||||||
|
"ok": not errors,
|
||||||
|
"error": "; ".join(errors[:2]),
|
||||||
|
"per_user_ports_supported": False,
|
||||||
|
"note": "telemt has one server.port per instance; real per-client ports need multiple telemt services.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_routing_settings(port: int, mask_host: str) -> dict[str, Any]:
|
||||||
|
listeners, _ = collect_port_listeners(port)
|
||||||
|
conflicts = [
|
||||||
|
item for item in listeners
|
||||||
|
if item.get("role") != "mtproxy" and "telemt" not in str(item.get("process", "")).lower()
|
||||||
|
]
|
||||||
|
if conflicts:
|
||||||
|
names = ", ".join(f"{item.get('process')} {item.get('address')}" for item in conflicts[:3])
|
||||||
|
raise RuntimeError(f"port is busy: {names}")
|
||||||
|
|
||||||
|
update_toml_scalar("server", "port", str(port))
|
||||||
|
update_toml_scalar("general.links", "public_port", str(port))
|
||||||
|
update_toml_scalar("censorship", "tls_domain", json.dumps(mask_host))
|
||||||
|
|
||||||
|
config = load_json(PCATELEGRAM_WEB_CONFIG, {}) or {}
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
config = {}
|
||||||
|
config["port"] = port
|
||||||
|
config["mask_host"] = mask_host
|
||||||
|
config["updated_at"] = utc_now()
|
||||||
|
save_json(PCATELEGRAM_WEB_CONFIG, config)
|
||||||
|
|
||||||
|
restarted = False
|
||||||
|
if service_status("telemt") != "not_installed":
|
||||||
|
restarted = request_service_restart("telemt")
|
||||||
|
payload = routing_payload(port)
|
||||||
|
payload["restart"] = {"requested": restarted}
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def load_shared443_config() -> dict[str, Any]:
|
def load_shared443_config() -> dict[str, Any]:
|
||||||
raw = load_json(SHARED_443_CONFIG, {}) or {}
|
raw = load_json(SHARED_443_CONFIG, {}) or {}
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
@@ -1171,7 +1284,8 @@ def proxy_link(secret: str) -> str:
|
|||||||
mask_host = str(config.get("mask_host", "") or "")
|
mask_host = str(config.get("mask_host", "") or "")
|
||||||
|
|
||||||
if mode == "pro" and domain:
|
if mode == "pro" and domain:
|
||||||
host_hex = domain.encode().hex()
|
link_mask = mask_host or domain
|
||||||
|
host_hex = link_mask.encode().hex()
|
||||||
return f"tg://proxy?server={domain}&port={port}&secret=ee{secret}{host_hex}"
|
return f"tg://proxy?server={domain}&port={port}&secret=ee{secret}{host_hex}"
|
||||||
|
|
||||||
server = public_ip()
|
server = public_ip()
|
||||||
@@ -1750,6 +1864,7 @@ def overview_payload() -> dict[str, Any]:
|
|||||||
"backups": list_backups(),
|
"backups": list_backups(),
|
||||||
"backup_schedule": backup_schedule_status(),
|
"backup_schedule": backup_schedule_status(),
|
||||||
"warp": public_warp_config(),
|
"warp": public_warp_config(),
|
||||||
|
"routing": routing_payload(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1868,6 +1983,15 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
path = parsed.path
|
path = parsed.path
|
||||||
if path == "/api/overview":
|
if path == "/api/overview":
|
||||||
self.send_json({"ok": True, "data": overview_payload()})
|
self.send_json({"ok": True, "data": overview_payload()})
|
||||||
|
elif path == "/api/routing":
|
||||||
|
qs = urllib.parse.parse_qs(parsed.query)
|
||||||
|
port_raw = qs.get("port", [""])[0]
|
||||||
|
try:
|
||||||
|
port = normalize_port(port_raw) if port_raw else None
|
||||||
|
except ValueError as exc:
|
||||||
|
self.send_error_json(400, str(exc))
|
||||||
|
return
|
||||||
|
self.send_json({"ok": True, "data": routing_payload(port)})
|
||||||
elif path == "/api/warp":
|
elif path == "/api/warp":
|
||||||
self.send_json({"ok": True, "data": public_warp_config()})
|
self.send_json({"ok": True, "data": public_warp_config()})
|
||||||
elif path == "/api/users":
|
elif path == "/api/users":
|
||||||
@@ -2009,6 +2133,21 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
restart_requested = request_service_restart("telemt")
|
restart_requested = request_service_restart("telemt")
|
||||||
self.send_json({"ok": True, "data": user_payload(name, secret, True, 0), "restart": {"mode": "async", "requested": restart_requested}})
|
self.send_json({"ok": True, "data": user_payload(name, secret, True, 0), "restart": {"mode": "async", "requested": restart_requested}})
|
||||||
|
elif path == "/api/routing":
|
||||||
|
try:
|
||||||
|
port = normalize_port(body.get("port"))
|
||||||
|
mask_host = normalize_mask_host(body.get("mask_host"))
|
||||||
|
payload = write_routing_settings(port, mask_host)
|
||||||
|
except ValueError as exc:
|
||||||
|
self.send_error_json(400, str(exc))
|
||||||
|
return
|
||||||
|
except RuntimeError as exc:
|
||||||
|
self.send_error_json(409, str(exc))
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
self.send_error_json(500, f"failed to save routing: {exc}")
|
||||||
|
return
|
||||||
|
self.send_json({"ok": True, "data": payload})
|
||||||
elif path.startswith("/api/users/") and path.endswith("/max-ips"):
|
elif path.startswith("/api/users/") and path.endswith("/max-ips"):
|
||||||
name = urllib.parse.unquote(path[len("/api/users/"):-len("/max-ips")])
|
name = urllib.parse.unquote(path[len("/api/users/"):-len("/max-ips")])
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -106,6 +106,16 @@ const i18n = {
|
|||||||
authNewPassword: "New password",
|
authNewPassword: "New password",
|
||||||
authSave: "Save login",
|
authSave: "Save login",
|
||||||
authSaved: "Login updated",
|
authSaved: "Login updated",
|
||||||
|
routingEyebrow: "Network",
|
||||||
|
routingTitle: "Port and mask site",
|
||||||
|
routingPort: "Public port",
|
||||||
|
routingMaskHost: "Mask site",
|
||||||
|
routingSave: "Save routing",
|
||||||
|
routingSaved: "Routing saved",
|
||||||
|
routingOk: "port free",
|
||||||
|
routingBusy: "port busy",
|
||||||
|
routingPerUserNote: "One telemt instance has one public port and one mask site for all links. Different ports per client need separate telemt services.",
|
||||||
|
routingCurrentMask: "Mask site: {value}",
|
||||||
warpEyebrow: "Routing",
|
warpEyebrow: "Routing",
|
||||||
warpTitle: "WARP / WARP+",
|
warpTitle: "WARP / WARP+",
|
||||||
warpMode: "Mode",
|
warpMode: "Mode",
|
||||||
@@ -350,6 +360,16 @@ const i18n = {
|
|||||||
authNewPassword: "Новый пароль",
|
authNewPassword: "Новый пароль",
|
||||||
authSave: "Сохранить вход",
|
authSave: "Сохранить вход",
|
||||||
authSaved: "Данные входа обновлены",
|
authSaved: "Данные входа обновлены",
|
||||||
|
routingEyebrow: "Сеть",
|
||||||
|
routingTitle: "Порт и маскировка",
|
||||||
|
routingPort: "Публичный порт",
|
||||||
|
routingMaskHost: "Сайт маскировки",
|
||||||
|
routingSave: "Сохранить маршрут",
|
||||||
|
routingSaved: "Маршрутизация сохранена",
|
||||||
|
routingOk: "порт свободен",
|
||||||
|
routingBusy: "порт занят",
|
||||||
|
routingPerUserNote: "Один telemt-инстанс имеет один публичный порт и один сайт маскировки для всех ссылок. Разные порты на клиентов требуют отдельные telemt-сервисы.",
|
||||||
|
routingCurrentMask: "Маскировка: {value}",
|
||||||
warpEyebrow: "Маршрутизация",
|
warpEyebrow: "Маршрутизация",
|
||||||
warpTitle: "WARP / WARP+",
|
warpTitle: "WARP / WARP+",
|
||||||
warpMode: "Режим",
|
warpMode: "Режим",
|
||||||
@@ -509,6 +529,7 @@ const state = {
|
|||||||
userTraffic: null,
|
userTraffic: null,
|
||||||
userTrafficLoading: false,
|
userTrafficLoading: false,
|
||||||
backupSchedule: null,
|
backupSchedule: null,
|
||||||
|
routing: null,
|
||||||
warp: null,
|
warp: null,
|
||||||
qrLink: "",
|
qrLink: "",
|
||||||
pendingUsers: new Set(),
|
pendingUsers: new Set(),
|
||||||
@@ -638,6 +659,7 @@ function applyI18n() {
|
|||||||
updateTrafficControls();
|
updateTrafficControls();
|
||||||
updateUserTrafficControls();
|
updateUserTrafficControls();
|
||||||
renderBackupSchedule();
|
renderBackupSchedule();
|
||||||
|
renderRoutingSettings();
|
||||||
renderWarpSettings();
|
renderWarpSettings();
|
||||||
updatePageTitle();
|
updatePageTitle();
|
||||||
updateAutoRefreshToggle();
|
updateAutoRefreshToggle();
|
||||||
@@ -1383,8 +1405,11 @@ function renderEvents() {
|
|||||||
function renderConfig() {
|
function renderConfig() {
|
||||||
const cfg = state.overview?.config || {};
|
const cfg = state.overview?.config || {};
|
||||||
const site = state.overview?.site_status || {};
|
const site = state.overview?.site_status || {};
|
||||||
|
const routing = state.routing || state.overview?.routing || {};
|
||||||
const items = [
|
const items = [
|
||||||
[t("configMode"), cfg.mode || "--"],
|
[t("configMode"), cfg.mode || "--"],
|
||||||
|
[t("routingPort"), routing.port || cfg.port || "--"],
|
||||||
|
[t("routingMaskHost"), routing.mask_host || cfg.mask_host || cfg.domain || "--"],
|
||||||
[t("configDomain"), cfg.domain || cfg.mask_host || "--"],
|
[t("configDomain"), cfg.domain || cfg.mask_host || "--"],
|
||||||
[t("configSiteStatus"), siteStatusText(site)],
|
[t("configSiteStatus"), siteStatusText(site)],
|
||||||
[t("configTemplate"), cfg.template_id || cfg.template || "--"],
|
[t("configTemplate"), cfg.template_id || cfg.template || "--"],
|
||||||
@@ -1399,6 +1424,34 @@ function renderConfig() {
|
|||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routingSummary(payload) {
|
||||||
|
const conflicts = payload?.conflicts || [];
|
||||||
|
if (conflicts.length) {
|
||||||
|
return `${t("routingBusy")}: ${conflicts.map((item) => `${item.process} ${item.address}`).join(", ")}`;
|
||||||
|
}
|
||||||
|
const listeners = payload?.listeners || [];
|
||||||
|
const listenerText = listeners.length
|
||||||
|
? listeners.map((item) => `${item.process} ${item.address}`).join(", ")
|
||||||
|
: t("routingOk");
|
||||||
|
const mask = t("routingCurrentMask").replace("{value}", payload?.mask_host || "--");
|
||||||
|
return `${listenerText}. ${mask}. ${t("routingPerUserNote")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoutingSettings(payload = null) {
|
||||||
|
const routing = payload || state.routing || state.overview?.routing || {};
|
||||||
|
const portEl = $("#routingPort");
|
||||||
|
const maskEl = $("#routingMaskHost");
|
||||||
|
if (!portEl || !maskEl) return;
|
||||||
|
if (!payload) {
|
||||||
|
portEl.value = routing.port || 443;
|
||||||
|
maskEl.value = routing.mask_host || "google.com";
|
||||||
|
}
|
||||||
|
const conflicts = routing.conflicts || [];
|
||||||
|
$("#routingStatus").textContent = conflicts.length ? t("routingBusy") : t("routingOk");
|
||||||
|
$("#routingStatus").className = `status-pill ${conflicts.length ? "health-error" : "health-ok"}`;
|
||||||
|
$("#routingRuntimeNote").textContent = routingSummary(routing);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
if (state.refreshingAll) return;
|
if (state.refreshingAll) return;
|
||||||
state.refreshingAll = true;
|
state.refreshingAll = true;
|
||||||
@@ -1407,6 +1460,7 @@ async function refreshAll() {
|
|||||||
try {
|
try {
|
||||||
state.overview = await api("/api/overview");
|
state.overview = await api("/api/overview");
|
||||||
state.backupSchedule = state.overview.backup_schedule || state.backupSchedule;
|
state.backupSchedule = state.overview.backup_schedule || state.backupSchedule;
|
||||||
|
state.routing = state.overview.routing || state.routing;
|
||||||
state.warp = state.overview.warp || state.warp;
|
state.warp = state.overview.warp || state.warp;
|
||||||
updateLanguageFromOverview(state.overview);
|
updateLanguageFromOverview(state.overview);
|
||||||
state.users = await api("/api/users");
|
state.users = await api("/api/users");
|
||||||
@@ -1427,6 +1481,7 @@ async function refreshAll() {
|
|||||||
}
|
}
|
||||||
renderOverview();
|
renderOverview();
|
||||||
renderUsers();
|
renderUsers();
|
||||||
|
renderRoutingSettings();
|
||||||
renderWarpSettings();
|
renderWarpSettings();
|
||||||
if (state.page === "traffic") {
|
if (state.page === "traffic") {
|
||||||
await refreshStats();
|
await refreshStats();
|
||||||
@@ -1585,6 +1640,45 @@ async function setUserMaxUniqueIps(name, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkRoutingPort() {
|
||||||
|
const port = Number.parseInt($("#routingPort").value, 10);
|
||||||
|
if (!Number.isFinite(port) || port < 1 || port > 65535) return;
|
||||||
|
try {
|
||||||
|
const data = await api(`/api/routing?port=${encodeURIComponent(port)}`);
|
||||||
|
renderRoutingSettings(data);
|
||||||
|
} catch (err) {
|
||||||
|
$("#routingStatus").textContent = t("routingBusy");
|
||||||
|
$("#routingStatus").className = "status-pill health-error";
|
||||||
|
$("#routingRuntimeNote").textContent = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRoutingSettings(eventObj) {
|
||||||
|
eventObj.preventDefault();
|
||||||
|
const form = eventObj.currentTarget;
|
||||||
|
const controls = Array.from(form.querySelectorAll("input, button"));
|
||||||
|
controls.forEach((control) => { control.disabled = true; });
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
port: Number.parseInt($("#routingPort").value, 10),
|
||||||
|
mask_host: $("#routingMaskHost").value.trim(),
|
||||||
|
};
|
||||||
|
const data = await api("/api/routing", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
state.routing = data;
|
||||||
|
renderRoutingSettings();
|
||||||
|
addEvent(t("routingSaved"), `${data.port} / ${data.mask_host}`);
|
||||||
|
toast(t("routingSaved"));
|
||||||
|
setTimeout(() => refreshAll().catch((err) => toast(err.message)), 1400);
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message);
|
||||||
|
} finally {
|
||||||
|
controls.forEach((control) => { control.disabled = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveWarpSettings(eventObj) {
|
async function saveWarpSettings(eventObj) {
|
||||||
eventObj.preventDefault();
|
eventObj.preventDefault();
|
||||||
const form = eventObj.currentTarget;
|
const form = eventObj.currentTarget;
|
||||||
@@ -1853,6 +1947,8 @@ $("#loadLogsBtn").addEventListener("click", loadLogs);
|
|||||||
$("#repairStatsBtn").addEventListener("click", repairStats);
|
$("#repairStatsBtn").addEventListener("click", repairStats);
|
||||||
$("#collectStatsBtn").addEventListener("click", collectStats);
|
$("#collectStatsBtn").addEventListener("click", collectStats);
|
||||||
$("#authSettingsForm").addEventListener("submit", updateAuthSettings);
|
$("#authSettingsForm").addEventListener("submit", updateAuthSettings);
|
||||||
|
$("#routingSettingsForm").addEventListener("submit", saveRoutingSettings);
|
||||||
|
$("#routingPort").addEventListener("change", checkRoutingPort);
|
||||||
$("#warpSettingsForm").addEventListener("submit", saveWarpSettings);
|
$("#warpSettingsForm").addEventListener("submit", saveWarpSettings);
|
||||||
$("#warpScope").addEventListener("change", renderWarpSettings);
|
$("#warpScope").addEventListener("change", renderWarpSettings);
|
||||||
$("#warpUserSelect").addEventListener("change", renderWarpSettings);
|
$("#warpUserSelect").addEventListener("change", renderWarpSettings);
|
||||||
|
|||||||
@@ -117,6 +117,36 @@
|
|||||||
<div class="service-grid" id="services"></div>
|
<div class="service-grid" id="services"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="routingEyebrow">Network</p>
|
||||||
|
<h2 data-i18n="routingTitle">Port and mask site</h2>
|
||||||
|
</div>
|
||||||
|
<span id="routingStatus" class="status-pill">--</span>
|
||||||
|
</div>
|
||||||
|
<form id="routingSettingsForm" class="settings-form">
|
||||||
|
<label>
|
||||||
|
<span data-i18n="routingPort">Public port</span>
|
||||||
|
<input id="routingPort" name="port" type="number" min="1" max="65535" step="1" inputmode="numeric" placeholder="443">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span data-i18n="routingMaskHost">Mask site</span>
|
||||||
|
<input id="routingMaskHost" name="mask_host" list="maskHostOptions" autocomplete="off" spellcheck="false" placeholder="google.com">
|
||||||
|
<datalist id="maskHostOptions">
|
||||||
|
<option value="google.com"></option>
|
||||||
|
<option value="cloudflare.com"></option>
|
||||||
|
<option value="microsoft.com"></option>
|
||||||
|
<option value="apple.com"></option>
|
||||||
|
<option value="github.com"></option>
|
||||||
|
<option value="wikipedia.org"></option>
|
||||||
|
</datalist>
|
||||||
|
</label>
|
||||||
|
<p id="routingRuntimeNote" class="modal-note"></p>
|
||||||
|
<button type="submit" data-i18n="routingSave">Save routing</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
@@ -440,6 +470,6 @@
|
|||||||
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/app.js?v=2.5.0-admin22" type="module"></script>
|
<script src="/app.js?v=2.5.0-admin23" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user