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