betterchess

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit b5b8e9b5efe9a97a565a17c725cea3bd1b6d4d96
parent d8338ac1476a480f35c19e5560292e39f84089bb
Author: thing1 <thing1@seacrossedlovers.xyz>
Date:   Tue, 20 Jan 2026 22:46:17 +0000

fixed things up

Diffstat:
Mchess/chess.ha | 38++++++++++++++++++++++++++------------
Achess/moves.ha | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/betterchess/main.ha | 35+----------------------------------
Dnet/http/README | 17-----------------
Dnet/http/client.ha | 102-------------------------------------------------------------------------------
Dnet/http/constants.ha | 112-------------------------------------------------------------------------------
Dnet/http/do.ha | 148-------------------------------------------------------------------------------
Dnet/http/error.ha | 27---------------------------
Dnet/http/header.ha | 108-------------------------------------------------------------------------------
Dnet/http/request.ha | 332-------------------------------------------------------------------------------
Dnet/http/response.ha | 176-------------------------------------------------------------------------------
Dnet/http/server.ha | 283-------------------------------------------------------------------------------
Dnet/http/status.ha | 111-------------------------------------------------------------------------------
Dnet/http/transport.ha | 330-------------------------------------------------------------------------------
Dnet/websocket/websocket.ha | 358-------------------------------------------------------------------------------
15 files changed, 97 insertions(+), 2150 deletions(-)

