19 Commits

Author SHA1 Message Date
Андрей Бобырев
4c1c82afec stable 1.2.8: scannable QR (ECC L + native size)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:21:38 +03:00
Андрей Бобырев
0f98c074c8 docs: add full server uninstall section + link
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:11:06 +03:00
Андрей Бобырев
286142bd7a docs: APK downloaded via in-panel button (beta/dev), drop manual .apk placement note
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:07:47 +03:00
Андрей Бобырев
cf4b5f79b5 docs: CHANGELOG + troubleshooting (handshake/aarch64/phobos-import/uninstall) + update steps
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:06:00 +03:00
Андрей Бобырев
5f97c6c6d7 feat: server uninstall.sh (full removal: panel, obfuscator, wg0, nginx, cron, iptables, dirs)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:59:49 +03:00
Андрей Бобырев
14bb6ae589 stable 1.2.7: phobos:// pad missing fields with '= none' (PhobosWG import fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:57:21 +03:00
Андрей Бобырев
51712310f6 fix: force OBFUSCATOR_PORT from OBFUSCATOR_PORTS[0] (override lib-core 51821 default)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 13:56:01 +03:00
Андрей Бобырев
6ec1782d8b fix(stable): client obfuscator targets real listening port (OBFUSCATOR_PORTS[0])
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 13:53:37 +03:00
Андрей Бобырев
e61ec09b18 fix: find -type f for wg-obfuscator binary in archive (tar unpacks to dir)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:26:40 +03:00
Андрей Бобырев
ed3071a971 bump stable to 1.2.6 (aarch64 router binary fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:24:35 +03:00
Андрей Бобырев
db4cab7a38 fix(stable): fetch router-arch binaries from ClusterM (aarch64, mipsel, armv7)
Keenetic routers are aarch64 but Ground-Zerro only ships x86_64. Installer
now copies all Ground-Zerro binaries + fetches missing arm64/mipsel/armv7
from ClusterM/wg-obfuscator latest release so action_package includes the
right binary for every router architecture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:24:35 +03:00
Андрей Бобырев
9f402be886 fix(stable): api.py in managed_files (port from dev)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 02:48:35 +03:00
Андрей Бобырев
8971565e6e fix: add server/api.py to managed_files (phobos-update now deploys agent)
The secondary agent api.py was not included in the update manifest, so
phobos-update left stale api.py on the server after channel switches.
Adding it ensures bandwidth-aware /api/health reaches secondaries on update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 02:48:35 +03:00
Андрей Бобырев
b6525bc0ae stable 1.2.5: token field + channel buttons in version card
Ports the channel UI to stable so users who installed the open stable
version can paste their subscriber token (save_token) and switch to
beta/dev straight from Настройки → Версия и обновления. Adds
gh_token_present/save_gh_token helpers and stable/beta/dev quick buttons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:57:19 +03:00
Андрей Бобырев
d072c8227f feat: private dev/beta channels via subscriber token (repo split)
Closed channels (beta, dev) now live in a separate PRIVATE repository
andrey271192/PCA_Phobos-dev. Access is gated by a read-only subscriber
GH_TOKEN (distributed via Boosty) instead of a shared hash key — the
token both authenticates the private raw fetch AND acts as the gate, so
there is a single secret and the code is genuinely hidden (public repo
holds stable/main only).

install.sh: CHANNEL=beta|dev -> private repo, requires GH_TOKEN, persists
it to $PANEL_DIR/.gh_token + writes .channel. stable stays public/no-token.
update.sh: per-ref repo routing (repo_for), gh_curl injects the token for
private channels, token read from env or saved .gh_token, --check/--versions
reflect public vs private, stable path unchanged and tokenless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:12:32 +03:00
Андрей Бобырев
7b941dc532 docs: README channels — stable open, beta/dev key-gated (Boosty)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 00:59:45 +03:00
Андрей Бобырев
8c1654ecaa feat: 3-channel model (stable open / beta + dev key-gated)
Channels:
  stable -> branch main  (open, no key)
  beta   -> branch beta  (release candidate, key-gated)
  dev    -> branch dev    (active development, key-gated)

beta and dev require PHOBOS_KEY (subscriber key distributed via Boosty);
only the sha256 hash is embedded in install.sh / update.sh. stable stays
open. install.sh honours CHANNEL=stable|beta|dev; phobos-update accepts
stable|beta|dev|<tag> and gates the closed channels. --check now lists all
three channels with a [закрыт ключом] marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 00:58:06 +03:00
phobos
dc83d90a35 v1.2.4: install stable by default; beta opt-in via CHANNEL=beta (documented) 2026-05-31 00:38:50 +03:00
phobos
e0ea54e83a v1.2.3: debounce server DOWN alerts (3 cycles + retry) fix flapping; update.sh aborts on missing version + --check/--versions + beta channel; panel Check button 2026-05-30 23:52:47 +03:00
8 changed files with 457 additions and 46 deletions

70
CHANGELOG.md Normal file
View File

@@ -0,0 +1,70 @@
# Changelog — PCA Phobos
Каналы: **stable** (публичный, без токена) · **beta** / **dev** (приватный repo, токен подписчика).
Обновление: `phobos-update stable` · `GH_TOKEN=… phobos-update beta|dev` · из панели → «Версия и обновления».
---
## Как обновиться после фиксов
```bash
# stable (открытый)
phobos-update stable
# beta/dev (токен уже сохранён в /opt/phobos-panel/.gh_token)
phobos-update beta # или: phobos-update dev
```
Затем **пересоздать клиентов** в панели (старые конфиги могли содержать неверный порт обфускатора).
Обновляются: панель, скрипты онбординга, агент, updater. Ключи WireGuard, `server.env`, клиенты — НЕ трогаются.
---
## stable
### v1.2.7
- **fix:** импорт `phobos://` в PhobosWG. Конфиги дополняются `= none` для недостающих полей (`PresharedKey` и др.) перед base64 — иначе приложение писало «невозможно импортировать туннель».
### v1.2.6
- **fix:** бинарники wg-obfuscator для роутеров. Keenetic = aarch64, а Ground-Zerro раздаёт только x86_64. Теперь install.sh и сборка пакета клиента авто-качают aarch64/mipsel/armv7 из ClusterM/wg-obfuscator releases.
### v1.2.5
- **feat:** поле токена подписчика + кнопки каналов (stable/beta/dev) прямо в карточке «Версия и обновления». Подписчик вставляет токен в панель и переключается на beta/dev без консоли.
### v1.2.4
- **feat:** установка по умолчанию стабильная; beta/dev — опционально (`CHANNEL=`).
### v1.2.3
- **fix:** дебаунс DOWN-алертов (3 цикла + повтор) — убирает ложный флап серверов.
- **fix:** `phobos-update` отменяет обновление на несуществующую версию (текущая не трогается).
- **feat:** `--check` / `--versions`, кнопка «Проверить обновления».
### v1.2.2
- контакт во всех местах → группа `@PCAdministration`.
---
## Критичные фиксы (общие, во всех каналах)
| Версия | Проблема | Решение |
|--------|----------|---------|
| 1.2.7 / 1.3.x | PhobosWG: «невозможно импортировать туннель» | `phobos://` дополняется `= none` для всех обязательных полей (`PresharedKey`) по спеке формата |
| 1.2.6 / 1.3.x | Роутер aarch64: «бинарник не найден в архиве» | авто-загрузка aarch64/mipsel/armv7 из ClusterM releases |
| (внутр.) | Клиент не подключается, «НЕТ СОЕДИНЕНИЯ», handshake не встаёт | клиентский обфускатор целился в порт 51821 (никто не слушал); теперь берёт первый из `OBFUSCATOR_PORTS` (2083) |
| (внутр.) | агент secondary не обновлялся через `phobos-update` | `server/api.py` добавлен в managed_files |
---
## beta (приватный)
### v1.3.0-beta
- скачивание `.conf` для телефона (Android `phobos://` + QR, iOS обычный WireGuard + QR);
- кнопка скачивания приложения **PhobosWG (APK)** — авто-загрузка из GitHub Release приватного repo;
- все критичные фиксы выше.
---
## dev (приватный, активная разработка)
### v1.3.1-dev
- **баланс по трафику:** агенты отдают пропускную способность канала (rx/tx NIC), `server_load = 0.45·cpu + 0.30·mem + 0.25·bw`; карточки серверов показывают ↕ Mbps;
- все фиксы и фичи из beta.

View File

@@ -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
@@ -101,7 +113,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей. > Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
**Приложения:** **Приложения:**
- **Android (PhobosWG):** автор пока не публикует APK официально. Положи `.apk` на сервер в `/opt/Phobos/www/app/PhobosWG.apk` — панель сама покажет кнопку «Скачать приложение PhobosWG» на странице Android-клиента (раздаётся по `http://<сервер>/app/PhobosWG.apk`). - **Android (PhobosWG):** на странице Android-клиента в панели есть кнопка **«Скачать приложение PhobosWG (APK)»** — приложение скачивается автоматически (доступно в каналах beta/dev). Установи APK, затем импортируй `phobos://`-ссылку или QR.
- **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента. - **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента.
--- ---
@@ -126,6 +138,22 @@ phobos-update --list # список бэкапов
--- ---
## Полное удаление сервера
Одной командой (спросит подтверждение):
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)
```
Без подтверждения: `PURGE=1 bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)`
Удаляет: веб-панель, обфускатор-сервисы, WireGuard `wg0`, агент, nginx-сайт, watchdog-cron, iptables-правила, бинарники (`wg-obfuscator`, `phobos-update`) и каталоги `/opt/Phobos`, `/opt/phobos-panel`. Общие пакеты (wireguard, nginx, cron) НЕ трогает.
**Удаление с роутера** (на самом роутере): `/opt/etc/Phobos/phobos-uninstall.sh`
---
## Возможные проблемы и решения ## Возможные проблемы и решения
| Симптом | Причина | Решение | | Симптом | Причина | Решение |
@@ -140,6 +168,10 @@ phobos-update --list # список бэкапов
| **Смена сервера применяется не сразу** | роутер тянет конфиг по cron | Применяется за ~12–15 сек, не мгновенно | | **Смена сервера применяется не сразу** | роутер тянет конфиг по cron | Применяется за ~12–15 сек, не мгновенно |
| **Забыл порт панели** | случайный порт | `cat /opt/phobos-panel/.port` | | **Забыл порт панели** | случайный порт | `cat /opt/phobos-panel/.port` |
| **Забыл API key** | | `grep server_api_key /opt/phobos-panel/settings.json` | | **Забыл API key** | | `grep server_api_key /opt/phobos-panel/settings.json` |
| **PhobosWG: «невозможно импортировать туннель»** | старая панель генерила неполный `phobos://` (без `PresharedKey`) | Обнови панель, заново скопируй `phobos://`-ссылку или пересканируй QR. Исправлено в v1.2.7 |
| **Роутер aarch64: «бинарник не найден в архиве»** | Ground-Zerro раздаёт только x86_64 | Обнови (`phobos-update`) — aarch64/mipsel/armv7 авто-качаются из ClusterM, затем пересоздай клиента. Исправлено в v1.2.6 |
| **Клиент не подключается, «НЕТ СОЕДИНЕНИЯ»** | клиентский обфускатор целился в неверный порт (51821) | Обнови панель → **пересоздай клиента** → переустанови на роутере. Порт берётся из `OBFUSCATOR_PORTS` |
| **Удалить сервер полностью** | | `bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)` |
Логи: `journalctl -u phobos-panel -n 50` · сторож: `/opt/Phobos/server/watchdog.log` · роутер: `/opt/etc/Phobos/health.log`. Логи: `journalctl -u phobos-panel -n 50` · сторож: `/opt/Phobos/server/watchdog.log` · роутер: `/opt/etc/Phobos/health.log`.

