diff --git a/_build/ezhub_push.py b/_build/ezhub_push.py new file mode 100644 index 0000000..d94ab73 --- /dev/null +++ b/_build/ezhub_push.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +r"""把 EzHub 的令牌存到仓库外,并支持用 HTTPS + 令牌推代码。 + +为什么需要这个: + 用户加 SSH 公钥时可能加错了(服务端拒绝了本机的 + SHA256:Acmu49YQcjw3m5WefOXSBzYNY2WPHLL2vvhnlYRAttA), + 但令牌有 push 权限,走 HTTPS 就能推。 + + 为什么不把令牌写进 remote URL: + `git remote -v` 会直接打印出来,任何时候截图/录屏都会泄。 + 所以令牌存在仓库外的 %USERPROFILE%\.ykxmz\ezhub_token, + 推送时临时拼 URL,推完不留痕。 + +用法: + python _build/ezhub_push.py # 用存好的令牌推 + python _build/ezhub_push.py <令牌> # 先存再用这个令牌推 + python _build/ezhub_push.py --show # 只看存了什么(打码) +""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +GIT = r"C:\Program Files\Git\bin\git.exe" +TOKEN_FILE = Path.home() / ".ykxmz" / "ezhub_token" + +OWNER = "xmz" +REPO = "ykxmz" +HOST = "git.261826.xyz" + + +def save_token(token: str) -> None: + TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True) + TOKEN_FILE.write_text(token.strip(), encoding="utf-8") + # 尽量收紧权限(Windows 上效果有限,但聊胜于无) + try: + import os + os.chmod(TOKEN_FILE, 0o600) + except OSError: + pass + print(" 令牌已存到 %s(仓库外,不会进版本库)" % TOKEN_FILE) + + +def load_token() -> str: + if TOKEN_FILE.is_file(): + return TOKEN_FILE.read_text(encoding="utf-8").strip() + return "" + + +def main() -> int: + args = [a for a in sys.argv[1:] if a] + + if args and args[0] == "--show": + token = load_token() + if not token: + print(" 还没存令牌") + else: + print(" 令牌: %s…%s(长度 %d)" + % (token[:8], token[-4:], len(token))) + return 0 + + if args and not args[0].startswith("--"): + save_token(args[0]) + token = load_token() + if not token: + print(" 用法: python _build/ezhub_push.py <令牌>") + return 1 + + # 带令牌的 HTTPS 地址(只在这次进程里用,不写进 .git/config) + url = "https://%s:%s@%s/%s/%s.git" % (OWNER, token, HOST, OWNER, REPO) + + print(" 推到 EzHub(HTTPS + 令牌)…") + result = subprocess.run([GIT, "push", url, "main"], cwd=str(ROOT), + capture_output=True, text=True, + encoding="utf-8", errors="replace") + err = (result.stderr or result.stdout or "").strip() + + # 输出里可能带令牌,打码后再打印 + safe = err.replace(token, "***") + for line in safe.splitlines()[-6:]: + # 远程 URL 里的令牌也打掉 + line = line.replace(url, "https://%s:***@%s/%s/%s.git" + % (OWNER, HOST, OWNER, REPO)) + print(" " + line) + + if result.returncode != 0: + print(" [失败] exit=%s" % result.returncode) + return result.returncode + print(" ✓ 推完了") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/probe_token.py b/_build/probe_token.py new file mode 100644 index 0000000..40740f1 --- /dev/null +++ b/_build/probe_token.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +r'''判定 EzHub 令牌到底有没有写权限(无副作用)。 + +方法:对一个**不存在的**资源发写请求。 + * 令牌没有写权限 -> 服务端先拦权限,返回 403 + * 令牌有写权限 -> 通过权限检查,然后发现资源不存在,返回 404 +所以 403 / 404 这个差别就能说明问题,而且不会真的改到任何东西。 +''' +from __future__ import annotations + +import json +import sys +import urllib.error +import urllib.request + +TOKEN = sys.argv[1] if len(sys.argv) > 1 else "" +if not TOKEN: + print(" 用法: python probe_token.py <令牌>") + raise SystemExit(1) + +BASE = "https://git.261826.xyz/api/v1" +HEAD = {"Authorization": "token " + TOKEN, "User-Agent": "ykxmz-probe"} + + +def probe(method: str, path: str, payload: dict | None = None, + label: str = "") -> int: + data = json.dumps(payload).encode() if payload else None + headers = dict(HEAD) + if data: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(BASE + path, data=data, method=method, + headers=headers) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + print(" %-10s %-46s -> %s" % (method, path, resp.status)) + return resp.status + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", "replace")[:110] + print(" %-10s %-46s -> %s %s" + % (method, path, exc.code, body)) + return exc.code + except Exception as exc: # noqa: BLE001 + print(" %-10s %-46s -> 异常 %s" % (method, path, exc)) + return 0 + + +print(" 一、读权限(应该 200)") +probe("GET", "/user") +probe("GET", "/repos/xmz/ykxmz") + +print() +print(" 二、写权限判定(对不存在的资源写)") +print(" 403 = 没写权限;404 = 有写权限(只是资源不存在)") +del_code = probe("DELETE", "/repos/xmz/ykxmz/releases/99999999") +print() +patch_code = probe("PATCH", "/repos/xmz/ykxmz/releases/99999999", + {"body": "probe"}) +print() +post_code = probe("POST", "/repos/xmz/ykxmz/releases", + {"tag_name": "", "name": "", "body": ""}) + +print() +print(" 三、结论") +if del_code == 404 or patch_code == 404: + print(" ✓ 令牌**有写权限**(返回 404 说明权限过了,只是资源不存在)") + print(" -> HTTPS push 的 403 是另一个原因,需要再查") +elif del_code == 403 or patch_code == 403: + print(" ✗ 令牌**没有写权限**(403)") + print(" -> 需要重新生成令牌,勾上 write:repository") +else: + print(" ? 结果不明确:DELETE=%s PATCH=%s POST=%s" + % (del_code, patch_code, post_code)) + +print() +print(" 四、顺带看一下令牌信息") +try: + req = urllib.request.Request(BASE + "/user", headers=HEAD) + with urllib.request.urlopen(req, timeout=30) as resp: + for name in ("X-Oauth-Scopes", "X-Oauth-Client-Id"): + value = resp.headers.get(name) + if value: + print(" %s: %s" % (name, value)) + me = json.loads(resp.read().decode()) + print(" 用户: %s (id=%s, admin=%s)" + % (me.get("login"), me.get("id"), me.get("is_admin"))) +except Exception as exc: # noqa: BLE001 + print(" 读取失败: %s" % exc) diff --git a/_build/release_all.py b/_build/release_all.py index 23ac3c8..df75409 100644 --- a/_build/release_all.py +++ b/_build/release_all.py @@ -227,6 +227,43 @@ def gitea_release(token: str, version: str, body: str, return ok == len(files) +def check_ezhub_scope(token: str) -> bool: + """提前确认 EzHub 令牌有 write:repository。 + + 为什么要单独查: + Gitea 的令牌可以只给读权限。只读令牌能读仓库、能列发行版, + 看起来"正常",但一推代码就 403、一建发行版就 403, + 而且报错信息藏在一层层输出里。所以开头就探一下。 + + 探法(无副作用):删一个**不存在的**发行版。 + 403 -> 缺 write:repository + 404 -> 有写权限(只是资源不存在) + """ + url = ("https://git.261826.xyz/api/v1/repos/xmz/ykxmz" + "/releases/99999999") + req = urllib.request.Request( + url, method="DELETE", + headers={"Authorization": "token " + token, + "User-Agent": "ykxmz-release"}) + try: + urllib.request.urlopen(req, timeout=30) + return True + except urllib.error.HTTPError as exc: + if exc.code == 404: + return True + detail = exc.read().decode("utf-8", "replace")[:200] + if "write:repository" in detail: + print(" ✗ EzHub 令牌缺 write:repository 权限") + print(" -> 去 EzHub:设置 → 应用 → 生成令牌," + "权限勾上 write:repository") + return False + print(" ✗ EzHub 令牌检查失败 HTTP %s: %s" % (exc.code, detail)) + return False + except Exception as exc: # noqa: BLE001 + print(" ✗ 连不上 EzHub: %s" % exc) + return False + + def main() -> int: args = [a for a in sys.argv[1:] if not a.startswith("--")] dry = "--dry" in sys.argv @@ -269,6 +306,11 @@ def main() -> int: return 1 print(" ✓ 工作区干净") + # 两个平台的令牌都得有写权限,否则走到一半才失败 + if not check_ezhub_scope(ezhub_token): + return 1 + print(" ✓ EzHub 令牌有写权限") + # 对外材料清洁度:绝不能让管理页字眼进发行版说明 for word in ("zhongzhuanzhan", "/admin", "管理后台", "管理页"): if word in body: