1043 lines
35 KiB
Plaintext
1043 lines
35 KiB
Plaintext
import string
|
|
import socket
|
|
import stdio
|
|
import stdlib
|
|
import requests
|
|
import openssl
|
|
import json
|
|
import memhub
|
|
|
|
|
|
# ============================================================
|
|
# requests/https.vp - HTTPS client (openssl SSL/TLS + certificate verification)
|
|
#
|
|
# Same API names as requests (HTTP), but over TLS and by default it verifies the peer certificate:
|
|
# 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)
|
|
#
|
|
# Notes:
|
|
# - with verify=True it verifies the certificate chain + hostname (hostname check via SSL_set1_host),
|
|
# provide ca_file pointing to a PEM CA bundle; if not provided it uses the system default path
|
|
# (on Windows this is usually empty, so real sites fail with "unable to verify certificate chain" and return None).
|
|
# - on failure returns None; use last_error() to get the reason text.
|
|
# - after a successful handshake, use last_tls_info() to view the TLS version / cipher suite / peer certificate subject.
|
|
# ============================================================
|
|
|
|
define HTTPS_PORT = 443
|
|
|
|
# Module-internal state
|
|
_ossl_ready: bool = False
|
|
_socket_ready: bool = False
|
|
_err_store: [512]char # last failure reason (static buffer)
|
|
_info_store: [512]char # TLS metadata of last successful handshake
|
|
_scratch_store: [512]char # scratch concat buffer
|
|
_last_verify_val: int = -1 # last SSL_get_verify_result (0=ok; -1=disabled)
|
|
_last_verify_flag: int = -1 # last request verify flag (1=on; 0=off; -1=unknown)
|
|
|
|
# ============================================================
|
|
# SSE streaming receive area (post_stream only; single request, polled by UI thread).
|
|
# _st_len written bytes; _st_consumed consumed cursor; _st_state 0=running 1=done(2xx) 2=fail; _st_status HTTP code.
|
|
# ============================================================
|
|
define STREAM_CAP = 262144
|
|
_st_buf: [STREAM_CAP]char
|
|
_st_len: sizet = 0
|
|
_st_consumed: sizet = 0
|
|
_st_state: int = 0
|
|
_st_status: int = 0
|
|
_st_err: [512]char
|
|
|
|
|
|
def stream_begin():
|
|
"""Start a new streaming receive: reset buffer/cursor/state."""
|
|
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:
|
|
"""Copy unconsumed delta into out (NUL-terminated); return bytes copied."""
|
|
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
|
|
for i in range(int(sizet(avail))):
|
|
out[i] = _st_buf[_st_consumed + i]
|
|
out[sizet(avail)] = 0
|
|
_st_consumed += sizet(avail)
|
|
return avail
|
|
|
|
|
|
def stream_state() -> int:
|
|
"""0=running 1=done(2xx) 2=failed."""
|
|
return _st_state
|
|
|
|
|
|
def stream_status() -> int:
|
|
"""HTTP status code; negative means network/parse error."""
|
|
return _st_status
|
|
|
|
|
|
def stream_error_text() -> str:
|
|
"""Failure reason text (when _st_state==2)."""
|
|
return _st_err
|
|
|
|
|
|
def last_verify_result() -> int:
|
|
"""Last cert verify result: 0=ok; >0=failure code; -1=verify disabled."""
|
|
return _last_verify_val
|
|
|
|
|
|
def last_verify_enabled() -> int:
|
|
"""Whether last request enabled verify (1=yes; 0=no; -1=unknown)."""
|
|
return _last_verify_flag
|
|
|
|
|
|
# ============================================================
|
|
# internal helpers
|
|
# ============================================================
|
|
|
|
def last_error() -> str:
|
|
"""Last failure reason text (empty string if none)."""
|
|
return _err_store
|
|
|
|
|
|
def last_tls_info() -> str:
|
|
"""Last successful handshake TLS info (version/cipher/peer subject), empty if none."""
|
|
return _info_store
|
|
|
|
|
|
def _RecordErrorText(text: str):
|
|
"""Write failure reason into static buffer _err_store."""
|
|
if text is None: return
|
|
_err_store[0] = 0
|
|
stdio.snprintf(_err_store, 512, "%s", text)
|
|
|
|
|
|
def _RecordInfoText(text: str):
|
|
"""Write TLS metadata into static buffer _info_store."""
|
|
if text is None: return
|
|
_info_store[0] = 0
|
|
stdio.snprintf(_info_store, 512, "%s", text)
|
|
|
|
|
|
def _EnsureReady() -> int:
|
|
"""Initialize socket and openssl (both idempotent)."""
|
|
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*) -> int:
|
|
"""Parse https://host[:port][/path]; default port 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 bytes)
|
|
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
|
|
*port = port_val
|
|
else:
|
|
*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:
|
|
"""Return static concat buffer (512B, reused)."""
|
|
return _scratch_store
|
|
|
|
|
|
def _TlsErrText():
|
|
"""Write last openssl error-queue text into static scratch buffer."""
|
|
e: uint32 = 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)
|
|
|
|
|
|
# ============================================================
|
|
# Core request
|
|
# ============================================================
|
|
|
|
def _HttpsRequest(method: str, url: str, verify: bool, ca_file: str, data: str, content_type: str, auth: str = None) -> requests.Response*:
|
|
"""HTTPS GET/POST core. verify=False skips cert chain/hostname checks (not recommended);
|
|
verify=True uses ca_file PEM bundle (else system default); auth adds Authorization header on POST only.
|
|
"""
|
|
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* = requests.create_response()
|
|
if resp is None: return None
|
|
|
|
if _EnsureReady() != 0:
|
|
requests.release(resp)
|
|
return None
|
|
|
|
# Temp buffer for error text; openssl details go to module scratch.
|
|
errmsg: str = requests._mbuddy.alloc(512)
|
|
if errmsg is None:
|
|
requests.release(resp)
|
|
return None
|
|
|
|
# ---- Parse URL ----
|
|
host_buf: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
|
|
if _ParseHttpsUrl(url, host_buf, 256, path_buf, 1024, &port) != 0:
|
|
stdio.snprintf(errmsg, 512, "bad https url: %s", url)
|
|
_RecordErrorText(errmsg)
|
|
requests._mbuddy.free(errmsg)
|
|
requests.release(resp)
|
|
return None
|
|
|
|
# ---- Establish TCP connection ----
|
|
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:
|
|
# System default paths (usually empty on Windows; cert chain check will fail).
|
|
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 is a macro -> call SSL_ctrl directly)
|
|
openssl.SSL_ctrl(ssl, openssl.SslCtrlSetTlsextHostname, openssl.TlsextNametypeHostName, host_buf)
|
|
# Hostname verification (TLS core compares cert SAN/CN against host)
|
|
if verify:
|
|
openssl.SSL_set1_host(ssl, host_buf)
|
|
|
|
# ---- Handshake ----
|
|
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
|
|
|
|
# ---- Record last cert verify result (for last_verify_result) ----
|
|
if verify:
|
|
_last_verify_val = int(openssl.SSL_get_verify_result(ssl))
|
|
else:
|
|
_last_verify_val = -1
|
|
|
|
# ---- Re-check cert chain verify result ----
|
|
if verify:
|
|
vres: int32 = 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
|
|
|
|
# ---- Record TLS metadata (version / cipher / peer subject) ----
|
|
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)
|
|
|
|
# ---- Build request headers and send ----
|
|
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"
|
|
# Build header in stages so Authorization can be inserted on demand (only when auth non-empty).
|
|
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)
|
|
|
|
# ---- Receive response (buffer auto-doubles, supports MB bodies) ----
|
|
resp_cap: int = requests.HTTP_BUFSIZ
|
|
resp_buf: str = requests._mbuddy.alloc(sizet(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:
|
|
# Buffer full but more data may follow: double capacity (hard cap 256MB, runaway-guard).
|
|
if resp_cap >= 268435456:
|
|
break
|
|
resp_cap = resp_cap * 2
|
|
nb: rawptr = requests._mbuddy.realloc(resp_buf, sizet(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 = bytes(uint64(nb))
|
|
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
|
|
|
|
# Close TLS and 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
|
|
|
|
|
|
# ============================================================
|
|
# Public API
|
|
# ============================================================
|
|
|
|
def _HttpsRedirectLoop(method: str, url: str, verify: bool, ca_file: str, data: str, content_type: str, auth: str = None) -> requests.Response*:
|
|
"""HTTPS GET/POST with redirect following (cross http/https, max 5 hops).
|
|
auth is passed through to _HttpsRequest; note Authorization also follows cross-host redirects.
|
|
"""
|
|
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)
|
|
|
|
for hop in range(5):
|
|
resp: 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)
|
|
|
|
# Cross-scheme dispatch: https -> continue loop; http -> hand to requests (http) chain
|
|
if string.strncmp(nxt, "https://", 8) != 0:
|
|
requests._mbuddy.free(loc)
|
|
requests._mbuddy.free(cur)
|
|
http_resp: requests.Response* = requests.get(nxt)
|
|
requests._mbuddy.free(nxt)
|
|
return http_resp
|
|
|
|
requests._mbuddy.free(loc)
|
|
requests._mbuddy.free(cur)
|
|
cur = nxt
|
|
|
|
_RecordErrorText("too many redirects")
|
|
requests._mbuddy.free(cur)
|
|
return None
|
|
|
|
|
|
def get(url: str, verify: bool = True, ca_file: str = None) -> requests.Response*:
|
|
"""HTTPS GET (follows 3xx redirects, max 5 hops). verify checks cert chain + hostname;
|
|
ca_file is a PEM CA bundle path, None uses system default. Returns Response* or None.
|
|
"""
|
|
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) -> requests.Response*:
|
|
"""HTTPS POST (follows 3xx redirects, max 5 hops). verify checks cert chain + hostname;
|
|
ca_file PEM CA bundle (None = system default); auth optional "Bearer <key>", sent when non-empty.
|
|
"""
|
|
return _HttpsRedirectLoop("POST", url, verify, ca_file, data, content_type, auth)
|
|
|
|
|
|
# ============================================================
|
|
# SSE streaming Chat Completions (post_stream): sends Accept: text/event-stream and extracts
|
|
# choices[0].delta.content from data:{json} lines into _st_buf; blocking, returns HTTP status or -1/-2.
|
|
# ============================================================
|
|
|
|
def _SseDeltaContent(root: json.JsonValue*) -> str:
|
|
"""Extract choices[0].delta.content from a stream data event; None if absent."""
|
|
if root is None or not root.is_object():
|
|
return None
|
|
cv: json.JsonValue* = root["choices"]
|
|
if cv is None or not cv.is_array():
|
|
return None
|
|
m0: json.JsonValue* = cv.get_item(0)
|
|
if m0 is None or not m0.is_object():
|
|
return None
|
|
dv: json.JsonValue* = m0["delta"]
|
|
if dv is None or not dv.is_object():
|
|
return None
|
|
content_v: 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):
|
|
"""Append delta content to _st_buf (truncate on overflow)."""
|
|
global _st_len
|
|
if txt is None:
|
|
return
|
|
pos: sizet = _st_len
|
|
i: sizet = 0
|
|
while txt[i] != 0:
|
|
if pos >= sizet(STREAM_CAP) - 1:
|
|
break
|
|
_st_buf[pos] = txt[i]
|
|
pos += 1
|
|
i += 1
|
|
_st_len = pos
|
|
|
|
|
|
def _SetStreamFail(rc: int):
|
|
"""Finish stream failure: copy reason from _err_store and set state."""
|
|
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 streaming POST (blocking; call from a background thread). data must be a JSON body
|
|
containing "stream":true. Returns HTTP status; negative means network/parse failure.
|
|
"""
|
|
global _st_state, _st_status
|
|
if requests._mbuddy is None:
|
|
return -1
|
|
|
|
_st_state = 0
|
|
_st_status = 0
|
|
|
|
# Winsock/openssl init (idempotent; first call may run on a background thread)
|
|
if _EnsureReady() != 0:
|
|
_SetStreamFail(-1)
|
|
return -1
|
|
|
|
# ---- Connect/TLS (same as _HttpsRequest) ----
|
|
errmsg: str = requests._mbuddy.alloc(512)
|
|
if errmsg is None:
|
|
return -1
|
|
|
|
host_buf: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
if _ParseHttpsUrl(url, host_buf, 256, path_buf, 1024, &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: int32 = 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
|
|
|
|
# ---- Build request headers (POST + Accept: text/event-stream) and send ----
|
|
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)
|
|
|
|
# ---- Read response: skip headers -> parse SSE lines -> extract 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 pool (reset per event, sized for one data event)
|
|
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: memhub.MemPool* = memhub.MemPool(pmem, 524288)
|
|
|
|
leftover: [16384]char
|
|
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) Append to leftover
|
|
ai: int = 0
|
|
while ai < n and llen < 16383:
|
|
leftover[llen] = rd[ai]
|
|
llen += 1
|
|
ai += 1
|
|
# 2) Process line by line
|
|
while True:
|
|
# Find line end \n
|
|
nl: int = -1
|
|
for k in range(int(llen)):
|
|
if leftover[k] == 10:
|
|
nl = k
|
|
break
|
|
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:
|
|
# Status code: 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: json.JsonValue* = json.parse(jpool, pl)
|
|
dtx: str = _SseDeltaContent(ev)
|
|
if dtx is not None:
|
|
_StreamAppendContent(dtx)
|
|
# 3) Remove processed line
|
|
rem: int = llen - (nl + 1)
|
|
for j2 in range(int(rem)):
|
|
leftover[j2] = leftover[nl + 1 + j2]
|
|
llen = rem
|
|
|
|
# ---- Wrap up ----
|
|
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 (check Key/model/address or account quota)", status_code)
|
|
return status_code
|
|
|
|
|
|
# ============================================================
|
|
# Large-file Range download primitive for requests.download. Thread-safe: no _mbuddy,
|
|
# independent SSL_CTX/SSL/socket per connection. Returns body bytes>=0; -1 arg; -2 redirect; -3 failure.
|
|
# ============================================================
|
|
|
|
def _DlOnce(url: str, start: sizet, count: int, verify: bool, ca_file: str,
|
|
dst: str, dst_cap: int, out_status: int*,
|
|
out_loc: str, out_loccap: int,
|
|
out_total: sizet*) -> int:
|
|
"""HTTPS single Range fetch (thread-safe; see module comment)."""
|
|
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
|
|
*out_status = 0
|
|
*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: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
if _ParseHttpsUrl(url, host_buf, 256, path_buf, 1024, &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
|
|
|
|
# Build Range request header (no Accept-Encoding, so response is raw bytes)
|
|
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: sizet = start + sizet(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)
|
|
*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:
|
|
*out_total = requests._CRTotalOf(dst, total)
|
|
else:
|
|
cl2: int = requests._CLenOf(dst, total)
|
|
if cl2 > 0:
|
|
*out_total = sizet(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
|