View File

@@ -1 +1 @@
1.2.2 1.2.8

145
app.py
View File

@@ -479,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 = {}
@@ -503,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
@@ -653,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:
@@ -691,16 +721,56 @@ 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
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=4, border=3)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO() buf = io.BytesIO()
qrcode.make(data).save(buf, format="PNG") img.save(buf, format="PNG")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
except Exception: except Exception:
return "" return ""
@@ -777,7 +847,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()
@@ -814,19 +884,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)
@@ -1371,9 +1446,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()
@@ -1412,11 +1497,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>
@@ -1589,7 +1694,7 @@ def client_phone(cid, mode):
{nav('clients')} {nav('clients')}
<div class="card" style="text-align:center"> <div class="card" style="text-align:center">
<h2>{cid} — {title}</h2> <h2>{cid} — {title}</h2>
<img src="{qr}" alt="QR" style="width:300px;height:300px;background:#fff;padding:10px;border-radius:10px"><br> <img src="{qr}" alt="QR" style="max-width:340px;width:100%;background:#fff;padding:10px;border-radius:10px;image-rendering:pixelated"><br>
<p style="margin-top:10px">{tr("Отсканируй QR в приложении или импортируй конфиг:", "Scan the QR in the app or import the config:")}</p> <p style="margin-top:10px">{tr("Отсканируй QR в приложении или импортируй конфиг:", "Scan the QR in the app or import the config:")}</p>
{extra} {extra}
<textarea readonly onclick="this.select()" style="width:100%;height:250px;font-family:monospace;font-size:.78em">{conf}</textarea> <textarea readonly onclick="this.select()" style="width:100%;height:250px;font-family:monospace;font-size:.78em">{conf}</textarea>

