mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 15:36:03 +00:00
v2.5.0: harden admin key and traffic flows
This commit is contained in:
@@ -167,7 +167,7 @@ def read_telemt_users() -> dict[str, str]:
|
||||
if not in_users or not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
name, value = line.split("=", 1)
|
||||
name = name.strip()
|
||||
name = parse_toml_key(name)
|
||||
value = value.strip().split("#", 1)[0].strip()
|
||||
if value.startswith('"') and '"' in value[1:]:
|
||||
value = value[1:].split('"', 1)[0]
|
||||
@@ -221,7 +221,24 @@ def _ordered_user_lines(users: dict[str, str]) -> list[str]:
|
||||
if "main" in users:
|
||||
names.append("main")
|
||||
names.extend(sorted(n for n in users if n != "main"))
|
||||
return [f'{name} = "{users[name]}"' for name in names]
|
||||
return [f'{quote_toml_key(name)} = "{users[name]}"' for name in names]
|
||||
|
||||
|
||||
def parse_toml_key(raw: str) -> str:
|
||||
key = raw.strip()
|
||||
if len(key) >= 2 and key[0] == key[-1] == '"':
|
||||
try:
|
||||
return json.loads(key)
|
||||
except json.JSONDecodeError:
|
||||
return key[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
||||
if len(key) >= 2 and key[0] == key[-1] == "'":
|
||||
return key[1:-1]
|
||||
return key
|
||||
|
||||
|
||||
def quote_toml_key(name: str) -> str:
|
||||
escaped = name.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def write_telemt_users(users: dict[str, str]) -> None:
|
||||
@@ -267,16 +284,8 @@ def restart_service(name: str) -> bool:
|
||||
|
||||
|
||||
def request_service_restart(name: str) -> bool:
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["systemctl", "restart", name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
code, _, _ = run(["systemctl", "--no-block", "restart", name], timeout=5)
|
||||
return code == 0
|
||||
|
||||
|
||||
def service_status(name: str) -> str:
|
||||
@@ -539,8 +548,8 @@ def traffic_interval_summaries(rows: list[dict[str, int]]) -> list[dict[str, Any
|
||||
"points": len(window),
|
||||
"from": first.get("epoch", 0),
|
||||
"to": last.get("epoch", 0),
|
||||
"proxy_delta": max(0, int(last.get("proxy_bytes", 0)) - int(first.get("proxy_bytes", 0))),
|
||||
"site_delta": max(0, int(last.get("site_bytes", 0)) - int(first.get("site_bytes", 0))),
|
||||
"proxy_delta": sum(max(0, int(item.get("proxy_delta", 0))) for item in window),
|
||||
"site_delta": sum(max(0, int(item.get("site_delta", 0))) for item in window),
|
||||
"proxy_total": int(last.get("proxy_bytes", 0)),
|
||||
"site_total": int(last.get("site_bytes", 0)),
|
||||
})
|
||||
|
||||
@@ -142,6 +142,8 @@ const i18n = {
|
||||
port443NoListeners: "No 443 listeners found",
|
||||
port443Listeners: "listeners",
|
||||
port443Error: "Port check failed",
|
||||
port443Public: "public",
|
||||
port443Configured: "telemt: {port}",
|
||||
roleMtproxy: "MTProxy",
|
||||
roleSite: "Website",
|
||||
roleXray: "Xray / 3x-ui",
|
||||
@@ -322,6 +324,8 @@ const i18n = {
|
||||
port443NoListeners: "Слушателей 443 не найдено",
|
||||
port443Listeners: "слушателей",
|
||||
port443Error: "Проверка порта не удалась",
|
||||
port443Public: "публичный",
|
||||
port443Configured: "telemt: {port}",
|
||||
roleMtproxy: "MTProxy",
|
||||
roleSite: "Сайт",
|
||||
roleXray: "Xray / 3x-ui",
|
||||
@@ -374,6 +378,7 @@ const state = {
|
||||
theme: document.documentElement.dataset.theme || "light",
|
||||
trafficRange: "1h",
|
||||
trafficView: "chart",
|
||||
trafficLoading: false,
|
||||
pendingUsers: new Set(),
|
||||
};
|
||||
|
||||
@@ -634,6 +639,9 @@ function renderPort443(payload = {}) {
|
||||
const listeners = Array.isArray(payload.listeners) ? payload.listeners : [];
|
||||
const summary = $("#port443Summary");
|
||||
const list = $("#port443List");
|
||||
const configuredPort = Number(payload.configured_port) || 443;
|
||||
$("#port443Number").textContent = "443";
|
||||
$("#port443Configured").textContent = configuredPort === 443 ? t("port443Public") : t("port443Configured").replace("{port}", configuredPort);
|
||||
if (payload.error) {
|
||||
summary.textContent = t("port443Error");
|
||||
summary.className = "port-status error";
|
||||
@@ -760,14 +768,21 @@ function fallbackTrafficSummaries(rows) {
|
||||
return {
|
||||
range,
|
||||
points: windowRows.length,
|
||||
proxy_delta: Math.max(0, (Number(last.proxy_bytes) || 0) - (Number(first.proxy_bytes) || 0)),
|
||||
site_delta: Math.max(0, (Number(last.site_bytes) || 0) - (Number(first.site_bytes) || 0)),
|
||||
proxy_delta: windowRows.reduce((sum, item) => sum + Math.max(0, Number(item.proxy_delta) || 0), 0),
|
||||
site_delta: windowRows.reduce((sum, item) => sum + Math.max(0, Number(item.site_delta) || 0), 0),
|
||||
proxy_total: Number(last.proxy_bytes) || 0,
|
||||
site_total: Number(last.site_bytes) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderTrafficLoading() {
|
||||
$("#trafficChart").classList.toggle("is-hidden", state.trafficView !== "chart");
|
||||
$("#trafficTableWrap").classList.toggle("is-hidden", state.trafficView !== "table");
|
||||
$("#trafficChart").innerHTML = `<div class="empty-chart"><strong>${escapeHtml(t("loading"))}</strong></div>`;
|
||||
$("#historyTable").innerHTML = `<tr><td colspan="5" class="empty-cell">${escapeHtml(t("loading"))}</td></tr>`;
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const payload = statsPayload();
|
||||
const status = payload.status || {};
|
||||
@@ -784,6 +799,10 @@ function renderStats() {
|
||||
$("#metricProxyTraffic").textContent = fmtBytes(stats.proxy_bytes);
|
||||
$("#metricSiteTraffic").textContent = fmtBytes(stats.site_bytes);
|
||||
updateTrafficControls();
|
||||
if (state.trafficLoading) {
|
||||
renderTrafficLoading();
|
||||
return;
|
||||
}
|
||||
$("#trafficChart").classList.toggle("is-hidden", state.trafficView !== "chart");
|
||||
$("#trafficTableWrap").classList.toggle("is-hidden", state.trafficView !== "table");
|
||||
drawTrafficChart(historyRows);
|
||||
@@ -948,6 +967,9 @@ async function refreshAll() {
|
||||
}
|
||||
renderOverview();
|
||||
renderUsers();
|
||||
if (state.page === "traffic") {
|
||||
await refreshStats();
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err.message);
|
||||
} finally {
|
||||
@@ -955,10 +977,39 @@ async function refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStats() {
|
||||
const data = await api(`/api/stats?range=${encodeURIComponent(state.trafficRange)}`);
|
||||
state.stats = data;
|
||||
renderStats();
|
||||
async function refreshUsers() {
|
||||
state.users = await api("/api/users");
|
||||
renderUsers();
|
||||
}
|
||||
|
||||
async function refreshStats(options = {}) {
|
||||
if (options.showLoading) {
|
||||
state.trafficLoading = true;
|
||||
renderStats();
|
||||
}
|
||||
try {
|
||||
const data = await api(`/api/stats?range=${encodeURIComponent(state.trafficRange)}`);
|
||||
state.stats = data;
|
||||
return data;
|
||||
} finally {
|
||||
state.trafficLoading = false;
|
||||
renderStats();
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTrafficRange(range) {
|
||||
const next = trafficRanges.includes(range) ? range : "1h";
|
||||
if (next === state.trafficRange && state.stats?.range === next) return;
|
||||
const previous = state.trafficRange;
|
||||
state.trafficRange = next;
|
||||
try {
|
||||
await refreshStats({ showLoading: true });
|
||||
} catch (err) {
|
||||
state.trafficRange = previous;
|
||||
state.trafficLoading = false;
|
||||
renderStats();
|
||||
toast(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function addUser(name) {
|
||||
@@ -992,15 +1043,18 @@ async function setUserEnabled(name, enabled) {
|
||||
const message = data.enabled ? t("keyEnabled") : t("keyDisabled");
|
||||
addEvent(message, name);
|
||||
toast(t("changesApplyInBackground"));
|
||||
setTimeout(() => refreshAll().catch((err) => toast(err.message)), 1200);
|
||||
try {
|
||||
await refreshUsers();
|
||||
} catch (refreshErr) {
|
||||
toast(refreshErr.message);
|
||||
}
|
||||
setTimeout(() => refreshAll().catch((err) => toast(err.message)), 1400);
|
||||
} catch (err) {
|
||||
state.users = previousUsers;
|
||||
toast(err.message);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
state.pendingUsers.delete(name);
|
||||
renderUsers();
|
||||
}, 700);
|
||||
state.pendingUsers.delete(name);
|
||||
renderUsers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,10 +1178,7 @@ document.addEventListener("click", async (eventObj) => {
|
||||
} else if (button.id === "menuBtn") {
|
||||
$("#sidebar").classList.toggle("open");
|
||||
} else if (button.dataset.trafficRange) {
|
||||
state.trafficRange = trafficRanges.includes(button.dataset.trafficRange) ? button.dataset.trafficRange : "1h";
|
||||
updateTrafficControls();
|
||||
renderStats();
|
||||
refreshStats().catch((err) => toast(err.message));
|
||||
changeTrafficRange(button.dataset.trafficRange);
|
||||
} else if (button.dataset.trafficView) {
|
||||
state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart";
|
||||
renderStats();
|
||||
|
||||
@@ -67,7 +67,10 @@
|
||||
</div>
|
||||
<div class="port-map" id="port443Map">
|
||||
<div class="port-map-head">
|
||||
<span>443</span>
|
||||
<div class="port-badge">
|
||||
<span id="port443Number">443</span>
|
||||
<small id="port443Configured">public</small>
|
||||
</div>
|
||||
<strong id="port443Summary">--</strong>
|
||||
</div>
|
||||
<div id="port443List" class="port-list"></div>
|
||||
|
||||
@@ -360,7 +360,12 @@ h2 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.port-map-head span {
|
||||
.port-badge {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.port-badge span {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 54px;
|
||||
@@ -372,6 +377,13 @@ h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.port-badge small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.port-status {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
@@ -1163,6 +1175,7 @@ td small {
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user