From ff6f7553e62dc67e04b0ace49c6c93ce31b3c2f1 Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Wed, 8 Jun 2022 20:25:45 -0500 Subject: [PATCH] Initial http_protocol utility --- .gitignore | 1 + README.md | 7 +++ http_protocol.prs | 20 +++++++ http_translator.config-example.pr | 21 ++++++++ src/Tupfile | 2 + src/http_protocol.nim | 40 ++++++++++++++ src/http_translator.nim | 89 +++++++++++++++++++++++++++++++ syndicate_utils.nimble | 13 +++++ 8 files changed, 193 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 http_protocol.prs create mode 100644 http_translator.config-example.pr create mode 100644 src/Tupfile create mode 100644 src/http_protocol.nim create mode 100644 src/http_translator.nim create mode 100644 syndicate_utils.nimble diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b26d611 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +http_translator diff --git a/README.md b/README.md new file mode 100644 index 0000000..aabc460 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Syndicate utils + +## http_translator + +Dispatches HTTP requests to registered handlers. + +See [http_translator.config-example.pr](./http_translator.config-example.pr) for an example configuration. diff --git a/http_protocol.prs b/http_protocol.prs new file mode 100644 index 0000000..af2e8cf --- /dev/null +++ b/http_protocol.prs @@ -0,0 +1,20 @@ +version 1 . + +Method = =GET / =HEAD / =POST / =PUT / =DELETE / =CONNECT / =OPTIONS / =TRACE / =PATCH . +Methods = #{Method} . + +; A URL path split into elements +Path = [string ...] . + +; Register an entity that will handle requests at path prefix. +; TODO: assert the public base URL of the handler to the entity. +Handler = . + +Headers = {string: [string ...] ...:...} . + +; A request awaiting a response at handle. +; TODO: query parameters +Request = . + +; A response to handle. +Response = . diff --git a/http_translator.config-example.pr b/http_translator.config-example.pr new file mode 100644 index 0000000..09dc41d --- /dev/null +++ b/http_translator.config-example.pr @@ -0,0 +1,21 @@ +> + + + +let ?other = dataspace + +$other [ + ? [ + + ] +] + +? ?cap> [ + $cap [ + ; publish GET requests with prefix "/foo/bar" to other dataspace + + ] +] diff --git a/src/Tupfile b/src/Tupfile new file mode 100644 index 0000000..e5ee95f --- /dev/null +++ b/src/Tupfile @@ -0,0 +1,2 @@ +include_rules +: foreach ../*.prs |> !preserves_schema_nim |> %B.nim diff --git a/src/http_protocol.nim b/src/http_protocol.nim new file mode 100644 index 0000000..2f9117b --- /dev/null +++ b/src/http_protocol.nim @@ -0,0 +1,40 @@ + +import + std/typetraits, preserves, std/tables, std/sets + +type + Path* = seq[string] + Headers* = TableRef[string, seq[string]] + Response* {.preservesRecord: "http".} = object + `handle`*: int + `code`*: int + `headers`*: Headers + `body`*: string + + Handler*[E] {.preservesRecord: "handler".} = ref object + `methods`*: Methods + `path`*: Path + `entity`*: Preserve[E] + + `Method`* {.preservesOr, pure.} = enum + `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, + `PATCH` + Request* {.preservesRecord: "http".} = object + `handle`*: int + `method`*: Method + `headers`*: Headers + `path`*: Path + `body`*: string + + Methods* = HashSet[Method] +proc `$`*[E](x: Handler[E]): string = + `$`(toPreserve(x, E)) + +proc encode*[E](x: Handler[E]): seq[byte] = + encode(toPreserve(x, E)) + +proc `$`*(x: Path | Headers | Response | Request | Methods): string = + `$`(toPreserve(x)) + +proc encode*(x: Path | Headers | Response | Request | Methods): seq[byte] = + encode(toPreserve(x)) diff --git a/src/http_translator.nim b/src/http_translator.nim new file mode 100644 index 0000000..c6b19cc --- /dev/null +++ b/src/http_translator.nim @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[asyncdispatch, asynchttpserver, sets, strutils, tables, uri] +import preserves +import syndicate, syndicate/actors + +import ./http_protocol.nim + +func toHttpCore(methods: Methods): set[HttpMethod] = + # Convert the schema type to the type in the httpcore module. + for m in methods: + result.incl( + case m + of Method.GET: HttpGET + of Method.HEAD: HttpHEAD + of Method.POST: HttpPOST + of Method.PUT: HttpPUT + of Method.DELETE: HttpDELETE + of Method.CONNECT: HttpCONNECT + of Method.OPTIONS: HttpOPTIONS + of Method.TRACE: HttpTRACE + of Method.PATCH: HttpPATCH) + +proc splitPath(u: Uri): Path = + u.path.strip(chars = {'/'}).split("/") + +proc hitch(a, b: Future[void]) = + a.addCallback do (f: Future[void]): + if f.failed: fail(b, f.error) + else: complete(b) + +bootDataspace("main") do (ds: Ref; turn: var Turn): + connectStdio(ds, turn) + var + handlers: Table[seq[string], (Ref, set[HttpMethod])] + pathPrefixMaxLen = 0 + requestIdSource = 0 + + during(turn, ds, ?Handler[Ref]) do (methods: Methods; path: seq[string]; entity: Ref): + handlers[path] = (entity, methods.toHttpCore) + pathPrefixMaxLen = max(pathPrefixMaxLen, path.len) + do: + handlers.del(path) + pathPrefixMaxLen = 0 + for path in handlers.keys: + pathPrefixMaxLen = max(pathPrefixMaxLen, path.len) + + var parentFacet = turn.facet + proc handleRequest(req: asynchttpserver.Request): Future[void] = + # TODO: use pattern matching + var + entity: Ref + methods: set[HttpMethod] + path = req.url.splitPath() + block: + var prefix = path[0..min(pathPrefixMaxLen.succ, path.high)] + while entity.isNil: + (entity, methods) = handlers.getOrDefault(prefix) + if prefix.len == 0: break + else: discard prefix.pop() + if entity.isNil: + result = req.respond(Http503, "no handler registered for this path") + else: + var parentFut = newFuture[void]("handleRequest") + result = parentFut + if req.reqMethod notin methods: + result = req.respond(Http405, "method not valid for this handler") + else: + run(parentFacet) do (turn: var Turn): + inc requestIdSource + let + rId = requestIdSource + let rHandle = publish(turn, entity, http_protocol.Request( + handle: rId, + `method`: Method.GET, + headers: req.headers.table, + path: path, + body: req.body)) + + onPublish(turn, entity, Response ? { 0: ?rId, 1: ?int, 3: ?string }) do (code: HttpCode, body: string): + req.respond(code, body).addCallback do (fut: Future[void]): + run(parentFacet) do (turn: var Turn): retract(turn, rHandle) + hitch(fut, parentFut) + + var http = newAsyncHttpServer() + asyncCheck serve(http, Port 8888, handleRequest) + +runForever() diff --git a/syndicate_utils.nimble b/syndicate_utils.nimble new file mode 100644 index 0000000..30e79ce --- /dev/null +++ b/syndicate_utils.nimble @@ -0,0 +1,13 @@ +# Package + +version = "0.1.0" +author = "Emery Hemingway" +description = "Utilites for Syndicated Actors and Synit" +license = "unlicense" +srcDir = "src" +bin = @["http_translator"] + + +# Dependencies + +requires "nim >= 1.6.6", "syndicate >= 0.3.1"