diff --git a/cmd/http/main.ha b/cmd/http/main.ha index 8c05575..b7e9c16 100644 --- a/cmd/http/main.ha +++ b/cmd/http/main.ha @@ -4,18 +4,14 @@ use net::uri; use os; export fn main() void = { - const req = &http::request { - method = http::GET, - target = &uri::uri { - scheme = "http", - host = ip::LOCAL_V6, - path = "/", - ... - }, - body = 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 = "/", ... - }; - http::add_header(&req.header, "User-Agent", "Hare test client"); - http::add_header(&req.header, "Content-Length", "100"); + }); http::write_header(os::stdout, &req.header)!; }; diff --git a/net/http/client.ha b/net/http/client.ha index e69de29..43a77ac 100644 --- a/net/http/client.ha +++ b/net/http/client.ha @@ -0,0 +1,143 @@ +use fmt; +use io; +use net::ip; +use net::uri; +use strconv; +use strings; + +export type client = struct { + default_header: header, +}; + +// Creates a new HTTP [[client]] with the provided User-Agent string, which is +// borrowed from the caller. Pass the return value to [[client_finish]] to free +// resourfces associated with the HTTP client after use. +export fn newclient(ua: str) client = { + let client = client { ... }; + client_add_header(&client, "User-Agent", ua); + return 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); +}; + +// Adds a default header which is included on all HTTP requests using a given +// [[client]]. +export fn client_add_header(client: *client, name: str, val: str) void = { + add_header(&client.default_header, name, val); +}; + +fn new_request(client: *client, method: str, target: *uri::uri) request = { + let req = request { + method = method, + target = alloc(uri::dup(target)), + header = alloc(client.default_header...), + body = void, + }; + if (req.target.port == 0) { + switch (req.target.scheme) { + case "http" => + req.target.port = 80; + case "https" => + req.target.port = 443; + }; + }; + + let host = match (req.target.host) { + case let host: str => + yield host; + case let ip: ip::addr => + yield ip::string(ip); + }; + + if (req.target.scheme == "http" && req.target.port != 80) { + host = fmt::asprintf("{}:{}", host, req.target.port); + } else if (target.scheme == "https" && target.port != 443) { + host = fmt::asprintf("{}:{}", host, req.target.port); + } else { + host = strings::dup(host); + }; + + append(req.header, ("Host", host)); + + return req; +}; + +fn new_request_body( + client: *client, + method: str, + target: *uri::uri, + body: io::handle, +) request = { + let req = new_request(client, method, target); + req.body = body; + request_set_content_length(&req, body); + return req; +}; + +fn request_set_content_length(req: *request, body: io::handle) void = { + const prev = match (io::seek(body, 0, io::whence::CUR)) { + case let off: io::off => + yield off; + case io::error => + return; + }; + const ln = io::seek(body, 0, io::whence::END)!; + io::seek(body, prev, io::whence::SET)!; + add_header(&req.header, "Content-Length", strconv::ztos(ln: size)); +}; + +// 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]] +// to free resources without executing the request. +// +// The URI parameter is borrowed from the caller for the lifetime of the request +// object. +export fn get(client: *client, target: *uri::uri) request = { + return new_request(client, GET, target); +}; + +// Prepares a new HTTP HEAD 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]] to free resources without executing the request. +// +// The URI parameter is borrowed from the caller for the lifetime of the request +// object. +export fn head(client: *client, target: *uri::uri) request = { + return new_request(client, HEAD, target); +}; + +// Prepares a new HTTP POST 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]] to free resources without executing the request. +// +// If the provided I/O handle is seekable, the Content-Length header is added +// automatically. +// +// The URI parameter is borrowed from the caller for the lifetime of the request +// object. +export fn post(client: *client, target: *uri::uri, body: io::handle) request = { + return new_request_body(client, POST, target, body); +}; + +// Prepares a new HTTP PUT 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]] to free resources without executing the request. +// +// If the provided I/O handle is seekable, the Content-Length header is added +// automatically. +// +// The URI parameter is borrowed from the caller for the lifetime of the request +// object. +export fn put(client: *client, target: *uri::uri, body: io::handle) request = { + return new_request_body(client, POST, target, body); +}; diff --git a/net/http/header.ha b/net/http/header.ha index 12f8200..da92f47 100644 --- a/net/http/header.ha +++ b/net/http/header.ha @@ -1,19 +1,20 @@ -use io; use fmt; +use io; +use strings; // List of HTTP headers. export type header = [](str, str); -// Adds a given HTTP header, which may be added more than once. The provided -// name and value are borrowed from the caller. The provided header name should -// be canonicalized by the caller. +// 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. export fn add_header(head: *header, name: str, val: str) void = { - append(head, (name, val)); + append(head, (name, strings::dup(val))); }; // Sets the value of a given HTTP header, removing any previous values. The -// provided name and value are borrowed from the caller. The provided header -// name should be canonicalized by the caller. +// value is duplicated, but the name is borrowed from the caller. The name +// should be canonicalized by the caller. export fn set_header(head: *header, name: str, val: str) void = { del_header(head, name); add_header(head, name, val); @@ -24,6 +25,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].1); delete(head[i]); i -= 1; }; diff --git a/net/http/request.ha b/net/http/request.ha index d710a81..d081a8d 100644 --- a/net/http/request.ha +++ b/net/http/request.ha @@ -18,3 +18,14 @@ export type request = struct { // I/O reader for the request body. body: (io::handle | void), }; + +// 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); + + uri::finish(req.target); + free(req.target); +};