899 lines
27 KiB
Plaintext
899 lines
27 KiB
Plaintext
import t, c
|
||
from stdint import *
|
||
import string
|
||
import socket
|
||
import memhub
|
||
import stdio
|
||
import stdlib
|
||
import zlib.pyzlib as pyzlib
|
||
|
||
|
||
# ============================================================
|
||
# requests/__init__.vp — HTTP(S) 客户端库包入口(Python requests 风格)
|
||
#
|
||
# 包结构:
|
||
# requests HTTP 客户端(纯 socket 实现,本文件)
|
||
# requests.https HTTPS 客户端(openssl SSL/TLS + 证书核验,见 https.vp)
|
||
#
|
||
# Usage:
|
||
# requests._mbuddy = my_mbuddy
|
||
# resp = requests.get("http://example.com/path")
|
||
# if resp is not None:
|
||
# print(resp.status_code)
|
||
# print(resp.text)
|
||
# requests.release(resp)
|
||
# ============================================================
|
||
|
||
# Module-level global MBuddy pointer (assigned by the user in main)
|
||
_mbuddy: memhub.MemBuddy | t.CPtr
|
||
|
||
# Socket initialization flag
|
||
_socket_ready: bool = False
|
||
|
||
HTTP_PORT: t.CDefine = 80
|
||
HTTP_BUFSIZ: t.CDefine = 4096
|
||
GZIP_BUFSIZ: t.CDefine = 65536
|
||
|
||
# 模块内重定向 / 失败诊断(静态缓冲)
|
||
_http_err_store: t.CArray[t.CChar, 512]
|
||
|
||
|
||
def last_error() -> str:
|
||
"""返回 HTTP 层最近一次失败的原因(无则空串)。"""
|
||
return _http_err_store
|
||
|
||
|
||
def _SetHttpError(text: str):
|
||
if text is None: return
|
||
_http_err_store[0] = 0
|
||
stdio.snprintf(_http_err_store, 512, "%s", text)
|
||
|
||
|
||
# ============================================================
|
||
# Response — HTTP response object
|
||
# ============================================================
|
||
|
||
class Response:
|
||
"""HTTP response object, similar to how Python's requests.Response works.
|
||
|
||
Allocated from _mbuddy, call release(resp) to free it after use.
|
||
"""
|
||
status_code: INT # HTTP status code (e.g., 200, 404)
|
||
text: str # Response body text
|
||
body_len: INT # Response body length
|
||
_raw_buf: str # 原始响应缓冲(含头与正文,release 释放)
|
||
_body_owned: str # 独立解码正文缓冲(chunked 等解码结果,release 一并释放)
|
||
|
||
|
||
# ============================================================
|
||
# Internal auxiliary functions
|
||
# ============================================================
|
||
|
||
def _EnsureSocketInit() -> INT:
|
||
"""Ensure Socket library is initialized (idempotent)."""
|
||
global _socket_ready
|
||
if _socket_ready: return 0
|
||
result: INT = socket.SocketInit()
|
||
if result == 0:
|
||
_socket_ready = True
|
||
return result
|
||
|
||
|
||
def _AllocResponse() -> Response | t.CPtr:
|
||
"""Allocate Response struct from _mbuddy and initialize it in-place."""
|
||
if _mbuddy is None: return None
|
||
resp: Response | t.CPtr = _mbuddy.alloc(Response.__sizeof__())
|
||
if resp is None: return None
|
||
resp.status_code = 0
|
||
resp.text = None
|
||
resp.body_len = 0
|
||
resp._raw_buf = None
|
||
resp._body_owned = None
|
||
return resp
|
||
|
||
|
||
def _ParseUrl(url: str, host_buf: str, host_size: INT, path_buf: str, path_size: INT, port: INT | t.CPtr) -> INT:
|
||
"""Parse URL and extract host, port, and path."""
|
||
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 "http://"
|
||
if string.strncmp(p, "http://", 7) == 0:
|
||
p += 7
|
||
|
||
# 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, HTTP_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 _ParseStatusLine(resp_text: str, out_code: INT | t.CPtr) -> INT:
|
||
"""Parse HTTP status line and extract status code."""
|
||
if resp_text is None or out_code is None: return -1
|
||
|
||
p: str = resp_text
|
||
|
||
# Skip "HTTP/x.x "
|
||
while p[0] != '\0' and p[0] != ' ':
|
||
p += 1
|
||
if p[0] == ' ': p += 1
|
||
|
||
# Parse status code
|
||
code: INT = 0
|
||
while p[0] >= '0' and p[0] <= '9':
|
||
code = code * 10 + INT(p[0] - '0')
|
||
p += 1
|
||
|
||
c.DerefAs(out_code, code)
|
||
|
||
# Find \n
|
||
while p[0] != '\0' and p[0] != '\n':
|
||
p += 1
|
||
if p[0] == '\n': p += 1
|
||
|
||
return INT(t.CUInt64T(p) - t.CUInt64T(resp_text))
|
||
|
||
|
||
def _FindBodyStart(resp_text: str, resp_len: INT) -> INT:
|
||
"""Find the start of the HTTP response body (after rnrn)."""
|
||
if resp_text is None: return -1
|
||
|
||
end_of_headers: str = string.strstr(resp_text, "\r\n\r\n")
|
||
if end_of_headers is None:
|
||
end_of_headers = string.strstr(resp_text, "\n\n")
|
||
if end_of_headers is None:
|
||
return -1
|
||
return INT(t.CUInt64T(end_of_headers) - t.CUInt64T(resp_text)) + 2
|
||
|
||
return INT(t.CUInt64T(end_of_headers) - t.CUInt64T(resp_text)) + 4
|
||
|
||
|
||
# ============================================================
|
||
# 公共辅助(供 requests.https 等子模块复用)
|
||
# ============================================================
|
||
|
||
def create_response() -> Response | t.CPtr:
|
||
"""Create and initialize an empty HTTP Response (allocated from _mbuddy).
|
||
|
||
Call release(resp) to free it after use.
|
||
"""
|
||
return _AllocResponse()
|
||
|
||
|
||
def parse_status_line(resp_text: str, out_code: INT | t.CPtr) -> INT:
|
||
"""Parse HTTP status line, write status code into out_code."""
|
||
return _ParseStatusLine(resp_text, out_code)
|
||
|
||
|
||
def find_body_start(resp_text: str, resp_len: INT) -> INT:
|
||
"""Locate the start of the HTTP response body."""
|
||
return _FindBodyStart(resp_text, resp_len)
|
||
|
||
|
||
# ============================================================
|
||
# 响应解析(header 查询 / chunked 解码 / 统一 parse_response)
|
||
# ============================================================
|
||
|
||
def _CIEq(a: t.CChar, b: t.CChar) -> bool:
|
||
"""大小写不敏感字符相等(仅 ASCII)。"""
|
||
if a >= 'A' and a <= 'Z': a = t.CChar(INT(a) - INT('A') + INT('a'))
|
||
if b >= 'A' and b <= 'Z': b = t.CChar(INT(b) - INT('A') + INT('a'))
|
||
return a == b
|
||
|
||
|
||
def header_value(resp: Response | t.CPtr, name: str, out: str, out_size: INT) -> INT:
|
||
"""在原始响应中按名查询响应头(大小写不敏感,返回首值)。
|
||
|
||
Args:
|
||
resp: Response(其 _raw_buf 保留原始响应)
|
||
name: 头名称,如 "Content-Type"
|
||
out / out_size: 值写入缓冲
|
||
Returns:
|
||
0 找到并拷贝;-1 未找到或参数非法。
|
||
"""
|
||
if resp is None or resp._raw_buf is None or name is None or out is None: return -1
|
||
p: str = resp._raw_buf
|
||
|
||
# 跳过状态行
|
||
while p[0] != '\0' and p[0] != '\n':
|
||
p += 1
|
||
if p[0] == '\n': p += 1
|
||
|
||
# 逐头行匹配
|
||
while p[0] != '\0' and p[0] != '\r' and p[0] != '\n':
|
||
h: str = p
|
||
q: str = name
|
||
matched: bool = True
|
||
while q[0] != '\0':
|
||
if h[0] != q[0] and not _CIEq(h[0], q[0]):
|
||
matched = False
|
||
break
|
||
h += 1
|
||
q += 1
|
||
# name 匹配后允许 [空格]* ':'(RFC 无空格,宽松兼容)
|
||
while h[0] == ' ':
|
||
h += 1
|
||
if matched and h[0] == ':':
|
||
h += 1
|
||
while h[0] == ' ' or h[0] == '\t':
|
||
h += 1
|
||
i: INT = 0
|
||
while h[0] != '\0' and h[0] != '\r' and h[0] != '\n' and i < out_size - 1:
|
||
out[i] = h[0]
|
||
i += 1
|
||
h += 1
|
||
out[i] = 0
|
||
return 0
|
||
# 跳到下一行
|
||
while p[0] != '\0' and p[0] != '\n':
|
||
p += 1
|
||
if p[0] == '\n': p += 1
|
||
|
||
return -1
|
||
|
||
|
||
def _HexVal(ch: t.CChar) -> INT:
|
||
if ch >= '0' and ch <= '9': return INT(ch - '0')
|
||
if ch >= 'a' and ch <= 'f': return INT(ch - 'a') + 10
|
||
if ch >= 'A' and ch <= 'F': return INT(ch - 'A') + 10
|
||
return -1
|
||
|
||
|
||
def decode_chunked(src: str, src_len: INT, dst: str, dst_cap: INT) -> INT:
|
||
"""按 RFC 7230 解码 chunked 正文(不处理 chunk-ext,trailer 忽略)。
|
||
|
||
Args:
|
||
src / src_len: chunked 编码数据
|
||
dst / dst_cap: 解码输出缓冲
|
||
Returns:
|
||
解码字节数;失败返回 -1。
|
||
"""
|
||
if src is None or dst is None: return -1
|
||
|
||
r: INT = 0
|
||
w: INT = 0
|
||
while r < src_len:
|
||
# 1) chunk size(hex,可带 ;ext 扩展)
|
||
size: INT = 0
|
||
digit_seen: bool = False
|
||
while r < src_len:
|
||
ch: t.CChar = src[r]
|
||
if ch == '\r' or ch == '\n' or ch == ';': break
|
||
hv: INT = _HexVal(ch)
|
||
if hv < 0: return -1
|
||
digit_seen = True
|
||
if size > 0x7fffffff: return -1
|
||
size = size * 16 + hv
|
||
r += 1
|
||
if not digit_seen: return -1
|
||
# 2) 跳过扩展与行尾
|
||
while r < src_len and src[r] != '\n':
|
||
r += 1
|
||
if r < src_len and src[r] == '\n': r += 1
|
||
# 3) 结束块
|
||
if size == 0: return w
|
||
# 4) 拷贝数据 + 跳过 CRLF
|
||
if w + size > dst_cap: return -1
|
||
k: INT = 0
|
||
while k < size:
|
||
dst[w + k] = src[r + k]
|
||
k += 1
|
||
w += size
|
||
r += size
|
||
if r < src_len and src[r] == '\r': r += 1
|
||
if r < src_len and src[r] == '\n': r += 1
|
||
|
||
return w
|
||
|
||
|
||
def parse_response(resp: Response | t.CPtr, total: INT) -> INT:
|
||
"""统一解析已接收的原始响应并填充 Response 字段。
|
||
|
||
- 解析状态码 / 定位正文
|
||
- Transfer-Encoding: chunked 时解码到独立缓冲(_body_owned),
|
||
原始缓冲 _raw_buf 保留(供 header_value 查询)
|
||
Returns:
|
||
0 成功;-1 失败(此时 text=None / body_len=0)。
|
||
"""
|
||
if resp is None or resp._raw_buf is None: return -1
|
||
buf: str = resp._raw_buf
|
||
buf[total] = 0
|
||
|
||
_ParseStatusLine(buf, c.Addr(resp.status_code))
|
||
body_offset: INT = _FindBodyStart(buf, total)
|
||
if body_offset < 0 or body_offset >= total:
|
||
resp.text = None
|
||
resp.body_len = 0
|
||
return 0
|
||
|
||
# 检测 Transfer-Encoding: chunked
|
||
te_buf: t.CArray[t.CChar, 64]
|
||
chunked: bool = False
|
||
if header_value(resp, "Transfer-Encoding", te_buf, 64) == 0:
|
||
if string.strstr(te_buf, "chunked") is not None:
|
||
chunked = True
|
||
|
||
if chunked:
|
||
new_buf: str = _mbuddy.alloc(HTTP_BUFSIZ)
|
||
if new_buf is None:
|
||
resp.text = None
|
||
resp.body_len = 0
|
||
return -1
|
||
dec_len: INT = decode_chunked(buf + body_offset, total - body_offset, new_buf, HTTP_BUFSIZ)
|
||
if dec_len < 0:
|
||
_mbuddy.free(new_buf)
|
||
resp.text = None
|
||
resp.body_len = 0
|
||
return -1
|
||
new_buf[dec_len] = 0
|
||
resp._body_owned = new_buf
|
||
resp.text = new_buf
|
||
resp.body_len = dec_len
|
||
else:
|
||
resp.text = buf + body_offset
|
||
resp.body_len = total - body_offset
|
||
|
||
# ---- Content-Encoding: gzip 解压(zlib.pyzlib,纯 Viper 实现)----
|
||
ce_buf: t.CArray[t.CChar, 64]
|
||
if header_value(resp, "Content-Encoding", ce_buf, 64) == 0 \
|
||
and string.strstr(ce_buf, "gzip") is not None \
|
||
and resp.text is not None and resp.body_len > 0:
|
||
src_len2: t.CSizeT = t.CSizeT(resp.body_len)
|
||
olen2: t.CSizeT = 0
|
||
gz_out: BYTEPTR = pyzlib.decompress(_mbuddy, BYTEPTR(resp.text), src_len2, 15, GZIP_BUFSIZ, c.Addr(olen2))
|
||
if gz_out is not None:
|
||
old_owned: str = resp._body_owned
|
||
if old_owned is not None:
|
||
_mbuddy.free(old_owned)
|
||
gz_str: str = t.CChar(t.CUInt64T(gz_out), t.CPtr)
|
||
gz_str[olen2] = 0
|
||
resp._body_owned = gz_str
|
||
resp.text = gz_str
|
||
resp.body_len = INT(olen2)
|
||
else:
|
||
# 解压失败:保留原始 body,记录诊断信息
|
||
stdio.snprintf(_http_err_store, 512, "gzip decode failed: %s", pyzlib.get_error())
|
||
|
||
return 0
|
||
|
||
|
||
# ============================================================
|
||
# 公共 API
|
||
# ============================================================
|
||
|
||
def _HttpGetOnce(url: str) -> Response | t.CPtr:
|
||
"""HTTP GET 单次请求(不跟随重定向)。请求头带 Accept-Encoding: gzip。"""
|
||
if _mbuddy is None: return None
|
||
|
||
resp: Response | t.CPtr = _AllocResponse()
|
||
if resp is None: return None
|
||
|
||
if _EnsureSocketInit() != 0:
|
||
release(resp)
|
||
return None
|
||
|
||
# Parse URL
|
||
host_buf: t.CArray[t.CChar, 256]
|
||
path_buf: t.CArray[t.CChar, 1024]
|
||
port: INT = 0
|
||
|
||
if _ParseUrl(url, host_buf, 256, path_buf, 1024, c.Addr(port)) != 0:
|
||
release(resp)
|
||
return None
|
||
|
||
# Create Socket
|
||
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
if sock.fd == socket.INVALID_SOCKET:
|
||
release(resp)
|
||
return None
|
||
|
||
if sock.connect(host_buf, port) != 0:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
# Build HTTP request
|
||
req_buf: str = _mbuddy.alloc(HTTP_BUFSIZ)
|
||
if req_buf is None:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
stdio.snprintf(req_buf, 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)
|
||
|
||
req_len: INT = INT(string.strlen(req_buf))
|
||
sent: INT = sock.send(req_buf, req_len)
|
||
_mbuddy.free(req_buf)
|
||
|
||
if sent <= 0:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
# Receive response body
|
||
resp_buf: str = _mbuddy.alloc(HTTP_BUFSIZ)
|
||
if resp_buf is None:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
total: INT = 0
|
||
while total < HTTP_BUFSIZ - 1:
|
||
n: INT = sock.recv(resp_buf + total, HTTP_BUFSIZ - 1 - total)
|
||
if n <= 0: break
|
||
total += n
|
||
|
||
resp_buf[total] = 0
|
||
sock.close()
|
||
|
||
resp._raw_buf = resp_buf
|
||
|
||
parse_response(resp, total)
|
||
return resp
|
||
|
||
|
||
def _HttpPostOnce(url: str, data: str = None, content_type: str = "application/x-www-form-urlencoded") -> Response | t.CPtr:
|
||
"""HTTP POST 单次请求(不跟随重定向)。请求头带 Accept-Encoding: gzip。"""
|
||
if _mbuddy is None: return None
|
||
|
||
resp: Response | t.CPtr = _AllocResponse()
|
||
if resp is None: return None
|
||
|
||
if _EnsureSocketInit() != 0:
|
||
release(resp)
|
||
return None
|
||
|
||
# Parse URL
|
||
host_buf: t.CArray[t.CChar, 256]
|
||
path_buf: t.CArray[t.CChar, 1024]
|
||
port: INT = 0
|
||
|
||
if _ParseUrl(url, host_buf, 256, path_buf, 1024, c.Addr(port)) != 0:
|
||
release(resp)
|
||
return None
|
||
|
||
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/x-www-form-urlencoded"
|
||
|
||
# Create Socket
|
||
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
if sock.fd == socket.INVALID_SOCKET:
|
||
release(resp)
|
||
return None
|
||
|
||
if sock.connect(host_buf, port) != 0:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
# Build HTTP request
|
||
req_buf: str = _mbuddy.alloc(HTTP_BUFSIZ)
|
||
if req_buf is None:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
stdio.snprintf(req_buf, 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)
|
||
|
||
hdr_len: INT = INT(string.strlen(req_buf))
|
||
|
||
# Send headers + body
|
||
sent: INT = sock.send(req_buf, hdr_len)
|
||
if sent > 0 and body_len > 0 and data is not None:
|
||
sock.send(data, body_len)
|
||
|
||
_mbuddy.free(req_buf)
|
||
|
||
# Receive response body
|
||
resp_buf: str = _mbuddy.alloc(HTTP_BUFSIZ)
|
||
if resp_buf is None:
|
||
sock.close()
|
||
release(resp)
|
||
return None
|
||
|
||
total: INT = 0
|
||
while total < HTTP_BUFSIZ - 1:
|
||
n: INT = sock.recv(resp_buf + total, HTTP_BUFSIZ - 1 - total)
|
||
if n <= 0: break
|
||
total += n
|
||
|
||
resp_buf[total] = 0
|
||
sock.close()
|
||
|
||
resp._raw_buf = resp_buf
|
||
|
||
parse_response(resp, total)
|
||
return resp
|
||
|
||
|
||
# ============================================================
|
||
# 重定向跟随
|
||
# ============================================================
|
||
|
||
def _ResolveRedirectUrl(base: str, loc: str, out: str, cap: INT) -> INT:
|
||
"""把 Location 合并为完整 URL(支持绝对 / 协议相对 / 根相对),结果写 out。
|
||
|
||
Returns: 0 成功;-1 失败。
|
||
"""
|
||
if base is None or loc is None or out is None: return -1
|
||
lb: INT = INT(string.strlen(loc))
|
||
if string.strncmp(loc, "http://", 7) == 0 or string.strncmp(loc, "https://", 8) == 0:
|
||
if lb >= cap: return -1
|
||
string.strcpy(out, loc)
|
||
return 0
|
||
# 取 base 的 scheme://authority 前缀
|
||
q: str = base
|
||
while q[0] != '\0' and q[0] != ':': q += 1
|
||
if q[0] != ':': return -1
|
||
q += 1
|
||
if q[0] != '/': return -1
|
||
q += 1
|
||
if q[0] != '/': return -1
|
||
q += 1
|
||
while q[0] != '\0' and q[0] != '/': q += 1
|
||
prefix_len: INT = INT(t.CUInt64T(q) - t.CUInt64T(base))
|
||
need: INT = prefix_len + lb + 2
|
||
if need >= cap: return -1
|
||
i: INT = 0
|
||
while i < prefix_len:
|
||
out[i] = base[i]
|
||
i += 1
|
||
if loc[0] == '/':
|
||
j: INT = 0
|
||
while loc[j] != '\0':
|
||
out[prefix_len + j] = loc[j]
|
||
j += 1
|
||
out[prefix_len + j] = 0
|
||
else:
|
||
out[prefix_len] = '/'
|
||
j: INT = 0
|
||
while loc[j] != '\0':
|
||
out[prefix_len + 1 + j] = loc[j]
|
||
j += 1
|
||
out[prefix_len + 1 + j] = 0
|
||
return 0
|
||
|
||
|
||
def _HttpRedirectLoop(method: str, url: str, data: str, content_type: str) -> Response | t.CPtr:
|
||
"""GET/POST + 重定向跟随(http:// 链,最多 5 跳)。"""
|
||
if _mbuddy is None: return None
|
||
|
||
cur: str = _mbuddy.alloc(2048)
|
||
if cur is None: return None
|
||
if INT(string.strlen(url)) >= 2047:
|
||
_SetHttpError("url too long")
|
||
_mbuddy.free(cur)
|
||
return None
|
||
string.strcpy(cur, url)
|
||
|
||
hop: INT = 0
|
||
while hop < 5:
|
||
resp: Response | t.CPtr = None
|
||
if string.strcmp(method, "POST") == 0:
|
||
resp = _HttpPostOnce(cur, data, content_type)
|
||
else:
|
||
resp = _HttpGetOnce(cur)
|
||
if resp is None:
|
||
_mbuddy.free(cur)
|
||
return None
|
||
|
||
code: INT = resp.status_code
|
||
if not _IsHttpRedirect(code):
|
||
_mbuddy.free(cur)
|
||
return resp
|
||
|
||
loc: str = _mbuddy.alloc(2048)
|
||
if loc is None:
|
||
_mbuddy.free(cur)
|
||
return resp
|
||
if header_value(resp, "Location", loc, 2048) != 0:
|
||
_mbuddy.free(loc)
|
||
_mbuddy.free(cur)
|
||
return resp
|
||
|
||
nxt: str = _mbuddy.alloc(2048)
|
||
if nxt is None:
|
||
_mbuddy.free(loc)
|
||
_mbuddy.free(cur)
|
||
return resp
|
||
if _ResolveRedirectUrl(cur, loc, nxt, 2048) != 0:
|
||
_SetHttpError("unresolvable redirect Location")
|
||
release(resp)
|
||
_mbuddy.free(nxt)
|
||
_mbuddy.free(loc)
|
||
_mbuddy.free(cur)
|
||
return None
|
||
if string.strncmp(nxt, "https://", 8) == 0:
|
||
_SetHttpError("redirect to https:// needs requests.https")
|
||
release(resp)
|
||
_mbuddy.free(nxt)
|
||
_mbuddy.free(loc)
|
||
_mbuddy.free(cur)
|
||
return None
|
||
release(resp)
|
||
_mbuddy.free(loc)
|
||
_mbuddy.free(cur)
|
||
cur = nxt
|
||
hop += 1
|
||
|
||
_SetHttpError("too many redirects")
|
||
_mbuddy.free(cur)
|
||
return None
|
||
|
||
|
||
def _IsHttpRedirect(code: INT) -> bool:
|
||
return code >= 300 and code < 400
|
||
|
||
|
||
def get(url: str) -> Response | t.CPtr:
|
||
"""HTTP GET(自动跟随 3xx 重定向,最多 5 跳)。
|
||
|
||
Args:
|
||
url: Full URL (e.g., "http://example.com/path")
|
||
Returns:
|
||
Pointer to a Response object, None on failure (see last_error()).
|
||
"""
|
||
return _HttpRedirectLoop("GET", url, None, None)
|
||
|
||
|
||
def post(url: str, data: str = None, content_type: str = "application/x-www-form-urlencoded") -> Response | t.CPtr:
|
||
"""HTTP POST(自动跟随 3xx 重定向,最多 5 跳)。"""
|
||
return _HttpRedirectLoop("POST", url, data, content_type)
|
||
|
||
|
||
def release(resp: Response | t.CPtr):
|
||
"""Release Response object and its internal buffers.
|
||
|
||
Args:
|
||
resp: The Response object to release
|
||
"""
|
||
if resp is None: return
|
||
if _mbuddy is None: return
|
||
if resp._body_owned is not None:
|
||
_mbuddy.free(resp._body_owned)
|
||
if resp._raw_buf is not None:
|
||
_mbuddy.free(resp._raw_buf)
|
||
_mbuddy.free(t.CVoid(t.CUInt64T(resp), t.CPtr))
|
||
|
||
|
||
# ============================================================
|
||
# 大文件 Range 下载原语(requests.download 使用)
|
||
#
|
||
# _DlOnce 单次发起 "Range: bytes=start-end" 的 GET(不带 Accept-Encoding,
|
||
# 保证响应是原始字节),把响应体写入调用方提供的 dst 缓冲。
|
||
# 线程安全:本函数不使用 _mbuddy(响应/请求缓冲一律 stdlib),
|
||
# socket 连接独立,供多线程并行切片下载并发调用。
|
||
#
|
||
# 返回: >=0 响应体字节数;-1 参数错;-2 3xx 重定向(Location 写 out_loc,
|
||
# 由 download 模块跟随);-3 其它失败(last_error() 查看原因)。
|
||
# out_status/out_total 分别输出 HTTP 状态码与 Content-Range 总大小。
|
||
# ============================================================
|
||
|
||
def _RawHeaderValue(buf: str, total: INT, name: str, out: str, out_size: INT) -> INT:
|
||
"""对原始响应头区做大小写不敏感查询(0=命中并拷贝值;-1=未命中)。"""
|
||
if buf is None or name is None or out is None or out_size <= 0:
|
||
return -1
|
||
body: INT = _FindBodyStart(buf, total)
|
||
if body < 0:
|
||
body = total
|
||
nl: INT = 0
|
||
while nl < body:
|
||
eol: INT = nl
|
||
while eol < body and buf[eol] != '\r' and buf[eol] != '\n':
|
||
eol += 1
|
||
if eol > nl:
|
||
k: INT = 0
|
||
ok: bool = True
|
||
while name[k] != 0 and nl + k < eol:
|
||
if not _CIEq(buf[nl + k], name[k]):
|
||
ok = False
|
||
break
|
||
k += 1
|
||
if ok and name[k] == 0 and nl + k < eol and buf[nl + k] == ':':
|
||
v: INT = nl + k + 1
|
||
while v < eol and (buf[v] == ' ' or buf[v] == '\t'):
|
||
v += 1
|
||
vlen: INT = eol - v
|
||
if vlen > out_size - 1:
|
||
vlen = out_size - 1
|
||
for i in range(vlen):
|
||
out[i] = buf[v + i]
|
||
out[vlen] = 0
|
||
return 0
|
||
nl = eol
|
||
while nl < body and (buf[nl] == '\r' or buf[nl] == '\n'):
|
||
nl += 1
|
||
return -1
|
||
|
||
|
||
def _StatusRaw(buf: str) -> INT:
|
||
"""从 "HTTP/1.1 206 ..." 首行解析状态码。"""
|
||
if buf is None:
|
||
return 0
|
||
i: INT = 0
|
||
while buf[i] == ' ':
|
||
i += 1
|
||
while buf[i] != 0 and buf[i] != ' ':
|
||
i += 1
|
||
while buf[i] == ' ':
|
||
i += 1
|
||
code: INT = 0
|
||
while buf[i] >= '0' and buf[i] <= '9':
|
||
code = code * 10 + INT(buf[i] - '0')
|
||
i += 1
|
||
return code
|
||
|
||
|
||
def _CLenOf(buf: str, total: INT) -> INT:
|
||
"""读取 Content-Length(无则 -1)。"""
|
||
hbuf: t.CArray[t.CChar, 32]
|
||
if _RawHeaderValue(buf, total, "Content-Length", hbuf, 32) != 0:
|
||
return -1
|
||
v: INT = 0
|
||
i: INT = 0
|
||
while hbuf[i] >= '0' and hbuf[i] <= '9':
|
||
v = v * 10 + INT(hbuf[i] - '0')
|
||
i += 1
|
||
return v
|
||
|
||
|
||
def _CRTotalOf(buf: str, total: INT) -> t.CSizeT:
|
||
"""从 Content-Range: bytes s-e/TOTAL 解析 TOTAL(非 206/缺头则 0)。"""
|
||
hbuf: t.CArray[t.CChar, 128]
|
||
if _RawHeaderValue(buf, total, "Content-Range", hbuf, 128) != 0:
|
||
return 0
|
||
p: INT = 0
|
||
while hbuf[p] != 0 and hbuf[p] != '/':
|
||
p += 1
|
||
if hbuf[p] == 0:
|
||
return 0
|
||
p += 1
|
||
v: t.CSizeT = 0
|
||
while hbuf[p] != 0 and hbuf[p] >= '0' and hbuf[p] <= '9':
|
||
v = v * 10 + t.CSizeT(hbuf[p] - '0')
|
||
p += 1
|
||
return v
|
||
|
||
|
||
def _DlOnce(url: str, start: t.CSizeT, count: INT, dst: str, dst_cap: INT,
|
||
out_status: INT | t.CPtr, out_loc: str, out_loccap: INT,
|
||
out_total: t.CSizeT | t.CPtr) -> INT:
|
||
"""HTTP 单次 Range 拉取(线程安全,见模块注释)。"""
|
||
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
|
||
|
||
if _EnsureSocketInit() != 0:
|
||
_SetHttpError("socket init failed")
|
||
return -3
|
||
|
||
host_buf: t.CArray[t.CChar, 256]
|
||
path_buf: t.CArray[t.CChar, 1024]
|
||
port: INT = 0
|
||
if _ParseUrl(url, host_buf, 256, path_buf, 1024, c.Addr(port)) != 0:
|
||
_SetHttpError("bad http url")
|
||
return -3
|
||
|
||
sock = socket.Socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
if sock.fd == socket.INVALID_SOCKET:
|
||
_SetHttpError("socket() failed")
|
||
return -3
|
||
if sock.connect(host_buf, port) != 0:
|
||
_SetHttpError("connect failed")
|
||
sock.close()
|
||
return -3
|
||
|
||
req: str = stdlib.malloc(1024)
|
||
if req is None:
|
||
sock.close()
|
||
_SetHttpError("out of memory")
|
||
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 = sock.send(req, INT(string.strlen(req)))
|
||
stdlib.free(req)
|
||
if sent <= 0:
|
||
sock.close()
|
||
_SetHttpError("send failed")
|
||
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 = sock.recv(dst + total, room)
|
||
if n <= 0:
|
||
break
|
||
total += n
|
||
if tgt < 0:
|
||
bo: INT = _FindBodyStart(dst, total)
|
||
if bo >= 0:
|
||
cl: INT = _CLenOf(dst, total)
|
||
if cl >= 0:
|
||
tgt = bo + cl
|
||
if tgt >= 0 and total >= tgt:
|
||
break
|
||
sock.close()
|
||
dst[total] = 0
|
||
|
||
st: INT = _StatusRaw(dst)
|
||
c.Set(c.Deref(out_status), st)
|
||
if st == 0:
|
||
_SetHttpError("empty response")
|
||
return -3
|
||
if _IsHttpRedirect(st):
|
||
if out_loc is not None and out_loccap > 0:
|
||
if _RawHeaderValue(dst, total, "Location", out_loc, out_loccap) != 0:
|
||
out_loc[0] = 0
|
||
return -2
|
||
if st != 200 and st != 206:
|
||
stdio.snprintf(_http_err_store, 512, "http status %d", st)
|
||
return -3
|
||
|
||
if st == 206:
|
||
c.Set(c.Deref(out_total), _CRTotalOf(dst, total))
|
||
else:
|
||
cl2: INT = _CLenOf(dst, total)
|
||
if cl2 > 0:
|
||
c.Set(c.Deref(out_total), t.CSizeT(cl2))
|
||
bo: INT = _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
|