commit 5bdaeaa5a9b5893e572940faa2c78082055e3d45
Author: thing1 <thing1@seacrossedlovers.xyz>
Date: Wed, 14 Jan 2026 10:22:12 +0000
init commit
Diffstat:
17 files changed, 2259 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+./betterchess
diff --git a/Makefile b/Makefile
@@ -0,0 +1,32 @@
+.POSIX:
+.SUFFIXES:
+
+HARE=hare
+HAREFLAGS=-lc
+
+DESTDIR=
+PREFIX=/usr/local
+BINDIR=$(PREFIX)/bin
+
+HARE_SOURCES != find . -name '*.ha'
+
+all: betterchess
+
+betterchess: $(HARE_SOURCES)
+ $(HARE) build $(HAREFLAGS) -o $@ cmd/$@/
+
+check:
+ $(HARE) test $(HAREFLAGS)
+
+clean:
+ rm -f betterchess
+
+install:
+ install -Dm755 betterchess $(DESTDIR)$(BINDIR)/betterchess
+
+uninstall:
+ rm -f $(DESTDIR)$(BINDIR)/betterchess
+
+.PHONY: all check clean install uninstall
+
+
diff --git a/betterchess b/betterchess
Binary files differ.
diff --git a/chess/chess.ha b/chess/chess.ha
@@ -0,0 +1,69 @@
+use io;
+use fmt;
+use ascii;
+
+export type invalidplace = !void;
+
+export type ptype = enum rune {
+ KING = 'k',
+ QUEEN = 'q',
+ BISHOP = 'b',
+ KNIGHT = 'n',
+ ROOK = 'r',
+ PAWN = 'p'
+};
+
+export type color = enum {BLACK = 'b', WHITE = 'w'};
+
+export type piece = struct {
+ x: size,
+ y: size,
+ ty: ptype,
+ team: color
+};
+
+export type board = struct {
+ w: size,
+ h: size,
+ pieces: []piece,
+};
+
+export fn mkboard(w: size, h: size) board = {
+ return board{w = w, h = h, pieces = []};
+};
+
+export fn finish(b: *board) void = {
+ free(b.pieces);
+};
+
+export fn mkpiece(b: *board, x: size, y: size, ty: ptype, team: color) (*piece | invalidplace) = {
+ if (x < 0 || x >= b.w || y < 0 || y >= b.h) return invalidplace;
+ append(b.pieces, piece{x = x, y = y, ty = ty, team = team})!;
+ return &b.pieces[len(b.pieces) - 1];
+};
+
+export fn getpiece(b: *board, x: size, y: size) (*piece | void | invalidplace) = {
+ if (x < 0 || x >= b.w || y < 0 || y >= b.h) return invalidplace;
+ for (let p &.. b.pieces)
+ if (p.x == x && p.y == y)
+ return p;
+};
+
+export fn print_board(b: board, f: io::handle) void = {
+ const w = 500z;
+ const h = 500z;
+ fmt::fprintf(f, "<canvas id=\"myCanvas\" width=\"{}\" height=\"{}\" style=\"border:1px solid #000000;\"></canvas>\n", w, h)!;
+
+ for (let x = 0z; x < w; x += (w / b.w))
+ fmt::fprintf(f, "<script>var c = document.getElementById(\"myCanvas\");\nvar ctx = c.getContext(\"2d\");\nctx.moveTo({}, 0);\nctx.lineTo({}, {});\nctx.stroke();\n</script>\n", x, x, h)!;
+
+ for (let y = 0z; y < w; y += (w / b.w))
+ fmt::fprintf(f, "<script>var c = document.getElementById(\"myCanvas\");\nvar ctx = c.getContext(\"2d\");\nctx.moveTo(0, {});\nctx.lineTo({}, {});\nctx.stroke();\n</script>\n", y, w, y)!;
+
+ for (let p .. b.pieces) {
+ let x = (w / b.w) * p.x;
+ let y = (h / b.h) * p.y + 23;
+ let text = if (p.team == color::WHITE) ascii::toupper(p.ty: rune) else p.ty: rune;
+ fmt::fprintf(f, "<script>var c = document.getElementById(\"myCanvas\");\nvar ctx = c.getContext(\"2d\");\nctx.font = \"30px Arial\";\nctx.fillText(\"{}\", {}, {});\n</script>\n", text, x, y)!;
+ };
+};
diff --git a/cmd/betterchess/main.ha b/cmd/betterchess/main.ha
@@ -0,0 +1,53 @@
+use fmt;
+use os;
+use io;
+use net;
+use net::ip;
+use net::tcp;
+use net::http;
+
+use chess;
+
+
+export fn main() void = {
+ const port: u16 = 8080;
+ const addr = ip::LOCAL_V4;
+
+ let s = match (http::listen(addr, port, tcp::reuseaddr)) {
+ case let s: http::server => yield s;
+ case let e: net::error => fmt::fatal(net::strerror(e));
+ };
+ defer http::server_finish(&s);
+
+ let b = chess::mkboard(8, 8);
+
+ let p = match (chess::mkpiece(&b, 0, 0, chess::ptype::ROOK, chess::color::WHITE)) {
+ case let p: *chess::piece => yield p;
+ case => fmt::fatal("invalid placement");
+ };
+ let p = match (chess::mkpiece(&b, 1, 0, chess::ptype::BISHOP, chess::color::WHITE)) {
+ case let p: *chess::piece => yield p;
+ case => fmt::fatal("invalid placement");
+ };
+
+ for (true) {
+ let (req, rw) = http::serve(&s)!;
+ defer io::close(&rw)!;
+ defer http::request_parsed_finish(&req);
+
+ if (req.method != "GET") {
+ http::response_set_status(&rw, http::STATUS_METHOD_NOT_ALLOWED)!;
+ continue;
+ };
+
+ if (req.target.path != "/") {
+ http::response_set_status(&rw, http::STATUS_NOT_FOUND)!;
+ continue;
+ };
+
+ http::response_add_header(&rw, "Content-Type", "text/html")!;
+
+ chess::print_board(b, &rw);
+ chess::print_board(b, os::stdout);
+ };
+};
diff --git a/net/http/README b/net/http/README
@@ -0,0 +1,17 @@
+net::http provides an implementation of an HTTP 1.1 client and server as defined
+by RFC 9110 et al.
+
+TODO: Flesh me out
+
+Caveats:
+
+- No attempt is made to validate that the input for client requests or responses
+ 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
diff --git a/net/http/client.ha b/net/http/client.ha
@@ -0,0 +1,102 @@
+use errors;
+use io;
+use net::uri;
+
+export type client = struct {
+ default_header: header,
+ default_transport: transport,
+};
+
+// Creates a new HTTP [[client]] with the provided User-Agent string.
+//
+// The HTTP client implements a number of sane defaults, which may be tuned. The
+// set of default headers is configured with [[client_default_header]], and the
+// default transport behavior with [[client_default_transport]].
+//
+// TODO: Implement and document the connection pool
+//
+// The caller must pass the client object to [[client_finish]] to free resources
+// associated with this client after use.
+export fn newclient(ua: str) (client | nomem) = {
+ let client = client { ... };
+ header_add(&client, "User-Agent", ua)?;
+ return client;
+};
+
+// Frees resources associated with an HTTP [[client]].
+export fn client_finish(client: *client) void = {
+ header_finish(&client.default_header);
+};
+
+// Returns the default headers used by this HTTP client, so that the user can
+// examine or modify the net::http defaults (such as User-Agent or
+// Accept-Encoding), or add their own.
+export fn client_default_header(client: *client) *header = {
+ return &client.default_header;
+};
+
+// Returns the default [[transport]] configuration used by this HTTP client.
+export fn client_default_transport(client: *client) *transport = {
+ return &client.default_transport;
+};
+
+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;
+};
+
+// Performs a synchronous HTTP GET request with the given client.
+export fn get(
+ client: *client,
+ target: *uri::uri
+) (response | error | nomem | protoerr | io::error | errors::unsupported) = {
+ const req = new_request(client, GET, target)?;
+ defer request_finish(&req);
+ return do(client, &req);
+};
+
+// Performs a synchronous HTTP HEAD request with the given client.
+export fn head(
+ client: *client,
+ target: *uri::uri
+) (response | error | nomem | protoerr | io::error | errors::unsupported) = {
+ const req = new_request(client, HEAD, target)?;
+ defer request_finish(&req);
+ return do(client, &req);
+};
+
+// Performs a synchronous HTTP POST request with the given client.
+//
+// If the provided I/O handle is seekable, the Content-Length header is added
+// automatically. Otherwise, Transfer-Encoding: chunked will be used.
+export fn post(
+ client: *client,
+ target: *uri::uri,
+ body: io::handle,
+) (response | error | nomem | protoerr | io::error | errors::unsupported) = {
+ const req = new_request_body(client, POST, target, body)?;
+ defer request_finish(&req);
+ return do(client, &req);
+};
+
+// Performs a synchronous HTTP PUT request with the given client.
+//
+// If the provided I/O handle is seekable, the Content-Length header is added
+// automatically. Otherwise, Transfer-Encoding: chunked will be used.
+export fn put(
+ client: *client,
+ target: *uri::uri,
+ body: io::handle,
+) (response | error | nomem | protoerr | io::error | errors::unsupported) = {
+ const req = new_request_body(client, PUT, target, body)?;
+ defer request_finish(&req);
+ return do(client, &req);
+};
diff --git a/net/http/constants.ha b/net/http/constants.ha
@@ -0,0 +1,112 @@
+// HTTP "GET" method.
+export def GET: str = "GET";
+// HTTP "HEAD" method.
+export def HEAD: str = "HEAD";
+// HTTP "POST" method.
+export def POST: str = "POST";
+// HTTP "PUT" method.
+export def PUT: str = "PUT";
+// HTTP "DELETE" method.
+export def DELETE: str = "DELETE";
+// HTTP "OPTIONS" method.
+export def OPTIONS: str = "OPTIONS";
+// HTTP "PATCH" method.
+export def PATCH: str = "PATCH";
+// HTTP "CONNECT" method.
+export def CONNECT: str = "CONNECT";
+
+// HTTP "Continue" response status (100).
+export def STATUS_CONTINUE: uint = 100;
+// HTTP "Switching Protocols" response status (101).
+export def STATUS_SWITCHING_PROTOCOLS: uint = 101;
+
+// HTTP "OK" response status (200).
+export def STATUS_OK: uint = 200;
+// HTTP "Created" response status (201).
+export def STATUS_CREATED: uint = 201;
+// HTTP "Accepted" response status (202).
+export def STATUS_ACCEPTED: uint = 202;
+// HTTP "Non-authoritative Info" response status (203).
+export def STATUS_NONAUTHORITATIVE_INFO: uint = 203;
+// HTTP "No Content" response status (204).
+export def STATUS_NO_CONTENT: uint = 204;
+// HTTP "Reset Content" response status (205).
+export def STATUS_RESET_CONTENT: uint = 205;
+// HTTP "Partial Content" response status (206).
+export def STATUS_PARTIAL_CONTENT: uint = 206;
+
+// HTTP "Multiple Choices" response status (300).
+export def STATUS_MULTIPLE_CHOICES: uint = 300;
+// HTTP "Moved Permanently" response status (301).
+export def STATUS_MOVED_PERMANENTLY: uint = 301;
+// HTTP "Found" response status (302).
+export def STATUS_FOUND: uint = 302;
+// HTTP "See Other" response status (303).
+export def STATUS_SEE_OTHER: uint = 303;
+// HTTP "Not Modified" response status (304).
+export def STATUS_NOT_MODIFIED: uint = 304;
+// HTTP "Use Proxy" response status (305).
+export def STATUS_USE_PROXY: uint = 305;
+
+// HTTP "Temporary Redirect" response status (307).
+export def STATUS_TEMPORARY_REDIRECT: uint = 307;
+// HTTP "Permanent Redirect" response status (308).
+export def STATUS_PERMANENT_REDIRECT: uint = 308;
+
+// HTTP "Bad Request" response status (400).
+export def STATUS_BAD_REQUEST: uint = 400;
+// HTTP "Unauthorized" response status (401).
+export def STATUS_UNAUTHORIZED: uint = 401;
+// HTTP "Payment Required" response status (402).
+export def STATUS_PAYMENT_REQUIRED: uint = 402;
+// HTTP "Forbidden" response status (403).
+export def STATUS_FORBIDDEN: uint = 403;
+// HTTP "Not Found" response status (404).
+export def STATUS_NOT_FOUND: uint = 404;
+// HTTP "Method Not Allowed" response status (405).
+export def STATUS_METHOD_NOT_ALLOWED: uint = 405;
+// HTTP "Not Acceptable" response status (406).
+export def STATUS_NOT_ACCEPTABLE: uint = 406;
+// HTTP "Proxy Authentication Required" response status (407).
+export def STATUS_PROXY_AUTH_REQUIRED: uint = 407;
+// HTTP "Request Timeout" response status (408).
+export def STATUS_REQUEST_TIMEOUT: uint = 408;
+// HTTP "Conflict" response status (409).
+export def STATUS_CONFLICT: uint = 409;
+// HTTP "Gone" response status (410).
+export def STATUS_GONE: uint = 410;
+// HTTP "Length Required" response status (411).
+export def STATUS_LENGTH_REQUIRED: uint = 411;
+// HTTP "Precondition Failed" response status (412).
+export def STATUS_PRECONDITION_FAILED: uint = 412;
+// HTTP "Request Entity Too Large" response status (413).
+export def STATUS_REQUEST_ENTITY_TOO_LARGE: uint = 413;
+// HTTP "Request URI Too Long" response status (414).
+export def STATUS_REQUEST_URI_TOO_LONG: uint = 414;
+// HTTP "Unsupported Media Type" response status (415).
+export def STATUS_UNSUPPORTED_MEDIA_TYPE: uint = 415;
+// HTTP "Requested Range Not Satisfiable" response status (416).
+export def STATUS_REQUESTED_RANGE_NOT_SATISFIABLE: uint = 416;
+// HTTP "Expectation Failed" response status (417).
+export def STATUS_EXPECTATION_FAILED: uint = 417;
+// HTTP "I'm a Teapot" response status (418).
+export def STATUS_TEAPOT: uint = 418;
+// HTTP "Misdirected Request" response status (421).
+export def STATUS_MISDIRECTED_REQUEST: uint = 421;
+// HTTP "Unprocessable Entity" response status (422).
+export def STATUS_UNPROCESSABLE_ENTITY: uint = 422;
+// HTTP "Upgrade Required" response status (426).
+export def STATUS_UPGRADE_REQUIRED: uint = 426;
+
+// HTTP "Internal Server Error" response status (500).
+export def STATUS_INTERNAL_SERVER_ERROR: uint = 500;
+// HTTP "Not Implemented" response status (501).
+export def STATUS_NOT_IMPLEMENTED: uint = 501;
+// HTTP "Bad Gateway" response status (502).
+export def STATUS_BAD_GATEWAY: uint = 502;
+// HTTP "Service Unavailable" response status (503).
+export def STATUS_SERVICE_UNAVAILABLE: uint = 503;
+// HTTP "Gateway Timeout" response status (504).
+export def STATUS_GATEWAY_TIMEOUT: uint = 504;
+// HTTP "HTTP Version Not Supported" response status (505).
+export def STATUS_HTTP_VERSION_NOT_SUPPORTED: uint = 505;
diff --git a/net/http/do.ha b/net/http/do.ha
@@ -0,0 +1,148 @@
+use bufio;
+use encoding::utf8;
+use errors;
+use fmt;
+use io;
+use net::dial;
+use net::uri;
+use net;
+use os;
+use strconv;
+use strings;
+use types;
+
+// 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.
+//
+// If the provided [[response]] has a non-null body, the user must pass it to
+// [[io::close]] before calling [[response_finish]].
+export fn do(
+ client: *client,
+ req: *request)
+(response | error | nomem | protoerr | io::error | errors::unsupported) = {
+ assert(req.target.scheme == "http"); // TODO: https
+
+ if (req.body is *io::stream && req.body as *io::stream == io::empty) {
+ header_del(&req.header, "Transfer-Encoding");
+ header_add(&req.header, "Content-Length", "0")?;
+ };
+
+ const conn = dial::dial_uri("tcp", req.target)?;
+
+ let buf: [os::BUFSZ]u8 = [0...];
+ let file = bufio::init(conn, [], buf);
+ bufio::setflush(&file, []);
+
+ request_write(&file, req, client)?;
+
+ bufio::flush(&file)?;
+
+ const trans = match (req.transport) {
+ case let t: *transport =>
+ yield t;
+ case =>
+ yield &client.default_transport;
+ };
+ // TODO: Implement None
+ assert(trans.request_transport == transport_mode::AUTO);
+ assert(trans.response_transport == transport_mode::AUTO);
+ assert(trans.request_content == content_mode::AUTO);
+ assert(trans.response_content == content_mode::AUTO);
+
+ io::copy(conn, req.body)?;
+
+ let resp = response {
+ body = io::empty,
+ ...
+ };
+ const scan = bufio::newscanner(conn, 512);
+ defer bufio::finish(&scan);
+ read_statusline(&resp, &scan)?;
+ match (read_header(&resp.header, &scan)?) {
+ case void => void;
+ case io::EOF => return protoerr;
+ };
+
+ const response_complete =
+ req.method == "HEAD" ||
+ resp.status == STATUS_NO_CONTENT ||
+ resp.status == STATUS_NOT_MODIFIED ||
+ (resp.status >= 100 && resp.status < 200) ||
+ (req.method == "CONNECT" && resp.status >= 200 && resp.status < 300);
+ if (!response_complete) {
+ resp.body = new_reader(conn, &resp.header, &scan)?;
+ } else if (req.method != "CONNECT") {
+ io::close(conn)!;
+ };
+ return resp;
+};
+
+fn read_statusline(
+ resp: *response,
+ scan: *bufio::scanner,
+) (void | error | nomem) = {
+ 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 =>
+ return protoerr;
+ case io::EOF =>
+ return protoerr;
+ };
+
+ const tok = strings::tokenize(status, " ");
+
+ const version = match (strings::next_token(&tok)) {
+ case let ver: str =>
+ yield ver;
+ case done =>
+ return protoerr;
+ };
+
+ const status = match (strings::next_token(&tok)) {
+ case let status: str =>
+ yield status;
+ case done =>
+ return protoerr;
+ };
+
+ const reason = match (strings::next_token(&tok)) {
+ case let reason: str =>
+ yield reason;
+ case done =>
+ return protoerr;
+ };
+
+ const (_, version) = strings::cut(version, "/");
+ const (major, minor) = strings::cut(version, ".");
+
+ const major = match (strconv::stou(major)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+ const minor = match (strconv::stou(minor)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+ resp.version = (major, minor);
+
+ if (resp.version.0 > 1) {
+ return errors::unsupported;
+ };
+
+ resp.status = match (strconv::stou(status)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+
+ resp.reason = strings::dup(reason)?;
+};
diff --git a/net/http/error.ha b/net/http/error.ha
@@ -0,0 +1,27 @@
+use errors;
+use io;
+use net::dial;
+use net;
+
+// 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_NOT_FOUND]] are not handled by this type.
+export type error = !(dial::error | io::error | net::error | errors::unsupported | protoerr);
+
+// An HTTP protocol error occurred, indicating that the remote party is not
+// conformant with HTTP semantics.
+export type protoerr = !void;
+
+// 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);
+ case errors::unsupported =>
+ return "Unsupported HTTP feature";
+ case protoerr =>
+ return "HTTP protocol error";
+ };
+};
diff --git a/net/http/header.ha b/net/http/header.ha
@@ -0,0 +1,108 @@
+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 name should
+// be canonicalized by the caller.
+export fn header_add(head: *header, name: str, val: str) (void | nomem) = {
+ assert(len(name) >= 1 && len(val) >= 1);
+ append(head, (strings::dup(name)?, strings::dup(val)?))?;
+};
+
+// Sets the value of a given HTTP header, removing any previous values. The name
+// should be canonicalized by the caller.
+export fn header_set(head: *header, name: str, val: str) (void | nomem) = {
+ header_del(head, name);
+ header_add(head, name, val)?;
+};
+
+// Removes an HTTP header from a list of [[header]]. If multiple headers match
+// the given name, all matching headers are removed.
+export fn header_del(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;
+ };
+ };
+};
+
+// Retrieves a value, or values, from a header. An empty string indicates the
+// absence of a header.
+export fn header_get(head: *header, name: str) str = {
+ for (let i = 0z; i < len(head); i += 1) {
+ const (key, val) = head[i];
+ if (key == name) {
+ return val;
+ };
+ };
+ return "";
+};
+
+// Finish state associated with an HTTP [[header]].
+export fn header_finish(head: *header) void = {
+ for (let i = 0z; i < len(head); i += 1) {
+ free(head[i].0);
+ free(head[i].1);
+ };
+ free(*head);
+};
+
+// Duplicates a set of HTTP headers.
+export fn header_dup(head: *header) (header | nomem) = {
+ let new: header = [];
+ for (let i = 0z; i < len(head); i += 1) {
+ const (key, val) = head[i];
+ header_add(&new, key, val)?;
+ };
+ return new;
+};
+
+// 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) = {
+ let z = 0z;
+ for (let i = 0z; i < len(head); i += 1) {
+ const (name, val) = head[i];
+ z += fmt::fprintf(sink, "{}: {}\r\n", name, val)?;
+ };
+ return z;
+};
+
+fn read_header(
+ head: *header,
+ scan: *bufio::scanner
+) (void | io::EOF | io::error | protoerr | nomem) = {
+ for (true) {
+ const item = match (bufio::scan_string(scan, "\r\n")) {
+ case let line: const str =>
+ yield line;
+ case io::EOF =>
+ return io::EOF;
+ case let err: io::error =>
+ return err;
+ case utf8::invalid =>
+ return protoerr;
+ };
+ if (item == "") {
+ break;
+ };
+
+ let (name, val) = strings::cut(item, ":");
+ val = strings::trim(val);
+ if (val == "") {
+ return protoerr;
+ };
+ // TODO: validate field-name
+
+ header_add(head, name, val)?;
+ };
+};
diff --git a/net/http/request.ha b/net/http/request.ha
@@ -0,0 +1,332 @@
+use bufio;
+use encoding::utf8;
+use errors;
+use fmt;
+use io;
+use memio;
+use net::ip;
+use net::uri;
+use strconv;
+use strings;
+use types;
+
+// Stores state related to an HTTP request.
+//
+// For a request to be processable by an HTTP [[client]], i.e. via [[do]], the
+// method and target must be filled in appropriately. The target must include at
+// least a host, port, and scheme. The default values for other fields are
+// suitable if appropriate for the request you wish to perform.
+export type request = struct {
+ // HTTP request method, e.g. GET
+ method: str,
+
+ // Request target URI.
+ //
+ // Note that the normal constraints for [[uri::parse]] are not upheld in
+ // the case of a request using the origin-form (e.g. /index.html), i.e.
+ // the scheme field may be empty.
+ target: *uri::uri,
+
+ // List of HTTP request headers.
+ header: header,
+
+ // Transport configuration, or null to use the [[client]] default.
+ transport: nullable *transport,
+
+ // I/O reader for the request body, or [[io::empty]] if there is no
+ // body.
+ body: io::handle,
+};
+
+// Frees state associated with an HTTP [[request]].
+export fn request_finish(req: *request) void = {
+ header_finish(&req.header);
+ uri::finish(req.target);
+ io::close(req.body)!;
+ free(req.target);
+};
+
+// Creates a new HTTP [[request]] using the given HTTP [[client]] defaults.
+export fn new_request(
+ client: *client,
+ method: str,
+ target: *uri::uri,
+) (request | errors::unsupported | nomem) = {
+ let req = request {
+ method = method,
+ target = alloc(uri::dup(target)?)?,
+ header = header_dup(&client.default_header)?,
+ transport = null,
+ body = io::empty,
+ };
+ switch (req.target.scheme) {
+ case "http" =>
+ if (req.target.port == 0) {
+ req.target.port = 80;
+ };
+ case "https" =>
+ if (req.target.port == 0) {
+ req.target.port = 443;
+ };
+ case "ws" =>
+ if (req.target.port == 0) {
+ req.target.port = 80;
+ };
+ case "wss" =>
+ if (req.target.port == 0) {
+ req.target.port = 443;
+ };
+ case =>
+ return errors::unsupported;
+ };
+
+ let host = match (req.target.host) {
+ case let host: str =>
+ yield host;
+ case let ip: ip::addr4 =>
+ yield ip::string(ip);
+ case let ip: ip::addr6 =>
+ static let buf: [64 + 2]u8 = [0...];
+ yield fmt::bsprintf(buf, "[{}]", 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)?;
+ };
+ defer free(host);
+ header_add(&req.header, "Host", host)?;
+ return req;
+};
+
+// Creates a new HTTP [[request]] using the given HTTP [[client]] defaults and
+// the provided request body.
+//
+// If the provided I/O handle is seekable, the Content-Length header is added
+// automatically. Otherwise, Transfer-Encoding: chunked will be used.
+export fn new_request_body(
+ client: *client,
+ method: str,
+ target: *uri::uri,
+ body: io::handle,
+) (request | errors::unsupported | nomem) = {
+ let req = new_request(client, method, target)?;
+ req.body = body;
+
+ const offs = match (io::seek(body, 0, io::whence::CUR)) {
+ case let off: io::off =>
+ yield off;
+ case io::error =>
+ header_add(&req.header, "Transfer-Encoding", "chunked")?;
+ return req;
+ };
+ const ln = io::seek(body, 0, io::whence::END)!;
+ io::seek(body, offs, io::whence::SET)!;
+ header_add(&req.header, "Content-Length", strconv::ztos(ln: size))?;
+ return req;
+};
+
+export type request_server = void;
+export type authority = void;
+
+export type request_uri = (
+ request_server |
+ authority |
+ *uri::uri |
+ str |
+);
+
+export type request_line = struct {
+ method: str,
+ uri: request_uri,
+ version: version,
+};
+
+// Parse a [[request]] from an [[io::handle]].
+export fn request_parse(
+ file: io::handle
+) (request | protoerr | io::error | nomem | errors::unsupported) = {
+ let ok = false;
+
+ const scan = bufio::newscanner(file, types::SIZE_MAX);
+ defer bufio::finish(&scan);
+
+ const req_line = request_line_parse(&scan)?;
+ defer request_line_finish(&req_line);
+
+ let header: header = [];
+ defer if (!ok) header_finish(&header);
+ read_header(&header, &scan)?;
+
+ const target = match (req_line.uri) {
+ case let uri: request_server =>
+ return errors::unsupported;
+ case let uri: authority =>
+ return errors::unsupported;
+ case let uri: *uri::uri =>
+ yield &uri::dup(uri)?;
+ case let path: str =>
+ const uri = fmt::asprintf("http://{}", path)?;
+ defer free(uri);
+
+ const uri = match (uri::parse(uri)) {
+ case let uri: uri::uri =>
+ yield uri;
+ case uri::invalid =>
+ return protoerr;
+ };
+ yield alloc(uri)?;
+ };
+ defer if (!ok) free(target);
+
+ const body = match (new_reader(file, &header, &scan)) {
+ case let body: io::handle => yield body;
+ case let err: errors::unsupported => return err;
+ case let err: protoerr => return err;
+ case nomem => return nomem;
+ };
+ defer if (!ok) io::close(body)!;
+
+ const host = header_get(&header, "Host");
+ if (host != "") {
+ target.host = strings::dup(host)?;
+ };
+
+ const method = strings::dup(req_line.method)?;
+ defer if (!ok) free(method);
+
+ ok = true;
+ return request {
+ method = method,
+ target = target,
+ header = header,
+ body = body,
+ ...
+ };
+};
+
+fn request_line_parse(
+ scan: *bufio::scanner
+) (request_line | protoerr | io::error | errors::unsupported) = {
+ const line = match (bufio::scan_string(scan, "\r\n")) {
+ case let line: const str =>
+ yield line;
+ case let err: io::error =>
+ return err;
+ case (utf8::invalid | io::EOF) =>
+ return protoerr;
+ };
+
+ const tok = strings::tokenize(line, " ");
+
+ const method = match (strings::next_token(&tok)) {
+ case let method: str =>
+ yield strings::dup(method)?;
+ case done =>
+ return protoerr;
+ };
+
+ const uri: request_uri = match (strings::next_token(&tok)) {
+ case let req_uri: str =>
+ if ("*" == req_uri) {
+ yield request_server;
+ };
+
+ yield match (uri::parse(req_uri)) {
+ case let uri: uri::uri => yield alloc(uri)?;
+ case => yield strings::dup(req_uri)?; // as path
+ };
+ case done =>
+ return protoerr;
+ };
+
+ const version = match (strings::next_token(&tok)) {
+ case let ver: str =>
+ yield ver;
+ case done =>
+ return protoerr;
+ };
+
+ const (_, version) = strings::cut(version, "/");
+ const (major, minor) = strings::cut(version, ".");
+
+ const major = match (strconv::stou(major)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+ const minor = match (strconv::stou(minor)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+
+ if (major > 1) {
+ return errors::unsupported;
+ };
+
+ if (uri is request_server && method != OPTIONS) {
+ return protoerr;
+ };
+
+ return request_line {
+ method = method,
+ uri = uri,
+ version = (major, minor),
+ };
+};
+
+fn request_line_finish(line: *request_line) void = {
+ match (line.uri) {
+ case let path: str => free(path);
+ case let uri: *uri::uri => uri::finish(uri);
+ case => yield;
+ };
+ free(line.method);
+};
+
+export fn request_parsed_finish(req: *request) void = {
+ free(req.method);
+ uri::finish(req.target);
+ free(req.target);
+ header_finish(&req.header);
+
+ // Ignore double-close errors, in case the user closed it
+ io::close(req.body): void;
+};
+
+// Formats an HTTP [[request]] and writes it to the given [[io::handle]].
+export fn request_write(
+ out: io::handle,
+ req: *request,
+ cli: *client,
+) (void | io::error) = {
+ fmt::fprintf(out, "{} ", req.method)?;
+
+ // TODO: Support other request-targets than origin-form
+ const target = uri_origin_form(req.target);
+ uri::fmt(out, &target)?;
+ fmt::fprintf(out, " HTTP/1.1\r\n")?;
+
+ write_header(out, &req.header)?;
+ fmt::fprintf(out, "\r\n")?;
+
+ const trans = match (req.transport) {
+ case let t: *transport =>
+ yield t;
+ case =>
+ yield &cli.default_transport;
+ };
+ // TODO: Implement None
+ assert(trans.request_transport == transport_mode::AUTO);
+ assert(trans.response_transport == transport_mode::AUTO);
+ assert(trans.request_content == content_mode::AUTO);
+ assert(trans.response_content == content_mode::AUTO);
+
+ io::copy(out, req.body)?;
+};
diff --git a/net/http/response.ha b/net/http/response.ha
@@ -0,0 +1,176 @@
+use bufio;
+use encoding::utf8;
+use errors;
+use fmt;
+use io;
+use os;
+use strconv;
+use strings;
+use types;
+
+// HTTP protocol version (major, minor)
+export type version = (uint, uint);
+
+// Stores state related to an HTTP response.
+export type response = struct {
+ // HTTP protocol version (major, minor)
+ version: version,
+ // 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, or [[io::empty]].
+ body: io::handle,
+};
+
+// Frees state associated with an HTTP [[response]] and closes the response
+// body.
+export fn response_finish(resp: *response) void = {
+ // Ignore errors in case the caller closed it themselves
+ io::close(resp.body): void;
+ header_finish(&resp.header);
+ free(resp.reason);
+};
+
+// Parse a [[response]] from an [[io::handle]]. Return [[protoerr]] if the
+// HTTP message is incomplete.
+export fn response_parse(
+ file: io::handle
+) (response | protoerr | io::error | nomem | errors::unsupported) = {
+ let ok = false;
+
+ const scan = bufio::newscanner(file, types::SIZE_MAX);
+ defer bufio::finish(&scan);
+
+ const resp_line = response_line_parse(&scan)?;
+ defer response_line_finish(&resp_line);
+
+ let header: header = [];
+ defer if (!ok) header_finish(&header);
+ read_header(&header, &scan)?;
+
+ const body = match (new_reader(file, &header, &scan)) {
+ case let body: io::handle => yield body;
+ case let err: errors::unsupported => return err;
+ case let err: protoerr => return err;
+ case nomem => return nomem;
+ };
+ defer if (!ok) io::close(body)!;
+
+ const reason = strings::dup(resp_line.reason)?;
+ defer if (!ok) free(reason);
+
+ ok = true;
+ return response {
+ version = resp_line.version,
+ status = resp_line.status,
+ reason = reason,
+ header = header,
+ body = body,
+ };
+};
+
+// HTTP status line version, status, and reason
+export type response_line = struct {
+ version: version,
+ status: uint,
+ reason: str,
+};
+
+fn response_line_parse(
+ scan: *bufio::scanner
+) (response_line | protoerr | io::error | errors::unsupported) = {
+ 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 =>
+ return protoerr;
+ };
+
+ const tok = strings::tokenize(status, " ");
+
+ const version = match (strings::next_token(&tok)) {
+ case let ver: str =>
+ yield ver;
+ case done =>
+ return protoerr;
+ };
+
+ const status = match (strings::next_token(&tok)) {
+ case let status: str =>
+ yield status;
+ case done =>
+ return protoerr;
+ };
+
+ const reason = match (strings::next_token(&tok)) {
+ case let reason: str =>
+ yield reason;
+ case done =>
+ return protoerr;
+ };
+
+ const (_, version) = strings::cut(version, "/");
+ const (major, minor) = strings::cut(version, ".");
+
+ const major = match (strconv::stou(major)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+ const minor = match (strconv::stou(minor)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+
+ if (major > 1) {
+ return errors::unsupported;
+ };
+
+ const status = match (strconv::stou(status)) {
+ case let u: uint =>
+ yield u;
+ case =>
+ return protoerr;
+ };
+
+ const reason = strings::dup(reason)?;
+
+ return response_line {
+ version = (major, minor),
+ status = status,
+ reason = reason,
+ };
+};
+
+fn response_line_finish(line: *response_line) void = {
+ free(line.reason);
+};
+
+export fn response_parsed_finish(resp: *response) void = {
+ header_finish(&resp.header);
+ io::close(resp.body)!;
+ free(resp.reason);
+};
+
+// Formats an HTTP [[response]] and writes it to the given [[io::handle]].
+export fn response_write(
+ out: io::handle,
+ resp: *response,
+) (void | io::error | errors::unsupported) = {
+ fmt::fprintf(out, "HTTP/{}.{} {} {}\r\n",
+ resp.version.0, resp.version.1,
+ resp.status, resp.reason)?;
+
+ write_header(out, &resp.header)?;
+ fmt::fprint(out, "\r\n")?;
+
+ io::copy(out, resp.body)?;
+};
diff --git a/net/http/server.ha b/net/http/server.ha
@@ -0,0 +1,283 @@
+use errors;
+use fmt;
+use io;
+use net::ip;
+use net::tcp;
+use net::uri;
+use net;
+use strconv;
+use strings;
+
+export type server = struct {
+ sock: net::socket,
+};
+
+export type response_writer = struct {
+ stream: io::stream,
+ peerer: *peerer,
+ closer: nullable *closer,
+ sink: io::handle,
+ wrote_header: bool,
+
+ status: uint,
+ reason: str,
+ header: header,
+};
+
+export const response_writer_vt: io::vtable = io::vtable {
+ writer = &response_writer_write,
+ closer = &response_writer_close,
+ copier = &response_writer_copy,
+ ...
+};
+
+fn response_write_header(rw: *response_writer) (void | io::error) = {
+ fmt::fprintf(rw.sink, "HTTP/1.1 {} {}\r\n", rw.status, rw.reason)?;
+ write_header(rw.sink, &rw.header)?;
+ fmt::fprint(rw.sink, "\r\n")?;
+ rw.wrote_header = true;
+};
+
+fn response_writer_write(s: *io::stream, buf: []u8) (size | io::error) = {
+ let rw = s: *response_writer;
+
+ const cl = header_get(&rw.header, "Content-Length");
+ const te = header_get(&rw.header, "Transfer-Encoding");
+
+ if (!rw.wrote_header) {
+ if (cl == "" && te == "") {
+ response_add_header(rw, "Transfer-Encoding", "chunked")!;
+ };
+
+ assert(te != "chunked" || cl != "");
+ response_write_header(rw)?;
+ te = "chunked";
+ };
+
+ if (len(buf) == 0) {
+ return 0z;
+ };
+
+ if (te == "chunked") {
+ return write_chunk(rw.sink, buf);
+ } else if (cl != "") {
+ return io::write(rw.sink, buf);
+ } else {
+ abort("Unsupported Transport-Encoding");
+ };
+};
+
+fn response_writer_close(s: *io::stream) (void | io::error) = {
+ let rw = s: *response_writer;
+ if (rw.wrote_header) {
+ const te = header_get(&rw.header, "Transfer-Encoding");
+ if (te == "chunked") {
+ write_chunk(rw.sink, [])?;
+ };
+ } else {
+ // Send an empty response body
+ response_del_header(rw, "Transfer-Encoding");
+ response_set_header(rw, "Content-Length", "0")?;
+ response_write_header(rw)?;
+ };
+ header_finish(&rw.header);
+ free(rw.reason);
+ close(rw);
+};
+
+fn response_writer_copy(
+ rw: *io::stream,
+ from: io::handle,
+) (size | io::error) = {
+ let rw = rw: *response_writer;
+
+ // If the caller added a Content-Length just copy the body directly to
+ // the socket (ideally with sendfile).
+ if (response_get_header(rw, "Content-Length") != "") {
+ if (!rw.wrote_header) {
+ response_write_header(rw)?;
+ };
+ return io::copy(rw.sink, from);
+ };
+ // If the caller added Transfer-Encoding, fall back to a read/write loop
+ // in io::copy.
+ if (response_get_header(rw, "Transfer-Encoding") != "") {
+ return errors::unsupported;
+ };
+ // If this isn't the first write, fall back to read/write loop in
+ // io::copy.
+ if (rw.wrote_header) {
+ return errors::unsupported;
+ };
+
+ // Otherwise, try to determine the length and set the Content-Length
+ // accordingly.
+ const off = io::tell(from)?;
+ const end = io::seek(from, 0, io::whence::END)!;
+ const length = end - off;
+ io::seek(from, off, io::whence::SET)?;
+
+ response_set_header(rw, "Content-Length", strconv::i64tos(length))?;
+ response_write_header(rw)?;
+ return io::copy(rw.sink, from);
+};
+
+// Creates a [[server]] which listens for HTTP traffic on the given IP address
+// and port, binding the socket as appropriate.
+export fn listen(
+ ip: ip::addr,
+ port: u16,
+ options: net::tcp::listen_option...
+) (server | net::error) = {
+ return server {
+ sock = tcp::listen(ip, port, options...)?,
+ };
+};
+
+// Frees resources associated with a [[server]] and closes its socket.
+export fn server_finish(serv: *server) void = {
+ net::close(serv.sock)!;
+};
+
+// Listens for an incoming request on a [[server]], blocking until a request is
+// available and returning a ([[request]], [[response_writer]]) tuple.
+//
+// To write the HTTP response, the caller should perform the following steps, in
+// order:
+//
+// - Configure headers, if necessary, via [[response_add_header]] et al
+// - Set the response status (if not 200 OK) via [[response_set_status]]
+// - Write the response body via [[io::write]] et al, if necessary
+// - Close the response writer via [[io::close]]
+//
+// [[response_writer]] is an [[io::stream]] which the caller may write to using
+// [[io::write]] or any standard Hare I/O functions.
+//
+// Upon the first write to the [[response_writer]], the headers are consulted to
+// determine the response format. If the caller has not added a Content-Length
+// nor Transfer-Encoding header to the response, Transfer-Encoding: chunked is
+// assumed.
+//
+// It is recommended to use [[io::copy]] to write the response body, if
+// possible. The [[response_writer]] implementation of [[io::copier]] tests if
+// the "from" argument is seekable, and if so will prepare an appropriate
+// Content-Length header for you.
+//
+// Changing the headers or status code after writing to the response body is not
+// permitted, and will cause an assertion failure.
+//
+// The caller MUST call [[io::close]] on the response writer before calling
+// [[serve]] again.
+export fn serve(
+ serv: *server
+) ((request, response_writer) | error | nomem) = {
+ // TODO: connection pooling
+ for (true) {
+ const sock = net::accept(serv.sock)?;
+ const req = match (request_parse(sock)) {
+ case let req: request =>
+ yield req;
+ case (protoerr | io::error) =>
+ io::close(sock)!;
+ continue;
+ case nomem =>
+ return nomem;
+ };
+
+ const rw = response_writer {
+ stream = &response_writer_vt,
+ peerer = &tcp_peeraddr,
+ closer = &tcp_closer,
+ wrote_header = false,
+ sink = sock,
+ status = STATUS_OK,
+ reason = strings::dup(status_reason(STATUS_OK))?,
+ header = [],
+ };
+ return (req, rw);
+ };
+
+};
+
+export type peerer = fn(rw: *response_writer) (ip::addr, u16);
+
+// Returns the remote peer address associated with this connection.
+export fn peeraddr(rw: *response_writer) (ip::addr, u16) = {
+ return rw.peerer(rw);
+};
+
+fn tcp_peeraddr(rw: *response_writer) (ip::addr, u16) = {
+ return tcp::peeraddr(rw.sink: net::socket) as (ip::addr, u16);
+};
+
+export type closer = fn(rw: *response_writer) void;
+
+fn close(rw: *response_writer) void = {
+ if (rw.closer is *closer) {
+ (rw.closer as *closer)(rw);
+ };
+};
+
+fn tcp_closer(rw: *response_writer) void = {
+ io::close(rw.sink)!;
+};
+
+// Sets the response status for an [[http::response_writer]]. This may be called
+// any number of times until the first write to the response body. If you don't
+// provide a status reason, it will be filled in for you via [[status_reason]].
+//
+// [[net::http]] provides constants for standard status codes for your
+// convenience, such as [[STATUS_OK]].
+export fn response_set_status(
+ rw: *response_writer,
+ status: uint,
+ reason: str = "",
+) (void | nomem) = {
+ assert(!rw.wrote_header, "Modified response status after writing body");
+ free(rw.reason);
+ rw.status = status;
+ if (reason == "") {
+ reason = status_reason(status);
+ };
+ rw.reason = strings::dup(reason)?;
+};
+
+// Adds a header to a response. This may be called any number of times until the
+// first write to the response body. See [[header_add]].
+export fn response_add_header(
+ rw: *response_writer,
+ name: str,
+ val: str,
+) (void | nomem) = {
+ assert(!rw.wrote_header, "Modified response header after writing body");
+ return header_add(&rw.header, name, val);
+};
+
+// Sets the value of a response header, replacing any previous value(s). This
+// may be called any number of times until the first write to the response body.
+// See [[header_set]].
+export fn response_set_header(
+ rw: *response_writer,
+ name: str,
+ val: str,
+) (void | nomem) = {
+ assert(!rw.wrote_header, "Modified response header after writing body");
+ return header_set(&rw.header, name, val);
+};
+
+// Removes a response header. This may be called any number of times until the
+// first write to the response body. See [[header_del]].
+export fn response_del_header(rw: *response_writer, name: str) void = {
+ assert(!rw.wrote_header, "Modified response header after writing body");
+ header_del(&rw.header, name);
+};
+
+// Gets the value of a response header. See [[header_get]].
+export fn response_get_header(rw: *response_writer, name: str) const str = {
+ return header_get(&rw.header, name);
+};
+
+// Copies the response headers for a [[response_writer]] to a new [[header]].
+export fn response_dup_header(rw: *response_writer) (header | nomem) = {
+ return header_dup(&rw.header);
+};
diff --git a/net/http/status.ha b/net/http/status.ha
@@ -0,0 +1,111 @@
+// A semantic HTTP error and its status code.
+export type httperror = !uint;
+
+// Checks if an HTTP status code is semantically considered an error, returning
+// [[httperror]] if so, or otherwise returning the original status code.
+export fn check(status: uint) (uint | httperror) = {
+ if (status >= 400 && status < 600) {
+ return status: httperror;
+ };
+ return status;
+};
+
+// Converts a standard HTTP status code into the reason text typically
+// associated with this status code (or "Unknown Status" if the status code is
+// not known to net::http).
+export fn status_reason(status: uint) const str = {
+ switch (status) {
+ case STATUS_CONTINUE =>
+ return "Continue";
+ case STATUS_SWITCHING_PROTOCOLS =>
+ return "Switching Protocols";
+ case STATUS_OK =>
+ return "OK";
+ case STATUS_CREATED =>
+ return "Created";
+ case STATUS_ACCEPTED =>
+ return "Accepted";
+ case STATUS_NONAUTHORITATIVE_INFO =>
+ return "Non-Authoritative Information";
+ case STATUS_NO_CONTENT =>
+ return "No Content";
+ case STATUS_RESET_CONTENT =>
+ return "Reset Content";
+ case STATUS_PARTIAL_CONTENT =>
+ return "Partial Content";
+ case STATUS_MULTIPLE_CHOICES =>
+ return "Multiple Choices";
+ case STATUS_MOVED_PERMANENTLY =>
+ return "Moved Permanently";
+ case STATUS_FOUND =>
+ return "Found";
+ case STATUS_SEE_OTHER =>
+ return "See Other";
+ case STATUS_NOT_MODIFIED =>
+ return "Not Modified";
+ case STATUS_USE_PROXY =>
+ return "Use Proxy";
+ case STATUS_TEMPORARY_REDIRECT =>
+ return "Temporary Redirect";
+ case STATUS_PERMANENT_REDIRECT =>
+ return "Permanent Redirect";
+ case STATUS_BAD_REQUEST =>
+ return "Bad Request";
+ case STATUS_UNAUTHORIZED =>
+ return "Unauthorized";
+ case STATUS_PAYMENT_REQUIRED =>
+ return "Payment Required";
+ case STATUS_FORBIDDEN =>
+ return "Forbidden";
+ case STATUS_NOT_FOUND =>
+ return "Not Found";
+ case STATUS_METHOD_NOT_ALLOWED =>
+ return "Method Not Allowed";
+ case STATUS_NOT_ACCEPTABLE =>
+ return "Not Acceptable";
+ case STATUS_PROXY_AUTH_REQUIRED =>
+ return "Proxy Authentication Required";
+ case STATUS_REQUEST_TIMEOUT =>
+ return "Request Timeout";
+ case STATUS_CONFLICT =>
+ return "Conflict";
+ case STATUS_GONE =>
+ return "Gone";
+ case STATUS_LENGTH_REQUIRED =>
+ return "Length Required";
+ case STATUS_PRECONDITION_FAILED =>
+ return "Precondition Failed";
+ case STATUS_REQUEST_ENTITY_TOO_LARGE =>
+ return "Request Entity Too Large";
+ case STATUS_REQUEST_URI_TOO_LONG =>
+ return "Request URI Too Long";
+ case STATUS_UNSUPPORTED_MEDIA_TYPE =>
+ return "Unsupported Media Type";
+ case STATUS_REQUESTED_RANGE_NOT_SATISFIABLE =>
+ return "Requested Range Not Satisfiable";
+ case STATUS_EXPECTATION_FAILED =>
+ return "Expectation Failed";
+ case STATUS_TEAPOT =>
+ return "I'm A Teapot";
+ case STATUS_MISDIRECTED_REQUEST =>
+ return "Misdirected Request";
+ case STATUS_UNPROCESSABLE_ENTITY =>
+ return "Unprocessable Entity";
+ case STATUS_UPGRADE_REQUIRED =>
+ return "Upgrade Required";
+ case STATUS_INTERNAL_SERVER_ERROR =>
+ return "Internal Server Error";
+ case STATUS_NOT_IMPLEMENTED =>
+ return "Not Implemented";
+ case STATUS_BAD_GATEWAY =>
+ return "Bad Gateway";
+ case STATUS_SERVICE_UNAVAILABLE =>
+ return "Service Unavailable";
+ case STATUS_GATEWAY_TIMEOUT =>
+ return "Gateway Timeout";
+ case STATUS_HTTP_VERSION_NOT_SUPPORTED =>
+ return "HTTP Version Not Supported";
+ case =>
+ return "Unknown status";
+ };
+};
diff --git a/net/http/transport.ha b/net/http/transport.ha
@@ -0,0 +1,330 @@
+use bufio;
+use bytes;
+use errors;
+use fmt;
+use io;
+use os;
+use strconv;
+use strings;
+use types;
+
+// Configures the Transport-Encoding behavior.
+//
+// If set to NONE, no transport decoding or encoding is performed on the message
+// body, irrespective of the value of the Transport-Encoding header. The user
+// must perform any required encoding or decoding themselves in this mode. If
+// set to AUTO, the implementation will examine the Transport-Encoding header
+// and encode the message body appropriately.
+//
+// Most users will want this to be set to auto.
+export type transport_mode = enum {
+ AUTO = 0,
+ NONE,
+};
+
+// Configures the Content-Encoding behavior.
+//
+// If set to NONE, no transport decoding or encoding is performed on the message
+// body, irrespective of the value of the Content-Encoding header. The user must
+// perform any required encoding or decoding themselves in this mode. If set to
+// AUTO, the implementation will examine the Content-Encoding header and encode
+// the message body appropriately.
+//
+// Most users will want this to be set to AUTO.
+export type content_mode = enum {
+ AUTO = 0,
+ NONE,
+};
+
+// Describes an HTTP [[client]]'s transport configuration for a given request.
+//
+// The default value of this type sets all parameters to "auto".
+export type transport = struct {
+ // Desired Transport-Encoding configuration, see [[transport_mode]] for
+ // details.
+ request_transport: transport_mode,
+ response_transport: transport_mode,
+ // Desired Content-Encoding configuration, see [[content_mode]] for
+ // details.
+ request_content: content_mode,
+ response_content: content_mode,
+};
+
+fn new_reader(
+ conn: io::handle,
+ header: *header,
+ scan: *bufio::scanner,
+) (io::handle | errors::unsupported | protoerr | nomem) = {
+ // TODO: Content-Encoding support
+ const cl = header_get(header, "Content-Length");
+ const te = header_get(header, "Transfer-Encoding");
+
+ if (cl != "" || te == "") {
+ if (cl == "") {
+ return io::empty;
+ };
+ const length = match (strconv::stoz(cl)) {
+ case let z: size =>
+ yield z;
+ case =>
+ return protoerr;
+ };
+ return new_identity_reader(conn, scan, length)?;
+ };
+
+ // TODO: Figure out the semantics for closing the stream
+ // The caller should probably be required to close it
+ // It should close/free any intermediate transport/content decoders
+ // And it should not close the actual connection if it's still in the
+ // connection pool
+ // Unless it isn't in the pool, then it should!
+ // And this leaks, fix that too
+ let stream: io::handle = conn;
+ let buffer: []u8 = bufio::scan_buffer(scan);
+ const iter = strings::tokenize(te, ",");
+ for (true) {
+ const te = match (strings::next_token(&iter)) {
+ case let tok: str =>
+ yield strings::trim(tok);
+ case done =>
+ break;
+ };
+
+ // XXX: We could add lzw support if someone added it to
+ // hare-compress
+ const next = switch (te) {
+ case "chunked" =>
+ yield new_chunked_reader(stream, buffer)?;
+ case "deflate" =>
+ abort(); // TODO
+ case "gzip" =>
+ abort(); // TODO
+ case =>
+ return errors::unsupported;
+ };
+ stream = next;
+
+ buffer = [];
+ };
+
+ if (!(stream is *io::stream)) {
+ // Empty Transfer-Encoding header
+ return protoerr;
+ };
+ return stream;
+};
+
+type identity_reader = struct {
+ vtable: io::stream,
+ conn: io::handle,
+ scan: *bufio::scanner,
+ src: io::limitstream,
+};
+
+const identity_reader_vtable = io::vtable {
+ reader = &identity_read,
+ closer = &identity_close,
+ ...
+};
+
+// Creates a new reader that reads data until the response's Content-Length is
+// reached; i.e. the null Transport-Encoding.
+fn new_identity_reader(
+ conn: io::handle,
+ scan: *bufio::scanner,
+ content_length: size,
+) (*io::stream | nomem) = {
+ const nscan = alloc(bufio::scanner {
+ stream = scan.stream,
+ src = scan.src,
+ buffer = [],
+ maxread = scan.maxread,
+ start = scan.start,
+ pending = [],
+ opts = scan.opts,
+ })?;
+ append(nscan.buffer, scan.buffer...)?;
+ nscan.pending = nscan.buffer[nscan.start..];
+ return alloc(identity_reader {
+ vtable = &identity_reader_vtable,
+ conn = conn,
+ scan = nscan,
+ src = io::limitreader(nscan, content_length),
+ ...
+ })?;
+};
+
+fn identity_read(
+ s: *io::stream,
+ buf: []u8,
+) (size | io::EOF | io::error) = {
+ let rd = s: *identity_reader;
+ assert(rd.vtable == &identity_reader_vtable);
+ return io::read(&rd.src, buf)?;
+};
+
+fn identity_close(s: *io::stream) (void | io::error) = {
+ let rd = s: *identity_reader;
+ assert(rd.vtable == &identity_reader_vtable);
+
+ // Flush the remainder of the response in case the caller did not read
+ // it out entirely
+ io::copy(io::empty, &rd.src)?;
+
+ io::close(&rd.src)!;
+ bufio::finish(rd.scan);
+ free(rd.scan);
+
+ // TODO connection pool
+ free(rd);
+};
+
+type chunk_state = enum {
+ HEADER,
+ DATA,
+ FOOTER,
+};
+
+type chunked_reader = struct {
+ vtable: io::stream,
+ conn: io::handle,
+ buffer: [os::BUFSZ]u8,
+ state: chunk_state,
+ // Amount of read-ahead data in buffer
+ pending: size,
+ // Length of current chunk
+ length: size,
+};
+
+fn new_chunked_reader(
+ conn: io::handle,
+ buffer: []u8,
+) (*io::stream | nomem) = {
+ let rd = alloc(chunked_reader {
+ vtable = &chunked_reader_vtable,
+ conn = conn,
+ ...
+ })?;
+ rd.buffer[..len(buffer)] = buffer[..];
+ rd.pending = len(buffer);
+ return rd;
+};
+
+const chunked_reader_vtable = io::vtable {
+ reader = &chunked_read,
+ closer = &chunked_close,
+ ...
+};
+
+fn chunked_read(
+ s: *io::stream,
+ buf: []u8,
+) (size | io::EOF | io::error) = {
+ // XXX: I am not satisfied with this code
+ let rd = s: *chunked_reader;
+ assert(rd.vtable == &chunked_reader_vtable);
+
+ for (true) switch (rd.state) {
+ case chunk_state::HEADER =>
+ let crlf = 0z;
+ for (true) {
+ const n = rd.pending;
+ match (bytes::index(rd.buffer[..n], ['\r', '\n'])) {
+ case let z: size =>
+ crlf = z;
+ break;
+ case void =>
+ yield;
+ };
+ if (rd.pending >= len(rd.buffer)) {
+ // Chunk header exceeds buffer size
+ return errors::overflow;
+ };
+
+ match (io::read(rd.conn, rd.buffer[rd.pending..])?) {
+ case let n: size =>
+ rd.pending += n;
+ case io::EOF =>
+ if (rd.pending > 0) {
+ return errors::invalid;
+ };
+ return io::EOF;
+ };
+ };
+
+ // XXX: Should we do anything with chunk-ext?
+ const header = rd.buffer[..crlf];
+ const (ln, _) = bytes::cut(header, ';');
+ const ln = match (strings::fromutf8(ln)) {
+ case let s: str =>
+ yield s;
+ case =>
+ return errors::invalid;
+ };
+
+ match (strconv::stoz(ln, strconv::base::HEX)) {
+ case let z: size =>
+ rd.length = z;
+ case =>
+ return errors::invalid;
+ };
+ if (rd.length == 0) {
+ return io::EOF;
+ };
+
+ const n = crlf + 2;
+ rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending];
+ rd.pending -= n;
+ rd.state = chunk_state::DATA;
+ case chunk_state::DATA =>
+ for (rd.pending < rd.length) {
+ match (io::read(rd.conn, rd.buffer[rd.pending..])?) {
+ case let n: size =>
+ rd.pending += n;
+ case io::EOF =>
+ return io::EOF;
+ };
+ };
+ let n = len(buf);
+ if (n > rd.pending) {
+ n = rd.pending;
+ };
+ if (n > rd.length) {
+ n = rd.length;
+ };
+ buf[..n] = rd.buffer[..n];
+ rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending];
+ rd.pending -= n;
+ rd.length -= n;
+ rd.state = chunk_state::FOOTER;
+ return n;
+ case chunk_state::FOOTER =>
+ for (rd.pending < 2) {
+ match (io::read(rd.conn, rd.buffer[rd.pending..])?) {
+ case let n: size =>
+ rd.pending += n;
+ case io::EOF =>
+ return io::EOF;
+ };
+ };
+ if (!bytes::equal(rd.buffer[..2], ['\r', '\n'])) {
+ return errors::invalid;
+ };
+ rd.buffer[..rd.pending - 2] = rd.buffer[2..rd.pending];
+ rd.pending -= 2;
+ rd.state = chunk_state::HEADER;
+ };
+};
+
+fn chunked_close(s: *io::stream) (void | io::error) = {
+ let rd = s: *chunked_reader;
+ // TODO connection pool
+ free(rd);
+};
+
+fn write_chunk(sink: io::handle, buf: []u8) (size | io::error) = {
+ fmt::fprintf(sink, "{}\r\n", strconv::ztos(len(buf), strconv::base::HEX))?;
+ const wrote = io::write(sink, buf)?;
+ fmt::fprint(sink, "\r\n")?;
+ return wrote;
+};
diff --git a/net/websocket/websocket.ha b/net/websocket/websocket.ha
@@ -0,0 +1,358 @@
+use bufio;
+use crypto::random;
+use crypto::sha1;
+use encoding::base64;
+use errors;
+use fmt;
+use hash;
+use io;
+use net::dial;
+use net::uri;
+use net::http;
+use net;
+use strings;
+
+const FRAME_MAX = 0x7FFFFFFFFFFFFFFFz;
+const MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+export type opcode = u8;
+
+export const OPCODE_CONT = 0x0: opcode;
+export const OPCODE_TEXT = 0x1: opcode;
+export const OPCODE_BIN = 0x2: opcode;
+export const OPCODE_CLOSE = 0x8: opcode;
+export const OPCODE_PING = 0x9: opcode;
+export const OPCODE_PONG = 0xA: opcode;
+
+// Represent an unserialized and unmasked websocket frame.
+export type frame = struct {
+ op: opcode,
+ msg: []u8,
+};
+
+// Free resources associated to a [[frame]].
+export fn frame_finish(f: *frame) void = {
+ free(f.msg);
+};
+
+export type websocket = struct {
+ sock: io::handle,
+ server: bool,
+};
+
+// Performs a [[net::dial]] operation for a given URI, and continue with a
+// websocket initial handshake, returning a [[websocket]]. The caller must free
+// the associated resources with [[finish]].
+export fn connect(
+ client: *http::client,
+ targ: *uri::uri,
+) (websocket | http::protoerr | net::error | dial::error | nomem | io::error) = {
+ const sock = dial::dial_uri("tcp", targ)?;
+
+ const req = handshake_request(client, targ)?;
+ defer http::request_finish(&req);
+ http::request_write(sock, &req, client)!;
+
+ const resp = http::response_parse(sock)?;
+ defer http::response_parsed_finish(&resp);
+
+ const key = http::header_get(&req.header, "Sec-WebSocket-Key");
+ const proof_exp = proof(key)?;
+ defer free(proof_exp);
+ const proof = http::header_get(&resp.header, "Sec-WebSocket-Accept");
+ if (proof == "") {
+ proof = http::header_get(&resp.header, "sec-websocket-accept");
+ };
+ if (proof != proof_exp) {
+ return http::protoerr;
+ };
+
+ return websocket {
+ sock = sock,
+ server = false,
+ };
+};
+
+// Performs a [[net::accept]] operation over a [[net::socket]], and continue
+// with a websocket initial handshake, returning a [[websocket]]. The caller
+// must free the associated resources with [[finish]].
+export fn accept(
+ sock: net::socket,
+) (websocket | net::error | http::protoerr | io::error | nomem) = {
+ const sock = net::accept(sock)?;
+
+ const req = http::request_parse(sock)?;
+ defer http::request_parsed_finish(&req);
+
+ const resp = handshake_response(req)?;
+ defer http::response_finish(&resp);
+
+ fmt::fprintf(sock, "HTTP/1.1 101 Switching Protocols\r\n")!;
+ http::write_header(sock, &resp.header)!;
+ fmt::fprint(sock, "\r\n")!;
+
+ return websocket {
+ sock = sock,
+ server = true,
+ };
+};
+
+// Free associated resources to a [[websocket]].
+export fn finish(ws: *websocket) void = {
+ io::close(ws.sock)!;
+};
+
+// Send a message through a [[websocket]]. To send continuation frames, use
+// fin and cont respectively to indicate if the frame is final, and/or if it
+// is a continuation frame. Every non-first frames must be continuations.
+export fn write_msg(
+ ws: *websocket,
+ msg: []u8,
+ fin: bool = true,
+ cont: bool = false,
+) (void | io::error) = {
+ let op = OPCODE_TEXT;
+ if (cont) op = OPCODE_CONT;
+ write_websocket(
+ ws.sock,
+ frame {
+ op = op,
+ msg = msg,
+ },
+ !ws.server,
+ fin,
+ )?;
+};
+
+// Send a [[frame]] through a [[websocket]].
+export fn write(
+ ws: *websocket,
+ f: frame,
+ fin: bool = true,
+) (void | io::error) = {
+ write_websocket(ws.sock, f, !ws.server, fin)?;
+};
+
+// Read a [[frame]] through a [[websocket]].
+export fn read(
+ ws: *websocket,
+ f: *frame,
+) (size | io::EOF | io::error) = {
+ return read_websocket(
+ ws.sock,
+ f,
+ ws.server,
+ )?;
+};
+
+// Build an HTTP handshake [[net::http::request]].
+export fn handshake_request(
+ client: *http::client,
+ target: *uri::uri,
+) (http::request | nomem) = {
+ const req = http::new_request(client, "GET", target)!;
+
+ http::header_add(&req.header, "Upgrade", "websocket")?;
+ http::header_add(&req.header, "Connection", "Upgrade")?;
+ http::header_add(&req.header, "Sec-WebSocket-Version", "13")?;
+
+ const key: [16]u8 = [0...];
+ random::buffer(&key);
+ const key = base64::encodestr(&base64::std_encoding, key)?;
+ defer free(key);
+ http::header_add(&req.header, "Sec-WebSocket-Key", key)?;
+
+ return req;
+};
+
+// Build an HTTP handshake [[net::http::response]] for a [[net::http::request]].
+export fn handshake_response(req: http::request) (http::response | nomem) = {
+ const resp = http::response {
+ body = io::empty,
+ ...
+ };
+
+ const key = http::header_get(&req.header, "Sec-WebSocket-Key");
+ const proof = proof(key)?;
+ defer free(proof);
+ http::header_set(
+ &resp.header,
+ "Sec-WebSocket-Accept",
+ proof,
+ )?;
+ http::header_set(&resp.header, "Connection", "Upgrade")?;
+ http::header_set(&resp.header, "Upgrade", "websocket")?;
+
+ return resp;
+};
+
+// Build the "Sec-WebSocket-Accept" response proof for a "Sec-WebSocket-Key"
+// request key.
+export fn proof(key: str) (str | nomem) = {
+ const hash = sha1::sha1();
+ io::write(&hash, strings::toutf8(key))!;
+ io::write(&hash, strings::toutf8(MAGIC))!;
+ const key: [sha1::SZ]u8 = [0...];
+ hash::sum(&hash, &key);
+ return base64::encodestr(&base64::std_encoding, key)?;
+};
+
+// Formats a [[frame]] and write it to the given [[io::handle]].
+export fn write_websocket(
+ h: io::handle,
+ f: frame,
+ mask: bool,
+ fin: bool = true,
+) (size | io::error) = {
+ let first = true;
+ let work = 0: u8;
+ let remains = f.msg;
+ let doing: []u8 = [];
+
+ for (true) {
+ defer first = false;
+ doing = remains;
+ if (len(doing) > FRAME_MAX) {
+ remains = doing[FRAME_MAX..];
+ doing = doing[..FRAME_MAX];
+ } else {
+ remains = [];
+ };
+
+ work = 0;
+ if (!fin || len(remains) > 0) {
+ work = 0b00000000;
+ } else {
+ work = 0b10000000;
+ };
+ if (first) {
+ io::write(h, [work | f.op])?;
+ } else {
+ io::write(h, [work | OPCODE_CONT])?;
+ };
+
+ work = 0;
+ if (mask) work = 0b10000000;
+ if (len(doing) <= 125) { // u7
+ io::write(h, [work | len(doing): u8])?;
+ } else if (len(doing) <= 65535) { // u16
+ io::write(h, [ // Extended /16
+ work | 126: u8,
+ (len(doing): u16 >> 8): u8,
+ (len(doing): u16 & 0x00FF): u8,
+ ])?;
+ } else { // u64
+ io::write(h, [ // Extended /64
+ work | 127: u8,
+ (len(doing): u64 >> 8*7): u8,
+ (len(doing): u64 >> 8*6 & 0xFF): u8,
+ (len(doing): u64 >> 8*5 & 0xFF): u8,
+ (len(doing): u64 >> 8*4 & 0xFF): u8,
+ (len(doing): u64 >> 8*3 & 0xFF): u8,
+ (len(doing): u64 >> 8*2 & 0xFF): u8,
+ (len(doing): u64 >> 8*1 & 0xFF): u8,
+ (len(doing): u64 >> 8*0 & 0xFF): u8,
+ ])?;
+ };
+
+ if (!mask) {
+ io::write(h, doing)?;
+ } else {
+ const key: [4]u8 = [0...];
+ random::buffer(&key);
+ io::write(h, key)?;
+
+ for (let i = 0z; i < len(doing); i += 1) {
+ io::write(h, [doing[i] ^ key[i % 4]])?;
+ };
+ };
+
+ if (len(remains) == 0) break;
+ };
+
+ return len(f.msg);
+};
+
+// Read a [[frame]] from an [[io::handle]].
+export fn read_websocket(
+ h: io::handle,
+ f: *frame,
+ mask: bool,
+) (size | io::EOF | io::error) = {
+ const scan = bufio::newscanner(h);
+ defer bufio::finish(&scan);
+
+ let work: [1]u8 = [0];
+ let first = true;
+ let last = false;
+ let s = 0z;
+ let paylen = 0z;
+
+ for (!last) {
+ defer first = false;
+ if (io::readall(&scan, &work)? is io::EOF)
+ return io::EOF;
+ if (work[0] & 0b10000000 != 0)
+ last = true;
+ if (work[0] & 0b01110000 != 0) // TODO: extensions
+ return errors::unsupported;
+
+ if (first) {
+ f.op = (work[0] & 0b00001111): opcode;
+ } else if (work[0] & 0b00001111 != OPCODE_CONT) {
+ return errors::invalid;
+ };
+
+ if (io::readall(&scan, &work)? is io::EOF)
+ return io::EOF;
+ if (work[0] & 0b10000000 != 0) {
+ if (!mask) return errors::invalid;
+ } else {
+ if (mask) return errors::invalid;
+ };
+ switch (work[0] & 0b01111111) {
+ case 126 =>
+ paylen = 0z;
+ if (io::readall(&scan, &work)? is io::EOF)
+ return io::EOF;
+ paylen += work[0]: u16 << 8;
+ if (io::readall(&scan, &work)? is io::EOF)
+ return io::EOF;
+ paylen += work[0];
+ case 127 =>
+ paylen = 0z;
+ for (let i = 7u8; i >= 0; i -= 1) {
+ if (io::readall(&scan, &work)? is io::EOF)
+ return io::EOF;
+ paylen += work[0]: u64 << 8 * i;
+ };
+ case =>
+ paylen = work[0] & 0b01111111;
+ };
+ defer s += paylen;
+
+ if (paylen == 0) {
+ continue;
+ };
+
+ append(f.msg, [0...], paylen)?;
+
+ if (!mask) {
+ if (io::readall(&scan, f.msg[s..s+paylen])? is io::EOF)
+ return io::EOF;
+ } else {
+ const key: [4]u8 = [0...];
+ if (io::readall(&scan, key)? is io::EOF)
+ return io::EOF;
+
+ if (io::readall(&scan, f.msg[s..s+paylen])? is io::EOF)
+ return io::EOF;
+ for (let i = 0z; i < paylen; i += 1) {
+ f.msg[s+i] = f.msg[s+i] ^ key[i % 4];
+ };
+ };
+
+ };
+
+ return s;
+};