残留批次A:成员删除、PR needs_local_worker 落库、推送 DNS TOCTOU

- 成员删除:DELETE /projects/{id}/members/{user_id},限 owner 或
  全局管理员,禁删唯一 owner,记 remove_member 审计
  (AuthStore.remove_member + server 分发,沿用 PATCH 守卫口径)
- PR needs_local_worker 落库:非 local 扫到 queued 任务时 CAS
  写行状态为 needs_local_worker;claim/find_open 同步认该状态,
  标记过的行仍可被 local worker 认领、不被搁置;另修
  claim/find_open 对裸 sqlite3 tuple 行的 dict() 兜底
  (TypeError/ValueError)
- 推送 DNS TOCTOU:发送改走建连时解析+判段的连接类,解析
  一次、只拨判过的 IP,Host/SNI 仍为原域名;opener 直接构造
  以排除 ProxyHandler 改写(build_opener 默认会塞代理)
This commit is contained in:
host committed 2026-10-27 19:10:49 +00:00
1 parent 6e3eb7ba11
commit 9bb9a50e4e
10 files changed
+725 -38

No files matched your search

+43 -9
View File
@@ -54,18 +54,23 @@ def ensure_pr_jobs_table(conn):
def find_open_pr_job(conn, task_id):
"""找该任务未完成的 pr 任务(去重:同一任务同时只排一个)。"""
"""找该任务未完成的 pr 任务(去重:同一任务同时只排一个)。
needs_local_worker 也算未完成:非 local worker 标记过的任务
仍在等 local worker 认领,再调 /pr 必须复用它,不能重排。"""
ensure_pr_jobs_table(conn)
row = conn.execute(
"SELECT * FROM pr_jobs WHERE task_id = ?"
" AND status IN ('queued', 'running')"
" AND status IN ('queued', 'running', 'needs_local_worker')"
" ORDER BY created_at ASC LIMIT 1",
(task_id,)).fetchone()
if row is None:
return None
try:
return dict(row)
except TypeError:
except (TypeError, ValueError):
# 裸 sqlite3 连接(无 Row 工厂)返回 tuple:dict() 对
# tuple 抛 ValueError(对 Row 抛 TypeError 的旧路径不动)。
return {"id": row[0], "task_id": row[1], "project_id": row[2],
"host": row[3], "status": row[4]}
@@ -84,34 +89,63 @@ def enqueue_pr_job(conn, task_id, project_id, host):
def claim_pr_job(conn, host):
"""CAS 认领本主机的一个 queued pr 任务(最老优先)。
"""CAS 认领本主机的一个 pr 任务(最老优先)。
两个同主机 worker 抢同一任务时只有一个能拿到 rowcount == 1。
认领 queued 与 needs_local_worker 两种状态:后者是非 local
worker 标记的“需 local worker”任务,仍在等 local worker,
必须能被认领(否则标记等于搁置)。两个同主机 worker 抢
同一任务时只有一个能拿到 rowcount == 1。
返回任务 dict 或 None。
"""
ensure_pr_jobs_table(conn)
row = conn.execute(
"SELECT * FROM pr_jobs WHERE host = ? AND status = 'queued'"
"SELECT * FROM pr_jobs WHERE host = ?"
" AND status IN ('queued', 'needs_local_worker')"
" ORDER BY created_at ASC LIMIT 1",
(host,)).fetchone()
if row is None:
return None
job_id = row["id"] if hasattr(row, "__getitem__") else row[0]
try:
job_id = row["id"]
except TypeError:
# 裸 sqlite3 连接(无 Row 工厂)返回 tuple:按位置取。
# hasattr(__getitem__) 判据对 tuple 也为真,不能用它区分。
job_id = row[0]
now = time.time()
cur = conn.execute(
"UPDATE pr_jobs SET status = 'running', started_at = ?,"
" updated_at = ? WHERE id = ? AND status = 'queued'",
" updated_at = ? WHERE id = ?"
" AND status IN ('queued', 'needs_local_worker')",
(now, now, job_id))
conn.commit()
if cur.rowcount != 1:
return None
try:
return dict(row)
except TypeError:
except (TypeError, ValueError):
# 同上:tuple 行的 dict() 抛 ValueError。
return {"id": row[0], "task_id": row[1], "project_id": row[2],
"host": row[3], "status": "running"}
def mark_pr_job_needs_local_worker(conn, job_id, reason):
"""把 queued 的 pr 任务标为 needs_local_worker(CAS)。
只有仍是 queued 时才改写并返回 True;已被 local worker
认领(running)或已终态时返回 False,绝不覆盖。标记后
任务仍可被 local worker 认领(见 claim_pr_job),只是
重读时能看到它在等 local worker,而不是“没人处理”。
"""
ensure_pr_jobs_table(conn)
now = time.time()
cur = conn.execute(
"UPDATE pr_jobs SET status = 'needs_local_worker', error = ?,"
" updated_at = ? WHERE id = ? AND status = 'queued'",
(reason, now, job_id))
conn.commit()
return cur.rowcount == 1
def finish_pr_job(conn, job_id, status, pr_url=None, pr_number=None,
error=None):
"""写回 pr 任务终态(completed/failed)。"""