Implement server-side HTTP support
Co-authored-by: Drew DeVault <sir@cmpwn.com> Signed-off-by: Willow Barraco <contact@willowbarraco.fr> Signed-off-by: Drew DeVault <sir@cmpwn.com>
This commit is contained in:
parent
cfdb921520
commit
1d5c710f55
91
cmd/httpd/main.ha
Normal file
91
cmd/httpd/main.ha
Normal file
|
@ -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: <<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")!;
|
||||
};
|
||||
};
|
|
@ -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 =>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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)!;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
Loading…
Reference in a new issue