Compare commits
10 commits
dbeb079262
...
9237448725
Author | SHA1 | Date | |
---|---|---|---|
9237448725 | |||
1d5c710f55 | |||
cfdb921520 | |||
9d14e36566 | |||
a8da673321 | |||
2acf7fa873 | |||
380d174beb | |||
d798088e9e | |||
396d6caa76 | |||
159dd0d06f |
|
@ -1,23 +1,53 @@
|
||||||
|
use getopt;
|
||||||
use io;
|
use io;
|
||||||
use log;
|
use log;
|
||||||
use net::dial;
|
use net::dial;
|
||||||
use net::http;
|
use net::http;
|
||||||
use net::uri;
|
use net::uri;
|
||||||
use os;
|
use os;
|
||||||
|
use strings;
|
||||||
|
|
||||||
|
const usage: [_]getopt::help = [
|
||||||
|
"HTTP client",
|
||||||
|
('H', "Name:value", "Sets an HTTP header"),
|
||||||
|
('X', "method", "Sets the HTTP method verb"),
|
||||||
|
"url"
|
||||||
|
];
|
||||||
|
|
||||||
export fn main() void = {
|
export fn main() void = {
|
||||||
const client = http::newclient("Hare test client");
|
const client = http::newclient("Hare net::http test client");
|
||||||
defer http::client_finish(&client);
|
defer http::client_finish(&client);
|
||||||
|
|
||||||
const target = match (uri::parse(os::args[1])) {
|
const cmd = getopt::parse(os::args, usage...);
|
||||||
|
defer getopt::finish(&cmd);
|
||||||
|
|
||||||
|
if (len(cmd.args) != 1) {
|
||||||
|
getopt::printusage(os::stderr, "http", usage)!;
|
||||||
|
os::exit(os::status::FAILURE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const targ = match (uri::parse(cmd.args[0])) {
|
||||||
case let u: uri::uri =>
|
case let u: uri::uri =>
|
||||||
yield u;
|
yield u;
|
||||||
case uri::invalid =>
|
case uri::invalid =>
|
||||||
log::fatal("Invalid URI");
|
log::fatal("Invalid URI");
|
||||||
};
|
};
|
||||||
defer uri::finish(&target);
|
defer uri::finish(&targ);
|
||||||
|
|
||||||
const resp = match (http::get(&client, &target)) {
|
let req = http::new_request(&client, "GET", &targ)!;
|
||||||
|
for (let i = 0z; i < len(cmd.opts); i += 1) {
|
||||||
|
const (opt, val) = cmd.opts[i];
|
||||||
|
switch (opt) {
|
||||||
|
case 'H' =>
|
||||||
|
const (name, val) = strings::cut(val, ":");
|
||||||
|
http::header_add(&req.header, name, val);
|
||||||
|
case 'X' =>
|
||||||
|
req.method = val;
|
||||||
|
case => abort();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = match (http::do(&client, &req)) {
|
||||||
case let err: http::error =>
|
case let err: http::error =>
|
||||||
log::fatal("HTTP error:", http::strerror(err));
|
log::fatal("HTTP error:", http::strerror(err));
|
||||||
case let resp: http::response =>
|
case let resp: http::response =>
|
||||||
|
@ -34,6 +64,12 @@ export fn main() void = {
|
||||||
log::printfln("{}: {}", name, val);
|
log::printfln("{}: {}", name, val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = resp.body as *io::stream;
|
const body = match (resp.body) {
|
||||||
|
case let st: *io::stream =>
|
||||||
|
yield st;
|
||||||
|
case null =>
|
||||||
|
return;
|
||||||
|
};
|
||||||
io::copy(os::stdout, body)!;
|
io::copy(os::stdout, body)!;
|
||||||
|
io::close(body)!;
|
||||||
};
|
};
|
||||||
|
|
92
cmd/httpd/main.ha
Normal file
92
cmd/httpd/main.ha
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
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 let this: *http::server =>
|
||||||
|
yield this;
|
||||||
|
case net::error => abort("failure while listening");
|
||||||
|
};
|
||||||
|
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: <<EOF")!;
|
||||||
|
http::write_header(buf, &request.header)!;
|
||||||
|
fmt::fprintfln(buf, "EOF")!;
|
||||||
|
|
||||||
|
match (request.body) {
|
||||||
|
case void => void;
|
||||||
|
case let body: io::handle =>
|
||||||
|
fmt::fprintfln(buf, "Body: <<EOF")!;
|
||||||
|
for (true) {
|
||||||
|
match (bufio::read_line(body)!) {
|
||||||
|
case let line: []u8 =>
|
||||||
|
fmt::fprintln(buf, strings::fromutf8(line)!)!;
|
||||||
|
break;
|
||||||
|
case io::EOF =>
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
fmt::fprintfln(buf, "EOF")!;
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,10 +1,5 @@
|
||||||
use errors;
|
|
||||||
use fmt;
|
|
||||||
use io;
|
use io;
|
||||||
use net::ip;
|
|
||||||
use net::uri;
|
use net::uri;
|
||||||
use strconv;
|
|
||||||
use strings;
|
|
||||||
|
|
||||||
export type client = struct {
|
export type client = struct {
|
||||||
default_header: header,
|
default_header: header,
|
||||||
|
@ -44,76 +39,6 @@ export fn client_default_transport(client: *client) *transport = {
|
||||||
return &client.default_transport;
|
return &client.default_transport;
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
fn uri_origin_form(target: *uri::uri) uri::uri = {
|
fn uri_origin_form(target: *uri::uri) uri::uri = {
|
||||||
let target = *target;
|
let target = *target;
|
||||||
target.scheme = "";
|
target.scheme = "";
|
||||||
|
|
|
@ -8,6 +8,12 @@ export def POST: str = "POST";
|
||||||
export def PUT: str = "PUT";
|
export def PUT: str = "PUT";
|
||||||
// HTTP "DELETE" method.
|
// HTTP "DELETE" method.
|
||||||
export def DELETE: str = "DELETE";
|
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).
|
// HTTP "Continue" response status (100).
|
||||||
export def STATUS_CONTINUE: uint = 100;
|
export def STATUS_CONTINUE: uint = 100;
|
||||||
|
|
|
@ -14,6 +14,9 @@ use types;
|
||||||
// Performs an HTTP [[request]] with the given [[client]]. The request is
|
// Performs an HTTP [[request]] with the given [[client]]. The request is
|
||||||
// performed synchronously; this function blocks until the server has returned
|
// performed synchronously; this function blocks until the server has returned
|
||||||
// the response status and all HTTP headers associated with the response.
|
// 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) = {
|
export fn do(client: *client, req: *request) (response | error) = {
|
||||||
assert(req.target.scheme == "http"); // TODO: https
|
assert(req.target.scheme == "http"); // TODO: https
|
||||||
const conn = dial::dial_uri("tcp", req.target)?;
|
const conn = dial::dial_uri("tcp", req.target)?;
|
||||||
|
@ -53,10 +56,21 @@ export fn do(client: *client, req: *request) (response | error) = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let resp = response { ... };
|
let resp = response { ... };
|
||||||
const scan = bufio::newscanner_static(conn, buf);
|
const scan = bufio::newscanner(conn, 512);
|
||||||
read_statusline(&resp, &scan)?;
|
read_statusline(&resp, &scan)?;
|
||||||
read_header(&resp.header, &scan)?;
|
read_header(&resp.header, &scan)?;
|
||||||
resp.body = new_reader(conn, &resp, &scan)?;
|
|
||||||
|
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, &scan)?;
|
||||||
|
} else if (req.method != "CONNECT") {
|
||||||
|
io::close(conn)!;
|
||||||
|
};
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,35 +94,51 @@ fn read_statusline(
|
||||||
const version = match (strings::next_token(&tok)) {
|
const version = match (strings::next_token(&tok)) {
|
||||||
case let ver: str =>
|
case let ver: str =>
|
||||||
yield ver;
|
yield ver;
|
||||||
case void =>
|
case done =>
|
||||||
return protoerr;
|
return protoerr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const status = match (strings::next_token(&tok)) {
|
const status = match (strings::next_token(&tok)) {
|
||||||
case let status: str =>
|
case let status: str =>
|
||||||
yield status;
|
yield status;
|
||||||
case void =>
|
case done =>
|
||||||
return protoerr;
|
return protoerr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reason = match (strings::next_token(&tok)) {
|
const reason = match (strings::next_token(&tok)) {
|
||||||
case let reason: str =>
|
case let reason: str =>
|
||||||
yield reason;
|
yield reason;
|
||||||
case void =>
|
case done =>
|
||||||
return protoerr;
|
return protoerr;
|
||||||
};
|
};
|
||||||
|
|
||||||
const (_, version) = strings::cut(version, "/");
|
const (_, version) = strings::cut(version, "/");
|
||||||
const (major, minor) = strings::cut(version, ".");
|
const (major, minor) = strings::cut(version, ".");
|
||||||
resp.version = (
|
|
||||||
strconv::stou(major)!,
|
const major = match (strconv::stou(major)) {
|
||||||
strconv::stou(minor)!,
|
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) {
|
if (resp.version.0 > 1) {
|
||||||
return errors::unsupported;
|
return errors::unsupported;
|
||||||
};
|
};
|
||||||
|
|
||||||
resp.status = strconv::stou(status)!;
|
resp.status = match (strconv::stou(status)) {
|
||||||
|
case let u: uint =>
|
||||||
|
yield u;
|
||||||
|
case =>
|
||||||
|
return protoerr;
|
||||||
|
};
|
||||||
|
|
||||||
resp.reason = strings::dup(reason);
|
resp.reason = strings::dup(reason);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ export type header = [](str, str);
|
||||||
// Adds a given HTTP header, which may be added more than once. The name should
|
// Adds a given HTTP header, which may be added more than once. The name should
|
||||||
// be canonicalized by the caller.
|
// be canonicalized by the caller.
|
||||||
export fn header_add(head: *header, name: str, val: str) void = {
|
export fn header_add(head: *header, name: str, val: str) void = {
|
||||||
assert(len(name) > 1 && len(val) > 1);
|
assert(len(name) >= 1 && len(val) >= 1);
|
||||||
append(head, (strings::dup(name), strings::dup(val)));
|
append(head, (strings::dup(name), strings::dup(val)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ export fn write_header(sink: io::handle, head: *header) (size | io::error) = {
|
||||||
return z;
|
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) {
|
for (true) {
|
||||||
const item = match (bufio::scan_string(scan, "\r\n")) {
|
const item = match (bufio::scan_string(scan, "\r\n")) {
|
||||||
case let line: const str =>
|
case let line: const str =>
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
|
use errors;
|
||||||
|
use fmt;
|
||||||
use io;
|
use io;
|
||||||
|
use net::ip;
|
||||||
use net::uri;
|
use net::uri;
|
||||||
|
use strconv;
|
||||||
|
use strings;
|
||||||
|
use bufio;
|
||||||
|
use memio;
|
||||||
|
use encoding::utf8;
|
||||||
|
use types;
|
||||||
|
|
||||||
// Stores state related to an HTTP request.
|
// Stores state related to an HTTP request.
|
||||||
//
|
//
|
||||||
|
@ -33,3 +42,236 @@ export fn request_finish(req: *request) void = {
|
||||||
uri::finish(req.target);
|
uri::finish(req.target);
|
||||||
free(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 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 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
use io;
|
use io;
|
||||||
use os;
|
use os;
|
||||||
|
use fmt;
|
||||||
|
use strconv;
|
||||||
|
|
||||||
|
export type version = (uint, uint);
|
||||||
|
|
||||||
// Stores state related to an HTTP response.
|
// Stores state related to an HTTP response.
|
||||||
export type response = struct {
|
export type response = struct {
|
||||||
// HTTP protocol version (major, minor)
|
// HTTP protocol version (major, minor)
|
||||||
version: (uint, uint),
|
version: version,
|
||||||
// The HTTP status for this request as an integer.
|
// The HTTP status for this request as an integer.
|
||||||
status: uint,
|
status: uint,
|
||||||
// The HTTP status reason phrase.
|
// The HTTP status reason phrase.
|
||||||
|
@ -15,9 +19,45 @@ export type response = struct {
|
||||||
body: nullable *io::stream,
|
body: nullable *io::stream,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Frees state associated with an HTTP [[response]].
|
// Frees state associated with an HTTP [[response]]. If the response has a
|
||||||
|
// non-null body, the user must call [[io::close]] prior to calling this
|
||||||
|
// function.
|
||||||
export fn response_finish(resp: *response) void = {
|
export fn response_finish(resp: *response) void = {
|
||||||
header_free(&resp.header);
|
header_free(&resp.header);
|
||||||
free(resp.reason);
|
free(resp.reason);
|
||||||
free(resp.body);
|
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)!;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
|
@ -50,7 +50,7 @@ export type transport = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
fn new_reader(
|
fn new_reader(
|
||||||
conn: io::handle,
|
conn: io::file,
|
||||||
resp: *response,
|
resp: *response,
|
||||||
scan: *bufio::scanner,
|
scan: *bufio::scanner,
|
||||||
) (*io::stream | errors::unsupported | protoerr) = {
|
) (*io::stream | errors::unsupported | protoerr) = {
|
||||||
|
@ -68,8 +68,7 @@ fn new_reader(
|
||||||
return protoerr;
|
return protoerr;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const remain = bufio::scan_buffer(scan);
|
return new_identity_reader(conn, scan, length);
|
||||||
return new_identity_reader(conn, remain, length);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Figure out the semantics for closing the stream
|
// TODO: Figure out the semantics for closing the stream
|
||||||
|
@ -81,13 +80,8 @@ fn new_reader(
|
||||||
let stream: io::handle = conn;
|
let stream: io::handle = conn;
|
||||||
let buffer: []u8 = bufio::scan_buffer(scan);
|
let buffer: []u8 = bufio::scan_buffer(scan);
|
||||||
const iter = strings::tokenize(te, ",");
|
const iter = strings::tokenize(te, ",");
|
||||||
for (true) {
|
for (const tok => strings::next_token(&iter)) {
|
||||||
const te = match (strings::next_token(&iter)) {
|
const te = strings::trim(tok);
|
||||||
case let tok: str =>
|
|
||||||
yield strings::trim(tok);
|
|
||||||
case void =>
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
// XXX: We could add lzw support if someone added it to
|
// XXX: We could add lzw support if someone added it to
|
||||||
// hare-compress
|
// hare-compress
|
||||||
|
@ -115,33 +109,32 @@ fn new_reader(
|
||||||
|
|
||||||
type identity_reader = struct {
|
type identity_reader = struct {
|
||||||
vtable: io::stream,
|
vtable: io::stream,
|
||||||
conn: io::handle,
|
conn: io::file,
|
||||||
buffer: [os::BUFSZ]u8,
|
scan: *bufio::scanner,
|
||||||
pending: size,
|
src: io::limitstream,
|
||||||
length: size,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const identity_reader_vtable = io::vtable {
|
const identity_reader_vtable = io::vtable {
|
||||||
reader = &identity_read,
|
reader = &identity_read,
|
||||||
|
closer = &identity_close,
|
||||||
...
|
...
|
||||||
};
|
};
|
||||||
|
|
||||||
// Creates a new reader that reads data until the response's Content-Length is
|
// Creates a new reader that reads data until the response's Content-Length is
|
||||||
// reached; i.e. the null Transport-Encoding.
|
// reached; i.e. the null Transport-Encoding.
|
||||||
fn new_identity_reader(
|
fn new_identity_reader(
|
||||||
conn: io::handle,
|
conn: io::file,
|
||||||
buffer: []u8,
|
scan: *bufio::scanner,
|
||||||
content_length: size,
|
content_length: size,
|
||||||
) *io::stream = {
|
) *io::stream = {
|
||||||
let rd = alloc(identity_reader {
|
const scan = alloc(*scan);
|
||||||
|
return alloc(identity_reader {
|
||||||
vtable = &identity_reader_vtable,
|
vtable = &identity_reader_vtable,
|
||||||
conn = conn,
|
conn = conn,
|
||||||
length = content_length,
|
scan = scan,
|
||||||
|
src = io::limitreader(scan, content_length),
|
||||||
...
|
...
|
||||||
});
|
});
|
||||||
rd.buffer[..len(buffer)] = buffer[..];
|
|
||||||
rd.pending = len(buffer);
|
|
||||||
return rd;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn identity_read(
|
fn identity_read(
|
||||||
|
@ -150,34 +143,20 @@ fn identity_read(
|
||||||
) (size | io::EOF | io::error) = {
|
) (size | io::EOF | io::error) = {
|
||||||
let rd = s: *identity_reader;
|
let rd = s: *identity_reader;
|
||||||
assert(rd.vtable == &identity_reader_vtable);
|
assert(rd.vtable == &identity_reader_vtable);
|
||||||
|
return io::read(&rd.src, buf)?;
|
||||||
|
};
|
||||||
|
|
||||||
if (rd.length <= 0) {
|
fn identity_close(s: *io::stream) (void | io::error) = {
|
||||||
return io::EOF;
|
let rd = s: *identity_reader;
|
||||||
};
|
assert(rd.vtable == &identity_reader_vtable);
|
||||||
|
|
||||||
if (rd.pending == 0) {
|
// Flush the remainder of the response in case the caller did not read
|
||||||
let nread = rd.length;
|
// it out entirely
|
||||||
if (nread > len(rd.buffer)) {
|
io::copy(io::empty, &rd.src)?;
|
||||||
nread = len(rd.buffer);
|
|
||||||
};
|
|
||||||
|
|
||||||
match (io::read(rd.conn, rd.buffer[..nread])?) {
|
bufio::finish(rd.scan);
|
||||||
case let n: size =>
|
free(rd.scan);
|
||||||
rd.pending = n;
|
io::close(rd.conn)?;
|
||||||
case io::EOF =>
|
|
||||||
return io::EOF;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let n = len(buf);
|
|
||||||
if (n > rd.pending) {
|
|
||||||
n = rd.pending;
|
|
||||||
};
|
|
||||||
buf[..n] = rd.buffer[..n];
|
|
||||||
rd.buffer[..len(rd.buffer) - n] = rd.buffer[n..];
|
|
||||||
rd.pending -= n;
|
|
||||||
rd.length -= n;
|
|
||||||
return n;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type chunk_state = enum {
|
type chunk_state = enum {
|
||||||
|
@ -262,7 +241,7 @@ fn chunked_read(
|
||||||
return errors::invalid;
|
return errors::invalid;
|
||||||
};
|
};
|
||||||
|
|
||||||
match (strconv::stozb(ln, strconv::base::HEX)) {
|
match (strconv::stoz(ln, strconv::base::HEX)) {
|
||||||
case let z: size =>
|
case let z: size =>
|
||||||
rd.length = z;
|
rd.length = z;
|
||||||
case =>
|
case =>
|
||||||
|
@ -314,6 +293,4 @@ fn chunked_read(
|
||||||
rd.pending -= 2;
|
rd.pending -= 2;
|
||||||
rd.state = chunk_state::HEADER;
|
rd.state = chunk_state::HEADER;
|
||||||
};
|
};
|
||||||
|
|
||||||
abort(); // Unreachable
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue