diff --git a/cmd/http/main.ha b/cmd/http/main.ha index e86ce1d..ee5005c 100644 --- a/cmd/http/main.ha +++ b/cmd/http/main.ha @@ -1,5 +1,5 @@ -use fmt; use io; +use log; use net::dial; use net::http; use net::uri; @@ -13,19 +13,28 @@ export fn main() void = { case let u: uri::uri => yield u; case uri::invalid => - fmt::fatal("Invalid URI"); + log::fatal("Invalid URI"); }; defer uri::finish(&target); const req = http::get(&client, &target); const resp = match (http::do(&client, &req)) { case let err: http::error => - fmt::fatal("HTTP error:", http::strerror(err)); + log::fatal("HTTP error:", http::strerror(err)); case let resp: http::response => yield resp; }; + defer http::request_finish(&req); - // XXX TEMP - const body = resp.body as io::handle; + log::printfln("HTTP/{}.{}: {} {}", + resp.version.0, resp.version.1, + resp.status, resp.reason); + + for (let i = 0z; i < len(resp.header); i += 1) { + const (name, val) = resp.header[i]; + log::printfln("{}: {}", name, val); + }; + + const body = resp.body as *http::reader; io::copy(os::stdout, body)!; }; diff --git a/net/http/README b/net/http/README index 1045060..29f6b38 100644 --- a/net/http/README +++ b/net/http/README @@ -6,3 +6,9 @@ Caveats: are valid according to the HTTP grammar; such cases will fail when rejected by the other party. - Details indicated by RFC 7230 et al as "obsolete" are not implemented +- Max header length including "name: value" is 4KiB + +TODO: + +- Server stuff +- TLS diff --git a/net/http/do.ha b/net/http/do.ha index 09c1743..c68cb9b 100644 --- a/net/http/do.ha +++ b/net/http/do.ha @@ -1,10 +1,13 @@ use bufio; +use encoding::utf8; use fmt; use io; use net::dial; use net::uri; use net; use os; +use strconv; +use strings; // Performs an HTTP [[request]] with the given [[client]]. The request is // performed synchronously; this function blocks until the server has returned @@ -32,20 +35,61 @@ export fn do(client: *client, req: *request) (response | error) = { bufio::flush(&file)?; match (req.body) { - case void => - yield; case let body: io::handle => // Copy to conn directly so we can use sendfile(2) if // appropriate io::copy(conn, body)?; + case void => + yield; }; - // Switch buffer to read mode - file = bufio::buffered(conn, buf, []); + // TODO: Improve error handling + let resp = response { ... }; + const scan = bufio::newscanner_static(conn, buf); + read_statusline(&resp, &scan)?; + read_header(&resp.header, &scan)?; - // TODO: Parse resposne - return response { - body = conn, + const remain = bufio::scan_buffer(&scan); + const rd = alloc(reader { + vtable = &reader_vtable, + conn = conn, ... - }; + }); + rd.buffer[..len(remain)] = remain[..]; + rd.pending = len(remain); + resp.body = rd; + return resp; +}; + +fn read_statusline( + resp: *response, + scan: *bufio::scanner, +) (void | io::error) = { + const status = match (bufio::scan_string(scan, "\r\n")) { + case let line: const str => + yield line; + case let err: io::error => + return err; + case utf8::invalid => + abort(); // TODO + case io::EOF => + abort(); // TODO + }; + + // TODO: Error handling + const tok = strings::tokenize(status, " "); + const version = strings::next_token(&tok) as str; + const status = strings::next_token(&tok) as str; + const reason = strings::next_token(&tok) as str; + + assert(version == "HTTP/1.1"); // TODO + const (_, version) = strings::cut(version, "/"); + const (major, minor) = strings::cut(version, "."); + + resp.version = ( + strconv::stou(major)!, + strconv::stou(minor)!, + ); + resp.status = strconv::stou(status)!; + resp.reason = strings::dup(reason); }; diff --git a/net/http/header.ha b/net/http/header.ha index d2f4a7e..f973c7a 100644 --- a/net/http/header.ha +++ b/net/http/header.ha @@ -1,19 +1,20 @@ +use bufio; +use encoding::utf8; use fmt; use io; use strings; // List of HTTP headers. +// TODO: [](str, []str) export type header = [](str, str); -// Adds a given HTTP header, which may be added more than once. The value is -// duplicated, but the name is borrowed from the caller. The name should be -// canonicalized by the caller. +// Adds a given HTTP header, which may be added more than once. The name should +// be canonicalized by the caller. export fn add_header(head: *header, name: str, val: str) void = { - append(head, (name, strings::dup(val))); + append(head, (strings::dup(name), strings::dup(val))); }; -// Sets the value of a given HTTP header, removing any previous values. The -// value is duplicated, but the name is borrowed from the caller. The name +// Sets the value of a given HTTP header, removing any previous values. The name // should be canonicalized by the caller. export fn set_header(head: *header, name: str, val: str) void = { del_header(head, name); @@ -25,6 +26,7 @@ export fn set_header(head: *header, name: str, val: str) void = { export fn del_header(head: *header, name: str) void = { for (let i = 0z; i < len(head); i += 1) { if (head[i].0 == name) { + free(head[i].0); free(head[i].1); delete(head[i]); i -= 1; @@ -32,6 +34,15 @@ export fn del_header(head: *header, name: str) void = { }; }; +// Frees state associated with an HTTP [[header]]. +export fn header_free(head: *header) void = { + for (let i = 0z; i < len(head); i += 1) { + free(head[i].0); + free(head[i].1); + }; + free(*head); +}; + // Writes a list of HTTP headers to the provided I/O handle in the HTTP wire // format. export fn write_header(sink: io::handle, head: *header) (size | io::error) = { @@ -43,10 +54,25 @@ export fn write_header(sink: io::handle, head: *header) (size | io::error) = { return z; }; -// Frees state associated with an HTTP [[header]]. -export fn header_free(head: *header) void = { - for (let i = 0z; i < len(head); i += 1) { - free(head[i].1); +fn read_header(head: *header, scan: *bufio::scanner) (void | io::error) = { + for (true) { + const item = match (bufio::scan_string(scan, "\r\n")) { + case let line: const str => + yield line; + case io::EOF => + break; + case let err: io::error => + return err; + case utf8::invalid => + abort(); // TODO + }; + if (item == "") { + break; + }; + + // TODO: validate field-name + let (name, val) = strings::cut(item, ":"); + val = strings::trim(val); + add_header(head, name, val); }; - free(*head); }; diff --git a/net/http/response.ha b/net/http/response.ha index 7998c81..88a25b5 100644 --- a/net/http/response.ha +++ b/net/http/response.ha @@ -1,7 +1,10 @@ use io; +use os; // Stores state related to an HTTP response. export type response = struct { + // HTTP protocol version (major, minor) + version: (uint, uint), // The HTTP status for this request as an integer. status: uint, // The HTTP status reason phrase. @@ -9,10 +12,44 @@ export type response = struct { // The HTTP headers provided by the server. header: header, // The response body, if any. - body: (io::handle | void), + body: nullable *reader, }; // Frees state associated with an HTTP [[response]]. export fn response_finish(resp: *response) void = { header_free(&resp.header); + free(resp.reason); +}; + +export type reader = struct { + vtable: io::stream, + conn: io::handle, + buffer: [os::BUFSIZ]u8, + pending: size, +}; + +const reader_vtable = io::vtable { + reader = &reader_read, + ... +}; + +fn reader_read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = { + let rd = s: *reader; + if (rd.pending == 0) { + match (io::read(rd.conn, rd.buffer)?) { + case let n: size => + rd.pending = n; + case io::EOF => + return io::EOF; + }; + }; + + let n = len(buf); + if (n > rd.pending) { + n = rd.pending; + }; + buf[..n] = rd.buffer[..n]; + rd.buffer[..len(rd.buffer) - n] = rd.buffer[n..]; + rd.pending -= n; + return n; };