361 lines
9.6 KiB
Plaintext
361 lines
9.6 KiB
Plaintext
import t, c
|
|
from stdint import *
|
|
import string
|
|
import socket
|
|
import memhub
|
|
import stdio
|
|
|
|
|
|
# ============================================================
|
|
# requests.py — HTTP client library (Python original style)
|
|
#
|
|
# 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
|
|
|
|
|
|
# ============================================================
|
|
# 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 (points into _raw_buf)
|
|
body_len: INT # Response body length
|
|
_raw_buf: str # Raw buffer pointer
|
|
|
|
|
|
# ============================================================
|
|
# 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
|
|
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
|
|
|
|
|
|
# ============================================================
|
|
# 公共 API
|
|
# ============================================================
|
|
|
|
def get(url: str) -> Response | t.CPtr:
|
|
"""HTTP GET request.
|
|
|
|
Args:
|
|
url: Full URL (e.g., "http://example.com/path")
|
|
Returns:
|
|
Pointer to a Response object (allocated from _mbuddy), returns None on failure.
|
|
"""
|
|
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\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 status line
|
|
_ParseStatusLine(resp_buf, c.Addr(resp.status_code))
|
|
|
|
# Find body start
|
|
body_offset: INT = _FindBodyStart(resp_buf, total)
|
|
if body_offset < 0 or body_offset >= total:
|
|
resp.text = None
|
|
resp.body_len = 0
|
|
else:
|
|
resp.text = resp_buf + body_offset
|
|
resp.body_len = total - body_offset
|
|
|
|
return resp
|
|
|
|
|
|
def post(url: str, data: str = None, content_type: str = "application/x-www-form-urlencoded") -> Response | t.CPtr:
|
|
"""HTTP POST request.
|
|
|
|
Args:
|
|
url: Full URL
|
|
data: Request body data
|
|
content_type: Content-Type header (default is "application/x-www-form-urlencoded")
|
|
Returns:
|
|
Pointer to Response object (allocated from _mbuddy), returns None on failure.
|
|
"""
|
|
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\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
|
|
|
|
_ParseStatusLine(resp_buf, c.Addr(resp.status_code))
|
|
body_offset: INT = _FindBodyStart(resp_buf, total)
|
|
if body_offset < 0 or body_offset >= total:
|
|
resp.text = None
|
|
resp.body_len = 0
|
|
else:
|
|
resp.text = resp_buf + body_offset
|
|
resp.body_len = total - body_offset
|
|
|
|
return resp
|
|
|
|
|
|
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._raw_buf is not None:
|
|
_mbuddy.free(resp._raw_buf)
|
|
_mbuddy.free(t.CVoid(t.CUInt64T(resp), t.CPtr))
|