Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

service.nct

see ./index.nct

see ./configuration.nct

see ./operational.nct

use std/http.{Handler, IncomingRequest, Limits, OutgoingResponse, Responder, ResponseHead, RouteDispatch, RouteMatch, Router, Server, ServerConnection, SessionCookie, Status}
use std/config.{Configuration, PublishedConfiguration}
use std/fs
use std/log.{Event, Field, Level}
use std/net
use std/net.{SocketAddress, TcpStream}
use std/session.SessionId
use std/string.String
use std/store.Store
use std/sync.Mutex
use std/task
use std/task.{TaskGroup, Timeout}
use std/time.{Duration, SystemTime}
use std/vec.Vec

const CONNECTION_LIMIT: usize = 2

const REQUEST_COUNT: usize = 7

const LARGE_BODY_BYTES: usize = 32768

const TRANSFER_BYTES: usize = 1024

struct ServiceState {
    configuration: PublishedConfiguration
    session_cookie: SessionCookie
    event_records: Mutex<Vec<Event>>
    durable_state: Mutex<Store>
}

construct ServiceState {
    func new(configuration: PublishedConfiguration, durable_state: Store): Self! {
        return ServiceState {
            configuration: move configuration,
            session_cookie: SessionCookie.new("session", "/", false)?,
            event_records: Mutex<Vec<Event>>.new(Vec<Event>.empty())?,
            durable_state: Mutex<Store>.new(move durable_state)?,
        }
    }
}

instance ServiceState {
    async method &self.timeout(): Duration! {
        let view = await self.configuration.current()
        let timeout = configured_timeout(&view as &Configuration)?
        return timeout
    }

    async method &self.response(): String! {
        let view = await self.configuration.current()
        let response = configured_response(&view as &Configuration)?
        return move response
    }

    async method &self.token_display(): String! {
        let view = await self.configuration.current()
        let display = configured_token_display(&view as &Configuration)?
        return move display
    }

    noalloc method &self.cookie(): &SessionCookie from self { return &self.session_cookie }

    noalloc method &self.event_store(): &Mutex<Vec<Event>> from self {
        return &self.event_records
    }

    noalloc method &self.store(): &Mutex<Store> from self { return &self.durable_state }
}

func byte_buffer(length: usize): Vec<u8> {
    var output: Vec<u8> = Vec.with_capacity(length)
    while output.len() < length { output.push(u8.truncate(0)) }
    return move output
}

func append_bytes(output: &+Vec<u8>, bytes: &[u8]): void {
    var offset: usize = 0
    while offset < bytes.len() {
        output.push(bytes[offset])
        offset += 1
    }
    return
}

async func stream_large_response(
    responder: Responder,
    timeout: Duration,
): ServerConnection?! {
    let head = ResponseHead.ok()
    var writer = await responder.begin_chunked_with_timeout(move head, timeout)?
    var chunk = byte_buffer(TRANSFER_BYTES)
    var offset: usize = 0
    while offset < chunk.len() {
        chunk[offset] = u8.truncate(120)
        offset += 1
    }
    var written: usize = 0
    while written < LARGE_BODY_BYTES {
        await writer.write_with_timeout(&chunk, timeout)?
        written += chunk.len()
    }
    return await writer.finish_with_timeout(timeout)?
}

async func read_large_request(
    request: &+IncomingRequest,
    timeout: Duration,
): bool {
    var scratch = byte_buffer(TRANSFER_BYTES)
    var received: usize = 0
    loop {
        let count = await request.read_with_timeout(&+scratch, timeout)
        catch _ { return false }
        if count == 0 { return received == LARGE_BODY_BYTES }
        received += count
    }
}

async func handle_large_route(
    state: &ServiceState,
    request: IncomingRequest,
    _route: RouteMatch,
): ServerConnection?! {
    var request_owner = move request
    let timeout = await state.timeout()?
    if !await read_large_request(&+request_owner, timeout) {
        request_owner.close()
        return error.new("http-service.large_body", "large request body changed")
    }
    let responder = await request_owner.finish_body_with_timeout(timeout)?
    return await stream_large_response(move responder, timeout)?
}

async func handle_close_route(
    state: &ServiceState,
    request: IncomingRequest,
    _route: RouteMatch,
): ServerConnection?! {
    let timeout = await state.timeout()?
    var responder = await request.finish_body_with_timeout(timeout)?
    responder.close_after_response()
    let response = OutgoingResponse.text(Status.ok(), "done")?
    return await responder.respond_with_timeout(move response, timeout)?
}

async func record_event(state: &ServiceState, event: Event): void! {
    let event_output = event.to_json_line()?
    await persist_event(state, &event_output)?
    await retain_event(state, move event)
    return
}

async func persist_event(state: &ServiceState, event_output: &str): void! {
    let store = state.store()
    var durable = await store.lock()
    durable.set("last-event".bytes(), event_output.bytes())?
    let _sequence = await durable.commit()?
    return
}

async func retain_event(state: &ServiceState, event: Event): void {
    let event_store = state.event_store()
    var events = await event_store.lock()
    events.push(move event)
    return
}

func session_for_request(state: &ServiceState, request: &IncomingRequest): SessionId! {
    let session = state.cookie().read_headers(request.headers())?
    return session otherwise { return SessionId.issue()? }
}

async func handle_user_route(
    state: &ServiceState,
    request: IncomingRequest,
    route: RouteMatch,
): ServerConnection?! {
    let timeout = await state.timeout()?
    let identifier = session_for_request(state, &request)?
    let user = route.parameter("id") otherwise {
        let response = OutgoingResponse.text(Status.bad_request(), "missing user")?
        return await request.respond_with_timeout(
            move response,
            timeout,
        )?
    }
    var query = request.target().query_pairs()
    let parameter = query.next() otherwise {
        let response = OutgoingResponse.text(Status.bad_request(), "missing query")?
        return await request.respond_with_timeout(
            move response,
            timeout,
        )?
    }
    if user != "alice" || parameter.name() != "view" || parameter.value() != "full" {
        let response = OutgoingResponse.text(Status.bad_request(), "invalid user query")?
        return await request.respond_with_timeout(
            move response,
            timeout,
        )?
    }
    let timestamp = SystemTime.now()?
    var event = Event.new(timestamp, Level.info, "request.complete")?
    let route_field = Field.text("route", "user")?
    event.add(move route_field)?
    let status_field = Field.unsigned("status", 200)?
    event.add(move status_field)?
    let session_field = Field.redacted("session")?
    event.add(move session_field)?
    let token_display = await state.token_display()?
    let token_field = Field.text("configured_token", &token_display)?
    event.add(move token_field)?
    await record_event(state, move event)?
    let response_text = await state.response()?
    var response = OutgoingResponse.json(Status.ok(), &response_text)?
    let cookie_policy = state.cookie()
    let session_header = cookie_policy.set_header(&identifier)?
    response.append_header(move session_header)
    return await request.respond_with_timeout(
        move response,
        timeout,
    )?
}

async func handle_failure_route(
    _state: &ServiceState,
    _request: IncomingRequest,
    _route: RouteMatch,
): ServerConnection?! {
    return error.new("http-service.handler_failed", "intentional handler failure")
}

func build_router(
    configuration: PublishedConfiguration,
    durable_state: Store,
): Router<ServiceState>! {
    var router = Router.new(ServiceState.new(move configuration, move durable_state)?)
    let large: Handler<ServiceState> = (state, request, route) {
        handle_large_route(state, move request, move route)
    }
    router.post("/large", move large)?
    let close: Handler<ServiceState> = (state, request, route) {
        handle_close_route(state, move request, move route)
    }
    router.get("/close", move close)?
    let user: Handler<ServiceState> = (state, request, route) {
        handle_user_route(state, move request, move route)
    }
    router.get("/users/:id", move user)?
    let failure: Handler<ServiceState> = (state, request, route) {
        handle_failure_route(state, move request, move route)
    }
    router.get("/fail", move failure)?
    return move router
}

async func handle_connection(
    router: &Router<ServiceState>,
    connection: ServerConnection,
): bool {
    let timeout = await router.state().timeout() catch _ { return false }
    var owner = move connection
    loop {
        let request = await owner.read_request_with_timeout(timeout) catch failure {
            return failure.has_code("std.http.invalid_syntax")
            || failure.has_code("std.http.timed_out")
        }
        let dispatched = await router.dispatch(move request) catch failure {
            return failure.has_code("http-service.handler_failed")
        }
        match move dispatched {
            RouteDispatch.handled(next) {
                owner = move next otherwise { return true }
            }
            RouteDispatch.not_found(unmatched) {
                let response = OutgoingResponse.text(Status.not_found(), "not found")
                catch _ { return false }
                let next = await unmatched.respond_with_timeout(move response, timeout)
                catch _ { return false }
                owner = move next otherwise { return true }
            }
            RouteDispatch.method_not_allowed(unmatched) {
                let response = OutgoingResponse.text(
                    Status.method_not_allowed(),
                    "method not allowed",
                ) catch _ { return false }
                let next = await unmatched.respond_with_timeout(move response, timeout)
                catch _ { return false }
                owner = move next otherwise { return true }
            }
        }
    }
}

async func drain_handlers(
    handlers: TaskGroup<bool>,
): (usize, usize)! {
    var owner = move handlers
    var succeeded: usize = 0
    var failed: usize = 0
    while !owner.is_empty() {
        if await observe_handler(&+owner)? {
            succeeded += 1
        } else {
            failed += 1
        }
    }
    return (succeeded, failed)
}

async func drain_handlers_before(
    handlers: TaskGroup<bool>,
    deadline: Duration,
): (usize, usize)! {
    match await task.with_timeout(drain_handlers(move handlers), deadline) {
        Timeout.completed(result) { return move result? }
        Timeout.elapsed {
            return error.new(
                "http-service.shutdown_timed_out",
                "handler drain exceeded the shutdown deadline",
            )
        }
    }
}

async func observe_handler(
    handlers: &+TaskGroup<bool>,
): bool! {
    let completed = await handlers.next() otherwise {
        return error.new("http-service.group", "non-empty handler group produced no result")
    }
    return completed
}

async func serve_bounded(
    server: Server,
    configuration: PublishedConfiguration,
    durable_state: Store,
    state_path: &str,
    shutdown_deadline: Duration,
): (usize, usize, String)! {
    var owner = move server
    let router = build_router(move configuration, move durable_state)?
    var handlers: TaskGroup<bool> = TaskGroup.empty()
    var accepted: usize = 0
    var succeeded: usize = 0
    var failed: usize = 0
    while accepted < REQUEST_COUNT {
        if handlers.len() == CONNECTION_LIMIT {
            if await observe_handler(&+handlers)? {
                succeeded += 1
            } else {
                failed += 1
            }
        }
        let connection = await owner.accept_with_timeout(shutdown_deadline)?
        handlers.add(handle_connection(&router, move connection))
        accepted += 1
    }
    owner.close()
    let drained = await drain_handlers_before(move handlers, shutdown_deadline)?
    let state = router.into_state()
    let event_output = await rendered_event(&state)?
    await close_and_recover_event(&state, &event_output, state_path)?
    return (succeeded + drained.0, failed + drained.1, move event_output)
}

async func rendered_event(state: &ServiceState): String! {
    let event_store = state.event_store()
    var events = await event_store.lock()
    let stored_events: &Vec<Event> = &events as &Vec<Event>
    if stored_events.len() == 0 {
        return error.new("http-service.events", "no structured event was recorded")
    }
    let output = stored_events[stored_events.len() - 1].to_json_line()?
    return move output
}

async func close_and_recover_event(
    state: &ServiceState,
    event_output: &str,
    path: &str,
): void! {
    let committed_sequence = await close_store(state)?

    var restarted = await Store.open(path)?
    let recovered = restarted.get("last-event".bytes()) otherwise {
        return error.new("http-service.state", "committed event was not recovered")
    }
    if recovered != event_output.bytes()
    || restarted.committed_sequence() != committed_sequence {
        return error.new("http-service.state", "restarted state changed")
    }
    await restarted.close()?
    await fs.remove_file(path)?
    return
}

async func close_store(state: &ServiceState): u64! {
    let store = state.store()
    var durable = await store.lock()
    let committed_sequence = await durable.commit()?
    await durable.close()?
    return committed_sequence
}

async func read_until_eof(
    stream: &+TcpStream,
    timeout: Duration,
): usize! {
    var scratch = byte_buffer(TRANSFER_BYTES)
    var received: usize = 0
    loop {
        let count = await stream.read_with_timeout(&+scratch, timeout)?
        if count == 0 { return received }
        received += count
    }
}

async func read_response_text(
    stream: &+TcpStream,
    timeout: Duration,
): String! {
    var scratch = byte_buffer(TRANSFER_BYTES)
    var bytes: Vec<u8> = Vec.empty()
    loop {
        let received = await stream.read_with_timeout(&+scratch, timeout)?
        if received == 0 { return String.from_utf8(&bytes)? }
        var offset: usize = 0
        while offset < received {
            bytes.push(scratch[offset])
            offset += 1
        }
    }
}

async func text_exchange(
    address: SocketAddress,
    timeout: Duration,
    request: &str,
    expected: &str,
): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    await stream.write_with_timeout(request.bytes(), timeout) catch _ { return false }
    let response = await read_response_text(&+stream, timeout) catch _ { return false }
    return (&response as &str).contains(expected)
}

async func large_reused_exchange(address: SocketAddress, timeout: Duration): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    var request: Vec<u8> = Vec.with_capacity(LARGE_BODY_BYTES + 256)
    append_bytes(
        &+request,
        "POST /large HTTP/1.1\r\nHost: localhost\r\nContent-Length: 32768\r\n\r\n".bytes(),
    )
    let body_start = request.len()
    while request.len() - body_start < LARGE_BODY_BYTES {
        request.push(u8.truncate(97))
    }
    append_bytes(
        &+request,
        "GET /close HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".bytes(),
    )
    await stream.write_with_timeout(&request, timeout) catch _ { return false }
    let received = await read_until_eof(&+stream, timeout) catch _ { return false }
    return received > LARGE_BODY_BYTES
}

async func malformed_exchange(address: SocketAddress, timeout: Duration): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    await stream.write_with_timeout(
        "GET /bad HTTP/1.1\r\nHost: localhost\r\nConnection: close,\r\n\r\n".bytes(),
        timeout,
    ) catch _ { return false }
    let received = await read_until_eof(&+stream, timeout) catch _ { return false }
    return received == 0
}

async func timed_out_exchange(address: SocketAddress, timeout: Duration): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    let received = await read_until_eof(&+stream, timeout) catch _ { return false }
    return received == 0
}

async func routed_parameter_exchange(
    address: SocketAddress,
    timeout: Duration,
    expected_response: &str,
): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    await stream.write_with_timeout(
        "GET /users/alice?view=full HTTP/1.1\r\nHost: localhost\r\nCookie: session=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\nConnection: close\r\n\r\n".bytes(),
        timeout,
    ) catch _ { return false }
    let response = await read_response_text(&+stream, timeout) catch _ { return false }
    return (&response as &str).contains(expected_response)
    && (&response as &str).contains("set-cookie: session=")
    && (&response as &str).contains("HttpOnly; SameSite=Lax")
}

async func missing_route_exchange(address: SocketAddress, timeout: Duration): bool {
    return await text_exchange(
        address,
        timeout,
        "GET /missing HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
        "404",
    )
}

async func method_mismatch_exchange(address: SocketAddress, timeout: Duration): bool {
    return await text_exchange(
        address,
        timeout,
        "POST /users/alice HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
        "405",
    )
}

async func handler_failure_exchange(address: SocketAddress, timeout: Duration): bool {
    var stream = await net.connect_tcp_with_timeout(address, timeout) catch _ { return false }
    await stream.write_with_timeout(
        "GET /fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".bytes(),
        timeout,
    ) catch _ { return false }
    let received = await read_until_eof(&+stream, timeout) catch _ { return false }
    return received == 0
}

async func run_clients(
    address: SocketAddress,
    timeout: Duration,
    expected_response: &str,
): usize! {
    var requests: TaskGroup<bool> = TaskGroup.empty()
    requests.add(large_reused_exchange(address, timeout))
    requests.add(routed_parameter_exchange(address, timeout, expected_response))
    requests.add(missing_route_exchange(address, timeout))
    requests.add(method_mismatch_exchange(address, timeout))
    requests.add(malformed_exchange(address, timeout))
    requests.add(timed_out_exchange(address, timeout))
    requests.add(handler_failure_exchange(address, timeout))
    var passed: usize = 0
    while !requests.is_empty() {
        let result = await requests.next() otherwise {
            return error.new("http-service.group", "non-empty client group produced no result")
        }
        if result { passed += 1 }
    }
    return passed
}

async func run_service_example(): i32! {
    let arguments = parse_service_arguments()?
    let configuration = await build_service_configuration(&arguments)?
    if operational_mode(&arguments) {
        return await run_operational_service(&arguments, move configuration)
    }
    let listen_text = configured_listen(&configuration)?
    let listen = SocketAddress.parse(&listen_text) otherwise {
        return error.new("http-service.configuration", "listen is not a numeric socket address")
    }
    let expected_response = configured_response(&configuration)?
    let state_path = configured_state_path(&configuration)?
    let published = PublishedConfiguration.new(move configuration)?
    let limits = Limits.new(1024, 8192, 32, 4, 1024, 128, 65536)?
    let operation_timeout = Duration.from_seconds(3)
    let durable_state = await Store.open(&state_path)?
    let server = await Server.bind_with_limits(listen, limits)?
    let address = server.local_address()?
    let completed = await task.join(
        serve_bounded(
            move server,
            published.share(),
            move durable_state,
            &state_path,
            operation_timeout,
        ),
        run_clients(address, operation_timeout, &expected_response),
    )
    let service = move completed.0?
    let clients = move completed.1?
    if service.0 != REQUEST_COUNT {
        return error.new("http-service.handlers", "successful handler count changed")
    }
    if service.1 != 0 {
        return error.new("http-service.handlers", "failed handler count changed")
    }
    let event_output: &str = &service.2
    if !event_output.contains("\"event\":\"request.complete\"")
    || !event_output.contains("\"session\":\"[redacted]\"")
    || !event_output.contains("\"configured_token\":\"[redacted]\"")
    || event_output.contains("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
    || event_output.contains("environment-secret") {
        return error.new("http-service.events", "structured operational output changed")
    }
    if clients != REQUEST_COUNT {
        return error.new("http-service.clients", "successful client count changed")
    }
    return 0
}