1
Fork 0

Implement HTTP response parsing and reader

TODO: Handle Transfer-Encoding and Content-Length properly
This commit is contained in:
Drew DeVault 2023-02-10 14:09:23 +01:00
parent 1c1de31c1a
commit ca93a48b12
5 changed files with 147 additions and 25 deletions

View file

@ -1,5 +1,5 @@
use fmt;
use io; use io;
use log;
use net::dial; use net::dial;
use net::http; use net::http;
use net::uri; use net::uri;
@ -13,19 +13,28 @@ export fn main() void = {
case let u: uri::uri => case let u: uri::uri =>
yield u; yield u;
case uri::invalid => case uri::invalid =>
fmt::fatal("Invalid URI"); log::fatal("Invalid URI");
}; };
defer uri::finish(&target); defer uri::finish(&target);
const req = http::get(&client, &target); const req = http::get(&client, &target);
const resp = match (http::do(&client, &req)) { const resp = match (http::do(&client, &req)) {
case let err: http::error => case let err: http::error =>
fmt::fatal("HTTP error:", http::strerror(err)); log::fatal("HTTP error:", http::strerror(err));
case let resp: http::response => case let resp: http::response =>
yield resp; yield resp;
}; };
defer http::request_finish(&req);
// XXX TEMP log::printfln("HTTP/{}.{}: {} {}",
const body = resp.body as io::handle; 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)!; io::copy(os::stdout, body)!;
}; };

View file

@ -6,3 +6,9 @@ Caveats:
are valid according to the HTTP grammar; such cases will fail when rejected by are valid according to the HTTP grammar; such cases will fail when rejected by
the other party. the other party.
- Details indicated by RFC 7230 et al as "obsolete" are not implemented - Details indicated by RFC 7230 et al as "obsolete" are not implemented
- Max header length including "name: value" is 4KiB
TODO:
- Server stuff
- TLS

View file

@ -1,10 +1,13 @@
use bufio; use bufio;
use encoding::utf8;
use fmt; use fmt;
use io; use io;
use net::dial; use net::dial;
use net::uri; use net::uri;
use net; use net;
use os; use os;
use strconv;
use strings;
// Performs an HTTP [[request]] with the given [[client]]. The request is // Performs an HTTP [[request]] with the given [[client]]. The request is
// performed synchronously; this function blocks until the server has returned // 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)?; bufio::flush(&file)?;
match (req.body) { match (req.body) {
case void =>
yield;
case let body: io::handle => case let body: io::handle =>
// Copy to conn directly so we can use sendfile(2) if // Copy to conn directly so we can use sendfile(2) if
// appropriate // appropriate
io::copy(conn, body)?; io::copy(conn, body)?;
case void =>
yield;
}; };
// Switch buffer to read mode // TODO: Improve error handling
file = bufio::buffered(conn, buf, []); let resp = response { ... };
const scan = bufio::newscanner_static(conn, buf);
read_statusline(&resp, &scan)?;
read_header(&resp.header, &scan)?;
// TODO: Parse resposne const remain = bufio::scan_buffer(&scan);
return response { const rd = alloc(reader {
body = conn, 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);
}; };

View file

@ -1,19 +1,20 @@
use bufio;
use encoding::utf8;
use fmt; use fmt;
use io; use io;
use strings; use strings;
// List of HTTP headers. // List of HTTP headers.
// TODO: [](str, []str)
export type header = [](str, str); export type header = [](str, str);
// Adds a given HTTP header, which may be added more than once. The value is // Adds a given HTTP header, which may be added more than once. The name should
// duplicated, but the name is borrowed from the caller. The name should be // be canonicalized by the caller.
// canonicalized by the caller.
export fn add_header(head: *header, name: str, val: str) void = { 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 // Sets the value of a given HTTP header, removing any previous values. The name
// value is duplicated, but the name is borrowed from the caller. The name
// should be canonicalized by the caller. // should be canonicalized by the caller.
export fn set_header(head: *header, name: str, val: str) void = { export fn set_header(head: *header, name: str, val: str) void = {
del_header(head, name); 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 = { export fn del_header(head: *header, name: str) void = {
for (let i = 0z; i < len(head); i += 1) { for (let i = 0z; i < len(head); i += 1) {
if (head[i].0 == name) { if (head[i].0 == name) {
free(head[i].0);
free(head[i].1); free(head[i].1);
delete(head[i]); delete(head[i]);
i -= 1; 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 // Writes a list of HTTP headers to the provided I/O handle in the HTTP wire
// format. // format.
export fn write_header(sink: io::handle, head: *header) (size | io::error) = { 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; return z;
}; };
// Frees state associated with an HTTP [[header]]. fn read_header(head: *header, scan: *bufio::scanner) (void | io::error) = {
export fn header_free(head: *header) void = { for (true) {
for (let i = 0z; i < len(head); i += 1) { const item = match (bufio::scan_string(scan, "\r\n")) {
free(head[i].1); 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);
}; };

View file

@ -1,7 +1,10 @@
use io; use io;
use os;
// Stores state related to an HTTP response. // Stores state related to an HTTP response.
export type response = struct { export type response = struct {
// HTTP protocol version (major, minor)
version: (uint, uint),
// The HTTP status for this request as an integer. // The HTTP status for this request as an integer.
status: uint, status: uint,
// The HTTP status reason phrase. // The HTTP status reason phrase.
@ -9,10 +12,44 @@ export type response = struct {
// The HTTP headers provided by the server. // The HTTP headers provided by the server.
header: header, header: header,
// The response body, if any. // The response body, if any.
body: (io::handle | void), body: nullable *reader,
}; };
// Frees state associated with an HTTP [[response]]. // Frees state associated with an HTTP [[response]].
export fn response_finish(resp: *response) void = { export fn response_finish(resp: *response) void = {
header_free(&resp.header); 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;
}; };