From bf0b5d6b866f134cf156b50d954b35454efe39db Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Thu, 9 May 2024 06:58:00 +0200 Subject: [PATCH] Add http_client --- README.md | 57 ++++++++++++++++++++++++++ config.prs | 4 ++ src/http_client.nim | 91 +++++++++++++++++++++++++++++++++++++++++ src/http_client.nim.cfg | 1 + src/schema/config.nim | 13 +++++- syndicate_utils.nimble | 4 +- 6 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 src/http_client.nim create mode 100644 src/http_client.nim.cfg diff --git a/README.md b/README.md index 901c9fa..bca8064 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,63 @@ Examples: --- +## http_client + +The inverse of `http-driver`. + +### Caveats +- HTTPS is assumed unless the request is to port 80. +- If the request or response sets `Content-Type` to `application/json` or `…/preserves` + the body will be a parsed Preserves value. +- No cache support. +- Internal errors propagate using a `400 Internal client error` response. + +Sample Syndicate server script: +``` +# A top-level dataspace +let ?ds = dataspace + +# A dataspace for handling the HTTP response. +let ?response = dataspace +$response [ + ?? [ + $ds + ] +] + +$ds [ + + $response + > + + # Log all assertions. + ? ?any [ + $log ! + ] +] + +? ?cap> [ + $cap +] + +> + +? [ + +] +``` + ## mintsturdyref A utility for minting [Sturdyrefs](https://synit.org/book/operation/builtin/gatekeeper.html#sturdyrefs). diff --git a/config.prs b/config.prs index 4093676..0ccd83f 100644 --- a/config.prs +++ b/config.prs @@ -26,6 +26,10 @@ UnixAddress = . SocketAddress = TcpAddress / UnixAddress . +HttpClientArguments = . + HttpDriverArguments = . diff --git a/src/http_client.nim b/src/http_client.nim new file mode 100644 index 0000000..90ea44e --- /dev/null +++ b/src/http_client.nim @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +# TODO: write a TAPS HTTP client. Figure out how to externalise TLS. + +import + std/[httpclient, options, streams, strutils, tables, uri], + pkg/taps, + pkg/preserves, + pkg/syndicate, pkg/syndicate/protocols/http, + ./schema/config + +proc url(req: HttpRequest): Uri = + result.scheme = if req.port == 80: "http" else: "https" + result.hostname = req.host.present + result.port = $req.port + for i, p in req.path: + if 0 < i: result.path.add '/' + result.path.add p.encodeUrl + for key, vals in req.query: + if result.query.len > 0: + result.query.add '&' + result.query.add key.string.encodeUrl + for i, val in vals: + if i == 0: result.query.add '=' + elif i < vals.high: result.query.add ',' + result.query.add val.string.encodeUrl + +proc bodyString(req: HttpRequest): string = + if req.body.orKind == RequestBodyKind.present: + return cast[string](req.body.present) + +proc spawnHttpClient*(turn: Turn; root: Cap): Actor {.discardable.} = + + during(turn, root, ?:HttpClientArguments) do (ds: Cap): + spawn("http-client", turn) do (turn: Turn): + during(turn, ds, HttpContext.grabType) do (ctx: HttpContext): + let peer = ctx.res.unembed(Cap).get + var client = newHttpClient() + try: + var + headers = newHttpHeaders() + contentType = "" + for key, val in ctx.req.headers: + if key == Symbol"Content-Type": + contentType = val + client.headers[key.string] = val + let stdRes = client.request( + ctx.req.url, + ctx.req.method.string.toUpper, + ctx.req.bodyString, headers + ) + var resp = HttpResponse(orKind: HttpResponseKind.status) + resp.status.code = stdRes.status[0 .. 2].parseInt + resp.status.message = stdRes.status[3 .. ^1] + message(turn, peer, resp) + resp = HttpResponse(orKind: HttpResponseKind.header) + for key, val in stdRes.headers: + if key == "Content-Type": + contentType = val + resp.header.name = key.Symbol + resp.header.value = val + message(turn, peer, resp) + case contentType + of "application/json", "text/preserves": + message(turn, peer, + initRecord("done", stdRes.bodyStream.readAll.parsePreserves)) + of "application/preserves": + message(turn, peer, + initRecord("done", stdRes.bodyStream.decodePreserves)) + else: + resp = HttpResponse(orKind: HttpResponseKind.done) + resp.done.chunk.string = stdRes.bodyStream.readAll() + message(turn, peer, resp) + except CatchableError as err: + var resp = HttpResponse(orKind: HttpResponseKind.status) + resp.status.code = 400 + resp.status.message = "Internal client error" + message(turn, peer, resp) + resp = HttpResponse(orKind: HttpResponseKind.done) + resp.done.chunk.string = err.msg + message(turn, peer, resp) + client.close() + do: + client.close() + +when isMainModule: + import syndicate/relays + runActor("main") do (turn: Turn): + resolveEnvironment(turn) do (turn: Turn; ds: Cap): + spawnHttpClient(turn, ds) diff --git a/src/http_client.nim.cfg b/src/http_client.nim.cfg new file mode 100644 index 0000000..1f92ea5 --- /dev/null +++ b/src/http_client.nim.cfg @@ -0,0 +1 @@ +define:ssl diff --git a/src/schema/config.nim b/src/schema/config.nim index 4bcaaf4..efbd828 100644 --- a/src/schema/config.nim +++ b/src/schema/config.nim @@ -10,6 +10,12 @@ type WebsocketArguments* {.preservesRecord: "websocket".} = object `field0`*: WebsocketArgumentsField0 + HttpClientArgumentsField0* {.preservesDictionary.} = object + `dataspace`* {.preservesEmbedded.}: EmbeddedRef + + HttpClientArguments* {.preservesRecord: "http-client".} = object + `field0`*: HttpClientArgumentsField0 + JsonTranslatorArgumentsField0* {.preservesDictionary.} = object `argv`*: seq[string] `dataspace`* {.preservesEmbedded.}: EmbeddedRef @@ -117,7 +123,8 @@ type `host`*: string `port`*: BiggestInt -proc `$`*(x: WebsocketArguments | JsonTranslatorArguments | SocketAddress | +proc `$`*(x: WebsocketArguments | HttpClientArguments | JsonTranslatorArguments | + SocketAddress | Base64DecoderArguments | JsonTranslatorConnected | JsonSocketTranslatorArguments | @@ -136,7 +143,9 @@ proc `$`*(x: WebsocketArguments | JsonTranslatorArguments | SocketAddress | Tcp): string = `$`(toPreserves(x)) -proc encode*(x: WebsocketArguments | JsonTranslatorArguments | SocketAddress | +proc encode*(x: WebsocketArguments | HttpClientArguments | + JsonTranslatorArguments | + SocketAddress | Base64DecoderArguments | JsonTranslatorConnected | JsonSocketTranslatorArguments | diff --git a/syndicate_utils.nimble b/syndicate_utils.nimble index ffcb71c..f6e040a 100644 --- a/syndicate_utils.nimble +++ b/syndicate_utils.nimble @@ -1,11 +1,11 @@ # Package -version = "20240430" +version = "20240509" author = "Emery Hemingway" description = "Utilites for Syndicated Actors and Synit" license = "unlicense" srcDir = "src" -bin = @["mintsturdyref", "mount_actor", "msg", "postgre_actor", "preserve_process_environment", "rofi_script_actor", "sqlite_actor", "syndesizer", "syndump", "xslt_actor"] +bin = @["http_client", "mintsturdyref", "mount_actor", "msg", "postgre_actor", "preserve_process_environment", "rofi_script_actor", "sqlite_actor", "syndesizer", "syndump", "xslt_actor"] # Dependencies