View File

@@ -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"

View File

@@ -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

70
uninstall.sh Executable file
View File

@@ -0,0 +1,70 @@
#!/bin/bash
# ============================================================
# PCA Phobos — полное удаление сервера (primary / panel node)
#
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)
#
# Останавливает и удаляет: панель, обфускатор-сервисы, WireGuard wg0,
# nginx-сайт, watchdog-cron, бинарники, iptables-правила, каталоги Phobos.
# По умолчанию спрашивает подтверждение. PURGE=1 — без вопросов.
# ============================================================
set -u
PHOBOS_DIR="/opt/Phobos"
PANEL_DIR="/opt/phobos-panel"
[ "$(id -u)" -eq 0 ] || { echo "Запусти от root"; exit 1; }
echo "=================================================="
echo " PCA Phobos — УДАЛЕНИЕ сервера"
echo " Будут удалены: панель, obfuscator, wg0, nginx-сайт,"
echo " watchdog, бинарники, $PHOBOS_DIR, $PANEL_DIR"
echo "=================================================="
if [ "${PURGE:-0}" != "1" ]; then
printf "Точно удалить ВСЁ? [yes/NO]: "
read ans
[ "$ans" = "yes" ] || { echo "Отменено."; exit 0; }
fi
IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
IFACE="${IFACE:-eth0}"
echo "[1/8] stop + disable services..."
# obfuscator services (any port)
for svc in $(systemctl list-units --all --plain --no-legend 'wg-obfuscator-*' 2>/dev/null | awk '{print $1}'); do
systemctl stop "$svc" 2>/dev/null; systemctl disable "$svc" 2>/dev/null
done
for svc in phobos-panel phobos-api wg-quick@wg0; do
systemctl stop "$svc" 2>/dev/null; systemctl disable "$svc" 2>/dev/null
done
echo "[2/8] remove systemd units..."
rm -f /etc/systemd/system/wg-obfuscator-*.service 2>/dev/null
rm -f /etc/systemd/system/phobos-panel.service /etc/systemd/system/phobos-api.service 2>/dev/null
systemctl daemon-reload 2>/dev/null
echo "[3/8] remove WireGuard wg0..."
rm -f /etc/wireguard/wg0.conf 2>/dev/null
echo "[4/8] remove iptables rules..."
iptables -t nat -D POSTROUTING -o "$IFACE" -j MASQUERADE 2>/dev/null || true
iptables -D FORWARD -i wg0 -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true
echo "[5/8] remove nginx site..."
rm -f /etc/nginx/sites-enabled/phobos /etc/nginx/sites-available/phobos 2>/dev/null
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
echo "[6/8] remove watchdog cron..."
( crontab -l 2>/dev/null | grep -v 'phobos-router-watchdog' | grep -v '/opt/Phobos' || true ) | crontab - 2>/dev/null || true
echo "[7/8] remove binaries / symlinks..."
rm -f /usr/local/bin/wg-obfuscator /usr/local/bin/phobos-update 2>/dev/null
echo "[8/8] remove directories..."
rm -rf "$PHOBOS_DIR" "$PANEL_DIR" 2>/dev/null
echo "=================================================="
echo " Phobos удалён."
echo " (WireGuard/nginx/cron-пакеты НЕ удалены — общие; убрать вручную при желании)"
echo "=================================================="

105
update.sh
View File

@@ -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"