diff --git a/http.cm b/http.cm index 1cabcd11..3eb1c5e6 100644 --- a/http.cm +++ b/http.cm @@ -1,5 +1,6 @@ var socket = use('socket') var tls = use('net/tls') +var Blob = use('blob') def CRLF = "\r\n" @@ -625,20 +626,106 @@ var receive_response = function(callback, state) { } // ============================================================ -// fetch — composed requestor pipeline +// fetch — synchronous HTTP(S) GET, returns response body (stoned blob) // ============================================================ -var fetch = function(callback, value) { - def pipeline = runtime_env.sequence([ - parse_url, - resolve_dns, - open_connection, - send_request, - receive_response - ]) - return pipeline(callback, value) +var fetch = function(url) { + var scheme = "http" + var rest = url + var scheme_end = search(url, "://") + var slash = null + var host_port = null + var path = "/" + var hp = null + var host = null + var port = null + var fd = null + var ctx = null + var buf = Blob() + var raw_text = null + var hdr_end = null + var header_text = null + var body_start_bits = null + var body = null + var addrs = null + var address = null + var ok = true + + if (scheme_end != null) { + scheme = lower(text(url, 0, scheme_end)) + rest = text(url, scheme_end + 3, length(url)) + } + slash = search(rest, "/") + host_port = rest + if (slash != null) { + host_port = text(rest, 0, slash) + path = text(rest, slash, length(rest)) + } + hp = array(host_port, ":") + host = hp[0] + port = length(hp) > 1 ? number(hp[1]) : (scheme == "https" ? 443 : 80) + + addrs = socket.getaddrinfo(host, text(port)) + if (addrs == null || length(addrs) == 0) return null + address = addrs[0].address + + fd = socket.socket("AF_INET", "SOCK_STREAM") + var _do = function() { + var req = null + var chunk = null + socket.connect(fd, {address: address, port: port}) + if (scheme == "https") ctx = tls.wrap(fd, host) + req = "GET " + path + " HTTP/1.1" + CRLF + req = req + "Host: " + host_port + CRLF + req = req + "Connection: close" + CRLF + req = req + "User-Agent: cell/1.0" + CRLF + req = req + "Accept: */*" + CRLF + CRLF + if (ctx != null) tls.send(ctx, req) + else socket.send(fd, req) + while (true) { + if (ctx != null) chunk = tls.recv(ctx, 16384) + else chunk = socket.recv(fd, 16384) + if (chunk == null) break + stone(chunk) + if (length(chunk) == 0) break + buf.write_blob(chunk) + } + } disruption { + ok = false + } + _do() + var _cleanup = function() { + if (ctx != null) tls.close(ctx) + else socket.close(fd) + } disruption {} + _cleanup() + if (!ok) return null + stone(buf) + raw_text = text(buf) + hdr_end = search(raw_text, CRLF + CRLF) + if (hdr_end == null) return null + header_text = text(raw_text, 0, hdr_end) + if (search(lower(header_text), "transfer-encoding: chunked") != null) + return decode_chunked(text(raw_text, hdr_end + 4)) + // Headers are ASCII so char offset = byte offset + body_start_bits = (hdr_end + 4) * 8 + body = buf.read_blob(body_start_bits, length(buf)) + stone(body) + return body } +// ============================================================ +// fetch_requestor — async requestor pipeline for fetch +// ============================================================ + +var fetch_requestor = sequence([ + parse_url, + resolve_dns, + open_connection, + send_request, + receive_response +]) + function close(fd) { socket.close(fd) } @@ -650,5 +737,6 @@ return { sse_open: sse_open, sse_event: sse_event, sse_close: sse_close, // client fetch: fetch, + fetch_requestor: fetch_requestor, request: request } diff --git a/internal/engine.cm b/internal/engine.cm index e577fa85..7b441c6a 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -822,9 +822,8 @@ var $_ = {} use_cache['core/json'] = json -// Create runtime_env early (empty) -- filled after pronto loads. -// Shop accesses it lazily (in inject_env, called at module-use time, not load time) -// so it sees the filled version. +// Runtime env: passed to package modules via shop's inject_env. +// Requestor functions are added immediately below; actor/log/send added later. var runtime_env = {} // Populate core_extras with everything shop (and other core modules) need @@ -848,6 +847,18 @@ core_extras.ensure_build_dir = ensure_build_dir core_extras.compile_to_blob = compile_to_blob core_extras.native_mode = native_mode +// Load pronto early so requestor functions (sequence, parallel, etc.) are +// available to core modules loaded below (http, shop, etc.) +var pronto = use_core('internal/pronto') +var fallback = pronto.fallback +var parallel = pronto.parallel +var race = pronto.race +var sequence = pronto.sequence +core_extras.fallback = fallback +core_extras.parallel = parallel +core_extras.race = race +core_extras.sequence = sequence + // NOW load shop -- it receives all of the above via env var shop = use_core('internal/shop') use_core('build') @@ -1074,12 +1085,6 @@ actor_mod.set_log(log) // (before the full log was ready) captured the bootstrap function reference. _log_full = log -var pronto = use_core('pronto') -var fallback = pronto.fallback -var parallel = pronto.parallel -var race = pronto.race -var sequence = pronto.sequence - runtime_env.actor = actor runtime_env.log = log runtime_env.send = send diff --git a/pronto.cm b/internal/pronto.cm similarity index 100% rename from pronto.cm rename to internal/pronto.cm