Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14bb6ae589 | ||
|
|
51712310f6 | ||
|
|
6ec1782d8b | ||
|
|
e61ec09b18 | ||
|
|
ed3071a971 | ||
|
|
db4cab7a38 | ||
|
|
9f402be886 | ||
|
|
8971565e6e | ||
|
|
b6525bc0ae | ||
|
|
d072c8227f | ||
|
|
7b941dc532 | ||
|
|
8c1654ecaa | ||
|
|
dc83d90a35 | ||
|
|
e0ea54e83a | ||
|
|
628973374e | ||
|
|
041a533457 |
14
README.md
14
README.md
@@ -77,9 +77,21 @@ bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main
|
|||||||
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
||||||
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
||||||
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
||||||
|
| `CHANNEL` | `stable` | канал: `stable` (открытый) · `beta`/`dev` (под ключом `PHOBOS_KEY`) |
|
||||||
|
|
||||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
||||||
|
|
||||||
|
> **Каналы:**
|
||||||
|
> - **stable** — стабильная (ветка `main`), ставится **без ключа**, по умолчанию.
|
||||||
|
> - **beta** — кандидат в релиз (тест), **под ключом** подписчика.
|
||||||
|
> - **dev** — активная разработка (нестабильно), **под ключом** подписчика.
|
||||||
|
>
|
||||||
|
> Ключ `PHOBOS_KEY` выдаётся по подписке [Boosty](https://boosty.to/andrey27). Закрытый канал:
|
||||||
|
> ```bash
|
||||||
|
> PHOBOS_KEY=ваш_ключ CHANNEL=dev bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||||
|
> ```
|
||||||
|
> Переключение после установки: `phobos-update stable` (без ключа) · `PHOBOS_KEY=... phobos-update beta|dev` · статус: `phobos-update --check`.
|
||||||
|
|
||||||
### Вторичный сервер (для failover / балансировки)
|
### Вторичный сервер (для failover / балансировки)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -169,7 +181,7 @@ phobos-update --list # список бэкапов
|
|||||||
|
|
||||||
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
||||||
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
- 💳 **Ozon Bank (СБП):** [ссылка](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
||||||
- ✉️ **Telegram:** [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, баги, идеи
|
- ✉️ **Telegram:** [@PCAdministration](https://t.me/PCAdministration) — вопросы, баги, идеи
|
||||||
|
|
||||||
## На основе и благодарности
|
## На основе и благодарности
|
||||||
|
|
||||||
|
|||||||
226
app.py
226
app.py
@@ -185,6 +185,43 @@ def _bot_status_text():
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def qr_png_bytes(data):
|
||||||
|
import qrcode, io
|
||||||
|
buf = io.BytesIO(); qrcode.make(data).save(buf, format="PNG"); return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def tg_send_photo(chat, png, caption=""):
|
||||||
|
import urllib.request, secrets as _sec
|
||||||
|
token = load_settings().get("tg_bot_token", "")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
boundary = "----phobos" + _sec.token_hex(8)
|
||||||
|
def fld(name, val):
|
||||||
|
return ("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n" % (boundary, name, val)).encode()
|
||||||
|
body = fld("chat_id", str(chat)) + fld("caption", caption) + fld("parse_mode", "HTML")
|
||||||
|
body += ("--%s\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"qr.png\"\r\nContent-Type: image/png\r\n\r\n" % boundary).encode()
|
||||||
|
body += png + ("\r\n--%s--\r\n" % boundary).encode()
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request("https://api.telegram.org/bot%s/sendPhoto" % token, data=body,
|
||||||
|
headers={"Content-Type": "multipart/form-data; boundary=%s" % boundary})
|
||||||
|
return urllib.request.urlopen(req, timeout=25).read()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def bot_send_new_client(chat, name):
|
||||||
|
ok, rep = create_client_cli(name)
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
|
if ok:
|
||||||
|
try:
|
||||||
|
conf, link = build_phone_config(name, "android")
|
||||||
|
if link:
|
||||||
|
tg_send_photo(chat, qr_png_bytes(link), "📱 Android (PhobosWG) — отсканируй QR в приложении")
|
||||||
|
tg_api("sendMessage", {"chat_id": chat, "text": "phobos:// ссылка (Android):\n<code>%s</code>" % link, "parse_mode": "HTML"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def telegram_bot():
|
def telegram_bot():
|
||||||
offset = 0
|
offset = 0
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -217,23 +254,40 @@ def telegram_bot():
|
|||||||
text = (m.get("text") or "").strip()
|
text = (m.get("text") or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
low = text.lower()
|
# detect reply-keyboard button by its emoji prefix (NOT a command/arg)
|
||||||
if _bot_state.get(chat) == "add" and not text.startswith("/"):
|
btn = None
|
||||||
_bot_state.pop(chat, None)
|
if not text.startswith("/"):
|
||||||
_, rep = create_client_cli(text)
|
if text.startswith("➕"):
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
btn = "add"
|
||||||
continue
|
elif text.startswith("\U0001F5D1"):
|
||||||
toks = text.split()
|
btn = "del"
|
||||||
cmd = toks[0].split("@")[0].lower()
|
elif text.startswith("\U0001F4CB"):
|
||||||
arg = toks[1] if len(toks) > 1 else ""
|
btn = "list"
|
||||||
if text.startswith("➕") or cmd in ("/add", "/new", "/create"):
|
elif text.startswith("\U0001F4CA"):
|
||||||
|
btn = "status"
|
||||||
|
elif text.startswith("❓"):
|
||||||
|
btn = "help"
|
||||||
|
# conversation: awaiting a client name to create
|
||||||
|
if _bot_state.get(chat) == "add":
|
||||||
|
if btn is None and not text.startswith("/"):
|
||||||
|
_bot_state.pop(chat, None)
|
||||||
|
bot_send_new_client(chat, text)
|
||||||
|
continue
|
||||||
|
_bot_state.pop(chat, None) # a button/command cancels the prompt
|
||||||
|
# parse slash command + arg (buttons carry no arg)
|
||||||
|
cmd = ""
|
||||||
|
arg = ""
|
||||||
|
if text.startswith("/"):
|
||||||
|
toks = text.split()
|
||||||
|
cmd = toks[0].split("@")[0].lower()
|
||||||
|
arg = toks[1] if len(toks) > 1 else ""
|
||||||
|
if btn == "add" or cmd in ("/add", "/new", "/create"):
|
||||||
if arg:
|
if arg:
|
||||||
_, rep = create_client_cli(arg)
|
bot_send_new_client(chat, arg)
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
|
||||||
else:
|
else:
|
||||||
_bot_state[chat] = "add"
|
_bot_state[chat] = "add"
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": "Введите имя нового клиента (латиница, цифры, _ и -):"})
|
tg_api("sendMessage", {"chat_id": chat, "text": "Введите имя нового клиента (латиница, цифры, _ и -):"})
|
||||||
elif text.startswith("\U0001F5D1") or cmd in ("/del", "/delete", "/remove", "/rm"):
|
elif btn == "del" or cmd in ("/del", "/delete", "/remove", "/rm"):
|
||||||
if arg:
|
if arg:
|
||||||
_, rep = delete_client_cli(arg)
|
_, rep = delete_client_cli(arg)
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
@@ -243,14 +297,14 @@ def telegram_bot():
|
|||||||
tg_api("sendMessage", {"chat_id": chat, "text": "Клиентов нет.", "reply_markup": MAIN_KB})
|
tg_api("sendMessage", {"chat_id": chat, "text": "Клиентов нет.", "reply_markup": MAIN_KB})
|
||||||
else:
|
else:
|
||||||
kb = {"inline_keyboard": [[{"text": "\U0001F5D1 " + c.get("client_id", ""), "callback_data": "del:" + c.get("client_id", "")}] for c in cl]}
|
kb = {"inline_keyboard": [[{"text": "\U0001F5D1 " + c.get("client_id", ""), "callback_data": "del:" + c.get("client_id", "")}] for c in cl]}
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": "Кого удалить?", "reply_markup": kb})
|
tg_api("sendMessage", {"chat_id": chat, "text": "Кого удалить? Нажми на клиента:", "reply_markup": kb})
|
||||||
elif text.startswith("\U0001F4CB") or cmd in ("/list", "/ls"):
|
elif btn == "list" or cmd in ("/list", "/ls"):
|
||||||
cl = [c.get("client_id", "") for c in get_clients()]
|
cl = [c.get("client_id", "") for c in get_clients()]
|
||||||
rep = ("Клиенты (%d):\n" % len(cl)) + "\n".join("• " + c for c in cl) if cl else "Клиентов нет."
|
rep = ("Клиенты (%d):\n" % len(cl)) + "\n".join("• " + c for c in cl) if cl else "Клиентов нет."
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "reply_markup": MAIN_KB})
|
tg_api("sendMessage", {"chat_id": chat, "text": rep, "reply_markup": MAIN_KB})
|
||||||
elif text.startswith("\U0001F4CA") or cmd in ("/status", "/stat"):
|
elif btn == "status" or cmd in ("/status", "/stat"):
|
||||||
tg_api("sendMessage", {"chat_id": chat, "text": _bot_status_text(), "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
tg_api("sendMessage", {"chat_id": chat, "text": _bot_status_text(), "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||||
elif text.startswith("❓") or cmd in ("/help", "/start"):
|
elif btn == "help" or cmd in ("/help", "/start"):
|
||||||
rep = ("<b>PCA Phobos — бот</b>\n\n"
|
rep = ("<b>PCA Phobos — бот</b>\n\n"
|
||||||
"Кнопки внизу или команды:\n"
|
"Кнопки внизу или команды:\n"
|
||||||
"➕ /add <имя> — создать клиента\n"
|
"➕ /add <имя> — создать клиента\n"
|
||||||
@@ -425,6 +479,7 @@ def kick_client_by_id(client_id):
|
|||||||
|
|
||||||
prev_session_keys = None
|
prev_session_keys = None
|
||||||
prev_server_status = {}
|
prev_server_status = {}
|
||||||
|
_down_streak = {}
|
||||||
_last_fanout = 0
|
_last_fanout = 0
|
||||||
server_stats_cache = {}
|
server_stats_cache = {}
|
||||||
server_handshakes_cache = {}
|
server_handshakes_cache = {}
|
||||||
@@ -449,12 +504,19 @@ def get_local_stats():
|
|||||||
|
|
||||||
|
|
||||||
def get_remote_stats(server):
|
def get_remote_stats(server):
|
||||||
|
import urllib.request
|
||||||
|
url = f"http://{server['ip']}:8444/api/health"
|
||||||
|
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
||||||
|
data = None
|
||||||
|
for _attempt in range(2): # retry once: smooths transient timeouts (1-worker agent)
|
||||||
|
try:
|
||||||
|
data = json.loads(urllib.request.urlopen(req, timeout=6).read())
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
data = None
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
if data is None:
|
||||||
url = f"http://{server['ip']}:8444/api/health"
|
raise Exception("no response")
|
||||||
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
|
||||||
resp = urllib.request.urlopen(req, timeout=3)
|
|
||||||
data = json.loads(resp.read())
|
|
||||||
# cache handshakes so page renders never block on remote HTTP
|
# cache handshakes so page renders never block on remote HTTP
|
||||||
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
||||||
# Filter peers: only count known client public keys
|
# Filter peers: only count known client public keys
|
||||||
@@ -599,6 +661,28 @@ def panel_version():
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
GH_TOKEN_FILE = "/opt/phobos-panel/.gh_token"
|
||||||
|
|
||||||
|
|
||||||
|
def gh_token_present():
|
||||||
|
try:
|
||||||
|
return os.path.exists(GH_TOKEN_FILE) and os.path.getsize(GH_TOKEN_FILE) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_gh_token(tok):
|
||||||
|
tok = (tok or "").strip()
|
||||||
|
if not tok:
|
||||||
|
return False
|
||||||
|
fd = os.open(GH_TOKEN_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
try:
|
||||||
|
os.write(fd, tok.encode())
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_update(arg):
|
def run_update(arg):
|
||||||
import subprocess
|
import subprocess
|
||||||
try:
|
try:
|
||||||
@@ -637,11 +721,46 @@ def build_phone_config(cid, mode="android"):
|
|||||||
if os.path.exists(instpath):
|
if os.path.exists(instpath):
|
||||||
inst = open(instpath).read().strip()
|
inst = open(instpath).read().strip()
|
||||||
conf = wg + "\n\n" + inst + "\n"
|
conf = wg + "\n\n" + inst + "\n"
|
||||||
b64 = base64.urlsafe_b64encode(conf.encode()).decode().rstrip("=")
|
# PhobosWG requires ALL fields present; missing -> "= none" before base64
|
||||||
link = "phobos://" + b64 + "#" + urllib.parse.quote(cid)
|
# (spec 2.3). Ground-Zerro configs lack PresharedKey -> import fails otherwise.
|
||||||
|
padded = _pad_conf_with_none(conf)
|
||||||
|
b64 = base64.urlsafe_b64encode(padded.encode()).decode().rstrip("=")
|
||||||
|
link = "phobos://" + b64 + "#" + urllib.parse.quote(cid or "none")
|
||||||
return conf, link
|
return conf, link
|
||||||
|
|
||||||
|
|
||||||
|
def _pad_conf_with_none(conf):
|
||||||
|
"""Insert '<key> = none' for every missing mandatory PhobosWG field
|
||||||
|
(phobos:// spec 2.3). Only for phobos:// payload, NOT raw .conf."""
|
||||||
|
mandatory = {
|
||||||
|
"[interface]": ["PrivateKey", "Address", "MTU", "DNS"],
|
||||||
|
"[peer]": ["PublicKey", "PresharedKey", "AllowedIPs", "PersistentKeepalive", "Endpoint"],
|
||||||
|
"[instance]": ["source-if", "source-lport", "target", "key", "masking",
|
||||||
|
"verbose", "idle-timeout", "max-dummy"],
|
||||||
|
}
|
||||||
|
sections = []
|
||||||
|
cur = None
|
||||||
|
for ln in conf.split("\n"):
|
||||||
|
s = ln.strip()
|
||||||
|
if s.startswith("[") and s.endswith("]"):
|
||||||
|
cur = [s, []]
|
||||||
|
sections.append(cur)
|
||||||
|
elif cur is not None:
|
||||||
|
cur[1].append(ln)
|
||||||
|
out = []
|
||||||
|
for header, lines in sections:
|
||||||
|
keys = {ln.split("=", 1)[0].strip().lower() for ln in lines if "=" in ln}
|
||||||
|
while lines and lines[-1].strip() == "":
|
||||||
|
lines.pop()
|
||||||
|
for req in mandatory.get(header.lower(), []):
|
||||||
|
if req.lower() not in keys:
|
||||||
|
lines.append("%s = none" % req)
|
||||||
|
out.append(header)
|
||||||
|
out.extend(lines)
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out).rstrip("\n") + "\n"
|
||||||
|
|
||||||
|
|
||||||
def qr_datauri(data):
|
def qr_datauri(data):
|
||||||
try:
|
try:
|
||||||
import qrcode, io, base64
|
import qrcode, io, base64
|
||||||
@@ -723,7 +842,7 @@ def push_config_to_router(client_id):
|
|||||||
|
|
||||||
|
|
||||||
def session_monitor():
|
def session_monitor():
|
||||||
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout
|
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout, _down_streak
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -760,19 +879,24 @@ def session_monitor():
|
|||||||
local_stats = get_local_stats()
|
local_stats = get_local_stats()
|
||||||
server_stats_cache[SERVER_IP] = local_stats
|
server_stats_cache[SERVER_IP] = local_stats
|
||||||
|
|
||||||
|
DOWN_CONFIRM = 3 # consecutive failed cycles before declaring a server DOWN
|
||||||
for srv in servers:
|
for srv in servers:
|
||||||
ip = srv["ip"]
|
ip = srv["ip"]
|
||||||
stats = get_remote_stats(srv)
|
stats = get_remote_stats(srv)
|
||||||
server_stats_cache[ip] = stats
|
raw = stats.get("status", "down")
|
||||||
|
streak = _down_streak.get(ip, 0)
|
||||||
|
streak = streak + 1 if raw == "down" else 0
|
||||||
|
_down_streak[ip] = streak
|
||||||
|
# update cache on OK, or only once a DOWN is confirmed (keep last-good on transient)
|
||||||
|
if raw == "ok" or streak >= DOWN_CONFIRM:
|
||||||
|
server_stats_cache[ip] = stats
|
||||||
|
eff = "down" if streak >= DOWN_CONFIRM else "ok"
|
||||||
was_up = prev_server_status.get(ip, "ok")
|
was_up = prev_server_status.get(ip, "ok")
|
||||||
now_status = stats["status"]
|
if was_up == "ok" and eff == "down":
|
||||||
|
|
||||||
if was_up == "ok" and now_status == "down":
|
|
||||||
tg_send(f"🔴 <b>Server {ip}</b> DOWN!")
|
tg_send(f"🔴 <b>Server {ip}</b> DOWN!")
|
||||||
elif was_up == "down" and now_status == "ok":
|
elif was_up == "down" and eff == "ok":
|
||||||
tg_send(f"🟢 <b>Server {ip}</b> back ONLINE")
|
tg_send(f"🟢 <b>Server {ip}</b> back ONLINE")
|
||||||
|
prev_server_status[ip] = eff
|
||||||
prev_server_status[ip] = now_status
|
|
||||||
|
|
||||||
# periodic config fan-out to secondaries (covers CLI-created clients
|
# periodic config fan-out to secondaries (covers CLI-created clients
|
||||||
# and config drift; gated ~5 min, skips down servers)
|
# and config drift; gated ~5 min, skips down servers)
|
||||||
@@ -854,7 +978,7 @@ input:focus{outline:none;border-color:#4f46e5}
|
|||||||
CONTENT
|
CONTENT
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<b style="color:#a5b4fc">PCA Phobos Panel</b> — панель управления © <a href="https://github.com/andrey271192/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
<b style="color:#a5b4fc">PCA Phobos Panel</b> — панель управления © <a href="https://github.com/andrey271192/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
||||||
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/c/2474115507/78722" target="_blank" rel="noopener">✉️ Telegram</a>
|
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/PCAdministration" target="_blank" rel="noopener">✉️ Telegram</a>
|
||||||
<div style="margin-top:8px;font-size:.88em;color:#475569;line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
<div style="margin-top:8px;font-size:.88em;color:#475569;line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
||||||
<b>Создано на основе открытых проектов:</b><br>
|
<b>Создано на основе открытых проектов:</b><br>
|
||||||
• <a href="https://github.com/Ground-Zerro/Phobos" target="_blank" rel="noopener">Phobos</a> — обфусцированный WireGuard VPN, автор <b>Ground_Zerro</b> · <a href="https://boosty.to/ground_zerro" target="_blank" rel="noopener">поддержать</a><br>
|
• <a href="https://github.com/Ground-Zerro/Phobos" target="_blank" rel="noopener">Phobos</a> — обфусцированный WireGuard VPN, автор <b>Ground_Zerro</b> · <a href="https://boosty.to/ground_zerro" target="_blank" rel="noopener">поддержать</a><br>
|
||||||
@@ -1317,9 +1441,19 @@ def labels_page():
|
|||||||
def settings_page():
|
def settings_page():
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
msg = ""
|
msg = ""
|
||||||
if request.method == "POST" and request.form.get("action") in ("update", "rollback"):
|
if request.method == "POST" and request.form.get("action") in ("update", "rollback", "check", "save_token"):
|
||||||
if request.form.get("action") == "update":
|
_act = request.form.get("action")
|
||||||
|
_tok = request.form.get("gh_token", "").strip()
|
||||||
|
if _tok:
|
||||||
|
save_gh_token(_tok)
|
||||||
|
if _act == "save_token":
|
||||||
|
_out = tr("Токен подписчика сохранён. Теперь можно переключаться на beta/dev.",
|
||||||
|
"Subscriber token saved. You can now switch to beta/dev.") if _tok else \
|
||||||
|
tr("Поле токена пустое.", "Token field is empty.")
|
||||||
|
elif _act == "update":
|
||||||
_out = run_update(request.form.get("target", "").strip() or "main")
|
_out = run_update(request.form.get("target", "").strip() or "main")
|
||||||
|
elif _act == "check":
|
||||||
|
_out = run_update("--check")
|
||||||
else:
|
else:
|
||||||
_out = run_update("--rollback")
|
_out = run_update("--rollback")
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
@@ -1358,11 +1492,31 @@ def settings_page():
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>{tr("Версия и обновления","Version & updates")} {hlp("Скачивает свежие файлы PCA-слоя (панель + скрипты) с GitHub и перезапускает панель. Ключи WireGuard, server.env и клиенты НЕ трогаются. Пусто или main = последняя версия; можно указать тег (например v1.1.0). Откат восстанавливает предыдущую версию из автоматического бэкапа.", "Fetches the latest PCA files (panel + scripts) from GitHub and restarts the panel. WireGuard keys, server.env and clients are NOT touched. Empty or main = latest; you can pass a tag (e.g. v1.1.0). Rollback restores the previous version from an automatic backup.")}</h2>
|
<h2>{tr("Версия и обновления","Version & updates")} {hlp("Скачивает свежие файлы PCA-слоя (панель + скрипты) с GitHub и перезапускает панель. Ключи WireGuard, server.env и клиенты НЕ трогаются. Пусто или main = последняя версия; можно указать тег (например v1.1.0). Откат восстанавливает предыдущую версию из автоматического бэкапа.", "Fetches the latest PCA files (panel + scripts) from GitHub and restarts the panel. WireGuard keys, server.env and clients are NOT touched. Empty or main = latest; you can pass a tag (e.g. v1.1.0). Rollback restores the previous version from an automatic backup.")}</h2>
|
||||||
<p>{tr("Установленная версия","Installed version")}: <b style="color:#a5b4fc">{panel_version()}</b></p>
|
<p>{tr("Установленная версия","Installed version")}: <b style="color:#a5b4fc">{panel_version()}</b></p>
|
||||||
|
<p style="font-size:.85em;color:#94a3b8;margin:4px 0 8px">{tr("Каналы","Channels")}:
|
||||||
|
<b style="color:#22c55e">stable</b> — {tr("стабильная","stable")} ·
|
||||||
|
<b style="color:#eab308">beta</b> — {tr("тест","testing")} ·
|
||||||
|
<b style="color:#f97316">dev</b> — {tr("разработка","development")}.
|
||||||
|
{tr("stable — без токена. Для beta/dev вставь токен подписчика (по подписке Boosty), сохрани — и жми канал.","stable needs no token. For beta/dev paste your subscriber token (Boosty), save it, then click the channel.")}</p>
|
||||||
|
<form method="post" class="form-row" style="margin-bottom:8px">
|
||||||
|
<input type="hidden" name="action" value="save_token">
|
||||||
|
<input type="password" name="gh_token" placeholder="{tr('токен подписчика (для beta/dev)','subscriber token (for beta/dev)')}" style="width:240px" autocomplete="off">
|
||||||
|
<button class="btn btn-sm" style="background:#475569" type="submit">{tr("Сохранить токен","Save token")}</button>
|
||||||
|
<span style="font-size:.82em;color:{'#22c55e' if gh_token_present() else '#f59e0b'}">{(tr("✓ токен сохранён","✓ token saved") if gh_token_present() else tr("токен не задан — beta/dev недоступны","no token — beta/dev locked"))}</span>
|
||||||
|
</form>
|
||||||
|
<div class="form-row" style="gap:6px;margin-bottom:8px">
|
||||||
|
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="stable"><button class="btn btn-sm" style="background:#16a34a" type="submit" onclick="return confirm('{tr("Перейти на стабильную?","Switch to stable?")}')">stable</button></form>
|
||||||
|
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="beta"><button class="btn btn-sm" style="background:#ca8a04" type="submit" onclick="return confirm('{tr("Перейти на beta?","Switch to beta?")}')">beta</button></form>
|
||||||
|
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="dev"><button class="btn btn-sm" style="background:#ea580c" type="submit" onclick="return confirm('{tr("Перейти на dev (нестабильно)?","Switch to dev (unstable)?")}')">dev</button></form>
|
||||||
|
</div>
|
||||||
<form method="post" class="form-row">
|
<form method="post" class="form-row">
|
||||||
<input type="hidden" name="action" value="update">
|
<input type="hidden" name="action" value="update">
|
||||||
<input type="text" name="target" placeholder="main / v1.1.0" style="width:150px">
|
<input type="text" name="target" placeholder="stable / beta / dev / v1.2.4" style="width:170px">
|
||||||
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Обновить панель и скрипты?","Update panel and scripts?")}')">{tr("Обновить","Update")}</button>
|
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Обновить панель и скрипты?","Update panel and scripts?")}')">{tr("Обновить","Update")}</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="post" style="display:inline">
|
||||||
|
<input type="hidden" name="action" value="check">
|
||||||
|
<button class="btn btn-sm" style="background:#0ea5e9" type="submit">{tr("Проверить обновления","Check for updates")}</button>
|
||||||
|
</form>
|
||||||
<form method="post" style="margin-top:8px">
|
<form method="post" style="margin-top:8px">
|
||||||
<input type="hidden" name="action" value="rollback">
|
<input type="hidden" name="action" value="rollback">
|
||||||
<button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('{tr("Откатить на предыдущую версию?","Roll back to the previous version?")}')">{tr("Откатить на предыдущую","Rollback")}</button>
|
<button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('{tr("Откатить на предыдущую версию?","Roll back to the previous version?")}')">{tr("Откатить на предыдущую","Rollback")}</button>
|
||||||
|
|||||||
52
install.sh
52
install.sh
@@ -25,11 +25,31 @@ PANEL_PASS="${PANEL_PASS:-OcAdmin2026!}"
|
|||||||
TG_TOKEN="${TG_TOKEN:-}"
|
TG_TOKEN="${TG_TOKEN:-}"
|
||||||
TG_CHAT="${TG_CHAT:-}"
|
TG_CHAT="${TG_CHAT:-}"
|
||||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
||||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
# Каналы: stable (открытый, публичный repo) · beta (тест) · dev (разработка).
|
||||||
GH_TOKEN="${GH_TOKEN:-}" # set for private-repo installs; empty for public
|
# beta и dev живут в ПРИВАТНОМ repo PCA_Phobos-dev — доступ по токену подписчика
|
||||||
|
# (GH_TOKEN, read-only, выдаётся по подписке Boosty). stable — без токена.
|
||||||
|
GH_TOKEN="${GH_TOKEN:-}"
|
||||||
|
PUB_REPO="andrey271192/PCA_Phobos"
|
||||||
|
DEV_REPO="andrey271192/PCA_Phobos-dev"
|
||||||
|
CHANNEL="${CHANNEL:-stable}"
|
||||||
|
case "$CHANNEL" in
|
||||||
|
beta) REPO="$DEV_REPO"; PCA_BRANCH="${PCA_BRANCH:-beta}";;
|
||||||
|
dev) REPO="$DEV_REPO"; PCA_BRANCH="${PCA_BRANCH:-dev}";;
|
||||||
|
stable|main|"") REPO="$PUB_REPO"; PCA_BRANCH="${PCA_BRANCH:-main}";;
|
||||||
|
*) REPO="$PUB_REPO"; PCA_BRANCH="${PCA_BRANCH:-$CHANNEL}";;
|
||||||
|
esac
|
||||||
|
if [ "$REPO" = "$DEV_REPO" ] && [ -z "$GH_TOKEN" ]; then
|
||||||
|
echo "Канал '$CHANNEL' закрыт — это $([ "$CHANNEL" = dev ] && echo 'разработка (нестабильно)' || echo 'бета (тест)'), приватный репозиторий."
|
||||||
|
echo "Нужен read-only токен подписчика:"
|
||||||
|
echo " GH_TOKEN=ваш_токен CHANNEL=$CHANNEL bash <(curl -fsSL https://raw.githubusercontent.com/$PUB_REPO/main/install.sh)"
|
||||||
|
echo "Токен выдаётся по подписке: https://boosty.to/andrey27"
|
||||||
|
echo "Стабильная версия ставится без токена (CHANNEL=stable, по умолчанию)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ "$REPO" = "$DEV_REPO" ] && echo "Токен принят — закрытый канал '$CHANNEL' (приватный repo)."
|
||||||
PHOBOS_DIR="/opt/Phobos"
|
PHOBOS_DIR="/opt/Phobos"
|
||||||
PANEL_DIR="/opt/phobos-panel"
|
PANEL_DIR="/opt/phobos-panel"
|
||||||
RAW="https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}"
|
RAW="https://raw.githubusercontent.com/${REPO}/${PCA_BRANCH}"
|
||||||
|
|
||||||
[ "$EUID" -eq 0 ] || { echo "Run as root"; exit 1; }
|
[ "$EUID" -eq 0 ] || { echo "Run as root"; exit 1; }
|
||||||
|
|
||||||
@@ -65,12 +85,27 @@ if [ ! -x /usr/local/bin/wg-obfuscator ]; then
|
|||||||
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
|
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
|
||||||
git config core.sparseCheckout true; echo "wg-obfuscator" > .git/info/sparse-checkout
|
git config core.sparseCheckout true; echo "wg-obfuscator" > .git/info/sparse-checkout
|
||||||
git pull origin main -q
|
git pull origin main -q
|
||||||
cp -f "wg-obfuscator/bin/wg-obfuscator-${ARCH}" "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
cp -f "wg-obfuscator/bin/"wg-obfuscator-* "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
||||||
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* 2>/dev/null || true
|
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* 2>/dev/null || true
|
||||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator
|
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
||||||
cd /; rm -rf "$R"
|
cd /; rm -rf "$R"
|
||||||
fi
|
fi
|
||||||
|
_OBF_TAG=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
|
||||||
|
_CB="https://github.com/ClusterM/wg-obfuscator/releases/download/${_OBF_TAG}"
|
||||||
|
for _am in "aarch64:linux-arm64" "mipsel:linux-mipsel-mips32" "mips:linux-mips-mips32" "armv7:linux-armv7-hf"; do
|
||||||
|
_da="${_am%%:*}"; _ss="${_am##*:}"
|
||||||
|
if [ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" ] && [ -n "$_OBF_TAG" ]; then
|
||||||
|
_t=$(mktemp -d)
|
||||||
|
curl -fsSL -m30 "${_CB}/wg-obfuscator-${_OBF_TAG}-${_ss}.tar.gz" -o "$_t/o.tgz" 2>/dev/null &&
|
||||||
|
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null &&
|
||||||
|
_b=$(find "$_t" -name "wg-obfuscator" -type f | head -1) &&
|
||||||
|
[ -n "$_b" ] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && echo " +wg-obfuscator-${_da} (ClusterM ${_OBF_TAG})"
|
||||||
|
rm -rf "$_t"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ -x /usr/local/bin/wg-obfuscator ] || ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
||||||
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
|
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
|
||||||
|
echo " bins: $(ls $PHOBOS_DIR/bin/ | tr '\n' ' ')"
|
||||||
|
|
||||||
# ── 3. Phobos repo (onboarding scripts) ──
|
# ── 3. Phobos repo (onboarding scripts) ──
|
||||||
echo "[3/9] Phobos repo (onboarding scripts)..."
|
echo "[3/9] Phobos repo (onboarding scripts)..."
|
||||||
@@ -240,6 +275,13 @@ if [ -f "$PHOBOS_DIR/server/phobos-router-watchdog.py" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
# persist channel + subscriber token so phobos-update can reach private channels
|
||||||
|
mkdir -p "$PANEL_DIR"
|
||||||
|
echo "$CHANNEL" > "$PANEL_DIR/.channel" 2>/dev/null || true
|
||||||
|
if [ "$REPO" = "$DEV_REPO" ] && [ -n "$GH_TOKEN" ]; then
|
||||||
|
( umask 077; printf %s "$GH_TOKEN" > "$PANEL_DIR/.gh_token" )
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " Installation complete"
|
echo " Installation complete"
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ check_root
|
|||||||
load_env
|
load_env
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
|
|
||||||
|
# server.env stores OBFUSCATOR_PORTS (plural, comma list). Client obfuscator
|
||||||
|
# must target an ACTUAL listening port. lib-core load_env defaults OBFUSCATOR_PORT
|
||||||
|
# to 51821 (NOT listened on) -> handshake failed. Override unconditionally from
|
||||||
|
# the real port list when present.
|
||||||
|
[ -n "${OBFUSCATOR_PORTS:-}" ] && export OBFUSCATOR_PORT="$(echo "$OBFUSCATOR_PORTS" | cut -d',' -f1)"
|
||||||
|
|
||||||
CMD="${1:-help}"
|
CMD="${1:-help}"
|
||||||
CLIENT_ARG="${2:-}"
|
CLIENT_ARG="${2:-}"
|
||||||
EXTRA_ARG="${3:-}"
|
EXTRA_ARG="${3:-}"
|
||||||
@@ -209,8 +215,21 @@ action_package() {
|
|||||||
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
|
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
|
||||||
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
|
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
|
||||||
|
|
||||||
for arch in mipsel mips aarch64 armv7 x86_64; do
|
# Fetch missing router-arch binaries from ClusterM on-demand
|
||||||
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$arch" "$pkg_root/bin/"
|
local _tag; _tag=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*//')
|
||||||
|
local _cb="https://github.com/ClusterM/wg-obfuscator/releases/download/${_tag}"
|
||||||
|
declare -A _am=([aarch64]="linux-arm64" [mipsel]="linux-mipsel-mips32" [mips]="linux-mips-mips32" [armv7]="linux-armv7-hf" [x86_64]="linux-x64")
|
||||||
|
for _arch in aarch64 mipsel mips armv7 x86_64; do
|
||||||
|
if [[ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && [[ -n "$_tag" ]]; then
|
||||||
|
local _t; _t=$(mktemp -d)
|
||||||
|
if curl -fsSL -m30 "${_cb}/wg-obfuscator-${_tag}-${_am[$_arch]}.tar.gz" -o "$_t/o.tgz" 2>/dev/null; then
|
||||||
|
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null || true
|
||||||
|
local _b; _b=$(find "$_t" -name "wg-obfuscator" -type f | head -1)
|
||||||
|
[[ -n "$_b" ]] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && log_info "+wg-obfuscator-$_arch (ClusterM ${_tag})"
|
||||||
|
fi
|
||||||
|
rm -rf "$_t"
|
||||||
|
fi
|
||||||
|
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" "$pkg_root/bin/"
|
||||||
done
|
done
|
||||||
|
|
||||||
local tpl_dir="$REPO_DIR/client/templates"
|
local tpl_dir="$REPO_DIR/client/templates"
|
||||||
@@ -249,7 +268,7 @@ action_package() {
|
|||||||
source "$PHOBOS_DIR/server/server.env"
|
source "$PHOBOS_DIR/server/server.env"
|
||||||
cat > "$pkg_root/failover.conf" <<FAILEOF
|
cat > "$pkg_root/failover.conf" <<FAILEOF
|
||||||
# Phobos Failover Configuration
|
# Phobos Failover Configuration
|
||||||
SERVER_1=${SERVER_PUBLIC_IP_V4}:51821,51822,51823
|
SERVER_1=${SERVER_PUBLIC_IP_V4}:${OBFUSCATOR_PORTS:-51821,51822,51823}
|
||||||
KEY_1=${OBFUSCATOR_KEY}
|
KEY_1=${OBFUSCATOR_KEY}
|
||||||
FAILEOF
|
FAILEOF
|
||||||
|
|
||||||
|
|||||||
105
update.sh
105
update.sh
@@ -2,24 +2,47 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# PCA Phobos — updater / rollback
|
# PCA Phobos — updater / rollback
|
||||||
#
|
#
|
||||||
# phobos-update # обновить до последней версии (main)
|
# phobos-update # обновить до стабильной (канал stable / main)
|
||||||
# phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
# phobos-update stable # стабильный канал (публичный, без токена)
|
||||||
|
# GH_TOKEN=... phobos-update beta # бета-канал (приватный, по токену подписчика)
|
||||||
|
# GH_TOKEN=... phobos-update dev # канал разработки (приватный, по токену)
|
||||||
|
# phobos-update v1.2.4 # конкретная версия (git tag)
|
||||||
# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
|
# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
|
||||||
# phobos-update --version # показать установленную и последнюю версию
|
# phobos-update --check # установленная + версии всех каналов
|
||||||
# phobos-update --list # список бэкапов для отката
|
# phobos-update --list # список бэкапов для отката
|
||||||
#
|
#
|
||||||
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты онбординга/служебные).
|
# Каналы:
|
||||||
# НЕ трогает ключи WireGuard, server.env, клиентов — они сохраняются.
|
# stable -> публичный репозиторий andrey271192/PCA_Phobos (открыт)
|
||||||
# Перед каждым обновлением делает бэкап; откат восстанавливает его.
|
# beta / dev -> приватный andrey271192/PCA_Phobos-dev (только подписчики)
|
||||||
|
# Для beta/dev нужен read-only токен подписчика: env GH_TOKEN или файл
|
||||||
|
# $PANEL_DIR/.gh_token (сохраняется при установке закрытого канала).
|
||||||
|
#
|
||||||
|
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты). НЕ трогает ключи WireGuard,
|
||||||
|
# server.env, клиентов. Перед каждым обновлением — бэкап; откат восстанавливает.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
set -e
|
set -e
|
||||||
PHOBOS_DIR=/opt/Phobos
|
PHOBOS_DIR=/opt/Phobos
|
||||||
PANEL_DIR=/opt/phobos-panel
|
PANEL_DIR=/opt/phobos-panel
|
||||||
REPO=andrey271192/PCA_Phobos
|
PUB_REPO=andrey271192/PCA_Phobos
|
||||||
RAW="https://raw.githubusercontent.com/$REPO"
|
DEV_REPO=andrey271192/PCA_Phobos-dev
|
||||||
BACKUPS="$PHOBOS_DIR/updates"
|
BACKUPS="$PHOBOS_DIR/updates"
|
||||||
VERFILE="$PANEL_DIR/.version"
|
VERFILE="$PANEL_DIR/.version"
|
||||||
CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
|
CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
|
||||||
|
# Токен подписчика для приватных каналов: env > сохранённый файл
|
||||||
|
GH_TOKEN="${GH_TOKEN:-$(cat "$PANEL_DIR/.gh_token" 2>/dev/null || true)}"
|
||||||
|
|
||||||
|
# repo for a ref/channel: beta/dev/*-beta/*-dev -> private, остальное -> public
|
||||||
|
repo_for() { case "$1" in beta|dev|*-beta|*-dev) echo "$DEV_REPO";; *) echo "$PUB_REPO";; esac; }
|
||||||
|
is_private() { case "$1" in beta|dev|*-beta|*-dev) return 0;; *) return 1;; esac; }
|
||||||
|
# curl with token header only when ref is a private channel
|
||||||
|
gh_curl() { # $1=ref, rest=curl args
|
||||||
|
local ref="$1"; shift
|
||||||
|
if is_private "$ref" && [ -n "$GH_TOKEN" ]; then
|
||||||
|
curl -H "Authorization: token $GH_TOKEN" "$@"
|
||||||
|
else
|
||||||
|
curl "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
managed_files() {
|
managed_files() {
|
||||||
cat <<LIST
|
cat <<LIST
|
||||||
@@ -35,7 +58,13 @@ $PHOBOS_DIR/server/update.sh|update.sh
|
|||||||
LIST
|
LIST
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_version() { curl -fsSL -m10 "$RAW/main/VERSION" 2>/dev/null | tr -d ' \r\n'; }
|
# fetch VERSION of a channel (public repos / or private if token present)
|
||||||
|
chan_version() { # $1 = ref(channel)
|
||||||
|
local repo; repo=$(repo_for "$1"); local repo_ref="$1"
|
||||||
|
if is_private "$1" && [ -z "$GH_TOKEN" ]; then echo ""; return; fi
|
||||||
|
gh_curl "$1" -fsSL -m8 "https://raw.githubusercontent.com/$repo/$1/VERSION" 2>/dev/null | tr -d ' \r\n'
|
||||||
|
}
|
||||||
|
latest_version() { curl -fsSL -m10 "https://raw.githubusercontent.com/$PUB_REPO/main/VERSION" 2>/dev/null | tr -d ' \r\n'; }
|
||||||
|
|
||||||
do_backup() {
|
do_backup() {
|
||||||
local dir="$BACKUPS/${CUR}-$(date +%Y%m%d-%H%M%S)"
|
local dir="$BACKUPS/${CUR}-$(date +%Y%m%d-%H%M%S)"
|
||||||
@@ -48,10 +77,12 @@ do_backup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply_ref() {
|
apply_ref() {
|
||||||
local ref="$1"
|
local ref="$1"; local repo; repo=$(repo_for "$ref")
|
||||||
|
local repo_ref="$ref"
|
||||||
|
local RAW="https://raw.githubusercontent.com/$repo/$ref"
|
||||||
managed_files | while IFS='|' read -r lp rp; do
|
managed_files | while IFS='|' read -r lp rp; do
|
||||||
local tmp; tmp=$(mktemp)
|
local tmp; tmp=$(mktemp)
|
||||||
if curl -fsSL -m25 "$RAW/$ref/$rp" -o "$tmp" && [ -s "$tmp" ]; then
|
if gh_curl "$ref" -fsSL -m25 "$RAW/$rp" -o "$tmp" && [ -s "$tmp" ]; then
|
||||||
mkdir -p "$(dirname "$lp")"; mv "$tmp" "$lp"
|
mkdir -p "$(dirname "$lp")"; mv "$tmp" "$lp"
|
||||||
case "$lp" in *.sh|*.py) chmod +x "$lp" 2>/dev/null || true;; esac
|
case "$lp" in *.sh|*.py) chmod +x "$lp" 2>/dev/null || true;; esac
|
||||||
else
|
else
|
||||||
@@ -60,10 +91,33 @@ apply_ref() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref_exists() {
|
||||||
|
local repo; repo=$(repo_for "$1"); local repo_ref="$1"
|
||||||
|
gh_curl "$1" -fsSL -m12 "https://raw.githubusercontent.com/$repo/$1/app.py" -o /dev/null 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--version|-v)
|
--version|-v|--check)
|
||||||
echo "Установлено: $CUR"
|
echo "Установлено: $CUR"
|
||||||
echo "Доступно: $(latest_version)"
|
echo "Стабильная: $(latest_version) [открытый]"
|
||||||
|
b=$(chan_version beta); d=$(chan_version dev)
|
||||||
|
if [ -n "$GH_TOKEN" ]; then
|
||||||
|
[ -n "$b" ] && echo "Бета (beta): $b [приватный, токен ок]"
|
||||||
|
[ -n "$d" ] && echo "Разработка(dev):$d [приватный, токен ок]"
|
||||||
|
else
|
||||||
|
echo "Бета (beta): — [приватный: нужен GH_TOKEN подписчика]"
|
||||||
|
echo "Разработка(dev):— [приватный: нужен GH_TOKEN подписчика]"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--versions|--tags)
|
||||||
|
echo "Стабильные версии (публичные теги):"
|
||||||
|
curl -fsSL -m10 "https://api.github.com/repos/$PUB_REPO/tags?per_page=100" 2>/dev/null \
|
||||||
|
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
|
||||||
|
if [ -n "$GH_TOKEN" ]; then
|
||||||
|
echo "Закрытые версии (приватные теги, beta/dev):"
|
||||||
|
curl -fsSL -m10 -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$DEV_REPO/tags?per_page=100" 2>/dev/null \
|
||||||
|
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
--list)
|
--list)
|
||||||
echo "Бэкапы (для отката):"
|
echo "Бэкапы (для отката):"
|
||||||
@@ -82,8 +136,27 @@ case "${1:-}" in
|
|||||||
echo "Откат выполнен -> $fromver"
|
echo "Откат выполнен -> $fromver"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
|
ref="${1:-main}"
|
||||||
target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
case "$ref" in latest|stable) ref="main";; esac
|
||||||
|
# Каналы beta/dev — приватный репозиторий, нужен токен подписчика
|
||||||
|
if is_private "$ref" && [ -z "$GH_TOKEN" ]; then
|
||||||
|
echo "Канал '$ref' закрыт (бета/разработка) — приватный репозиторий."
|
||||||
|
echo "Нужен read-only токен подписчика:"
|
||||||
|
echo " GH_TOKEN=ваш_токен phobos-update $ref"
|
||||||
|
echo "Токен — по подписке: https://boosty.to/andrey27 . Стабильная (без токена): phobos-update stable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! ref_exists "$ref"; then
|
||||||
|
echo "Версия/ветка '$ref' не найдена (или токен неверный). Обновление отменено (текущая $CUR не тронута)."
|
||||||
|
echo "Доступные: phobos-update --versions · стабильная: $(latest_version)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# сохранить токен для будущих обновлений приватного канала
|
||||||
|
if is_private "$ref" && [ -n "$GH_TOKEN" ]; then
|
||||||
|
( umask 077; printf %s "$GH_TOKEN" > "$PANEL_DIR/.gh_token" )
|
||||||
|
fi
|
||||||
|
repo_ref="$ref"
|
||||||
|
target=$(gh_curl "$ref" -fsSL -m10 "https://raw.githubusercontent.com/$(repo_for "$ref")/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
||||||
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
||||||
b=$(do_backup); echo "Бэкап: $b"
|
b=$(do_backup); echo "Бэкап: $b"
|
||||||
apply_ref "$ref"
|
apply_ref "$ref"
|
||||||
|
|||||||
Reference in New Issue
Block a user