diff --git a/cmd/httpd/main.ha b/cmd/httpd/main.ha new file mode 100644 index 0000000..c6533fb --- /dev/null +++ b/cmd/httpd/main.ha @@ -0,0 +1,91 @@ +use getopt; +use net; +use net::ip; +use net::http; +use net::dial; +use os; +use memio; +use io; +use fmt; +use bufio; +use strings; + +const usage: [_]getopt::help = [ + "HTTP server", + ('a', "address", "listened address (ex: 127.0.0.1:8080)") +]; + +export fn main() void = { + const cmd = getopt::parse(os::args, usage...); + defer getopt::finish(&cmd); + + let port: u16 = 8080; + let ip_addr: ip::addr4 = [127, 0, 0, 1]; + + for (let i = 0z; i < len(cmd.opts); i += 1) { + const opt = cmd.opts[i]; + switch (opt.0) { + case 'a' => + match (dial::splitaddr(opt.1, "")) { + case let value: (str, u16) => + ip_addr = ip::parsev4(value.0)!; + port = value.1; + case dial::invalid_address => + abort("Invalid address"); + }; + case => abort(); // unreachable + }; + }; + + const server = match (http::listen(ip_addr, port)) { + case net::error => abort("failure while listening"); + case let this: *http::server => yield this; + }; + defer http::server_finish(server); + + for (true) { + const serv_req = match (http::serve(server)) { + case let this: *http::server_request => + yield this; + case net::error => abort("failure while serving"); + }; + defer http::serve_finish(serv_req); + + let buf = memio::dynamic(); + defer io::close(&buf)!; + handlereq(&buf, &serv_req.request); + + http::response_write( + serv_req.socket, + http::STATUS_OK, + &buf, + ("Content-Type", "text/plain") + )!; + }; +}; + +export fn handlereq(buf: *io::stream, request: *http::request) void = { + fmt::fprintfln(buf, "Method: {}", request.method)!; + fmt::fprintfln(buf, "Path: {}", request.target.path)!; + fmt::fprintfln(buf, "Fragment: {}", request.target.fragment)!; + fmt::fprintfln(buf, "Query: {}", request.target.query)!; + fmt::fprintfln(buf, "Headers: < void; + case let body: io::handle => + fmt::fprintfln(buf, "Body: < + fmt::fprintln(buf, strings::fromutf8(line)!)!; + break; + case io::EOF => + break; + }; + }; + fmt::fprintfln(buf, "EOF")!; + }; +}; diff --git a/net/http/header.ha b/net/http/header.ha index 431be64..c615484 100644 --- a/net/http/header.ha +++ b/net/http/header.ha @@ -77,7 +77,7 @@ export fn write_header(sink: io::handle, head: *header) (size | io::error) = { return z; }; -fn read_header(head: *header, scan: *bufio::scanner) (void | error) = { +fn read_header(head: *header, scan: *bufio::scanner) (void | io::error | protoerr) = { for (true) { const item = match (bufio::scan_string(scan, "\r\n")) { case let line: const str => diff --git a/net/http/request.ha b/net/http/request.ha index 11747fa..45cdf6b 100644 --- a/net/http/request.ha +++ b/net/http/request.ha @@ -5,6 +5,10 @@ use net::ip; use net::uri; use strconv; use strings; +use bufio; +use memio; +use encoding::utf8; +use types; // Stores state related to an HTTP request. // @@ -113,3 +117,161 @@ export fn new_request_body( 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, +}; + +export fn request_parse(file: io::handle) (request | protoerr | io::error) = { + 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 = []; + 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; + case let path: str => + const uri = fmt::asprintf("http:{}", path); + defer free(uri); + yield alloc(uri::parse(uri)!); + }; + + const length: (void | size) = void; + const head_length = header_get(&header, "Content-Length"); + if ("" != head_length) { + match (strconv::stoz(head_length)) { + case let s: size => length = s; + case strconv::invalid => return protoerr; + case strconv::overflow => return protoerr; + }; + }; + + let body: (io::handle | void) = void; + if (length is size) { + const limit = io::limitreader(&scan, length as size); + let _body = alloc(memio::dynamic()); + io::copy(_body, &limit)!; + io::seek(_body, 0, io::whence::SET)!; + body = _body; + }; + + return request { + method = req_line.method, + target = target, + header = header, + body = body, + ... + }; +}; + +export fn parsed_request_finish(request: *request) void = { + uri::finish(request.target); + free(request.target); + + match (request.body) { + case void => yield; + case let body: io::handle => + io::close(body)!; + free(body: *memio::stream); + }; + + header_free(&request.header); +}; + +export fn request_line_parse(scan: *bufio::scanner) (request_line | protoerr | io::error) = { + 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 => + return protoerr; + case 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 void => + 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 void => + return protoerr; + }; + + const version = match (strings::next_token(&tok)) { + case let ver: str => + yield ver; + case void => + 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), + }; +}; + +export fn request_line_finish(line: *request_line) void = { + match (line.uri) { + case let path: str => free(path); + case => yield; + }; +}; diff --git a/net/http/response.ha b/net/http/response.ha index 36c0087..84f0c81 100644 --- a/net/http/response.ha +++ b/net/http/response.ha @@ -1,10 +1,14 @@ use io; use os; +use fmt; +use strconv; + +export type version = (uint, uint); // Stores state related to an HTTP response. export type response = struct { // HTTP protocol version (major, minor) - version: (uint, uint), + version: version, // The HTTP status for this request as an integer. status: uint, // The HTTP status reason phrase. @@ -23,3 +27,37 @@ export fn response_finish(resp: *response) void = { free(resp.reason); free(resp.body); }; + +export fn response_write( + rw: io::handle, + status: uint, + body: (void | io::handle), + header: (str, str)... +) (void | io::error) = { + fmt::fprintfln(rw, "HTTP/1.1 {} {}", status, status_reason(status))?; + + let header = header_dup(&header); + defer header_free(&header); + + match (body) { + case void => void; + case let body: io::handle => + match (io::tell(body)) { + case io::error => void; + case let off: io::off => + header_add(&header, "Content-Length", strconv::i64tos(off)); + io::seek(body, 0, io::whence::SET)!; + body = &io::limitreader(body, off: size); + }; + }; + + write_header(rw, &header)?; + + fmt::fprintln(rw)!; + + match (body) { + case void => void; + case let body: io::handle => + io::copy(rw, body)!; + }; +}; diff --git a/net/http/server.ha b/net/http/server.ha index e69de29..44e76ac 100644 --- a/net/http/server.ha +++ b/net/http/server.ha @@ -0,0 +1,43 @@ +use net; +use net::ip; +use net::tcp; + +export type server = struct { + socket: net::socket, +}; + +export type server_request = struct { + socket: net::socket, + request: request, +}; + +export fn listen(ip: ip::addr, port: u16) (*server | net::error) = { + return alloc(server { + socket = tcp::listen(ip, port)?, + }); +}; + +export fn server_finish(server: *server) void = { + net::close(server.socket)!; + free(server); +}; + +export fn accept(server: *server) (net::socket | net::error) = { + return net::accept(server.socket)?; +}; + +export fn serve(server: *server) (*server_request | net::error) = { + const socket = accept(server)?; + const request = request_parse(socket)!; + + return alloc(server_request { + request = request, + socket = socket, + }); +}; + +export fn serve_finish(serv_req: *server_request) void = { + parsed_request_finish(&serv_req.request); + net::close(serv_req.socket)!; + free(serv_req); +};