/** * ## Grain API for [http-wasm ABI](https://http-wasm.io/http-handler-abi/) * * Implements WASM foreign binding from [`./http-wasm-abi.gr`](./http-wasm-abi.gr). * Traefik plugin logic code can `include HttpWasm` to use native http-wasm functions, * and register functions that will be called when http-wasm's `handle_request` or `handle_response` * are called. */ module HttpWasm from "./http-wasm-abi.gr" include HttpWasmAbi from "./json-to.gr" include JsonTo from "runtime/unsafe/wasmi32" include WasmI32 from "runtime/unsafe/wasmi64" include WasmI64 from "runtime/unsafe/memory" include Memory from "runtime/dataStructures" include DataStructures as DS from "runtime/unsafe/conv" include Conv from "runtime/numberUtils" include NumberUtils from "runtime/debugPrint" include DebugPrint from "uint32" include Uint32 from "uri" include Uri from "runtime/string" include String as StringRuntime from "json" include Json from "string" include String from "result" include Result from "option" include Option from "json" include Json from "marshal" include Marshal from "map" include Map from "array" include Array from "bytes" include Bytes from "stack" include Stack from "list" include List from "queue" include Queue use HttpWasmAbi.* /** `Reguest` is provided as argument to any functions provided to `registerRequestHandler` */ provide record Request { method: String, path: String, headers: List, sourceAddr: String, protocolVersion: String, } /** `Response` is provided as argument to any functions provided to `registerResponseHandler` */ provide record Response { headers: List, } /* Function signature callback in `registerResponseHandler` */ provide type RequestHandler = Request => Bool /* Function signature callback in `registerResponseHandler` */ provide type ResponseHandler = Response => Void let defaultRequestHandler = (req: Request) => { // log(Debug, "hello world") // debug use true } let defaultResponseHandler = (resp: Response) => { // log(Info, "grain says hi world") // debug use void } let mut requestHandlers = List.init(1, n => defaultRequestHandler) let mut responseHandlers = List.init(1, n => defaultResponseHandler) /** See http-wasm ABI spec for [log_level](./http-wasm-abi.md#log_level) */ provide enum LogLevel { Debug, Info, Warn, Err, } /** See http-wasm ABI spec for [`features`](./http-wasm-abi.md#features) */ provide enum Features { BufferRequest, BufferResponse, Trailers, } /** See http-wasm ABI spec for [HttpWasmAbi._log](./http-wasm-abi.md#httpwasmabi_enable_features) > "whether the next handler on the host flushes the response prior to returning is implementation-specific..." */ @unsafe provide let enableFeatures = (features: List) => { use Uint32.{(+)} let mut flags = 0ul List.forEach(feature => { match (feature) { BufferRequest => flags = flags + 1ul, BufferResponse => flags = flags + 2ul, Trailers => flags = flags + 4ul, } }, features) _enableFeatures(Conv.fromUint32(flags)) void } /** See http-wasm ABI spec for [HttpWasmAbi._log](./http-wasm-abi.md#httpwasmabi_log) */ @unsafe provide let log = (level: LogLevel, msg: String) => { use WasmI32.{ (+) } let logLevel = match (level) { Debug => -1n, Info => 0n, Warn => 1n, Err => 2n, } let namePtr = WasmI32.fromGrain(msg) let nameLen = WasmI32.load(namePtr, 4n) _log(logLevel, namePtr + 8n, nameLen) ignore(msg) void } /** Get middleware dynamic configuration exposed from Traefik as string containing unparsed JSON string See http-wasm ABI spec for [HttpWasmAbi._getConfig](./http-wasm-abi.md#httpwasmabi_getconfig) */ @unsafe provide let getConfig = () => { use WasmI32.{ (+) } let lenbuf = _getConfig(0n, 0n) let strbuf = DS.allocateString(lenbuf) let len = _getConfig(strbuf + 8n, lenbuf) // _log(-1n, strbuf + 8n, len) // debug use WasmI32.toGrain(strbuf): String } /** _Grain helper function to parse JSON from Traefik, so not part of http-wasm ABI._ Middleware configuration exposed from Traefik via [`get_config`](./http-wasm-abi#get_config), as "raw" **Grain `Json.Json`** types. */ provide let hostJson = Json.parse(getConfig()) /** _Grain helper function to parse JSON from Traefik, so not part of http-wasm ABI_ Middleware configuration exposed from Traefik via [`get_config`](./http-wasm-abi#get_config), as **Grain `Map`** type. This can be used like: `Map.get("Headers.Foo", configMap)` */ provide let configMap = match (hostJson) { Ok(hostJson) => JsonTo.map(hostJson), _ => Map.make() } /** _Grain helper function to parse JSON from Traefik, so not part of http-wasm ABI_ Quick access to config data from `plugin.gr` code. For example: ```grain let fooHeaderValue = getConfigItem("Headers.Foo") ``` */ provide let getConfigItem = (name) => Map.get(name, configMap) /** See http-wasm ABI spec for [HttpWasmAbi._getMethod](./http-wasm-abi.md#httpwasmabi_getmethod) */ @unsafe provide let getMethod = () => { use WasmI32.{ (+) } let lenbuf = _getMethod(0n, 0n) let strbuf = DS.allocateString(lenbuf) let len = _getMethod(strbuf + 8n, lenbuf) // _log(-1n, strbuf + 8n, len) // debug use WasmI32.toGrain(strbuf): String } /** See http-wasm ABI spec for [HttpWasmAbi._getUri](./http-wasm-abi.md#httpwasmabi_geturi) */ @unsafe provide let getUri = () => { use WasmI32.{ (+) } let lenbuf = _getUri(0n, 0n) let strbuf = DS.allocateString(lenbuf) let len = _getUri(strbuf + 8n, lenbuf) // _log(-1n, strbuf + 8n, len) // debug use WasmI32.toGrain(strbuf): String } /** See http-wasm ABI spec for [HttpWasmAbi._getProtocolVersion](./http-wasm-abi.md#httpwasmabi_getprotocolversion) */ @unsafe provide let getProtocolVersion = () => { use WasmI32.{ (+) } let lenbuf = _getProtocolVersion(0n, 0n) let strbuf = DS.allocateString(lenbuf) let len = _getProtocolVersion(strbuf + 8n, lenbuf) // _log(-1n, strbuf + 8n, len) // debug use WasmI32.toGrain(strbuf): String } /** See http-wasm ABI spec for [HttpWasmAbi._getSourceAddr](./http-wasm-abi.md#httpwasmabi_getsourceaddr) */ @unsafe provide let getSourceAddr = () => { use WasmI32.{ (+) } let lenbuf = _getSourceAddr(0n, 0n) let strbuf = DS.allocateString(lenbuf) let len = _getSourceAddr(strbuf + 8n, lenbuf) // _log(-1n, strbuf + 8n, len) // debug use WasmI32.toGrain(strbuf): String } /** See http-wasm ABI spec for [header_kind](./http-wasm-abi.md#header_kind) */ provide enum HeaderKind { RequestHeader, ResponseHeader, } /** See http-wasm ABI spec for [HttpWasmAbi._getHeaderNames](./http-wasm-abi.md#httpwasmabi_getheadernames) */ @unsafe provide let getHeaderNames = (headerKind: HeaderKind) => { use WasmI32.{ (+), (-), (<), (>), (<) } use WasmI64.{ (>>) } // convert enum to i32 let kind = match (headerKind) { RequestHeader => 0n, ResponseHeader => 1n, } // figure out length of headers let lenmem = WasmI32.wrapI64(_getHeaderNames(kind, 0n, 0n)) // alloc memory for null-seperated list (which will be parsed) let strbuf = Memory.malloc(lenmem) //DS.allocateBytes(lenbuf) // get null-terminated headers let leni64 = _getHeaderNames(kind, strbuf, lenmem) let len = WasmI32.wrapI64(leni64) let lenheader = WasmI32.wrapI64(leni64 >> 32N) // parse for \0 and add found header to a stack let headers = Stack.make() let mut last = 0n for (let mut i = 0n; i < len; i += 1n) { let pos = strbuf + i // only if found null \0, do something if (WasmI32.eqz(WasmI32.load8U(pos, 0n))) { let headlen = i - last // now we allocate a Grain string to store header found let headmem = DS.allocateString(headlen) Memory.copy(headmem + 8n, strbuf + last, headlen) // build a grain strings from it, and push grain stack let grainStr = WasmI32.toGrain(headmem): String Stack.push(grainStr, headers) last = i + 1n } } // now free out memory used for the all header string just parsed Memory.free(strbuf) // finally, convert the stack to more friendly list. List.init(Stack.size(headers), e => match (Stack.pop(headers)) { Some(val) => val, _ => "", }): List } /** See http-wasm ABI spec for [HttpWasmAbi._getHeaderValues](./http-wasm-abi.md#httpwasmabi_getheadervalues) */ @unsafe provide let getHeaderValues = (headerKind: HeaderKind, name: String) => { use WasmI32.{ (+), (-) } use WasmI64.{ (>>) } // convert enum to i32 let kind = match (headerKind) { RequestHeader => 0n, ResponseHeader => 1n, } // get native ptr to grain inputs let namePtr = WasmI32.fromGrain(name) let nameLen = WasmI32.load(namePtr, 4n) // _log(-1n, namePtr + 8n, nameLen) // debug use // figure out length of headers let rawLength = _getHeaderValues(kind, namePtr + 8n, nameLen, 0n, 0n) let bufLen = WasmI32.wrapI64(rawLength) let elemLen = WasmI32.wrapI64(rawLength >> 32N) // early return if size == 0 if (WasmI32.eqz(elemLen) || WasmI32.eqz(bufLen)) { return "" } // get memory to store values let grainStrMem = DS.allocateString(bufLen) let ptrStr = grainStrMem + 8n // make http-wasm call to fill string buffer with header let newRawLength = _getHeaderValues( kind, namePtr + 8n, nameLen, ptrStr, bufLen ) let newBufLen = WasmI32.wrapI64(newRawLength) let newElemLen = WasmI32.wrapI64(newRawLength >> 32N) // _log(2n, grainStrMem + 8n, newBufLen) // debug use let grainStr = WasmI32.toGrain(grainStrMem): String ignore(name) return grainStr } /** **NOTE** plugin will panic if `addHeaderValue()` is called on existing header. Check `getHeaderName()` if header already exists _before_ call `addHeaderValue()`. See http-wasm ABI spec for [HttpWasmAbi._addHeaderValue](./http-wasm-abi.md#httpwasmabi_addheadervalue) */ @unsafe provide let addHeaderValue = ( headerKind: HeaderKind, name: String, value: String, ) => { use WasmI32.{ (+) } let kind = match (headerKind) { RequestHeader => 0n, ResponseHeader => 1n, } let namePtr = WasmI32.fromGrain(name) let nameLen = WasmI32.load(namePtr, 4n) let valuePtr = WasmI32.fromGrain(value) let valueLen = WasmI32.load(valuePtr, 4n) //_log(-1n, namePtr + 8n, nameLen) _addHeaderValue(kind, namePtr + 8n, nameLen, valuePtr + 8n, valueLen) ignore(name) ignore(value) void } /** **NOTE** plugin will panic if `setHeaderValue()` is called on non-existing header. Check `getHeaderName()` to make sure header exists _before_ call `setHeaderValue()`. See http-wasm ABI spec for [HttpWasmAbi._setHeaderValue](./http-wasm-abi.md#httpwasmabi_setheadervalue) */ @unsafe provide let setHeaderValue = ( headerKind: HeaderKind, name: String, value: String, ) => { use WasmI32.{ (+) } let kind = match (headerKind) { RequestHeader => 0n, ResponseHeader => 1n, } let namePtr = WasmI32.fromGrain(name) let nameLen = WasmI32.load(namePtr, 4n) let valuePtr = WasmI32.fromGrain(value) let valueLen = WasmI32.load(valuePtr, 4n) //_log(-1n, namePtr + 8n, nameLen) _setHeaderValue(kind, namePtr + 8n, nameLen, valuePtr + 8n, valueLen) ignore(name) ignore(value) void } /** See http-wasm ABI spec for [HttpWasmAbi._removeHeader](./http-wasm-abi.md#httpwasmabi_removeheader) */ @unsafe provide let removeHeader = (headerKind: HeaderKind, name: String) => { use WasmI32.{ (+) } let kind = match (headerKind) { RequestHeader => 0n, ResponseHeader => 1n, } let namePtr = WasmI32.fromGrain(name) let nameLen = WasmI32.load(namePtr, 4n) //_log(-1n, namePtr + 8n, nameLen) _removeHeader(kind, namePtr + 8n, nameLen) ignore(name) void } /** #### Response Only **NOTE** will panic if status code has not be previously set. See http-wasm ABI spec for [HttpWasmAbi._getStatusCode](./http-wasm-abi.md#httpwasmabi_getstatuscode) */ @unsafe provide let getStatusCode = () => { let statusCode = _getStatusCode() Conv.toInt32(statusCode) } /** **This is critical to how `plugin.gr` works.** The interface between `handle_request` and Grain Traefik Plugin code is one or more calls to `registerRequestHandler`. This module will use list of "registered handlers" when a call from host (Traefik) to `handle_request` is invoked. _All registered functions must return `true` or `false` to indicate whether HTTP processing should continue._ */ provide let registerRequestHandler = (fn: RequestHandler) => requestHandlers = List.insert( List.length(requestHandlers), fn, requestHandlers ) provide let registerResponseHandler = (fn: ResponseHandler) => responseHandlers = List.insert( List.length(responseHandlers), fn, responseHandlers ) /** The interface between `handle_response` and Grain Traefik Plugin code is one or more calls to `registerResponseHandler`. This module will use list of "registered handlers" when a call from host (Traefik) to `handle_response` is invoked. _Once in `handle_response`, the request is being sent regardless. So response handlers in `plugin.gr` always return `void`._ */ /** See http-wasm ABI spec for [handle_request](./http-wasm-abi.md#handle_request) */ @unsafe provide let handle_request = () => { log(Debug, "wasm-grain plugin start at handle_request") let next = List.every( onRequest => onRequest( { method: getMethod(), path: getUri(), headers: getHeaderNames(RequestHeader), sourceAddr: getSourceAddr(), protocolVersion: getProtocolVersion(), } ), requestHandlers ) match (next) { true => 1N, false => 0N, } } /** See http-wasm ABI spec for [handle_response](./http-wasm-abi.md#handle_response) */ @unsafe provide let handle_response = (high: WasmI32, low: WasmI32) => { List.forEach( onResponse => onResponse({ headers: getHeaderNames(ResponseHeader), }), responseHandlers ) log(Debug, "wasm-grain plugin done after handle_response") void } /** Helper to remove C-like null-terminated strings that http-wasm returns. **Returns** String to the null-terminator > Traefik logging seems to ignore them. > However Grain `Number.parse()` treats null-terminated string as invalid char. */ provide let stripNulls = (str) => { match (String.contains("\0", str)) { true => Array.get(0, String.split("\0", str)), false => str } } /** * For debugging, dump strings as codepoints to see tailing non-printables */ provide let dumpCodePoints = (str) => { let fifo = Queue.make() String.forEachCodePoint(n => { Queue.push(toString(n), fifo) }, str) Array.join(",", Queue.toArray(fifo)) }