#!/usr/bin/env python3 """git credential helper:向 chv-authd 按次取凭证(feat-chv-muse-p3)。 git credential 协议:git 把 `protocol/host/path`(每行 `key=value`, 空行结束)写进本进程 stdin;本进程向 authd 的 Unix socket 发 一次 JSON 请求,拿到用户名/口令后按协议回给 git(属性 + 空行)。 口令只进 git 子进程内存:本进程不写文件、不打日志(拒绝时只 报 kind,不含值);`password_expiry_utc` 按释放的 expires_at 回给 git,git 在该时间后重新问 helper(不缓存过期凭证)。 authd 拒绝 / socket 不通时本进程 exit 1——git 操作随之失败 (fail-closed,绝不回落成“无凭证也照推”)。 用法(chv-sync boot 配好后 git 自动调,不用手工跑): git config --global credential.helper /usr/local/bin/git-credential-chv-authd git config --global credential.useHttpPath true (后者不可少:git 默认不把 path 发给 helper,没有 path 本进程 无法推断仓库与操作,会直接拒绝。) socket 路径可用 CHV_AUTHD_SOCKET 覆盖(测试用 /tmp 下的假 socket)。 """ import json import os import socket import sys DEFAULT_SOCKET_PATH = "/run/chv-authd/authd.sock" SOCKET_TIMEOUT_SECONDS = 5 def _read_credential_request(): """读 git credential 协议的请求属性(空行结束)→ dict。""" attrs = {} for line in sys.stdin: line = line.rstrip("\n") if not line: break if "=" in line: key, value = line.split("=", 1) attrs[key.strip()] = value.strip() return attrs def _ask_authd(socket_path, host, path): """向 authd 要一次凭证:返回 (username, password, expires_at)。 拒绝/通信失败即 RuntimeError(只含 kind,不含值)。""" req = {"connector": "gitea", "host": host, "path": path} sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(SOCKET_TIMEOUT_SECONDS) try: sock.connect(socket_path) sock.sendall((json.dumps(req) + "\n").encode("utf-8")) chunks = [] while True: data = sock.recv(65536) if not data: break chunks.append(data) if b"\n" in data: break except OSError as exc: raise RuntimeError("authd_unreachable:%s" % exc) finally: sock.close() try: resp = json.loads(b"".join(chunks).decode("utf-8")) except ValueError: raise RuntimeError("authd_bad_response") if not resp.get("ok"): raise RuntimeError("authd_refused:%s" % resp.get("error", "refused")) return (resp["username"], resp["password"], resp.get("expires_at")) def main(argv=None): """helper 入口:读协议 → 问 authd → 回属性。拒绝即 exit 1。""" attrs = _read_credential_request() host = attrs.get("host", "") path = attrs.get("path", "") if attrs.get("protocol") != "https" or not host or not path: print("git-credential-chv-authd: 请求不完整(须 protocol=https " "+ host + path),拒绝", file=sys.stderr) return 1 if not path.startswith("/"): path = "/" + path # git 的 info/refs 路径带 ?service=... 查询串:Sentinel 的 # L7 规则按干净路径匹配,这里先去掉查询串再问 authd。 path = path.split("?", 1)[0] socket_path = os.environ.get("CHV_AUTHD_SOCKET") \ or DEFAULT_SOCKET_PATH try: username, password, expires_at = _ask_authd( socket_path, host, path) except RuntimeError as exc: print("git-credential-chv-authd: %s" % exc, file=sys.stderr) return 1 print("username=%s" % username) print("password=%s" % password) if expires_at: print("password_expiry_utc=%d" % int(expires_at)) print() return 0 if __name__ == "__main__": sys.exit(main())