Files
TransPyC/includes/requests/https.vp
T
TermiNexus ac3e235406 feat(requests): 新增大文件切片下载(requests.download,串行+多线程 Range)
- download.vp: Content-Range 探测总大小,threads=1 串行切片 / threads>1 按带并行(≤8 线程),按偏移 Win32 落盘,http/https 按 scheme 分发,切片内跟随重定向
- requests/__init__.vp: 新增原始头/Content-Length/Content-Range 解析 helper 与线程安全 _DlOnce(Range 原语,全 stdlib 分配,不碰 _mbuddy)
- requests/https.vp: 镜像 _DlOnce(openssl + verify/ca/SNI/主机名校验)
2026-09-03 08:27:52 +08:00

665 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import t, c
from stdint import *
import string
import socket
import stdio
import stdlib
import requests
import openssl
# ============================================================
# requests/https.vp — HTTPS 客户端(openssl SSL/TLS + 证书核验)
#
# 与 requests(HTTP)同名 API,但走 TLS 并默认校验对端证书:
# import requests.https as rhttps
# resp = rhttps.get("https://example.com/", verify=True)
# if resp is not None:
# print(resp.status_code)
# print(resp.text)
# requests.release(resp)
#
# 说明:
# - verify=True 时校验证书链 + 主机名(hostname 校验 SSL_set1_host),
# 提供 ca_file 指向 PEM CA bundle;不提供则走系统默认路径
# (Windows 上通常为空,真实站点会因"无法验证证书链"失败返回 None)。
# - 失败时返回 None,可用 last_error() 获取原因文本。
# - 握手成功后可用 last_tls_info() 查看 TLS 版本 / 加密套件 / 对端证书主题。
# ============================================================
HTTPS_PORT: t.CDefine = 443
# 模块内部状态
_ossl_ready: bool = False
_socket_ready: bool = False
_err_store: t.CArray[t.CChar, 512] # 最近一次失败原因(静态缓冲)
_info_store: t.CArray[t.CChar, 512] # 最近一次成功握手的 TLS 元信息
_scratch_store: t.CArray[t.CChar, 512] # 临时拼接缓冲
_last_verify_val: INT = -1 # 最近一次握手后 SSL_get_verify_result(0=通过;-1=未启用)
_last_verify_flag: INT = -1 # 最近一次请求 verify 标志(1=开启;0=关闭;-1=未知)
def last_verify_result() -> int:
"""返回最近一次握手的证书校验结果:0 通过;>0 失败码;-1 未开启校验。"""
return _last_verify_val
def last_verify_enabled() -> int:
"""返回最近一次请求是否开启校验(1=是;0=否;-1=未知)。"""
return _last_verify_flag
# ============================================================
# 内部辅助
# ============================================================
def last_error() -> str:
"""返回最近一次失败的原因文本(无则空串)。"""
return _err_store
def last_tls_info() -> str:
"""返回最近一次成功握手的 TLS 信息(版本/套件/对端主题),无则空串。"""
return _info_store
def _RecordErrorText(text: str):
"""把失败原因写入静态缓冲 _err_store。"""
if text is None: return
_err_store[0] = 0
stdio.snprintf(_err_store, 512, "%s", text)
def _RecordInfoText(text: str):
"""把 TLS 元信息写入静态缓冲 _info_store。"""
if text is None: return
_info_store[0] = 0
stdio.snprintf(_info_store, 512, "%s", text)
def _EnsureReady() -> INT:
"""初始化 socket 与 openssl(均幂等)。"""
global _socket_ready, _ossl_ready
if not _socket_ready:
if socket.SocketInit() != 0:
_RecordErrorText("socket init failed")
return -1
_socket_ready = True
if not _ossl_ready:
if openssl.OPENSSL_init_ssl(0, None) != 1:
_RecordErrorText("OPENSSL_init_ssl failed")
return -1
_ossl_ready = True
return 0
def _ParseHttpsUrl(url: str, host_buf: str, host_size: INT, path_buf: str, path_size: INT, port: INT | t.CPtr) -> INT:
"""解析 https://host[:port][/path],端口缺省 443。"""
if url is None: return -1
if host_buf is None or path_buf is None or port is None: return -1
p: str = url
# Skip "https://"(8 字节)
if string.strncmp(p, "https://", 8) == 0:
p += 8
# Parse host
host_start: str = p
host_len: INT = 0
while p[0] != '\0' and p[0] != ':' and p[0] != '/':
host_len += 1
p += 1
if host_len == 0 or host_len >= host_size: return -1
string.strncpy(host_buf, host_start, host_len)
host_buf[host_len] = 0
# Parse port
if p[0] == ':':
p += 1
port_val: INT = 0
while p[0] >= '0' and p[0] <= '9':
port_val = port_val * 10 + INT(p[0] - '0')
p += 1
c.DerefAs(port, port_val)
else:
c.DerefAs(port, HTTPS_PORT)
# Parse path
if p[0] == '/':
path_start: str = p
path_len: INT = 0
while p[0] != '\0':
path_len += 1
p += 1
if path_len >= path_size: return -1
string.strncpy(path_buf, path_start, path_len)
path_buf[path_len] = 0
else:
if path_size < 2: return -1
path_buf[0] = '/'
path_buf[1] = 0
return 0
def _ScratchBuf() -> str:
"""返回静态拼接缓冲(512B,复用)。"""
return _scratch_store
def _TlsErrText():
"""把最近一次 openssl 错误队列文本写入静态 scratch 缓冲。"""
e: t.CUInt32T = openssl.ERR_get_error()
if e == 0:
_scratch_store[0] = 0
string.strncpy(_scratch_store, "openssl error (no detail)", 26)
return
openssl.ERR_error_string_n(e, _scratch_store, 512)
# ============================================================
# 核心请求
# ============================================================
def _HttpsRequest(method: str, url: str, verify: bool, ca_file: str, data: str, content_type: str) -> requests.Response | t.CPtr:
"""HTTPS GET/POST 核心实现。
verify=False 时跳过证书链/主机名校验(不推荐);verify=True 时
ca_file 提供 PEM CA bundle,否则使用系统默认 CA 路径。
"""
global _last_verify_val, _last_verify_flag
if requests._mbuddy is None: return None
_last_verify_flag = 1 if verify else 0
resp: requests.Response | t.CPtr = requests.create_response()
if resp is None: return None
if _EnsureReady() != 0:
requests.release(resp)
return None
# 临时缓冲(错误文本格式化;openssl 详情走模块级 scratch)
errmsg: str = requests._mbuddy.alloc(512)
if errmsg is None:
requests.release(resp)
return None
# ---- 解析 URL ----
host_buf: t.CArray[t.CChar, 256]
path_buf: t.CArray[t.CChar, 1024]
port: INT = 0
if _ParseHttpsUrl(url, host_buf, 256, path_buf, 1024, c.Addr(port)) != 0:
stdio.snprintf(errmsg, 512, "bad https url: %s", url)
_RecordErrorText(errmsg)
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
# ---- 建立 TCP 连接 ----
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
if sock.fd == socket.INVALID_SOCKET:
_RecordErrorText("socket() failed")
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
if sock.connect(host_buf, port) != 0:
stdio.snprintf(errmsg, 512, "connect %s:%d failed", host_buf, port)
_RecordErrorText(errmsg)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
# ---- SSL_CTX / SSL ----
ctx: openssl.SslCtxPtr = openssl.SSL_CTX_new(openssl.TLS_client_method())
if ctx is None:
_RecordErrorText("SSL_CTX_new failed")
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
if verify:
openssl.SSL_CTX_set_verify(ctx, openssl.SslVerifyPeer, None)
if ca_file is not None and ca_file[0] != 0:
if openssl.SSL_CTX_load_verify_locations(ctx, ca_file, None) != 1:
_TlsErrText()
stdio.snprintf(errmsg, 512, "load CA file failed: %s (%s)", ca_file, _ScratchBuf())
_RecordErrorText(errmsg)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
else:
# 系统默认路径(Windows 上通常为空,证书链校验将失败)
openssl.SSL_CTX_set_default_verify_paths(ctx)
else:
openssl.SSL_CTX_set_verify(ctx, openssl.SslVerifyNone, None)
ssl: openssl.SslPtr = openssl.SSL_new(ctx)
if ssl is None:
_RecordErrorText("SSL_new failed")
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
openssl.SSL_set_fd(ssl, sock.fd)
# SNI(SSL_set_tlsext_host_name 是宏 → 直接 SSL_ctrl)
openssl.SSL_ctrl(ssl, openssl.SslCtrlSetTlsextHostname, openssl.TlsextNametypeHostName, host_buf)
# 主机名校验(TLS 内核对证书 SAN/CN 与 host 比对)
if verify:
openssl.SSL_set1_host(ssl, host_buf)
# ---- 握手 ----
hret: INT = openssl.SSL_connect(ssl)
if hret <= 0:
want: INT = openssl.SSL_get_error(ssl, hret)
_TlsErrText()
stdio.snprintf(errmsg, 512, "SSL_connect failed (want=%d): %s", want, _ScratchBuf())
_RecordErrorText(errmsg)
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
# ---- 记录最近一次证书校验结果(供 last_verify_result 查询)----
if verify:
_last_verify_val = INT(openssl.SSL_get_verify_result(ssl))
else:
_last_verify_val = -1
# ---- 证书链校验结果复核 ----
if verify:
vres: t.CInt32T = openssl.SSL_get_verify_result(ssl)
if vres != 0:
reason: str = openssl.X509_verify_cert_error_string(vres)
if reason is None:
stdio.snprintf(errmsg, 512, "SSL certificate verify failed: error %d", INT(vres))
else:
stdio.snprintf(errmsg, 512, "SSL certificate verify failed (%d): %s", INT(vres), reason)
_RecordErrorText(errmsg)
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
# ---- 记录 TLS 元信息(版本 / 套件 / 对端证书主题)----
name_buf: str = requests._mbuddy.alloc(512)
if name_buf is not None:
peer: openssl.X509Ptr = openssl.SSL_get1_peer_certificate(ssl)
if peer is not None:
subj: openssl.X509NamePtr = openssl.X509_get_subject_name(peer)
if subj is not None:
name_buf[0] = 0
openssl.X509_NAME_oneline(subj, name_buf, 512)
name_buf[511] = 0
openssl.X509_free(peer)
else:
name_buf[0] = 0
string.strncpy(name_buf, "(no peer certificate)", 21)
name_buf[21] = 0
ver: str = openssl.SSL_get_version(ssl)
cipher: str = openssl.SSL_CIPHER_get_name(openssl.SSL_get_current_cipher(ssl))
stdio.snprintf(errmsg, 512, "%s | cipher %s | peer: %s",
ver if ver is not None else "?", cipher if cipher is not None else "?", name_buf)
_RecordInfoText(errmsg)
requests._mbuddy.free(name_buf)
# ---- 组请求头并发送 ----
req_buf: str = requests._mbuddy.alloc(requests.HTTP_BUFSIZ)
if req_buf is None:
_RecordErrorText("alloc request buffer failed")
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
body_len: INT = 0
is_post: bool = string.strcmp(method, "POST") == 0
if is_post:
body_len = INT(string.strlen(data)) if data is not None else 0
ct: str = content_type if content_type is not None else "application/x-www-form-urlencoded"
stdio.snprintf(req_buf, requests.HTTP_BUFSIZ,
"POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
path_buf, host_buf, ct, body_len)
else:
stdio.snprintf(req_buf, requests.HTTP_BUFSIZ,
"GET %s HTTP/1.1\r\nHost: %s\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
path_buf, host_buf)
hdr_len: INT = INT(string.strlen(req_buf))
sent: INT = openssl.SSL_write(ssl, req_buf, hdr_len)
if sent <= 0:
_TlsErrText()
stdio.snprintf(errmsg, 512, "SSL_write header failed: %s", _ScratchBuf())
_RecordErrorText(errmsg)
requests._mbuddy.free(req_buf)
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
if is_post and body_len > 0 and data is not None:
openssl.SSL_write(ssl, data, body_len)
requests._mbuddy.free(req_buf)
# ---- 接收响应 ----
resp_buf: str = requests._mbuddy.alloc(requests.HTTP_BUFSIZ)
if resp_buf is None:
_RecordErrorText("alloc response buffer failed")
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
requests._mbuddy.free(errmsg)
requests.release(resp)
return None
total: INT = 0
while total < requests.HTTP_BUFSIZ - 1:
n: INT = openssl.SSL_read(ssl, resp_buf + total, requests.HTTP_BUFSIZ - 1 - total)
if n <= 0: break
total += n
resp_buf[total] = 0
# 关闭 TLS 与 TCP
openssl.SSL_shutdown(ssl)
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
resp._raw_buf = resp_buf
requests.parse_response(resp, total)
requests._mbuddy.free(errmsg)
return resp
# ============================================================
# 公共 API
# ============================================================
def _HttpsRedirectLoop(method: str, url: str, verify: bool, ca_file: str, data: str, content_type: str) -> requests.Response | t.CPtr:
"""HTTPS GET/POST + 重定向跟随(跨 http/https,最多 5 跳)。"""
if requests._mbuddy is None: return None
cur: str = requests._mbuddy.alloc(2048)
if cur is None: return None
if INT(string.strlen(url)) >= 2047:
_RecordErrorText("url too long")
requests._mbuddy.free(cur)
return None
string.strcpy(cur, url)
hop: INT = 0
while hop < 5:
resp: requests.Response | t.CPtr = _HttpsRequest(method, cur, verify, ca_file, data, content_type)
if resp is None:
requests._mbuddy.free(cur)
return None
code: INT = resp.status_code
if not (code >= 300 and code < 400):
requests._mbuddy.free(cur)
return resp
loc: str = requests._mbuddy.alloc(2048)
if loc is None:
requests._mbuddy.free(cur)
return resp
if requests.header_value(resp, "Location", loc, 2048) != 0:
requests._mbuddy.free(loc)
requests._mbuddy.free(cur)
return resp
nxt: str = requests._mbuddy.alloc(2048)
if nxt is None:
requests._mbuddy.free(loc)
requests._mbuddy.free(cur)
return resp
if requests._ResolveRedirectUrl(cur, loc, nxt, 2048) != 0:
_RecordErrorText("unresolvable redirect Location")
requests.release(resp)
requests._mbuddy.free(nxt)
requests._mbuddy.free(loc)
requests._mbuddy.free(cur)
return None
requests.release(resp)
# 跨协议分流:https → 继续本循环;http → 交给 requests(http)链
if string.strncmp(nxt, "https://", 8) != 0:
requests._mbuddy.free(loc)
requests._mbuddy.free(cur)
http_resp: requests.Response | t.CPtr = requests.get(nxt)
requests._mbuddy.free(nxt)
return http_resp
requests._mbuddy.free(loc)
requests._mbuddy.free(cur)
cur = nxt
hop += 1
_RecordErrorText("too many redirects")
requests._mbuddy.free(cur)
return None
def get(url: str, verify: bool = True, ca_file: str = None) -> requests.Response | t.CPtr:
"""HTTPS GET request(自动跟随 3xx 重定向,最多 5 跳)。
Args:
url: Full URL (e.g., "https://example.com/path")
verify: True 时校验证书链 + 主机名(默认)
ca_file: PEM CA bundle 路径;None 时使用系统默认 CA 路径
Returns:
Pointer to a Response object, None on failure (see last_error()).
"""
return _HttpsRedirectLoop("GET", url, verify, ca_file, None, None)
def post(url: str, data: str = None, content_type: str = "application/x-www-form-urlencoded",
verify: bool = True, ca_file: str = None) -> requests.Response | t.CPtr:
"""HTTPS POST request(自动跟随 3xx 重定向,最多 5 跳)。
Args:
url: Full URL
data: Request body data
content_type: Content-Type header
verify: True 时校验证书链 + 主机名(默认)
ca_file: PEM CA bundle 路径;None 时使用系统默认 CA 路径
Returns:
Pointer to a Response object, None on failure (see last_error()).
"""
return _HttpsRedirectLoop("POST", url, verify, ca_file, data, content_type)
# ============================================================
# 大文件 Range 下载原语(requests.download 使用,与 requests._DlOnce 镜像)
#
# 线程安全:本函数不使用 _mbuddy(请求/响应缓冲一律 stdlib),每个连接独立
# SSL_CTX/SSL/socket,供多线程并行切片并发调用(OPENSSL_init_ssl 由主线程
# 先 _EnsureReady 预热一次)。
#
# 返回: >=0 响应体字节数;-1 参数错;-2 3xx 重定向(Location 写 out_loc);
# -3 其它失败(last_error() 查看原因)。
# ============================================================
def _DlOnce(url: str, start: t.CSizeT, count: INT, verify: bool, ca_file: str,
dst: str, dst_cap: INT, out_status: INT | t.CPtr,
out_loc: str, out_loccap: INT,
out_total: t.CSizeT | t.CPtr) -> INT:
"""HTTPS 单次 Range 拉取(线程安全,见模块注释)。"""
global _last_verify_val, _last_verify_flag
if url is None or dst is None or dst_cap <= 0 \
or out_status is None or out_total is None:
return -1
c.Set(c.Deref(out_status), 0)
c.Set(c.Deref(out_total), 0)
if out_loc is not None and out_loccap > 0:
out_loc[0] = 0
_last_verify_flag = 1 if verify else 0
if _EnsureReady() != 0:
return -3
host_buf: t.CArray[t.CChar, 256]
path_buf: t.CArray[t.CChar, 1024]
port: INT = 0
if _ParseHttpsUrl(url, host_buf, 256, path_buf, 1024, c.Addr(port)) != 0:
_RecordErrorText("bad https url")
return -3
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
if sock.fd == socket.INVALID_SOCKET:
_RecordErrorText("socket() failed")
return -3
if sock.connect(host_buf, port) != 0:
_RecordErrorText("connect failed")
sock.close()
return -3
ctx: openssl.SslCtxPtr = openssl.SSL_CTX_new(openssl.TLS_client_method())
if ctx is None:
_RecordErrorText("SSL_CTX_new failed")
sock.close()
return -3
if verify:
openssl.SSL_CTX_set_verify(ctx, openssl.SslVerifyPeer, None)
if ca_file is not None and ca_file[0] != 0:
if openssl.SSL_CTX_load_verify_locations(ctx, ca_file, None) != 1:
_RecordErrorText("load CA file failed")
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
else:
openssl.SSL_CTX_set_default_verify_paths(ctx)
else:
openssl.SSL_CTX_set_verify(ctx, openssl.SslVerifyNone, None)
ssl: openssl.SslPtr = openssl.SSL_new(ctx)
if ssl is None:
_RecordErrorText("SSL_new failed")
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
openssl.SSL_set_fd(ssl, sock.fd)
openssl.SSL_ctrl(ssl, openssl.SslCtrlSetTlsextHostname, openssl.TlsextNametypeHostName, host_buf)
if verify:
openssl.SSL_set1_host(ssl, host_buf)
hret: INT = openssl.SSL_connect(ssl)
if hret <= 0:
_RecordErrorText("SSL_connect failed")
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
if verify:
_last_verify_val = INT(openssl.SSL_get_verify_result(ssl))
if _last_verify_val != 0:
_RecordErrorText("SSL certificate verify failed")
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
else:
_last_verify_val = -1
# 组 Range 请求头(不带 Accept-Encoding,保证响应为原始字节)
req: str = stdlib.malloc(1024)
if req is None:
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
cnt: INT = count
if cnt < 0:
cnt = 0
end: t.CSizeT = start + t.CSizeT(cnt) - 1
if cnt <= 0:
end = start
stdio.snprintf(req, 1024,
"GET %s HTTP/1.1\r\nHost: %s\r\nRange: bytes=%zu-%zu\r\nConnection: close\r\n\r\n",
path_buf, host_buf, start, end)
sent: INT = openssl.SSL_write(ssl, req, INT(string.strlen(req)))
stdlib.free(req)
if sent <= 0:
_RecordErrorText("SSL_write failed")
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
return -3
total: INT = 0
tgt: INT = -1
while total < dst_cap - 1:
room: INT = dst_cap - 1 - total
if room > 4096:
room = 4096
n: INT = openssl.SSL_read(ssl, dst + total, room)
if n <= 0:
break
total += n
if tgt < 0:
bo: INT = requests._FindBodyStart(dst, total)
if bo >= 0:
cl: INT = requests._CLenOf(dst, total)
if cl >= 0:
tgt = bo + cl
if tgt >= 0 and total >= tgt:
break
openssl.SSL_shutdown(ssl)
openssl.SSL_free(ssl)
openssl.SSL_CTX_free(ctx)
sock.close()
dst[total] = 0
st: INT = requests._StatusRaw(dst)
c.Set(c.Deref(out_status), st)
if st == 0:
_RecordErrorText("empty response")
return -3
if requests._IsHttpRedirect(st):
if out_loc is not None and out_loccap > 0:
if requests._RawHeaderValue(dst, total, "Location", out_loc, out_loccap) != 0:
out_loc[0] = 0
return -2
if st != 200 and st != 206:
_RecordErrorText("http status not 200/206")
return -3
if st == 206:
c.Set(c.Deref(out_total), requests._CRTotalOf(dst, total))
else:
cl2: INT = requests._CLenOf(dst, total)
if cl2 > 0:
c.Set(c.Deref(out_total), t.CSizeT(cl2))
bo: INT = requests._FindBodyStart(dst, total)
if bo < 0 or bo >= total:
return 0
blen: INT = total - bo
if blen > cnt and cnt > 0:
blen = cnt
return blen