1
Fork 0

(Partially) implement net::http::do

This commit is contained in:
Drew DeVault 2023-02-10 10:13:30 +01:00
parent 8cde44d639
commit 26b21689b5
7 changed files with 139 additions and 20 deletions

View file

@ -1,5 +1,7 @@
use fmt;
use io;
use net::dial;
use net::http;
use net::ip;
use net::uri;
use os;
@ -7,12 +9,23 @@ export fn main() void = {
const client = http::newclient("Hare test client");
defer http::client_finish(&client);
const req = http::get(&client, &uri::uri {
scheme = "http",
host = ip::LOCAL_V6,
path = "/",
port = 8080,
...
});
http::write_header(os::stdout, &req.header)!;
const target = match (uri::parse(os::args[1])) {
case let u: uri::uri =>
yield u;
case uri::invalid =>
fmt::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));
case let resp: http::response =>
yield resp;
};
// XXX TEMP
const body = resp.body as io::handle;
io::copy(os::stdout, body)!;
};

View file

@ -20,10 +20,7 @@ export fn newclient(ua: str) client = {
// Frees resources associated with an HTTP [[client]].
export fn client_finish(client: *client) void = {
for (let i = 0z; i < len(client.default_header); i += 1) {
free(client.default_header[i].1);
};
free(client.default_header);
header_free(&client.default_header);
};
// Adds a default header which is included on all HTTP requests using a given
@ -95,6 +92,19 @@ fn request_set_content_length(req: *request, body: io::handle) void = {
add_header(&req.header, "Content-Length", strconv::ztos(ln: size));
};
fn uri_origin_form(target: *uri::uri) uri::uri = {
let target = *target;
target.scheme = "";
target.host = "";
target.fragment = "";
target.userinfo = "";
target.port = 0;
if (target.path == "") {
target.path = "/";
};
return target;
};
// Prepares a new HTTP GET request for the given client and fills in the default
// headers, such as User-Agent and Host. Provide the return value to [[do]] to
// execute the request and free associated resources, or use [[request_finish]]
@ -123,7 +133,7 @@ export fn head(client: *client, target: *uri::uri) request = {
// [[request_finish]] to free resources without executing the request.
//
// If the provided I/O handle is seekable, the Content-Length header is added
// automatically.
// automatically. Otherwise, Transfer-Encoding: chunked will be used.
//
// The URI parameter is borrowed from the caller for the lifetime of the request
// object.
@ -137,7 +147,7 @@ export fn post(client: *client, target: *uri::uri, body: io::handle) request = {
// [[request_finish]] to free resources without executing the request.
//
// If the provided I/O handle is seekable, the Content-Length header is added
// automatically.
// automatically. Otherwise, Transfer-Encoding: chunked will be used.
//
// The URI parameter is borrowed from the caller for the lifetime of the request
// object.

57
net/http/do.ha Normal file
View file

@ -0,0 +1,57 @@
use bufio;
use fmt;
use io;
use net::dial;
use net::uri;
use net;
use os;
type context = struct {
conn: io::handle,
file: bufio::bufstream,
buf: []u8,
};
// Performs an HTTP [[request]] with the given [[client]]. The request is
// performed synchronously; this function blocks until the server has returned
// the response status and all HTTP headers associated with the response.
export fn do(client: *client, req: *request) (response | error) = {
let ctx = context {
buf = alloc([0...], os::BUFSIZ),
...
};
defer free(ctx.buf);
const conn = dial::dial_uri("tcp", req.target)?;
ctx.file = bufio::buffered(conn, [], ctx.buf);
bufio::setflush(&ctx.file, []);
fmt::fprintf(&ctx.file, "{} ", req.method)?;
// TODO: Support other request-targets than origin-form
let target = uri_origin_form(req.target);
uri::fmt(&ctx.file, &target)?;
fmt::fprintf(&ctx.file, " HTTP/1.1\r\n")?;
// TODO: Handle Content-Length and Transfer-Encoding chunked/gzip
// properly
write_header(&ctx.file, &req.header)?;
fmt::fprintf(&ctx.file, "\r\n")?;
bufio::flush(&ctx.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)?;
};
// TODO: Parse resposne
return response {
body = conn,
...
};
};

17
net/http/error.ha Normal file
View file

@ -0,0 +1,17 @@
use io;
use net::dial;
// Errors possible while servicing HTTP requests. Note that these errors are for
// errors related to the processing of the HTTP connection; semantic HTTP errors
// such as [[STATUS_NOTFOUND]] are not handled by this type.
export type error = !(dial::error | io::error);
// Converts an [[error]] to a string.
export fn strerror(err: error) const str = {
match (err) {
case let err: dial::error =>
return dial::strerror(err);
case let err: io::error =>
return io::strerror(err);
};
};

View file

@ -42,3 +42,11 @@ 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);
};
free(*head);
};

View file

@ -21,11 +21,7 @@ export type request = struct {
// Frees state associated with an HTTP [[request]].
export fn request_finish(req: *request) void = {
for (let i = 0z; i < len(req.header); i += 1) {
free(req.header[i].1);
};
free(req.header);
header_free(&req.header);
uri::finish(req.target);
free(req.target);
};

View file

@ -0,0 +1,18 @@
use io;
// Stores state related to an HTTP response.
export type response = struct {
// The HTTP status for this request as an integer.
status: uint,
// The HTTP status reason phrase.
reason: str,
// The HTTP headers provided by the server.
header: header,
// The response body, if any.
body: (io::handle | void),
};
// Frees state associated with an HTTP [[response]].
export fn response_finish(resp: *response) void = {
header_free(&resp.header);
};