Initial commit of http driver

This commit is contained in:
Emery Hemingway 2024-03-18 19:31:45 +00:00
parent 0dc419ebab
commit dbe363052d
2 changed files with 146 additions and 0 deletions

4
src/drivers/Tupfile Normal file
View File

@ -0,0 +1,4 @@
include_rules
NIM_FLAGS += --path:$(TUP_CWD)/../../../taps/pkg
: foreach *.nim | $(SYNDICATE_PROTOCOL) ../<schema> |> !nim_bin |> | {bin}
: foreach {bin} |> !assert_built |>

142
src/drivers/http.nim Normal file
View File

@ -0,0 +1,142 @@
# 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/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
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
proc connectionHandler(conn: Connection) =
# linkActor("http-conn") do (turn: var Turn):
block:
# let facet = turn.facet
var seqNum: BiggestInt
conn.onClosed do ():
# stopActor(facet)
echo "connection closing"
close(conn)
conn.onReceived do (data: seq[byte]; ctx: MessageContext):
echo "connection received ", data.len, " bytes"
var (n, req) = parseRequest(conn, cast[string](data))
if n < 1:
close(conn)
else:
inc seqNum
req.sequenceNumber = seqNum
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)
# TODO: deal with transfer-encoding
conn.receive()
proc listenHttp: Listener =
var lp = newLocalEndpoint()
lp.with Port 80
result = listen newPreconnection(local=[lp])
proc main =
var listener = listenHttp()
listener.onConnectionReceived(connectionHandler)
ioqueue.run()
main()