862 lines
25 KiB
Plaintext
862 lines
25 KiB
Plaintext
import string
|
|
import socket
|
|
import memhub
|
|
import stdio
|
|
import stdlib
|
|
import zlib.pyzlib as pyzlib
|
|
|
|
|
|
# ============================================================
|
|
# requests/__init__.vp - HTTP(S) client library package entry point (Python requests style)
|
|
#
|
|
# Package structure:
|
|
# requests HTTP client (pure socket implementation, this file)
|
|
# requests.https HTTPS client (openssl SSL/TLS + certificate verification, see 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*
|
|
|
|
# Socket initialization flag
|
|
_socket_ready: bool = False
|
|
|
|
define HTTP_PORT = 80
|
|
define HTTP_BUFSIZ = 4096
|
|
define GZIP_BUFSIZ = 65536
|
|
|
|
# Module-internal redirect / failure diagnostics (static buffer)
|
|
_http_err_store: [512]char
|
|
|
|
|
|
def last_error() -> str:
|
|
"""Last HTTP-layer failure reason (empty string if none)."""
|
|
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 # raw response buffer (headers+body, freed by release)
|
|
_body_owned: str # separately decoded body buffer (chunked etc., freed by 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*:
|
|
"""Allocate Response struct from _mbuddy and initialize it in-place."""
|
|
if _mbuddy is None: return None
|
|
resp: Response* = _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*) -> 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
|
|
*port = port_val
|
|
else:
|
|
*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*) -> 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
|
|
|
|
*out_code = code
|
|
|
|
# Find \n
|
|
while p[0] != '\0' and p[0] != '\n':
|
|
p += 1
|
|
if p[0] == '\n': p += 1
|
|
|
|
return int(uint64(p) - uint64(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(uint64(end_of_headers) - uint64(resp_text)) + 2
|
|
|
|
return int(uint64(end_of_headers) - uint64(resp_text)) + 4
|
|
|
|
|
|
# ============================================================
|
|
# Public helpers (reused by requests.https and other submodules)
|
|
# ============================================================
|
|
|
|
def create_response() -> Response*:
|
|
"""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*) -> 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)
|
|
|
|
|
|
# ============================================================
|
|
# Response parsing (header lookup / chunked decode / unified parse_response)
|
|
# ============================================================
|
|
|
|
def _CIEq(a: char, b: char) -> bool:
|
|
"""Case-insensitive char equality (ASCII only)."""
|
|
if a >= 'A' and a <= 'Z': a = char(int(a) - int('A') + int('a'))
|
|
if b >= 'A' and b <= 'Z': b = char(int(b) - int('A') + int('a'))
|
|
return a == b
|
|
|
|
|
|
def header_value(resp: Response*, name: str, out: str, out_size: int) -> int:
|
|
"""Look up a response header by name in the raw response (case-insensitive, first value).
|
|
Returns 0 on found+copied; -1 if missing or bad args.
|
|
"""
|
|
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
|
|
|
|
# Skip status line
|
|
while p[0] != '\0' and p[0] != '\n':
|
|
p += 1
|
|
if p[0] == '\n': p += 1
|
|
|
|
# Match each header line
|
|
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
|
|
# Allow [spaces]* ':' after name (RFC has none; lenient)
|
|
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
|
|
# Skip to next line
|
|
while p[0] != '\0' and p[0] != '\n':
|
|
p += 1
|
|
if p[0] == '\n': p += 1
|
|
|
|
return -1
|
|
|
|
|
|
def _HexVal(ch: char) -> 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:
|
|
"""Decode chunked body per RFC 7230 (chunk-ext ignored, trailer ignored).
|
|
Returns decoded byte count; -1 on failure.
|
|
"""
|
|
if src is None or dst is None: return -1
|
|
|
|
r: int = 0
|
|
w: int = 0
|
|
while r < src_len:
|
|
# 1) chunk size (hex, optional ;ext)
|
|
size: int = 0
|
|
digit_seen: bool = False
|
|
while r < src_len:
|
|
ch: char = 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) Skip extension and line end
|
|
while r < src_len and src[r] != '\n':
|
|
r += 1
|
|
if r < src_len and src[r] == '\n': r += 1
|
|
# 3) Terminator chunk
|
|
if size == 0: return w
|
|
# 4) Copy data + skip CRLF
|
|
if w + size > dst_cap: return -1
|
|
for k in range(int(size)):
|
|
dst[w + k] = src[r + k]
|
|
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*, total: int) -> int:
|
|
"""Parse the received raw response into Response fields; chunked Transfer-Encoding decodes
|
|
into _body_owned while _raw_buf is kept. Returns 0 ok; -1 fail (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, &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
|
|
|
|
# Detect Transfer-Encoding: chunked
|
|
te_buf: [64]char
|
|
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 decompress (zlib.pyzlib, pure Viper) ----
|
|
ce_buf: [64]char
|
|
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: sizet = sizet(resp.body_len)
|
|
olen2: sizet = 0
|
|
gz_out: bytes = pyzlib.decompress(_mbuddy, bytes(resp.text), src_len2, 15, GZIP_BUFSIZ, &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 = str(gz_out)
|
|
gz_str[olen2] = 0
|
|
resp._body_owned = gz_str
|
|
resp.text = gz_str
|
|
resp.body_len = int(olen2)
|
|
else:
|
|
# Decompress failed: keep raw body, record diagnostics
|
|
stdio.snprintf(_http_err_store, 512, "gzip decode failed: %s", pyzlib.get_error())
|
|
|
|
return 0
|
|
|
|
|
|
# ============================================================
|
|
# Public API
|
|
# ============================================================
|
|
|
|
def _HttpGetOnce(url: str) -> Response*:
|
|
"""HTTP GET single request (no redirect). Sends Accept-Encoding: gzip."""
|
|
if _mbuddy is None: return None
|
|
|
|
resp: Response* = _AllocResponse()
|
|
if resp is None: return None
|
|
|
|
if _EnsureSocketInit() != 0:
|
|
release(resp)
|
|
return None
|
|
|
|
# Parse URL
|
|
host_buf: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
|
|
if _ParseUrl(url, host_buf, 256, path_buf, 1024, &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*:
|
|
"""HTTP POST single request (no redirect). Sends Accept-Encoding: gzip."""
|
|
if _mbuddy is None: return None
|
|
|
|
resp: Response* = _AllocResponse()
|
|
if resp is None: return None
|
|
|
|
if _EnsureSocketInit() != 0:
|
|
release(resp)
|
|
return None
|
|
|
|
# Parse URL
|
|
host_buf: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
|
|
if _ParseUrl(url, host_buf, 256, path_buf, 1024, &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
|
|
|
|
|
|
# ============================================================
|
|
# Redirect following
|
|
# ============================================================
|
|
|
|
def _ResolveRedirectUrl(base: str, loc: str, out: str, cap: int) -> int:
|
|
"""Merge Location into a full URL (absolute / protocol-relative / root-relative), written to out.
|
|
Returns 0 on success; -1 on failure.
|
|
"""
|
|
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
|
|
# Take scheme://authority prefix of base
|
|
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(uint64(q) - uint64(base))
|
|
need: int = prefix_len + lb + 2
|
|
if need >= cap: return -1
|
|
for i in range(int(prefix_len)):
|
|
out[i] = base[i]
|
|
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*:
|
|
"""GET/POST with redirect following (http:// chain, max 5 hops)."""
|
|
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)
|
|
|
|
for hop in range(5):
|
|
resp: Response* = 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
|
|
|
|
_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*:
|
|
"""HTTP GET (follows 3xx redirects, max 5 hops). Returns Response* or 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*:
|
|
"""HTTP POST (follows 3xx redirects, max 5 hops)."""
|
|
return _HttpRedirectLoop("POST", url, data, content_type)
|
|
|
|
|
|
def release(resp: Response*):
|
|
"""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(rawptr(resp))
|
|
|
|
|
|
# ============================================================
|
|
# Large-file Range download primitive for requests.download: _DlOnce issues a "Range: bytes=
|
|
# start-end" GET (no Accept-Encoding) into dst. Thread-safe (no _mbuddy, own socket). >=0 bytes; -1/-2/-3.
|
|
# ============================================================
|
|
|
|
def _RawHeaderValue(buf: str, total: int, name: str, out: str, out_size: int) -> int:
|
|
"""Case-insensitive lookup on raw response header area (0=hit+copied; -1=miss)."""
|
|
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(int(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:
|
|
"""Parse status code from the first line ("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:
|
|
"""Read Content-Length (-1 if absent)."""
|
|
hbuf: [32]char
|
|
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) -> sizet:
|
|
"""Parse TOTAL from Content-Range: bytes s-e/TOTAL (0 if not 206 / header absent)."""
|
|
hbuf: [128]char
|
|
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: sizet = 0
|
|
while hbuf[p] != 0 and hbuf[p] >= '0' and hbuf[p] <= '9':
|
|
v = v * 10 + sizet(hbuf[p] - '0')
|
|
p += 1
|
|
return v
|
|
|
|
|
|
def _DlOnce(url: str, start: sizet, count: int, dst: str, dst_cap: int,
|
|
out_status: int*, out_loc: str, out_loccap: int,
|
|
out_total: sizet*) -> int:
|
|
"""HTTP single Range fetch (thread-safe; see module comment)."""
|
|
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
|
|
|
|
if _EnsureSocketInit() != 0:
|
|
_SetHttpError("socket init failed")
|
|
return -3
|
|
|
|
host_buf: [256]char
|
|
path_buf: [1024]char
|
|
port: int = 0
|
|
if _ParseUrl(url, host_buf, 256, path_buf, 1024, &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: 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 = 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)
|
|
*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:
|
|
*out_total = _CRTotalOf(dst, total)
|
|
else:
|
|
cl2: int = _CLenOf(dst, total)
|
|
if cl2 > 0:
|
|
*out_total = sizet(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
|