"""Shared TLS config parsing + connection builder (dispatcher + heartbeat).
单一事实源:cert/key 成对校验(fail-loud)+ `ca or (cert and key)` HTTPS 门。
Stdlib-only (http.client + ssl) — shared leaf consumed by dispatcher and
instance_base via heartbeat; no ttk.* import to avoid cycles.
"""
import http.client
import ssl
def tls_from_config(config) -> dict:
"""RemoteConfig -> tls dict.
cert/key 不成对则 raise(fail-loud);无 TLS 配置返回 {}。
"""
ca = getattr(config, "tls_ca", "") or ""
cert = getattr(config, "tls_cert", "") or ""
key = getattr(config, "tls_key", "") or ""
skip_verify = bool(getattr(config, "tls_skip_verify", False))
if (cert and not key) or (key and not cert):
raise RuntimeError(f"TLS 配置错误:tls_cert/tls_key 必须成对(cert={cert!r}, key={key!r})")
if not (ca or cert):
return {}
return {"ca_cert": ca, "cert": cert, "key": key, "skip_verify": skip_verify}
def build_tls_connection(host, port, timeout, tls):
"""tls dict -> HTTP(S)Connection.
{} 或 None -> HTTP;ca 或 cert+key 成对 -> HTTPS。
"""
ca = (tls or {}).get("ca_cert", "") or ""
cert = (tls or {}).get("cert", "") or ""
key = (tls or {}).get("key", "") or ""
skip_verify = bool((tls or {}).get("skip_verify", False))
if ca or (cert and key):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
if skip_verify:
ctx.check_hostname = False
if ca:
ctx.load_verify_locations(ca)
if cert and key:
ctx.load_cert_chain(cert, key)
return http.client.HTTPSConnection(host, port, timeout=timeout, context=ctx)
return http.client.HTTPConnection(host, port, timeout=timeout)