fix(dufe): 修复 base64 %3D 容错与 commit 判定 fail-open

ginkgo_sign_in.py:
- 顶部新增 import urllib.parse
- _b64_decode: 先 urllib.parse.unquote 再做 空格->+ 替换
  (iOS 快捷指令会把 base64 的 = 编码成 %3D,旧实现直接 binascii.Error;
    顺序必须是 unquote 在 空格->+ 之前,否则 %20 会被误当成被 form-urlencoded 吃掉的 +)
- commit 判定改为 fail-closed:要求 commit_info 是 dict 且 success is True,
  否则一律返回 msg="sign-in failed: <原因>";
  do_commit=false 时不检查 commit 字段,msg 仍为 "ok"
- reason 取值修正:非 dict 真值(字符串/数字)不再触发 AttributeError 500,
  统一回落到 "no commit result returned"
- parse_course_url 报错文案改为
  "URL is not a sign-in page (missing /signin/<signInId>/<type>): <url>"

文档:README 版本要点与 docs/部署与运维.md 变更记录同步
This commit is contained in:
RRRosmontis
2026-09-13 11:08:40 +08:00
parent a436bf58b7
commit 9ba194ae97
3 changed files with 33 additions and 17 deletions
+1 -1
View File
@@ -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/`
+4
View File
@@ -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/<signInId>/<type>): <url>`
**2026-09-12**
+23 -11
View File
@@ -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<courseId>\d+)/(?P<batchNo>[^/]+)/"
r"(?P<weekTimeId>\d+)/signin/(?P<signInId>\d+)/(?P<type>\d+)", url)
if not m:
raise ValueError("Cannot parse course URL: %s" % url)
raise ValueError(
"URL is not a sign-in page (missing /signin/<signInId>/<type>): %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):
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] + "***", commit_info.get("message"))
username[:2] + "***", reason)
return jsonify({
"code": 200,
"msg": "sign-in failed: %s" % commit_info.get("message", "unknown reason"),
"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: