279 lines
6.6 KiB
Hare
279 lines
6.6 KiB
Hare
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;
|
|
};
|
|
};
|