241 lines
6.7 KiB
Nim
241 lines
6.7 KiB
Nim
# SPDX-FileCopyrightText: ☭ Emery Hemingway
|
|
# SPDX-License-Identifier: Unlicense
|
|
|
|
import std/[httpcore, options, parseutils, streams, strutils, tables, times, uri]
|
|
import pkg/sys/ioqueue
|
|
import pkg/preserves
|
|
import pkg/syndicate
|
|
import pkg/syndicate/protocols/http
|
|
import taps
|
|
|
|
proc echo(args: varargs[string, `$`]) =
|
|
stderr.writeLine(args)
|
|
|
|
proc `$`(b: seq[byte]): string = cast[string](b)
|
|
|
|
# a Date header on responses must be present if a clock is available
|
|
|
|
# An upgrade header can be used to switch over to native syndicate protocol.
|
|
|
|
# Check the response encoding matches or otherwise return 415
|
|
|
|
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 = "date"
|
|
result.header.value = now().format(IMF)
|
|
|
|
method publish(e: Handler; 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, cap)
|
|
|
|
proc respond404(turn: var Turn; req: HttpRequest; cap: Cap) =
|
|
message(turn, cap, HttpResponse(
|
|
orKind: HttpResponseKind.status,
|
|
status: HttpResponseStatus(code: "404"),
|
|
))
|
|
message(turn, cap, dateResponse())
|
|
message(turn, cap, HttpResponse(orKind: HttpResponseKind.done))
|
|
|
|
proc bind404Handler(turn: var Turn; ds: Cap; port: Port) =
|
|
var b: HttpBinding
|
|
b.host = HostPattern(orKind: HostPatternKind.any)
|
|
b.port = BiggestInt port
|
|
b.method = MethodPattern(orKind: MethodPatternKind.any)
|
|
b.path = @[PathPatternElement(orKind: PathPatternElementKind.rest)]
|
|
p.handler = newCap(turn, HandlerEntity(handler: respond404))
|
|
discard publish(turn, ds, b)
|
|
|
|
const
|
|
SP = { ' ', '\x09', '\x0b', '\x0c', '\x0d' }
|
|
SupportedVersion = "HTTP/1.1"
|
|
IMF = initTimeFormat"ddd, dd MMM yyyy HH:mm:ss"
|
|
|
|
proc badRequest(conn: Connection; msg: string) =
|
|
conn.send(SupportedVersion & " 400 " & msg)
|
|
close(conn)
|
|
|
|
proc extractQuery(s: var string): Table[Symbol, seq[QueryValue]] =
|
|
let start = succ skipUntil(s, '?')
|
|
if start < s.len:
|
|
var query = s[start..s.high]
|
|
s.setLen(start)
|
|
for key, val in uri.decodeQuery(query):
|
|
var list = result.getOrDefault(Symbol key)
|
|
list.add QueryValue(orKind: QueryValueKind.string, string: val)
|
|
result[Symbol key] = list
|
|
|
|
proc parseRequest(conn: Connection; text: string): (int, HttpRequest) =
|
|
var
|
|
token: string
|
|
off: int
|
|
|
|
template advanceSp =
|
|
let n = skipWhile(text, SP, off)
|
|
if n < 1:
|
|
badRequest(conn, "invalid request")
|
|
return
|
|
inc(off, n)
|
|
|
|
# method
|
|
off.inc parseUntil(text, token, SP, off)
|
|
result[1].method = Symbol(move token)
|
|
advanceSp()
|
|
|
|
# target
|
|
off.inc parseUntil(text, token, SP, off)
|
|
advanceSp()
|
|
|
|
block:
|
|
var version: string
|
|
off.inc parseUntil(text, version, SP, off)
|
|
advanceSp()
|
|
if version != SupportedVersion:
|
|
badRequest(conn, "version not supported")
|
|
return
|
|
|
|
result[1].query = extractQuery(token)
|
|
|
|
result[1].path = split(token, '/')
|
|
for p in result[1].path.mitems:
|
|
# normalize the path
|
|
for i, c in p:
|
|
if c in {'A'..'Z'}:
|
|
p[i] = char c.ord + 0x20
|
|
|
|
template advanceLine =
|
|
inc off, skipWhile(text, {'\x0d'}, off)
|
|
if text.high < off or text[off] != '\x0a':
|
|
badRequest(conn, "invalid request")
|
|
return
|
|
inc off, 1
|
|
|
|
advanceLine()
|
|
while off < text.len:
|
|
off.inc parseUntil(text, token, {'\x0d', '\x0a'}, off)
|
|
if token == "": break
|
|
advanceLine()
|
|
var
|
|
(key, val) = 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
|
|
|
|
result[0] = off
|
|
|
|
#[
|
|
var body = $req
|
|
var stream = newStringStream()
|
|
stream.writeLine(SupportedVersion, " 200 OK")
|
|
stream.writeLine("date: ", now().format(IMF))
|
|
stream.writeLine("content-length: ", body.len)
|
|
stream.writeLine()
|
|
stream.write(body)
|
|
echo "send ", stream.data.len, " bytes"
|
|
conn.send(stream.data, endOfMessage = true)
|
|
]#
|
|
|
|
proc runSubfacet(facet: Facet, act: TurnAction) =
|
|
run(facet) do (t: var Turn): inFacet(t, act)
|
|
|
|
type
|
|
Driver = ref object
|
|
facet: Facet
|
|
ds: Cap
|
|
Session = ref object
|
|
facet: Facet
|
|
driver: Driver
|
|
conn: Connection
|
|
port: Port
|
|
Exchange = ref object
|
|
facet: Facet
|
|
ses: Session
|
|
req: HttpRequest
|
|
handlers: seq[Cap]
|
|
|
|
proc service(turn: var Turn; exch: Exchange) =
|
|
## Service an HTTP message exchange.
|
|
let pat = HttpService ?:{
|
|
0: drop(),
|
|
1: ?ses.port,
|
|
2: ?req.method,
|
|
3: grab()
|
|
4: grab()
|
|
}
|
|
onPublish(turn, ses.driver.ds, pat) do (
|
|
|
|
proc service(ses: Session) =
|
|
## Service a connection to an HTTP client.
|
|
ses.facet.onStop do (turn: var Turn):
|
|
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"
|
|
var (n, req) = parseRequest(conn, cast[string](data))
|
|
echo "parseRequest parsed ", n, " bytes"
|
|
if n < 1:
|
|
stop(ses.facet)
|
|
else:
|
|
runSubfacet(facet) do (turn: var Turn):
|
|
service Exchange(
|
|
facet: turn.facet,
|
|
ses: session,
|
|
req: req,
|
|
)
|
|
conn.receive()
|
|
conn.receive()
|
|
|
|
proc newListener(port: Port): Listener =
|
|
var lp = newLocalEndpoint()
|
|
lp.with port
|
|
listen newPreconnection(local=[lp])
|
|
|
|
proc httpListen(turn: var Turn; driver: Driver; port: Port) =
|
|
let listener = newListener(port)
|
|
turn.facet.onStop do (turn: var Turn):
|
|
stop listener
|
|
listener.onConnectionReceived do (conn: Connection):
|
|
run(facet) do (turn: var Turn):
|
|
# start a new turn
|
|
linkActor("http-conn") do (turn: var Turn):
|
|
# start a new actor
|
|
service Session(
|
|
facet: turn.facet,
|
|
driver: driver,
|
|
conn: conn,
|
|
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
|
|
|
|
proc spawnHttpDriver*(turn: var Turn; root: Cap) =
|
|
during(turn, root, ?HttpDriverArguments) do (ds: Cap):
|
|
spawnActor("http-driver") do (turn: var Turn):
|
|
httpDriver(turn, ds)
|
|
|
|
when isMainModule:
|
|
import syndicate/relays
|
|
runActor("main") do (turn: var Turn):
|
|
resolveEnvironment(turn, spawnHttpDriver)
|