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 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)!;
};

View file

@ -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

View file

@ -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);
};

View file

@ -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);
};

View file

@ -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;
};