diff --git a/cmd/http/main.ha b/cmd/http/main.ha index c297459..e86ce1d 100644 --- a/cmd/http/main.ha +++ b/cmd/http/main.ha @@ -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)!; }; diff --git a/net/http/client.ha b/net/http/client.ha index 0d6a41e..ec138e1 100644 --- a/net/http/client.ha +++ b/net/http/client.ha @@ -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. diff --git a/net/http/do.ha b/net/http/do.ha new file mode 100644 index 0000000..6b229eb --- /dev/null +++ b/net/http/do.ha @@ -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, + ... + }; +}; diff --git a/net/http/error.ha b/net/http/error.ha new file mode 100644 index 0000000..9a5bf26 --- /dev/null +++ b/net/http/error.ha @@ -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); + }; +}; diff --git a/net/http/header.ha b/net/http/header.ha index da92f47..d2f4a7e 100644 --- a/net/http/header.ha +++ b/net/http/header.ha @@ -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); +}; diff --git a/net/http/request.ha b/net/http/request.ha index d081a8d..eb536f8 100644 --- a/net/http/request.ha +++ b/net/http/request.ha @@ -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); }; diff --git a/net/http/response.ha b/net/http/response.ha index e69de29..7998c81 100644 --- a/net/http/response.ha +++ b/net/http/response.ha @@ -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); +};