http_driver: bindings matches work

This commit is contained in:
Emery Hemingway 2024-03-20 19:43:55 +00:00
parent 128df6dc03
commit 16926a789e
1 changed files with 127 additions and 53 deletions

View File

@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense # 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/sys/ioqueue
import pkg/preserves import pkg/preserves
import pkg/syndicate import pkg/syndicate
@ -28,27 +28,34 @@ proc `$`(b: seq[byte]): string = cast[string](b)
type HandlerEntity = ref object of Entity type HandlerEntity = ref object of Entity
handler: proc (turn: var Turn; req: HttpRequest; cap: Cap) 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) = method publish(e: HandlerEntity; turn: var Turn; a: AssertionRef; h: Handle) =
var ctx = a.value.preservesTo HttpContext var ctx = a.value.preservesTo HttpContext
if ctx.isSome: if ctx.isSome:
var res = ctx.get.res.unembed Cap var res = ctx.get.res.unembed Cap
if res.isSome: if res.isSome:
e.handler(turn, ctx.get.req, res.get) 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) = proc respond404(turn: var Turn; req: HttpRequest; cap: Cap) =
message(turn, cap, HttpResponse( message(turn, cap, HttpResponse(
orKind: HttpResponseKind.status, orKind: HttpResponseKind.status,
status: HttpResponseStatus(code: 404), status: HttpResponseStatus(
)) code: 404,
message(turn, cap, dateResponse()) 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)) message(turn, cap, HttpResponse(orKind: HttpResponseKind.done))
proc bind404Handler(turn: var Turn; ds: Cap; port: Port) = proc bind404Handler(turn: var Turn; ds: Cap; port: Port) =
stderr.writeLine "bind 404 handler to ", port
var b: HttpBinding var b: HttpBinding
b.host = HostPattern(orKind: HostPatternKind.any) b.host = HostPattern(orKind: HostPatternKind.any)
b.port = BiggestInt port b.port = BiggestInt port
@ -58,7 +65,7 @@ proc bind404Handler(turn: var Turn; ds: Cap; port: Port) =
discard publish(turn, ds, b) discard publish(turn, ds, b)
proc badRequest(conn: Connection; msg: string) = proc badRequest(conn: Connection; msg: string) =
conn.send(SupportedVersion & " 400 " & msg) conn.send(SupportedVersion & " 400 " & msg, endOfMessage = true)
close(conn) close(conn)
proc extractQuery(s: var string): Table[Symbol, seq[QueryValue]] = proc extractQuery(s: var string): Table[Symbol, seq[QueryValue]] =
@ -86,10 +93,11 @@ proc parseRequest(conn: Connection; text: string): (int, HttpRequest) =
# method # method
off.inc parseUntil(text, token, SP, off) off.inc parseUntil(text, token, SP, off)
result[1].method = Symbol(move token) result[1].method = token.toLowerAscii.Symbol
advanceSp() advanceSp()
# target # target
if text[off] == '/': inc(off) #TODO: always a leading slash?
off.inc parseUntil(text, token, SP, off) off.inc parseUntil(text, token, SP, off)
advanceSp() advanceSp()
@ -123,30 +131,35 @@ proc parseRequest(conn: Connection; text: string): (int, HttpRequest) =
if token == "": break if token == "": break
advanceLine() advanceLine()
var var
(key, val) = httpcore.parseHeader(token) (key, vals) = httpcore.parseHeader(token)
k = key.toLowerAscii.Symbol k = key.toLowerAscii.Symbol
list = result[1].headers.getOrDefault(k) v = result[1].headers.getOrDefault(k)
for v in val.mitems: for e in vals.mitems:
list.add v.move.toLowerAscii e = e.toLowerAscii
result[1].headers[k] = list 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 result[0] = off
proc send(conn: Connection; chunk: Chunk) = proc send(conn: Connection; chunk: Chunk) =
case chunk.orKind case chunk.orKind
of ChunkKind.string: of ChunkKind.string:
conn.send chunk.string conn.send(chunk.string, endOfMessage = false)
of ChunkKind.bytes: of ChunkKind.bytes:
conn.send chunk.bytes conn.send(chunk.bytes, endOfMessage = false)
proc runSubfacet(facet: Facet, act: TurnAction) =
run(facet) do (t: var Turn):
discard inFacet(t, act)
type type
Driver = ref object Driver = ref object
facet: Facet facet: Facet
ds: Cap ds: Cap
bindings: seq[HttpBinding]
Session = ref object Session = ref object
facet: Facet facet: Facet
driver: Driver driver: Driver
@ -156,38 +169,91 @@ type
ses: Session ses: Session
req: HttpRequest req: HttpRequest
stream: StringStream 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] = proc match(driver: Driver; req: HttpRequest): Option[HttpBinding] =
discard for b in driver.bindings:
# TODO 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) = method message(e: Exchange; turn: var Turn; a: AssertionRef) =
# Send responses back into a connection. # Send responses back into a connection.
var res: HttpResponse var res: HttpResponse
if res.fromPreserves a.value: if e.mode != HttpResponseKind.done and res.fromPreserves a.value:
case res.orKind case res.orKind
of HttpResponseKind.status: of HttpResponseKind.status:
e.stream.writeLine(SupportedVersion, " ", res.status.code, " ", res.status.message) if e.mode == res.orKind:
e.stream.writeLine("date: ", now().format(IMF)) e.stream.writeLine(SupportedVersion, " ", res.status.code, " ", res.status.message)
# add Date header automatically - RFC 9110 Section 6.6.1. e.stream.writeLine("date: ", now().format(IMF))
# add Date header automatically - RFC 9110 Section 6.6.1.
e.mode = HttpResponseKind.header
of 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: of HttpResponseKind.chunk:
if e.stream.data.len > 0: if e.mode == HttpResponseKind.header:
e.mode = res.orKind
e.stream.writeLine() 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) e.ses.conn.send(res.chunk.chunk)
of HttpResponseKind.done: 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) e.ses.conn.send(res.done.chunk)
close e.ses.conn stop(turn)
# TODO: leave connection open. # stop the facet scoped to the exchange
# so that the response capability is withdrawn
proc service(turn: var Turn; exch: Exchange) = proc service(turn: var Turn; exch: Exchange) =
## Service an HTTP message exchange. ## Service an HTTP message exchange.
var binding = exch.ses.driver.match exch.req 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 var handler = binding.get.handler.unembed Cap
if handler.isSome: if handler.isNone:
stop(turn)
else:
publish(turn, handler.get, HttpContext( publish(turn, handler.get, HttpContext(
req: exch.req, req: exch.req,
res: embed newCap(turn, exch), res: embed newCap(turn, exch),
@ -199,22 +265,22 @@ proc service(ses: Session) =
close ses.conn close ses.conn
ses.conn.onClosed do (): ses.conn.onClosed do ():
stop ses.facet stop ses.facet
ses.conn.onReceived do (data: seq[byte]; ctx: MessageContext): ses.conn.onReceivedPartial do (data: seq[byte]; ctx: MessageContext; eom: bool):
echo "connection received ", data.len, " bytes"
ses.facet.run do (turn: var Turn): ses.facet.run do (turn: var Turn):
var (n, req) = parseRequest(ses.conn, cast[string](data)) var (n, req) = parseRequest(ses.conn, cast[string](data))
echo "parseRequest parsed ", n, " bytes" if n > 0:
if n < 1: req.port = BiggestInt ses.port
stop(turn, ses.facet)
else:
inFacet(turn) do (turn: var Turn): inFacet(turn) do (turn: var Turn):
# start a facet preventInertCheck(turn)
# start a new facet for this message exchange
turn.service Exchange( turn.service Exchange(
facet: turn.facet, facet: turn.facet,
ses: ses, ses: ses,
req: req, req: req,
stream: newStringStream(),
mode: HttpResponseKind.status
) )
ses.conn.receive() # ses.conn.receive()
ses.conn.receive() ses.conn.receive()
proc newListener(port: Port): Listener = proc newListener(port: Port): Listener =
@ -223,15 +289,19 @@ proc newListener(port: Port): Listener =
listen newPreconnection(local=[lp]) listen newPreconnection(local=[lp])
proc httpListen(turn: var Turn; driver: Driver; port: Port) = proc httpListen(turn: var Turn; driver: Driver; port: Port) =
let facet = turn.facet
var listener = newListener(port) var listener = newListener(port)
# TODO: let listener # 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 stop listener
listener.onConnectionReceived do (conn: Connection): listener.onConnectionReceived do (conn: Connection):
driver.facet.run do (turn: var Turn): driver.facet.run do (turn: var Turn):
# start a new turn # start a new turn
linkActor(turn, "http-conn") do (turn: var 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( service Session(
facet: turn.facet, facet: turn.facet,
driver: driver, driver: driver,
@ -242,20 +312,24 @@ proc httpListen(turn: var Turn; driver: Driver; port: Port) =
proc httpDriver(turn: var Turn; ds: Cap) = proc httpDriver(turn: var Turn; ds: Cap) =
let driver = Driver(facet: turn.facet, ds: ds) 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?:{ during(turn, ds, HttpBinding?:{
1: grab(), 1: grab(),
}) do (port: BiggestInt): }) do (port: BiggestInt):
publish(turn, ds, HttpListener(port: port)) publish(turn, ds, HttpListener(port: port))
publish(turn, ds, HttpListener(port: 80)) during(turn, ds, ?:HttpBinding) do (
# TODO: only here for testing 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, ds, ?:HttpListener) do (port: uint16):
during(turn, root, ?:HttpDriverArguments) do (ds: Cap): 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): spawnActor("http-driver", turn) do (turn: var Turn):
httpDriver(turn, ds) httpDriver(turn, ds)