diff --git a/README.md b/README.md index cc2717a..778cc1f 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ for p in 9005 9006 9007 9008; do timeout 3 bash -c "cat < /dev/null > /dev/tcp/< ## 版本要点 -- **2026-09-13**:新增 **dufe 银杏签到码查询服务**(`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`,Flask + waitress 16 线程,`127.0.0.1:9005`,端点 `GET|POST /dufe/ginkgostu_sign_in`),并在聚合层 `main.py` 注册该路由——为此新增 `proxy_forward()`(保留请求方法与请求体,原 `proxy()` 保持固定 GET 不变,零改动既有行为) +- **2026-09-13**:新增 **dufe 银杏签到码查询服务**(`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`,Flask + waitress 16 线程,`127.0.0.1:9005`,端点 `GET|POST /dufe/ginkgostu_sign_in`),并在聚合层 `main.py` 注册该路由——为此新增 `proxy_forward()`(保留请求方法与请求体,原 `proxy()` 保持固定 GET 不变,零改动既有行为)。同期修复该服务两处缺陷:base64 参数补 `urllib.parse.unquote`(兼容 iOS 快捷指令的 `%3D` 编码),`commit` 判定改为 fail-closed(须 `commit.success is True` 才算签到成功) - **2026-09-12** - 端口重排 `9888/9088/19990 → 9008/9007/9006`,全部仅绑 `127.0.0.1`;pixiv 由 `0.0.0.0:9088` 收口 - 主入口 `main.py` 从 `source/openapi/` 上移到 `source/`,systemd `WorkingDirectory` 同步为 `source/` diff --git a/docs/部署与运维.md b/docs/部署与运维.md index 1e0ab1c..67d15fd 100644 --- a/docs/部署与运维.md +++ b/docs/部署与运维.md @@ -122,6 +122,10 @@ - 新增 **dufe 银杏签到码查询服务**:`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` + `ginkgo_sign_in.service`(`127.0.0.1:9005`,Flask + waitress 16 线程,端点 `GET|POST /dufe/ginkgostu_sign_in`) - 在聚合层 `main.py` 注册该端点:新增 `DUFE_BACKEND` 与 `proxy_forward()`(保留请求方法/请求体;原 `proxy()` 固定 GET 行为未改动) - nginx **无需改动**:`location /` 兜底已把 `/dufe/...` 转发给 `127.0.0.1:9008` +- 修复 `ginkgo_sign_in.py` 两处缺陷: + - `_b64_decode` 先做 `urllib.parse.unquote`(iOS 快捷指令会把 base64 的 `=` 编码成 `%3D`,旧实现直接 `binascii.Error`);顺序为先 unquote、再 `" "→"+"` + - `commit` 判定由 fail-open 改为 fail-closed:须 `isinstance(dict)` 且 `success is True`,否则一律判失败并返回 `msg = "sign-in failed: <原因>"` + - `parse_course_url` 报错文案改为 `URL is not a sign-in page (missing /signin//): ` **2026-09-12** diff --git a/source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py b/source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py index c3bde0b..07a4be6 100644 --- a/source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py +++ b/source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py @@ -15,6 +15,7 @@ import json import logging import re import sys +import urllib.parse import uuid try: @@ -250,7 +251,8 @@ def parse_course_url(url): r"/course/detail/(?P\d+)/(?P[^/]+)/" r"(?P\d+)/signin/(?P\d+)/(?P\d+)", url) if not m: - raise ValueError("Cannot parse course URL: %s" % url) + raise ValueError( + "URL is not a sign-in page (missing /signin//): %s" % url) return m.groupdict() @@ -462,13 +464,18 @@ def _b64_decode(value, field): """ Decode a base64 query/form parameter tolerantly. - Handles: - * '+' turned into ' ' by application/x-www-form-urlencoded - * trailing newline from `base64` CLI without -w0 - * both standard (+/) and urlsafe (-_) alphabets + Handles, in order: + * '%3D' / '%2B' / '%20' -- iOS Shortcuts and some HTTP clients + URL-encode the base64 payload (notably the trailing '='), so we + percent-decode first. + * '+' turned into ' ' by application/x-www-form-urlencoded. + * trailing newline from `base64` CLI without -w0. + * both standard (+/) and urlsafe (-_) alphabets. """ - v = (value or "").strip() - v = v.replace(" ", "+").replace("\r", "").replace("\n", "") + v = urllib.parse.unquote(value or "") + v = v.strip() + v = v.replace(" ", "+") + v = v.replace("\r", "").replace("\n", "") if not v: raise ValueError("Empty base64 for '%s'" % field) try: @@ -503,15 +510,20 @@ def ginkgostu_sign_in(): log.info("query start user=%s commit=%s", username[:2] + "***", do_commit) try: data = fetch_sign_in_info(username, password, course_url, do_commit=do_commit) - commit_info = data.get("commit") or {} - if do_commit and not commit_info.get("success", True): - log.warning("sign-in commit failed user=%s reason=%s", - username[:2] + "***", commit_info.get("message")) - return jsonify({ - "code": 200, - "msg": "sign-in failed: %s" % commit_info.get("message", "unknown reason"), - "data": data, - }) + + if do_commit: + # Fail-closed: only an explicit success=true counts as signed in. + commit_info = data.get("commit") + if not isinstance(commit_info, dict) or commit_info.get("success") is not True: + reason = (commit_info.get("message") if isinstance(commit_info, dict) else None) or "no commit result returned" + log.warning("sign-in commit failed user=%s reason=%s", + username[:2] + "***", reason) + return jsonify({ + "code": 200, + "msg": "sign-in failed: %s" % reason, + "data": data, + }) + log.info("query ok user=%s code=%s", username[:2] + "***", data.get("signInCode")) return jsonify({"code": 200, "msg": "ok", "data": data}) except AuthError as e: