1093 lines
38 KiB
Plaintext
1093 lines
38 KiB
Plaintext
import t, c
|
||
from stdint import *
|
||
import string
|
||
import socket
|
||
import stdio
|
||
import stdlib
|
||
import requests
|
||
import openssl
|
||
import json
|
||
import memhub
|
||
|
||
|
||
# ============================================================
|
||
# 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=未知)
|
||
|
||
# ============================================================
|
||
# SSE 流式内容接收区(post_stream 专用;单路请求,UI 线程轮询拉取)
|
||
# _st_len: 工作线程已写入的内容字节数
|
||
# _st_consumed: UI 已消费游标(仅拉取方推进)
|
||
# _st_state: 0=运行中 1=完成(2xx) 2=失败
|
||
# _st_status: HTTP 状态码(失败网络路径为负错误码)
|
||
# ============================================================
|
||
STREAM_CAP: t.CDefine = 262144
|
||
_st_buf: t.CArray[t.CChar, 262144]
|
||
_st_len: t.CSizeT = 0
|
||
_st_consumed: t.CSizeT = 0
|
||
_st_state: INT = 0
|
||
_st_status: INT = 0
|
||
_st_err: t.CArray[t.CChar, 512]
|
||
|
||
|
||
def stream_begin():
|
||
"""开始新一轮流式接收:重置缓冲/游标/状态。"""
|
||
global _st_len, _st_consumed, _st_state, _st_status
|
||
_st_len = 0
|
||
_st_consumed = 0
|
||
_st_state = 0
|
||
_st_status = 0
|
||
_st_err[0] = 0
|
||
|
||
|
||
def stream_pull(out: str, cap: INT) -> INT:
|
||
"""拉取未消费的增量内容到 out(自动补 0),返回拷贝字节数。"""
|
||
global _st_consumed
|
||
if out is None or cap <= 1:
|
||
return 0
|
||
avail: INT = 0
|
||
if _st_len > _st_consumed:
|
||
avail = INT(_st_len - _st_consumed)
|
||
if avail > cap - 1:
|
||
avail = cap - 1
|
||
i: t.CSizeT = 0
|
||
while i < t.CSizeT(avail):
|
||
out[i] = _st_buf[_st_consumed + i]
|
||
i += 1
|
||
out[t.CSizeT(avail)] = 0
|
||
_st_consumed += t.CSizeT(avail)
|
||
return avail
|
||
|
||
|
||
def stream_state() -> INT:
|
||
"""0=运行中 1=完成(2xx) 2=失败。"""
|
||
return _st_state
|
||
|
||
|
||
def stream_status() -> INT:
|
||
"""HTTP 状态码;负数为网络/解析错误。"""
|
||
return _st_status
|
||
|
||
|
||
def stream_error_text() -> str:
|
||
"""失败原因文本(_st_state==2 时)。"""
|
||
return _st_err
|
||
|
||
|
||
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, auth: str = None) -> rawptr[requests.Response]:
|
||
"""HTTPS GET/POST 核心实现。
|
||
|
||
verify=False 时跳过证书链/主机名校验(不推荐);verify=True 时
|
||
ca_file 提供 PEM CA bundle,否则使用系统默认 CA 路径。
|
||
auth(可选):形如 "Bearer <key>" 的 Authorization 头值,
|
||
非空时仅在 POST 请求头中插入 Authorization 头(默认 None = 行为不变)。
|
||
"""
|
||
global _last_verify_val, _last_verify_flag
|
||
if requests._mbuddy is None: return None
|
||
|
||
_last_verify_flag = 1 if verify else 0
|
||
|
||
resp: rawptr[requests.Response] = 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"
|
||
# 分段组头,便于按需插入 Authorization(auth 非空才加,默认路径输出与原来完全一致)
|
||
hdr_off: INT = 0
|
||
hdr_cap: INT = requests.HTTP_BUFSIZ
|
||
stdio.snprintf(req_buf, hdr_cap, "POST %s HTTP/1.1\r\nHost: %s\r\n", path_buf, host_buf)
|
||
hdr_off = INT(string.strlen(req_buf))
|
||
if auth is not None and auth[0] != 0:
|
||
stdio.snprintf(req_buf + hdr_off, hdr_cap - hdr_off, "Authorization: %s\r\n", auth)
|
||
hdr_off = INT(string.strlen(req_buf))
|
||
stdio.snprintf(req_buf + hdr_off, hdr_cap - hdr_off,
|
||
"Content-Type: %s\r\nContent-Length: %d\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n",
|
||
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)
|
||
|
||
# ---- 接收响应(缓冲自动翻倍增长, 支持 MB 级正文, 如 MNIST .gz 下载) ----
|
||
resp_cap: INT = requests.HTTP_BUFSIZ
|
||
resp_buf: str = requests._mbuddy.alloc(t.CSizeT(resp_cap))
|
||
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 True:
|
||
room: INT = resp_cap - 1 - total
|
||
if room <= 0:
|
||
# 缓冲已满仍可能有数据: 翻倍扩容(硬上限 256MB, 防失控流)
|
||
if resp_cap >= 268435456:
|
||
break
|
||
resp_cap = resp_cap * 2
|
||
nb: t.CPtr = requests._mbuddy.realloc(resp_buf, t.CSizeT(resp_cap))
|
||
if nb is None:
|
||
_RecordErrorText("realloc response buffer failed")
|
||
requests._mbuddy.free(resp_buf)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
requests.release(resp)
|
||
return None
|
||
resp_buf = t.CChar(t.CUInt64T(nb), t.CPtr)
|
||
room = resp_cap - 1 - total
|
||
n: INT = openssl.SSL_read(ssl, resp_buf + total, room)
|
||
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, auth: str = None) -> rawptr[requests.Response]:
|
||
"""HTTPS GET/POST + 重定向跟随(跨 http/https,最多 5 跳)。
|
||
|
||
auth(可选)原样透传给 _HttpsRequest;None 时行为与原来完全一致。
|
||
注意:跨主机重定向时 Authorization 同样会透传(仅提示,逻辑未改动)。
|
||
"""
|
||
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: rawptr[requests.Response] = _HttpsRequest(method, cur, verify, ca_file, data, content_type, auth)
|
||
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: rawptr[requests.Response] = 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) -> rawptr[requests.Response]:
|
||
"""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, auth: str = None) -> rawptr[requests.Response]:
|
||
"""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 路径
|
||
auth: 可选 Authorization 头值(形如 "Bearer <key>"),
|
||
非空时随请求头发送;None/空串 = 与旧版完全一致。
|
||
Returns:
|
||
Pointer to a Response object, None on failure (see last_error()).
|
||
"""
|
||
return _HttpsRedirectLoop("POST", url, verify, ca_file, data, content_type, auth)
|
||
|
||
|
||
# ============================================================
|
||
# SSE 流式 Chat Completions(post_stream)
|
||
#
|
||
# 与 post() 不同:请求头带 Accept: text/event-stream;响应体按行解析
|
||
# SSE(data: {json}),把每段 choices[0].delta.content 抽进 _st_buf 供
|
||
# UI 线程 stream_pull() 增量拉取显示。全程阻塞读取,应在后台线程调用。
|
||
#
|
||
# 返回: >=0 HTTP 状态码(2xx 正常完成;3xx/4xx/5xx 状态仍返回给调用方,
|
||
# 正文中的非 SSE 内容不会进 _st_buf);-1/-2 网络/解析失败
|
||
# (_st_state=2,失败原因见 stream_error_text()/last_error())。
|
||
# ============================================================
|
||
|
||
def _SseDeltaContent(root: rawptr[json.JsonValue]) -> str:
|
||
"""从流式 data 事件取 choices[0].delta.content;缺失返回 None。"""
|
||
if root is None or not root.is_object():
|
||
return None
|
||
cv: rawptr[json.JsonValue] = root["choices"]
|
||
if cv is None or not cv.is_array():
|
||
return None
|
||
m0: rawptr[json.JsonValue] = cv.get_item(0)
|
||
if m0 is None or not m0.is_object():
|
||
return None
|
||
dv: rawptr[json.JsonValue] = m0["delta"]
|
||
if dv is None or not dv.is_object():
|
||
return None
|
||
content_v: rawptr[json.JsonValue] = dv["content"]
|
||
if content_v is None or not content_v.is_string():
|
||
return None
|
||
return content_v.as_string()
|
||
|
||
|
||
def _StreamAppendContent(txt: str):
|
||
"""把一段增量内容追加到 _st_buf(超容量则截断丢弃并记录)。"""
|
||
global _st_len
|
||
if txt is None:
|
||
return
|
||
pos: t.CSizeT = _st_len
|
||
i: t.CSizeT = 0
|
||
while txt[i] != 0:
|
||
if pos >= t.CSizeT(STREAM_CAP) - 1:
|
||
break
|
||
_st_buf[pos] = txt[i]
|
||
pos += 1
|
||
i += 1
|
||
_st_len = pos
|
||
|
||
|
||
def _SetStreamFail(rc: INT):
|
||
"""流式失败收尾:从 _err_store 拷原因文本并置状态。"""
|
||
global _st_state, _st_status
|
||
_st_state = 2
|
||
_st_status = rc
|
||
stdio.snprintf(_st_err, 512, "%s", _err_store)
|
||
|
||
|
||
def post_stream(url: str, data: str, content_type: str = "application/json",
|
||
verify: bool = True, ca_file: str = None, auth: str = None) -> INT:
|
||
"""SSE 流式 POST(阻塞,应在后台线程调用)。
|
||
|
||
data 应为已含 "stream":true 的 JSON 请求体。
|
||
返回 HTTP 状态码;负数为网络/解析失败(_st_state=2)。
|
||
"""
|
||
global _st_state, _st_status
|
||
if requests._mbuddy is None:
|
||
return -1
|
||
|
||
_st_state = 0
|
||
_st_status = 0
|
||
|
||
# Winsock/openssl 初始化(幂等;流式首次可能在后台线程执行)
|
||
if _EnsureReady() != 0:
|
||
_SetStreamFail(-1)
|
||
return -1
|
||
|
||
# ---- 连接/TLS(与 _HttpsRequest 一致)----
|
||
errmsg: str = requests._mbuddy.alloc(512)
|
||
if errmsg is None:
|
||
return -1
|
||
|
||
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)
|
||
_SetStreamFail(-1)
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
if sock.fd == socket.INVALID_SOCKET:
|
||
_RecordErrorText("socket() failed")
|
||
_SetStreamFail(-1)
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
if sock.connect(host_buf, port) != 0:
|
||
stdio.snprintf(errmsg, 512, "connect %s:%d failed", host_buf, port)
|
||
_RecordErrorText(errmsg)
|
||
_SetStreamFail(-1)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
ctx: openssl.SslCtxPtr = openssl.SSL_CTX_new(openssl.TLS_client_method())
|
||
if ctx is None:
|
||
_RecordErrorText("SSL_CTX_new failed")
|
||
_SetStreamFail(-1)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
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)
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
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")
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
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:
|
||
want: INT = openssl.SSL_get_error(ssl, hret)
|
||
_TlsErrText()
|
||
stdio.snprintf(errmsg, 512, "SSL_connect failed (want=%d): %s", want, _ScratchBuf())
|
||
_RecordErrorText(errmsg)
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -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)
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
# ---- 组请求头(POST + Accept: text/event-stream)并发送 ----
|
||
req_buf: str = requests._mbuddy.alloc(requests.HTTP_BUFSIZ)
|
||
if req_buf is None:
|
||
_RecordErrorText("alloc request buffer failed")
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
body_len: INT = INT(string.strlen(data)) if data is not None else 0
|
||
ct: str = content_type if content_type is not None else "application/json"
|
||
hdr_off: INT = 0
|
||
stdio.snprintf(req_buf, requests.HTTP_BUFSIZ, "POST %s HTTP/1.1\r\nHost: %s\r\n", path_buf, host_buf)
|
||
hdr_off = INT(string.strlen(req_buf))
|
||
if auth is not None and auth[0] != 0:
|
||
stdio.snprintf(req_buf + hdr_off, requests.HTTP_BUFSIZ - hdr_off, "Authorization: %s\r\n", auth)
|
||
hdr_off = INT(string.strlen(req_buf))
|
||
stdio.snprintf(req_buf + hdr_off, requests.HTTP_BUFSIZ - hdr_off,
|
||
"Content-Type: %s\r\nContent-Length: %d\r\nAccept: text/event-stream\r\nConnection: close\r\n\r\n",
|
||
ct, body_len)
|
||
|
||
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)
|
||
_SetStreamFail(-1)
|
||
requests._mbuddy.free(req_buf)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
if body_len > 0 and data is not None:
|
||
openssl.SSL_write(ssl, data, body_len)
|
||
requests._mbuddy.free(req_buf)
|
||
|
||
# ---- 读取响应:跳过响应头 → SSE 行解析 → 抽 delta.content ----
|
||
rd: str = requests._mbuddy.alloc(16384)
|
||
if rd is None:
|
||
_RecordErrorText("alloc stream buffer failed")
|
||
_SetStreamFail(-1)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
|
||
# JSON 解析池(事件粒度 reset,足够容纳单个 data 事件)
|
||
pmem: str = stdlib.malloc(524288)
|
||
if pmem is None:
|
||
_RecordErrorText("alloc json pool failed")
|
||
_SetStreamFail(-1)
|
||
requests._mbuddy.free(rd)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(errmsg)
|
||
return -1
|
||
jpool: rawptr[memhub.MemPool] = memhub.MemPool(pmem, 524288)
|
||
|
||
leftover: t.CArray[t.CChar, 16384]
|
||
llen: INT = 0
|
||
header_done: INT = 0
|
||
status_code: INT = 0
|
||
had_read: INT = 0
|
||
|
||
while True:
|
||
n: INT = openssl.SSL_read(ssl, rd, 16384)
|
||
if n <= 0:
|
||
break
|
||
had_read = 1
|
||
# 1) 追加到 leftover
|
||
ai: INT = 0
|
||
while ai < n and llen < 16383:
|
||
leftover[llen] = rd[ai]
|
||
llen += 1
|
||
ai += 1
|
||
# 2) 逐行处理
|
||
while True:
|
||
# 找行尾 \n
|
||
nl: INT = -1
|
||
k: INT = 0
|
||
while k < llen:
|
||
if leftover[k] == 10:
|
||
nl = k
|
||
break
|
||
k += 1
|
||
if nl < 0:
|
||
break
|
||
e: INT = nl
|
||
if e > 0 and leftover[e - 1] == 13:
|
||
e -= 1
|
||
leftover[e] = 0
|
||
line: str = leftover
|
||
if header_done == 0:
|
||
if string.strncmp(line, "HTTP/", 5) == 0:
|
||
# 状态码:HTTP/1.1 200 OK
|
||
sp: INT = 0
|
||
while line[sp] != 0 and line[sp] != ' ':
|
||
sp += 1
|
||
if line[sp] == ' ':
|
||
sp += 1
|
||
code_val: INT = 0
|
||
while line[sp] >= '0' and line[sp] <= '9':
|
||
code_val = code_val * 10 + INT(line[sp] - '0')
|
||
sp += 1
|
||
status_code = code_val
|
||
if e == 0:
|
||
header_done = 1
|
||
else:
|
||
if e > 0 and line[0] != ':':
|
||
if string.strncmp(line, "data:", 5) == 0:
|
||
pl: str = line + 5
|
||
while pl[0] == ' ':
|
||
pl += 1
|
||
if pl[0] != 0:
|
||
if string.strncmp(pl, "[DONE]", 6) != 0 or pl[6] != 0:
|
||
jpool.reset()
|
||
ev: rawptr[json.JsonValue] = json.parse(jpool, pl)
|
||
dtx: str = _SseDeltaContent(ev)
|
||
if dtx is not None:
|
||
_StreamAppendContent(dtx)
|
||
# 3) 移除已处理行
|
||
rem: INT = llen - (nl + 1)
|
||
j2: INT = 0
|
||
while j2 < rem:
|
||
leftover[j2] = leftover[nl + 1 + j2]
|
||
j2 += 1
|
||
llen = rem
|
||
|
||
# ---- 收尾 ----
|
||
openssl.SSL_shutdown(ssl)
|
||
openssl.SSL_free(ssl)
|
||
openssl.SSL_CTX_free(ctx)
|
||
sock.close()
|
||
requests._mbuddy.free(rd)
|
||
stdlib.free(pmem)
|
||
requests._mbuddy.free(errmsg)
|
||
|
||
if status_code == 0:
|
||
if had_read == 0:
|
||
_RecordErrorText("no response data")
|
||
else:
|
||
_RecordErrorText("missing HTTP status line")
|
||
_SetStreamFail(-2)
|
||
return -2
|
||
|
||
_st_status = status_code
|
||
if status_code >= 200 and status_code < 300:
|
||
_st_state = 1
|
||
else:
|
||
_st_state = 2
|
||
stdio.snprintf(_st_err, 512, "HTTP %d(请检查 Key/模型/地址或账号额度)", status_code)
|
||
return status_code
|
||
|
||
|
||
# ============================================================
|
||
# 大文件 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
|