commit 94eeacc5a3d331d18f064fe93df5209168a3d444 from: Omar Polo date: Thu Dec 21 18:23:45 2023 UTC http: introduce a reswriter; change API accordingly since it uses a bufio::stream it is limited to 1024 bytes of response, an auto-resizing implementation will follow... commit - b28be21f0f8cb1d884d3831c67115e34ee16c793 commit + 94eeacc5a3d331d18f064fe93df5209168a3d444 blob - 7faeb6ca6a4b1e410be10ebe39e6c64bd47f88df blob + 6e5a7417185ee781c80bc0a32e61db0ffe0436f1 --- http/http.ha +++ http/http.ha @@ -61,16 +61,14 @@ export type request = struct { chunked: bool, }; -export type routefn = *fn(*client) (void | error); +export type routefn = *fn(*request, *reswriter) (void | error); -export type client = struct { +type client = struct { fp: io::file, in: bufio::scanner, first_line: bool, request: request, - chunked: bool, // whether to chunk the reply - hdrdone: bool, - done: bool, + response: reswriter, // routefn: (void | routefn), mux: *mux, }; @@ -115,6 +113,7 @@ fn acceptclt(f: io::file, ev: ev::event, data: nullabl // routefn = void, in = bufio::newscanner(sock, types::SIZE_MAX), first_line = true, + response = new_reswriter(sock), ... }); @@ -152,10 +151,10 @@ fn parse_req(c: *client) (bool | error | encoding::utf switch (version) { case "HTTP/1.0" => - c.request.version = version::HTTP_1_1; - case "HTTP/1.1" => c.request.version = version::HTTP_1_0; - c.chunked = true; + case "HTTP/1.1" => + c.request.version = version::HTTP_1_1; + c.response.chunked = true; case => return badrequest; }; @@ -179,6 +178,10 @@ fn delclient(c: *client) void = { case => yield; }; + match (io::close(&c.response)) { + case => yield; + }; + free(c); }; @@ -191,8 +194,10 @@ fn handleclt(f: io::file, ev: ev::event, data: nullabl delclient(clt); return; case => - if (clt.done) { + // XXX wait if there is still data pending. + if (clt.response.done) { delclient(clt); + return; }; }; }; @@ -205,8 +210,9 @@ fn do_request(clt: *client) (void | error) = { yield; case errors::again => return; // try again later case badrequest => - route_badrequest(clt)?; - clt.done = true; + route_badrequest(&clt.request, &clt.response)?; + clt.response.done = true; + reswriter_flush(&clt.response)?; return; case let err: io::error => log::printfln("parse_req: {}", io::strerror(err)); @@ -220,59 +226,27 @@ fn do_request(clt: *client) (void | error) = { for (let i = 0z; i < len(clt.mux); i += 1) { if (fnmatch::fnmatch(clt.mux[i].pattern, path)) { found = true; - clt.mux[i].routefn(clt)?; + clt.mux[i].routefn(&clt.request, &clt.response)?; break; }; }; if (!found) { - route_notfound(clt)?; - }; - - write(clt, "")?; // flush - - clt.done = true; -}; - -export fn reply(clt: *client, code: int, status: str) (void | error) = { - log::printfln("> {} {}", code, status); - - let version = if (clt.request.version == version::HTTP_1_0) - "HTTP/1.0" else "HTTP/1.1"; - - fmt::fprintf(clt.fp, "{} {} {}\r\n", version, code, status)?; -}; - -export fn header(clt: *client, header: str, value: str) (void | error) = { - fmt::fprintf(clt.fp, "{}: {}\r\n", header, value)?; -}; - -export fn write(clt: *client, body: str) (void | error) = { - if (!clt.hdrdone) { - if (clt.chunked) { - header(clt, "Transfer-Encoding", "chunked")?; - }; - header(clt, "Connection", "closed")?; - - clt.hdrdone = true; - fmt::fprint(clt.fp, "\r\n")?; + route_notfound(&clt.request, &clt.response)?; }; - if (clt.chunked) { - fmt::fprintf(clt.fp, "{:x}\r\n{}\r\n", len(body), body)?; - } else { - fmt::fprintf(clt.fp, "{}", body)?; - }; + clt.response.done = true; + reswriter_flush(&clt.response)?; }; -fn route_notfound(clt: *client) (void | error) = { - reply(clt, 404, "Not Found")?; - header(clt, "Content-Type", "text/plain")?; - write(clt, "Page not found.\n")?; +fn route_notfound(req: *request, res: *reswriter) (void | error) = { + reply(req, res, 404, "Not Found")?; + header(res, "Content-Type", "text/plain")?; + fmt::fprintln(res, "Page not found.")?; }; -fn route_badrequest(clt: *client) (void | error) = { - reply(clt, 400, "Bad Request")?; - header(clt, "Content-Type", "text/plain")?; - write(clt, "Bad Request")?; +fn route_badrequest(req: *request, res: *reswriter) (void | error) = { + reply(req, res, 400, "Bad Request")?; + header(res, "Content-Type", "text/plain")?; + fmt::fprintln(res, "Bad Request")?; }; blob - /dev/null blob + cd59096f2c893f258fd125e72bc37ffc808c2dc7 (mode 644) --- /dev/null +++ http/reswriter.ha @@ -0,0 +1,120 @@ +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +use bufio; +use fmt; +use io; +use log; + +export type reswriter = struct { + vtable: io::stream, + dst: bufio::stream, + hdrdone: bool, // done with the headers + done: bool, // done with the response + wrotedone: bool, // written the done chunk + chunked: bool, // chunk the reply + + // XXX + buf: []u8, +}; + +const reswriter_vtable: io::vtable = io::vtable { + writer = &reswriter_write, + closer = &reswriter_close, + ... +}; + +fn new_reswriter(s: io::handle) reswriter = { + let buf: []u8 = alloc([0...], 1024); + + // XXX using a bufio::stream means we can't append more than + // len(buf) data... + let w = reswriter { + vtable = &reswriter_vtable, + dst = bufio::init(s, [], buf), + buf = buf, + ... + }; + + bufio::setflush(&w.dst, []); + + return w; +}; + +fn reswriter_write(s: *io::stream, buf: const []u8) (size | io::error) = { + let res = s: *reswriter; + + if (!res.hdrdone) { + if (res.chunked) { + header(res, "Transfer-Encoding", "chunked")?; + }; + header(res, "Connection", "closed")?; + + fmt::fprint(&res.dst, "\r\n")?; + res.hdrdone = true; + }; + + if (res.chunked) { + let ret = 0z; + + ret += fmt::fprintf(&res.dst, "{:x}\r\n", len(buf))?; + ret += io::write(&res.dst, buf)?; + ret += fmt::fprint(&res.dst, "\r\n")?; + return ret; + } else { + return io::write(&res.dst, buf)?; + }; +}; + +fn reswriter_flush(res: *reswriter) (void | io::error) = { + if (res.done && !res.wrotedone) { + fmt::fprint(&res.dst, "0\r\n\r\n")?; + res.wrotedone = true; + }; + bufio::flush(&res.dst)?; +}; + +fn reswriter_close(s: *io::stream) (void | io::error) = { + let w = s: *reswriter; + + io::close(&w.dst)?; + free(w.buf); +}; + +export fn reply( + req: *request, + res: *reswriter, + code: int, + status: str +) (void | io::error) = { + log::printfln("> {} {}", code, status); + + let version = if (req.version == version::HTTP_1_0) + "HTTP/1.0" else "HTTP/1.1"; + + fmt::fprintf(&res.dst, "{} {} {}\r\n", version, code, status)?; +}; + +export fn header(res: *reswriter, header: str, value: str) (void | io::error) = { + fmt::fprintf(&res.dst, "{}: {}\r\n", header, value)?; +};