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 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)!;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue