diff --git a/src/drivers/http_driver.nim b/src/drivers/http_driver.nim index 9bd49b3..7ed47d1 100644 --- a/src/drivers/http_driver.nim +++ b/src/drivers/http_driver.nim @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense -import std/[httpcore, options, parseutils, streams, strutils, tables, times, uri] +import std/[httpcore, options, parseutils, sets, streams, strutils, tables, times, uri] import pkg/sys/ioqueue import pkg/preserves import pkg/syndicate @@ -28,27 +28,34 @@ proc `$`(b: seq[byte]): string = cast[string](b) type HandlerEntity = ref object of Entity handler: proc (turn: var Turn; req: HttpRequest; cap: Cap) -proc dateResponse(): HttpResponse = - result = HttpResponse(orKind: HttpResponseKind.header) - result.header.name = Symbol"date" - result.header.value = now().format(IMF) - method publish(e: HandlerEntity; turn: var Turn; a: AssertionRef; h: Handle) = var ctx = a.value.preservesTo HttpContext if ctx.isSome: var res = ctx.get.res.unembed Cap if res.isSome: e.handler(turn, ctx.get.req, res.get) + else: + echo "HandlerEntity got a non-Cap ", ctx.get.res + else: + echo "HandlerEntity got a non-HttpContext ", a.value proc respond404(turn: var Turn; req: HttpRequest; cap: Cap) = message(turn, cap, HttpResponse( orKind: HttpResponseKind.status, - status: HttpResponseStatus(code: 404), - )) - message(turn, cap, dateResponse()) + status: HttpResponseStatus( + code: 404, + message: "resource not found", + ))) + message(turn, cap, HttpResponse( + orKind: HttpResponseKind.header, + header: HttpResponseHeader( + name: Symbol"content-length", + value: "0", + ))) message(turn, cap, HttpResponse(orKind: HttpResponseKind.done)) proc bind404Handler(turn: var Turn; ds: Cap; port: Port) = + stderr.writeLine "bind 404 handler to ", port var b: HttpBinding b.host = HostPattern(orKind: HostPatternKind.any) b.port = BiggestInt port @@ -58,7 +65,7 @@ proc bind404Handler(turn: var Turn; ds: Cap; port: Port) = discard publish(turn, ds, b) proc badRequest(conn: Connection; msg: string) = - conn.send(SupportedVersion & " 400 " & msg) + conn.send(SupportedVersion & " 400 " & msg, endOfMessage = true) close(conn) proc extractQuery(s: var string): Table[Symbol, seq[QueryValue]] = @@ -86,10 +93,11 @@ proc parseRequest(conn: Connection; text: string): (int, HttpRequest) = # method off.inc parseUntil(text, token, SP, off) - result[1].method = Symbol(move token) + result[1].method = token.toLowerAscii.Symbol advanceSp() # target + if text[off] == '/': inc(off) #TODO: always a leading slash? off.inc parseUntil(text, token, SP, off) advanceSp() @@ -123,30 +131,35 @@ proc parseRequest(conn: Connection; text: string): (int, HttpRequest) = if token == "": break advanceLine() var - (key, val) = httpcore.parseHeader(token) + (key, vals) = httpcore.parseHeader(token) k = key.toLowerAscii.Symbol - list = result[1].headers.getOrDefault(k) - for v in val.mitems: - list.add v.move.toLowerAscii - result[1].headers[k] = list + v = result[1].headers.getOrDefault(k) + for e in vals.mitems: + e = e.toLowerAscii + if k == Symbol"host": + result[1].host = e + if v == "": v = move e + else: + v.add ", " + v.add e + if k == Symbol"host": + result[1].host = v + result[1].headers[k] = v result[0] = off proc send(conn: Connection; chunk: Chunk) = case chunk.orKind of ChunkKind.string: - conn.send chunk.string + conn.send(chunk.string, endOfMessage = false) of ChunkKind.bytes: - conn.send chunk.bytes - -proc runSubfacet(facet: Facet, act: TurnAction) = - run(facet) do (t: var Turn): - discard inFacet(t, act) + conn.send(chunk.bytes, endOfMessage = false) type Driver = ref object facet: Facet ds: Cap + bindings: seq[HttpBinding] Session = ref object facet: Facet driver: Driver @@ -156,38 +169,91 @@ type ses: Session req: HttpRequest stream: StringStream + mode: HttpResponseKind + +proc match(b: HttpBinding, r: HttpRequest): bool = + ## Check if `HttpBinding` `b` matches `HttpRequest` `r`. + result = + (b.host.orKind == HostPatternKind.any or + b.host.host == r.host) and + (b.port == r.port) and + (b.method.orKind == MethodPatternKind.any or + b.method.specific == r.method) + if result: + for i, p in b.path: + if i > r.path.high: return false + case p.orKind + of PathPatternElementKind.wildcard: discard + of PathPatternElementKind.label: + if p.label != r.path[i]: return false + of PathPatternElementKind.rest: + return i == b.path.high + # return false if ... isn't the last element + +proc strongerThan(a, b: HttpBinding): bool = + ## Check if `a` is a stronger `HttpBinding` than `b`. + result = + (a.host.orKind != b.host.orKind and + a.host.orKind == HostPatternKind.host) or + (a.method.orKind != b.method.orKind and + a.method.orKind == MethodPatternKind.specific) + if not result: + if a.path.len > b.path.len: return true + for i in a.path.low..b.path.high: + if a.path[i].orKind != b.path[i].orKind and + a.path[i].orKind == PathPatternElementKind.label: + return true proc match(driver: Driver; req: HttpRequest): Option[HttpBinding] = - discard - # TODO + for b in driver.bindings: + if b.match req: + if result.isNone or b.strongerThan(result.get): + result = some b + else: + echo b, " does not match ", req method message(e: Exchange; turn: var Turn; a: AssertionRef) = # Send responses back into a connection. var res: HttpResponse - if res.fromPreserves a.value: + if e.mode != HttpResponseKind.done and res.fromPreserves a.value: case res.orKind of HttpResponseKind.status: - e.stream.writeLine(SupportedVersion, " ", res.status.code, " ", res.status.message) - e.stream.writeLine("date: ", now().format(IMF)) - # add Date header automatically - RFC 9110 Section 6.6.1. + if e.mode == res.orKind: + e.stream.writeLine(SupportedVersion, " ", res.status.code, " ", res.status.message) + e.stream.writeLine("date: ", now().format(IMF)) + # add Date header automatically - RFC 9110 Section 6.6.1. + e.mode = HttpResponseKind.header of HttpResponseKind.header: - e.stream.writeLine(res.header.name, ": ", res.header.value) + if e.mode == res.orKind: + e.stream.writeLine(res.header.name, ": ", res.header.value) of HttpResponseKind.chunk: - if e.stream.data.len > 0: + if e.mode == HttpResponseKind.header: + e.mode = res.orKind e.stream.writeLine() - e.ses.conn.send(move e.stream.data) + e.ses.conn.send(move e.stream.data, endOfMessage = false) e.ses.conn.send(res.chunk.chunk) of HttpResponseKind.done: + if e.mode == HttpResponseKind.header: + e.stream.writeLine() + e.ses.conn.send(move e.stream.data, endOfMessage = false) + e.mode = res.orKind e.ses.conn.send(res.done.chunk) - close e.ses.conn - # TODO: leave connection open. + stop(turn) + # stop the facet scoped to the exchange + # so that the response capability is withdrawn proc service(turn: var Turn; exch: Exchange) = ## Service an HTTP message exchange. var binding = exch.ses.driver.match exch.req - if binding.isSome: + if binding.isNone: + echo "no binding for ", exch.req + stop(turn) + else: + echo "driver matched binding ", binding.get var handler = binding.get.handler.unembed Cap - if handler.isSome: + if handler.isNone: + stop(turn) + else: publish(turn, handler.get, HttpContext( req: exch.req, res: embed newCap(turn, exch), @@ -199,22 +265,22 @@ proc service(ses: Session) = close ses.conn ses.conn.onClosed do (): stop ses.facet - ses.conn.onReceived do (data: seq[byte]; ctx: MessageContext): - echo "connection received ", data.len, " bytes" + ses.conn.onReceivedPartial do (data: seq[byte]; ctx: MessageContext; eom: bool): ses.facet.run do (turn: var Turn): var (n, req) = parseRequest(ses.conn, cast[string](data)) - echo "parseRequest parsed ", n, " bytes" - if n < 1: - stop(turn, ses.facet) - else: + if n > 0: + req.port = BiggestInt ses.port inFacet(turn) do (turn: var Turn): - # start a facet + preventInertCheck(turn) + # start a new facet for this message exchange turn.service Exchange( facet: turn.facet, ses: ses, req: req, + stream: newStringStream(), + mode: HttpResponseKind.status ) - ses.conn.receive() + # ses.conn.receive() ses.conn.receive() proc newListener(port: Port): Listener = @@ -223,15 +289,19 @@ proc newListener(port: Port): Listener = listen newPreconnection(local=[lp]) proc httpListen(turn: var Turn; driver: Driver; port: Port) = + let facet = turn.facet var listener = newListener(port) # TODO: let listener - turn.facet.onStop do (turn: var Turn): + listener.onListenError do (err: ref Exception): + terminateFacet(facet, err) + facet.onStop do (turn: var Turn): stop listener listener.onConnectionReceived do (conn: Connection): driver.facet.run do (turn: var Turn): # start a new turn linkActor(turn, "http-conn") do (turn: var Turn): - # start a new actor + preventInertCheck(turn) + # facet is scoped to the lifetime of the connection service Session( facet: turn.facet, driver: driver, @@ -242,20 +312,24 @@ proc httpListen(turn: var Turn; driver: Driver; port: Port) = proc httpDriver(turn: var Turn; ds: Cap) = let driver = Driver(facet: turn.facet, ds: ds) - during(turn, ds, ?:HttpListener) do (port: uint16): - bind404Handler(turn, ds, Port port) - httpListen(turn, driver, Port port) - during(turn, ds, HttpBinding?:{ 1: grab(), }) do (port: BiggestInt): publish(turn, ds, HttpListener(port: port)) - publish(turn, ds, HttpListener(port: 80)) - # TODO: only here for testing + during(turn, ds, ?:HttpBinding) do ( + ho: HostPattern, po: int, me: MethodPattern, pa: PathPattern, e: Value): + let b = HttpBinding(host: ho, port: po, `method`: me, path: pa, handler: e) + driver.bindings.add b + do: + raiseAssert("need to remove binding " & $b) -proc spawnHttpDriver*(turn: var Turn; root: Cap) = - during(turn, root, ?:HttpDriverArguments) do (ds: Cap): + during(turn, ds, ?:HttpListener) do (port: uint16): + bind404Handler(turn, ds, Port port) + httpListen(turn, driver, Port port) + +proc spawnHttpDriver*(turn: var Turn; ds: Cap) = + during(turn, ds, ?:HttpDriverArguments) do (ds: Cap): spawnActor("http-driver", turn) do (turn: var Turn): httpDriver(turn, ds)