commit b5b8e9b5efe9a97a565a17c725cea3bd1b6d4d96
parent d8338ac1476a480f35c19e5560292e39f84089bb
Author: thing1 <thing1@seacrossedlovers.xyz>
Date: Tue, 20 Jan 2026 22:46:17 +0000
fixed things up
Diffstat:
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;
-};