use errors; use fmt; use io; 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. // // 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 void if there is no body. body: (io::handle | void), }; // Frees state associated with an HTTP [[request]]. export fn request_finish(req: *request) void = { header_free(&req.header); uri::finish(req.target); 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) = { let req = request { method = method, target = alloc(uri::dup(target)), header = header_dup(&client.default_header), transport = null, body = void, }; 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 => 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) = { 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, }; 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 host = header_get(&header, "Host"); const uri = fmt::asprintf("http://{}{}", host, 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 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), }; }; export fn request_line_finish(line: *request_line) void = { match (line.uri) { case let path: str => free(path); case => yield; }; };