diff --git a/chess/chess.ha b/chess/chess.ha @@ -3,6 +3,8 @@ use fmt; use ascii; export type invalidplace = !void; +export type invalidmove = !void; +export type empty = !void; export type ptype = enum rune { KING = 'k', @@ -14,12 +16,14 @@ export type ptype = enum rune { }; export type color = enum {BLACK = 'b', WHITE = 'w'}; +export type movefn = *fn(_: i64, _: i64, _: i64, _: i64) bool; export type piece = struct { x: size, y: size, ty: ptype, - team: color + team: color, + valid: movefn }; export type board = struct { @@ -36,34 +40,44 @@ export fn finish(b: *board) void = { free(b.pieces); }; -export fn mkpiece(b: *board, x: size, y: size, ty: ptype, team: color) (*piece | invalidplace) = { +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})!; + + let valid = switch(ty) { + case ptype::KING => yield &kingmove; + case ptype::QUEEN => yield &queenmove; + case ptype::BISHOP => yield &bishopmove; + case ptype::KNIGHT => yield &knightmove; + case ptype::ROOK => yield &rookmove; + case ptype::PAWN => yield &truemove; + }; + + append(b.pieces, piece{x = x, y = y, ty = ty, team = team, valid = valid})!; return &b.pieces[len(b.pieces) - 1]; }; -export fn getpiece(b: *board, x: size, y: size) (*piece | void | invalidplace) = { +export fn getpiece(b: *board, x: size, y: size) (*piece | !empty | !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; + return empty; +}; + +export fn movepiece(b: *board, x1: size, y1: size, x2: size, y2: size) (void | !empty | !invalidmove | !invalidplace) = { + let p = getpiece(b, x1, x2)?; + if (p.ty == ptype::PAWN) + if (!p.valid(x1: i64, y1: i64, x2: i64, y2: i64)) return invalidmove; }; 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)!; + fmt::fprintf(f, "{} {} {}\n", text, x, y)!; }; }; diff --git a/chess/moves.ha b/chess/moves.ha @@ -0,0 +1,70 @@ +use math; + +fn invalid(x2: i64, y2: i64) bool = + if (x2 < 0 || y2 < 0 || x2 >= 8 || y2 >= 8) true + else false; + +export fn kingmove(x1: i64, y1: i64, x2: i64, y2: i64) bool = + if (invalid(x2, y2)) false + else if (math::absi64(x2 - x1) > 1 || math::absi64(y2 - y1) > 1) false + else true; + +export fn queenmove(x1: i64, y1: i64, x2: i64, y2: i64) bool = + if (invalid(x2, y2)) false + else if (math::absi64(x2 - x1) == math::absi64(y2 - y1) || x1 == x2 || y1 == y2) true + else false; + +export fn bishopmove(x1: i64, y1: i64, x2: i64, y2: i64) bool = + if (invalid(x2, y2)) false + else if (math::absi64(x2 - x1) == math::absi64(y2 - y1)) true + else false; + +export fn rookmove(x1: i64, y1: i64, x2: i64, y2: i64) bool = + if (invalid(x2, y2)) false + else if (x1 == x2 || y1 == y2) true + else false; + +export fn knightmove(x1: i64, y1: i64, x2: i64, y2: i64) bool = + if (invalid(x2, y2)) false + else if (math::absi64(x2 - x1) == 1 && math::absi64(y2 - y1) == 2) true + else if (math::absi64(x2 - x1) == 2 && math::absi64(y2 - y1) == 1) true + else false; + +export fn pawnmove(x1: i64, y1: i64, x2: i64, y2: i64, b: board) bool = { + if (invalid(x2, y2)) return false + + let p = getpiece(b, x1, y1)!; + // white is on the 0 row + if (p.team == color::WHITE) { + if (y2 <= y1) return false; + }; + else { + if (y2 >= y1) return false; + }; + // TODO here + // +}; + + +export fn truemove(x1: i64, y1: i64, x2: i64, y2: i64) bool = !invalid(x2, y2); + +@test fn kingmove() void = assert(kingmove(4, 0, 4, 1) == true, "(4, 0) -> (4, 1) should be valid"); +@test fn kingmove() void = assert(kingmove(4, 0, 4, 2) == false, "(4, 0) -> (4, 2) should be invalid"); +@test fn kingmove() void = assert(kingmove(4, 0, 4, -1) == false, "(4, 0) -> (4, -1) should be invalid"); +@test fn kingmove() void = assert(kingmove(4, 0, 3, 0) == true, "(4, 0) -> (3, 0) should be valid"); + +@test fn queenmove() void = assert(queenmove(3, 2, 3, 7) == true, "(3, 2) -> (3, 7) should be valid"); +@test fn queenmove() void = assert(queenmove(2, 3, 7, 2) == false, "(2, 3) -> (7, 2) should be invalid"); +@test fn queenmove() void = assert(queenmove(3, 3, 6, 6) == true, "(3, 3) -> (6, 6) should be valid"); +@test fn queenmove() void = assert(queenmove(3, 3, 6, 5) == false, "(3, 3) -> (6, 5) should be invalid"); + +@test fn bishopmove() void = assert(bishopmove(3, 3, 6, 6) == true, "(3, 3) -> (6, 6) should be valid"); +@test fn bishopmove() void = assert(bishopmove(3, 3, 6, 2) == false, "(3, 3) -> (6, 2) should be invalid"); + +@test fn rookmove() void = assert(rookmove(3, 3, 6, 6) == false, "(3, 3) -> (6, 6) should be invalid"); +@test fn rookmove() void = assert(rookmove(3, 3, 3, 7) == true, "(3, 3) -> (3, 7) should be valid"); + +@test fn knightmove() void = assert(knightmove(0, 1, 2, 2) == true, "(0, 1) -> (2, 2) should be valid"); +@test fn knightmove() void = assert(knightmove(2, 2, 0, 1) == true, "(2, 2) -> (0, 1) should be valid"); +@test fn knightmove() void = assert(knightmove(2, 3, 0, 1) == false, "(2, 3) -> (0, 1) should be invalid"); +@test fn knightmove() void = assert(knightmove(2, 0, 0, 1) == true, "(2, 0) -> (0, 1) should be valid"); diff --git a/cmd/betterchess/main.ha b/cmd/betterchess/main.ha @@ -1,24 +1,10 @@ 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)) { @@ -30,24 +16,5 @@ export fn main() void = { 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); - }; + chess::print_board(b, os::stdout); }; diff --git a/net/http/README b/net/http/README @@ -1,17 +0,0 @@ -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 @@ -1,102 +0,0 @@ -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 @@ -1,112 +0,0 @@ -// 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 @@ -1,148 +0,0 @@ -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 @@ -1,27 +0,0 @@ -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 @@ -1,108 +0,0 @@ -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 @@ -1,332 +0,0 @@ -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 @@ -1,176 +0,0 @@ -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 @@ -1,283 +0,0 @@ -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 @@ -1,111 +0,0 @@ -// 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 @@ -1,330 +0,0 @@ -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 @@ -1,358 +0,0 @@ -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; -};