Implement HTTP response parsing and reader
TODO: Handle Transfer-Encoding and Content-Length properly
This commit is contained in:
parent
1c1de31c1a
commit
ca93a48b12
|
@ -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)!;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue