http/src/main.nika

// The `http` package: a small HTTP/1.1 server, and the types a handler sees.
//
// It is a library with no `main`. The program that uses it is
// `examples/hello-http`:
//
//     cd examples/hello-http
//     nikaia run
//     http: listening on 127.0.0.1:8080
//
// Shows: a package written in Nikaia and reached by a path, a function passed
// as a value, options with defaults after the `;`, and `catch`.
//
// It speaks `GET` and `POST`, reads a body by `Content-Length`, and closes each
// connection after one answer. There is no chunked transfer and no TLS. The
// socket is `std::net` and the request parsing is `std::http1`; this package
// decides what a request means. A program hands it one handler function, not
// a route table: the handler looks at the path and decides.

use std::net
use std::http1
use std::io
use std::fs

// Only two methods. `std::http1` answers `400` to any other method before a
// handler runs, so a `match` over this covers every request a handler can get.
pub enum Method {
    Get,
    Post,
}

// What a handler is given. The accessors return views into the bytes read from
// the connection, so nothing here allocates. A handler that wants to keep a
// value after it returns has to copy it.
pub struct Request {
    // The head as `std::http1` parsed it. It is `pub` so a handler can ask it
    // for things the accessors below do not cover, such as the header count.
    pub head: http1::Head,
    // Filled by `listen`. A handler reads it through `body()`; the field is
    // `pub` for a program that wants to take the bytes and own them.
    pub body: String,
}

impl Request {
    pub fn method(ref self) -> Method {
        if self.head.method() == "POST" {
            return Method::Post
        }
        return Method::Get
    }

    // The method as the client wrote it, for a log.
    pub fn method_text(ref self) -> String {
        return self.head.method()
    }

    // The path without the query string. It is not decoded: a `%20` stays
    // `%20`.
    pub fn path(ref self) -> String {
        return self.head.path()
    }

    // The whole request target as the client wrote it, for a log.
    pub fn target(ref self) -> String {
        return self.head.target()
    }

    // What the client sent after the head, empty if it sent nothing. Treat it
    // as untrusted: it came off a socket.
    pub fn body(ref self) -> String {
        return self.body
    }

    // One query parameter, or nothing if the query does not have it. The `?`
    // in the return type is what allows "nothing". Not decoded either.
    pub fn query(ref self, name: ref String) -> String? {
        return self.head.query(name)
    }

    // One header, or nothing if the client did not send it. The name is
    // matched case-insensitively, as HTTP requires.
    pub fn header(ref self, name: ref String) -> String? {
        return self.head.header(name)
    }
}

// What a handler gives back: a status, a content type and a body.
pub struct Response {
    pub status: i32,
    pub content_type: String,
    pub body: Body,
}

// What a response carries (ADR-289 D18, D22, D23): text the program made,
// bytes it holds, or a file it never read.
pub enum Body {
    Text(String),
    Bytes(Bytes),
    File(File),
}

// **A file as a response body, which the program never reads** (ADR-289 D23,
// D27, D28). The file is looked at when this is made: its size becomes the
// `content-length`, a file that is not there is a `500`, and a name that
// leaves the root is a `404`. Nothing is read until the answer is sent.
pub struct File {
    pub path: String,
    pub root: fs::Root,
    // `200`, or the status the file already decided: `404` or `500`.
    pub status: i32,
    pub size: i64,
}

impl File {
    pub fn(path: String, root: fs::Root) -> File {
        let mut status = 200
        let mut size = 0
        let known = fs::metadata(path, root) catch {
            status = match error {
                io::IoError::Outside(_) => 404
                else => 500
            }
            return File { path: path.clone(), root: root, status: status, size: 0 }
        }
        if !known.is_file() {
            status = 404
        } else {
            size = known.len()
        }
        return File { path: path.clone(), root: root, status: status, size: size }
    }

    // What the name's extension says the file is, and what is not named
    // is bytes (ADR-289 D23).
    pub fn content_type(ref self) -> String {
        let name = self.path.to_lowercase()
        if name.ends_with(".html") || name.ends_with(".htm") {
            return "text/html; charset=utf-8"
        } else if name.ends_with(".txt") || name.ends_with(".nika") || name.ends_with(".toml") {
            return "text/plain; charset=utf-8"
        } else if name.ends_with(".css") {
            return "text/css; charset=utf-8"
        } else if name.ends_with(".js") {
            return "text/javascript; charset=utf-8"
        } else if name.ends_with(".json") {
            return "application/json"
        } else if name.ends_with(".svg") {
            return "image/svg+xml"
        } else if name.ends_with(".png") {
            return "image/png"
        } else if name.ends_with(".jpg") || name.ends_with(".jpeg") {
            return "image/jpeg"
        }
        return "application/octet-stream"
    }
}

impl Response {
    // A `200` with a plain-text body.
    pub fn text(body: String) -> Response {
        return Response { status: 200, content_type: "text/plain", body: Body::Text(body) }
    }

    // A `200` with bytes the program holds (ADR-289 D22).
    pub fn bytes(body: Bytes) -> Response {
        return Response { status: 200, content_type: "application/octet-stream", body: Body::Bytes(body) }
    }

    // A file, answered as `File` decided: `200` with its type, or the status
    // it already knows (ADR-289 D23, D27, D28).
    pub fn file(file: File) -> Response {
        if file.status != 200 {
            let status = file.status
            return Response { status: status, content_type: "text/plain", body: Body::Text(status_line(status)) }
        }
        let content_type = file.content_type()
        return Response { status: 200, content_type: content_type, body: Body::File(file) }
    }

    // A `404`, for a path the handler does not answer.
    pub fn not_found() -> Response {
        return Response { status: 404, content_type: "text/plain", body: Body::Text("not found") }
    }
}

// How long the body is, as `content-length` says it.
fn length_of(body: ref Body) -> i64 {
    match body {
        Body::Text(text) => text.len()
        Body::Bytes(bytes) => bytes.len()
        Body::File(file) => file.size
    }
}

// The status line and the headers, as the text that goes on the wire before
// the body.
pub fn render_head(response: ref Response) -> String {
    let status = status_line(response.status)
    let length = length_of(response.body)
    return f"HTTP/1.1 {status}\r\ncontent-type: {response.content_type}\r\ncontent-length: {length}\r\n\r\n"
}

// The head and a text body, as the text that goes on the wire. A body of
// bytes or a file is written after the head by `send`.
pub fn render(response: ref Response) -> String {
    let head = render_head(response)
    match response.body {
        Body::Text(text) => f"{head}{text}"
        else => head
    }
}

// The answer, written to the connection. What the client sees was settled by
// the head: a file that fails to read after it closes the connection short,
// and the failure is logged (ADR-289 D27).
fn send(mut connection: net::Connection, response: ref Response) throws {
    connection.write(render_head(response))
    match response.body {
        Body::Text(text) => connection.write(text)
        Body::Bytes(bytes) => connection.write(bytes)
        Body::File(file) => {
            let read = fs::read(file.path, file.root) catch {
                println(f"http: {file.path} could not be read after its head was sent: {error}")
                return
            }
            connection.write(read)
        }
    }
}

// The reason phrase for each status this package sends.
pub fn status_line(status: i32) -> String {
    if status == 200 {
        return "200 OK"
    } else if status == 404 {
        return "404 Not Found"
    } else if status == 400 {
        return "400 Bad Request"
    } else if status == 408 {
        return "408 Request Timeout"
    } else if status == 413 {
        return "413 Content Too Large"
    } else if status == 431 {
        return "431 Request Header Fields Too Large"
    } else {
        return "500 Internal Server Error"
    }
}

// The server. It takes one connection at a time: read the request, run the
// handler, send the answer, close. A slow client holds up everyone else, and
// `head_wait` limits how long. The limits are options after the `;`, so a
// program names only the ones it changes.
//
// Nothing here waits in a special way. `accept` and `read` are plain calls
// with no `async` or `await`.
pub fn listen(
    at: String,
    handler: fn(Request) -> Response;
    body_cap: i64 = 1048576,
    head_cap: i64 = 16384,
    head_wait: i64 = 10,
    connections: i64 = 0
) throws {
    let listener = net::listen(at)
    let bound = listener.address()
    println(f"http: listening on {bound}")

    let mut served = 0
    // `connections: 0` means serve forever. Any other number stops after that
    // many connections.
    while connections == 0 || served < connections {
        let mut connection = listener.accept()
        answer(connection, handler; body_cap: body_cap, head_cap: head_cap, head_wait: head_wait)
        served = served + 1
    }
}

// One connection, from its first byte to its close.
//
// A bad request is answered with a status instead of thrown, because it is
// one client's problem and must not stop the server. `catch` is
// where that happens; inside it, `error` is what was thrown.
//
// `mut connection` in the signature lends the connection for changing. The
// call site writes nothing extra.
fn answer(
    mut connection: net::Connection,
    handler: fn(Request) -> Response;
    body_cap: i64 = 1048576,
    head_cap: i64 = 16384,
    head_wait: i64 = 10
) {
    let mut buffer = http1::Buffer()

    // Read until the head ends. `head_end` returns `0` for "not yet" and throws
    // once the head is over `head_cap`. `head_wait` is how many reads a client
    // gets, so one that sends a byte at a time cannot hold the server forever.
    let mut ended = 0
    let mut reads = 0
    while ended == 0 && reads < head_wait {
        let more = connection.read() catch { return }
        if more.len() == 0 {
            // The client went away. There is nobody to answer.
            return
        }
        buffer.take(more)
        ended = buffer.head_end(head_cap) catch {
            refuse(connection, 431, f"{error}")
            return
        }
        reads = reads + 1
    }
    if ended == 0 {
        refuse(connection, 408, "408 a request head that never ended")
        return
    }

    let asked = buffer.head() catch {
        refuse(connection, 400, f"{error}")
        return
    }

    // The body limit is checked against the length the head announced, before
    // any of the body is read. Refusing is cheap only at this point.
    if asked.length() > body_cap {
        refuse(connection, 413, "413 a body over the cap")
        return
    }

    // Read the rest of the body. Some of it may have arrived with the head,
    // so this counts from where the head ends.
    while buffer.len() - asked.size() < asked.length() {
        let more = connection.read() catch { return }
        if more.len() == 0 { return }
        buffer.take(more)
    }
    let body = buffer.text_from(asked.size()) catch {
        refuse(connection, 400, f"{error}")
        return
    }

    let answered = handler(Request { head: asked, body: body })
    send(connection, answered) catch { return }
}

// Answers a request the server refuses. The text `http1` throws already starts
// with the status code, so it is passed on as the body.
fn refuse(mut connection: net::Connection, status: i32, why: String) {
    let answered = Response { status: status, content_type: "text/plain", body: Body::Text(why.clone()) }
    send(connection, answered) catch { return }
}

The file itself · all the examples

This site is open source. Improve this page.