Add http_client
This commit is contained in:
parent
b3a417a072
commit
bf0b5d6b86
57
README.md
57
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 [
|
||||
?? <done { "code": "EUR" "exchange_middle": ?middle } > [
|
||||
$ds <exchange EUR RSD $middle>
|
||||
]
|
||||
]
|
||||
|
||||
$ds [
|
||||
<request
|
||||
# Request Euro to Dinar exchange rate.
|
||||
<http-request 0 "kurs.resenje.org" 443
|
||||
get ["api" "v1" "currencies" "eur" "rates" "today"]
|
||||
{Content-Type: "application/json"} {} #f
|
||||
>
|
||||
$response
|
||||
>
|
||||
|
||||
# Log all assertions.
|
||||
? ?any [
|
||||
$log ! <log "-" { assertion: $any }>
|
||||
]
|
||||
]
|
||||
|
||||
? <service-object <daemon http-client> ?cap> [
|
||||
$cap <http-client {
|
||||
dataspace: $ds
|
||||
}>
|
||||
]
|
||||
|
||||
<require-service <daemon http-client>>
|
||||
|
||||
? <built http-client ?path ?sum> [
|
||||
<daemon http-client {
|
||||
argv: [ "/bin/http_client" ]
|
||||
clearEnv: #t
|
||||
protocol: application/syndicate
|
||||
}>
|
||||
]
|
||||
```
|
||||
|
||||
## mintsturdyref
|
||||
|
||||
A utility for minting [Sturdyrefs](https://synit.org/book/operation/builtin/gatekeeper.html#sturdyrefs).
|
||||
|
|
|
@ -26,6 +26,10 @@ UnixAddress = <unix @path string>.
|
|||
|
||||
SocketAddress = TcpAddress / UnixAddress .
|
||||
|
||||
HttpClientArguments = <http-client {
|
||||
dataspace: #:any
|
||||
}>.
|
||||
|
||||
HttpDriverArguments = <http-driver {
|
||||
dataspace: #:any
|
||||
}>.
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
define:ssl
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue