# ============================================================================ # http.ludic — the Http.* standard library (#6). # # A poll-based HTTP/HTTPS client. A request is opened, optionally given headers # and a body, then sent; each frame the game polls it, and when the reply lands # it reads the status, body and headers — the frame never blocks (the request # runs on a background thread in the platform backend, http.ll). TLS is the # system's, on by default, so an https:// URL just works. # # HTTP is explicitly OUT-OF-BAND: it depends on the network and wall clock and # must never feed the deterministic lockstep/replay simulation — it is for # scores, config, telemetry and downloads, exactly like Net.* and Time.now. The # JSON companion the proposal called for already shipped as Json.* (#44); pair # them: Json.parse(Http.text(h)). # # The transport (the http_* intrinsics) is macOS-only for now; the response # PARSER below (Http.parse and the header lookup) is pure Ludic, so it works on # any target and is what the test suite exercises without touching the network. # ============================================================================ # Streaming downloads (Http.save_to): the body goes to a file as it arrives, and # Http.received / Http.expected report progress while the request is pending. These # natives live beside the hs_* transport in http.ll / http_win.ll. extern function hs_save_to(slot: int, path: pointer) -> void = "hs_save_to" extern function hs_received(slot: int) -> int = "hs_received" extern function hs_expected(slot: int) -> int = "hs_expected" extern function hs_cancel(slot: int) -> void = "hs_cancel" const HTTP_SLOTS: int = 8 export state RtHttpState { h_ready: bool = false h_used: words = null # slot in use h_native: words = null # 1 = backed by the platform fetch, 0 = parsed offline h_sent: words = null # 1 = dispatched (native) h_res: words = null # 1 = a native fetch has been resolved into the fields below h_status: words = null # cached HTTP status (0 = network error / unset) h_blen: words = null # body length h_req: pointers = null # the pending NSURLRequest (native, before send) h_body: pointers = null # body bytes (NUL-terminated); owned by the slot h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot h_save: words = null # 1 = the body streams to a file (Http.save_to) } function http_init(rt_http_st: mut RtHttpState) -> void { if rt_http_st.h_ready { return } rt_http_st.h_used = words(HTTP_SLOTS) rt_http_st.h_native = words(HTTP_SLOTS) rt_http_st.h_sent = words(HTTP_SLOTS) rt_http_st.h_res = words(HTTP_SLOTS) rt_http_st.h_status = words(HTTP_SLOTS) rt_http_st.h_blen = words(HTTP_SLOTS) rt_http_st.h_req = pointers(HTTP_SLOTS) rt_http_st.h_body = pointers(HTTP_SLOTS) rt_http_st.h_hdr = pointers(HTTP_SLOTS) rt_http_st.h_save = words(HTTP_SLOTS) var i = 0 while i < HTTP_SLOTS { rt_http_st.h_used[i] = 0; rt_http_st.h_native[i] = 0; rt_http_st.h_sent[i] = 0; rt_http_st.h_res[i] = 0 rt_http_st.h_status[i] = 0; rt_http_st.h_blen[i] = 0 rt_http_st.h_req[i] = null; rt_http_st.h_body[i] = null; rt_http_st.h_hdr[i] = null rt_http_st.h_save[i] = 0 i += 1 } rt_http_st.h_ready = true } function http_slot_alloc(rt_http_st: mut RtHttpState) -> int { http_init(rt_http_st) var i = 0 while i < HTTP_SLOTS { if rt_http_st.h_used[i] == 0 { rt_http_st.h_used[i] = 1; rt_http_st.h_native[i] = 0; rt_http_st.h_sent[i] = 0; rt_http_st.h_res[i] = 0 rt_http_st.h_status[i] = 0; rt_http_st.h_blen[i] = 0 rt_http_st.h_req[i] = null; rt_http_st.h_body[i] = null; rt_http_st.h_hdr[i] = null rt_http_st.h_save[i] = 0 return i } i += 1 } return -1 } function http_valid(rt_http_st: mut RtHttpState, h: int) -> bool { http_init(rt_http_st) if (h < 1) or (h > HTTP_SLOTS) { return false } return rt_http_st.h_used[h - 1] == 1 } # A native request freed while its worker was still running is parked (h_used 2) rather # than released, so the worker never writes into a slot a new request has taken. Once the # worker has finished, the slot's native resources are freed and it is free again. function http_reap(rt_http_st: mut RtHttpState) -> void { http_init(rt_http_st) var i = 0 while i < HTTP_SLOTS { if (rt_http_st.h_used[i] == 2) and (hs_done(i) != 0) { hs_free(i) rt_http_st.h_used[i] = 0 } i += 1 } } # ---- length of a NUL-terminated C string ----------------------------------- function http_cstr_len(p: pointer) -> int { if p == null { return 0 } var n = 0 while p[n] != 0 { n += 1 } return n } # ---- request construction (native) ----------------------------------------- # Open a request without sending it, so headers / a body can be added first. function http_open(rt_http_st: mut RtHttpState, method: pointer, url: pointer) -> int { http_reap(rt_http_st) let req = hs_req_new(method, url) if req == null { return 0 } let s = http_slot_alloc(rt_http_st) if s < 0 { return 0 } rt_http_st.h_req[s] = req rt_http_st.h_native[s] = 1 return s + 1 } function http_set_header(rt_http_st: mut RtHttpState, h: int, name: pointer, value: pointer) -> void { if not http_valid(rt_http_st, h) { return } let s = h - 1 if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { hs_req_header(rt_http_st.h_req[s], name, value) } } # Attach a body of `len` bytes (use http_body for a NUL-terminated string body). function http_body_n(rt_http_st: mut RtHttpState, h: int, bytes_ptr: pointer, len: int) -> void { if not http_valid(rt_http_st, h) { return } let s = h - 1 if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { hs_req_body(rt_http_st.h_req[s], bytes_ptr, len) } } function http_body(rt_http_st: mut RtHttpState, h: int, body: pointer) -> void { http_body_n(rt_http_st, h, body, http_cstr_len(body)) } # Stream the response body to the file at `path` instead of keeping it in memory. Must be # called before Http.send. The file is created (or truncated) when the request is sent, # and written directly - no temporary name - so a failed or cancelled download leaves # whatever arrived there; the caller picks the name and renames it once the status is # good. Http.text of such a request is empty (null) and Http.body_len is the bytes written. function http_save_to(rt_http_st: mut RtHttpState, h: int, path: pointer) -> void { if not http_valid(rt_http_st, h) { return } let s = h - 1 if (rt_http_st.h_native[s] == 1) and (rt_http_st.h_sent[s] == 0) and (path != null) { hs_save_to(s, path) rt_http_st.h_save[s] = 1 } } # Body bytes received so far. Live while a Http.save_to request is pending; for an # in-memory request it is 0 until the reply lands, then the body length. function http_received(rt_http_st: mut RtHttpState, h: int) -> int { if not http_valid(rt_http_st, h) { return 0 } let s = h - 1 if rt_http_st.h_native[s] == 0 { return rt_http_st.h_blen[s] } if rt_http_st.h_sent[s] == 0 { return 0 } if rt_http_st.h_save[s] == 1 { return hs_received(s) } if rt_http_st.h_res[s] == 1 { return rt_http_st.h_blen[s] } return 0 } # The body's announced length (Content-Length), or -1 while it is not known. For a # Http.save_to request it is known as soon as the headers arrive; for an in-memory request # it is -1 until the reply lands, then the body length. function http_expected(rt_http_st: mut RtHttpState, h: int) -> int { if not http_valid(rt_http_st, h) { return -1 } let s = h - 1 if rt_http_st.h_native[s] == 0 { return rt_http_st.h_blen[s] } if rt_http_st.h_sent[s] == 0 { return -1 } if rt_http_st.h_save[s] == 1 { return hs_expected(s) } if rt_http_st.h_res[s] == 1 { return rt_http_st.h_blen[s] } return -1 } # Dispatch the request onto the background worker. function http_send_req(rt_http_st: mut RtHttpState, h: int) -> void { if not http_valid(rt_http_st, h) { return } let s = h - 1 if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { hs_send(s, rt_http_st.h_req[s]) rt_http_st.h_sent[s] = 1 } } # ---- convenience one-shots ------------------------------------------------- function http_get(rt_http_st: mut RtHttpState, url: pointer) -> int { let h = http_open(rt_http_st, "GET", url) if h != 0 { http_send_req(rt_http_st, h) } return h } function http_post(rt_http_st: mut RtHttpState, url: pointer, body: pointer) -> int { let h = http_open(rt_http_st, "POST", url) if h != 0 { http_body(rt_http_st, h, body); http_send_req(rt_http_st, h) } return h } function http_request(rt_http_st: mut RtHttpState, method: pointer, url: pointer, body: pointer) -> int { let h = http_open(rt_http_st, method, url) if h == 0 { return 0 } if http_cstr_len(body) > 0 { http_body(rt_http_st, h, body) } http_send_req(rt_http_st, h) return h } # ---- polling + response ---------------------------------------------------- # -1 while pending, 0 on a transport error, else the HTTP status code. function http_poll(rt_http_st: mut RtHttpState, h: int) -> int { if not http_valid(rt_http_st, h) { return 0 } let s = h - 1 if rt_http_st.h_native[s] == 0 { return rt_http_st.h_status[s] } # a parsed handle is ready immediately if rt_http_st.h_sent[s] == 0 { return -1 } if rt_http_st.h_res[s] == 1 { return rt_http_st.h_status[s] } if hs_done(s) == 0 { return -1 } # first time we see it finished: cache the fields off the worker. rt_http_st.h_status[s] = hs_status(s) rt_http_st.h_body[s] = hs_body(s) rt_http_st.h_blen[s] = hs_blen(s) rt_http_st.h_res[s] = 1 return rt_http_st.h_status[s] } function http_status_of(rt_http_st: mut RtHttpState, h: int) -> int { if not http_valid(rt_http_st, h) { return 0 } return rt_http_st.h_status[h - 1] } function http_ok(rt_http_st: mut RtHttpState, h: int) -> bool { let st = http_status_of(rt_http_st, h) return (st >= 200) and (st < 300) } function http_text(rt_http_st: mut RtHttpState, h: int) -> string { if not http_valid(rt_http_st, h) { return null } return rt_http_st.h_body[h - 1] } function http_body_len(rt_http_st: mut RtHttpState, h: int) -> int { if not http_valid(rt_http_st, h) { return 0 } return rt_http_st.h_blen[h - 1] } function http_header_of(rt_http_st: mut RtHttpState, h: int, name: pointer) -> string { if not http_valid(rt_http_st, h) { return null } let s = h - 1 if rt_http_st.h_native[s] == 1 { return hs_header(s, name) } return http_find_header(rt_http_st.h_hdr[s], name) } function http_close(rt_http_st: mut RtHttpState, h: int) -> void { if not http_valid(rt_http_st, h) { return } let s = h - 1 if rt_http_st.h_native[s] == 1 { if (rt_http_st.h_sent[s] == 1) and (rt_http_st.h_res[s] == 0) and (hs_done(s) == 0) { # still running: stop it, and park the slot until the worker has let go hs_cancel(s) rt_http_st.h_used[s] = 2 rt_http_st.h_req[s] = null; rt_http_st.h_body[s] = null; rt_http_st.h_hdr[s] = null return } hs_free(s) } else { if rt_http_st.h_body[s] != null { free(rt_http_st.h_body[s]) } if rt_http_st.h_hdr[s] != null { free(rt_http_st.h_hdr[s]) } } rt_http_st.h_used[s] = 0 rt_http_st.h_req[s] = null; rt_http_st.h_body[s] = null; rt_http_st.h_hdr[s] = null } # ---- the pure-Ludic response parser (transport-independent) ----------------- # Copy [a, b) of src into a fresh NUL-terminated buffer. function http_slice_dup(src: pointer, a: int, b: int) -> pointer { var n = b - a if n < 0 { n = 0 } let out = bytes(n + 1) var i = 0 while i < n { out[i] = src[a + i]; i += 1 } out[n] = 0 return out } function http_is_digit(c: int) -> bool { return (c >= '0') and (c <= '9') } function http_lower(c: int) -> int { if (c >= 'A') and (c <= 'Z') { return c + 32 }; return c } # Parse a raw HTTP/1.1 response (status line + headers + blank line + body) into # a ready handle. Useful for cached/custom transports and offline tests. function http_parse(rt_http_st: mut RtHttpState, resp: pointer, len: int) -> int { let s = http_slot_alloc(rt_http_st) if s < 0 { return 0 } rt_http_st.h_native[s] = 0 # status: the token after the first space on the status line. var i = 0 while (i < len) and (resp[i] != ' ') { i += 1 } # skip "HTTP/1.1" while (i < len) and (resp[i] == ' ') { i += 1 } # skip the space(s) var st = 0 while (i < len) and http_is_digit(resp[i]) { st = (st * 10) + (resp[i] - 48); i += 1 } rt_http_st.h_status[s] = st # header/body split at the first CRLFCRLF. var split = -1 var j = 0 while (j + 3) < len { if (resp[j] == '\r') and (resp[j + 1] == '\n') and (resp[j + 2] == '\r') and (resp[j + 3] == '\n') { split = j j = len } else { j += 1 } } var hdr_end = split var body_start = split + 4 if split < 0 { hdr_end = len; body_start = len } # headers only, no body # header block starts after the status line's CRLF. var hs = 0 while (hs + 1) < len { if (resp[hs] == '\r') and (resp[hs + 1] == '\n') { hs += 2; break } hs += 1 } if hs > hdr_end { hs = hdr_end } rt_http_st.h_hdr[s] = http_slice_dup(resp, hs, hdr_end) rt_http_st.h_body[s] = http_slice_dup(resp, body_start, len) rt_http_st.h_blen[s] = len - body_start if rt_http_st.h_blen[s] < 0 { rt_http_st.h_blen[s] = 0 } return s + 1 } # Look up a header value in a raw header block (case-insensitive), returning a # fresh NUL-terminated copy, or null if absent. Lines are "Name: value\r\n". function http_find_header(hdr: pointer, name: pointer) -> string { if hdr == null { return null } let nlen = http_cstr_len(name) let hlen = http_cstr_len(hdr) var i = 0 while i < hlen { # match name at the start of a line, followed by ':'. var k = 0 while (k < nlen) and ((i + k) < hlen) and (http_lower(hdr[i + k]) == http_lower(name[k])) { k += 1 } if (k == nlen) and ((i + k) < hlen) and (hdr[i + k] == ':') { var v = i + k + 1 while (v < hlen) and (hdr[v] == ' ') { v += 1 } # skip spaces after ':' var e = v while (e < hlen) and (hdr[e] != '\r') and (hdr[e] != '\n') { e += 1 } return http_slice_dup(hdr, v, e) } # advance to the next line. while (i < hlen) and (hdr[i] != '\n') { i += 1 } i += 1 } return null }