Using wasm plugins
WebAssembly (WASM) is a simple machine model and executable format with an extensive specification. It is designed to be portable, compact, and execute at or near native speeds. Otoroshi already supports the execution of WASM files by providing different plugins that can be applied on routes. You can find more about those plugins here
To simplify the process of WASM creation and usage, Otoroshi provides:
- otoroshi ui integration: a full set of plugins that let you pick which WASM function to runtime at any point in a route
- otoroshi
wasmo
: a code editor in the browser that let you write your plugin inRust
,TinyGo
,Javascript
orAssembly Script
without having to think about compiling it to WASM (you can find a complete tutorial about it here)
Tutorial
- Before your start
- Create the route with the plugin validator
- Test your validator
- Update the route by replacing the backend with a WASM file
- WASM backend test
- Expose a single file as WASM backend
After completing these steps you will have a route that uses WASM plugins written in Rust.
Before your start
If you already have an up and running otoroshi instance, you can skip the following instructions
Let’s start by downloading the latest Otoroshi.
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.20.1/otoroshi.jar'
then you can run start Otoroshi :
java -Dotoroshi.adminPassword=password -jar otoroshi.jar
Now you can log into Otoroshi at http://otoroshi.oto.tools:8080 with admin@otoroshi.io/password
Create a new route, exposed on http://myservice.oto.tools:8080
, which will forward all requests to the mirror https://request.otoroshi.io
. Each call to this service will returned the body and the headers received by the mirror.
curl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
"name": "my-service",
"frontend": {
"domains": ["myservice.oto.tools"]
},
"backend": {
"targets": [
{
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true
}
]
}
}
EOF
Create the route with the plugin validator
For this tutorial, we will start with an existing wasm file. The main function of this file will check the value of an http header to allow access or not. The can find this file at https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm
The main function of this validator, written in rust, should look like:
- validator.rs
-
mod types; use extism_pdk::*; #[plugin_fn] pub fn execute(Json(context): Json<types::WasmAccessValidatorContext>) -> FnResult<Json<types::WasmAccessValidatorResponse>> { let out = types::WasmAccessValidatorResponse { result: false, error: Some(types::WasmAccessValidatorError { message: "you're not authorized".to_owned(), status: 401 }) }; match context.request.headers.get("foo") { Some(foo) => if foo == "bar" { Ok(Json(types::WasmAccessValidatorResponse { result: true, error: None })) } else { Ok(Json(types::WasmAccessValidatorResponse { result: false, error: Some(types::WasmAccessValidatorError { message: format!("{} is not authorized", foo).to_owned(), status: 401 }) })) }, None => Ok(Json(out)) } }
- validator.js
-
export function execute() { let context = JSON.parse(Host.inputString()); if (context.request.headers["foo"] === "bar") { const out = { result: true }; Host.outputString(JSON.stringify(out)); } else { const error = { result: false, error: { message: "you're not authorized", status: 401 } }; Host.outputString(JSON.stringify(error)); } return 0; }
- validator.ts
-
import { WasmAccessValidatorContext, WasmAccessValidatorResponse } from './types'; export declare var Host: any; export function execute() { let context = JSON.parse(Host.inputString()) as WasmAccessValidatorContext; if (context.request.headers["foo"] === "bar") { const out: WasmAccessValidatorResponse = { result: true }; Host.outputString(JSON.stringify(out)); } else { const error: WasmAccessValidatorResponse = { result: false, error: { message: "you're not authorized", status: 401 } }; Host.outputString(JSON.stringify(error)); } return 0; }
- validator.js
-
export function execute() { let context = JSON.parse(Host.inputString()); if (context.request.headers["foo"] === "bar") { const out = { result: true }; Host.outputString(JSON.stringify(out)); } else { const error = { result: false, error: { message: "you're not authorized", status: 401 } }; Host.outputString(JSON.stringify(error)); } return 0; }
- validator.go
-
package main import ( "github.com/extism/go-pdk" "github.com/buger/jsonparser" ) //export execute func execute() int32 { input := pdk.Input() var foo, err = jsonparser.GetString(input, "request", "headers", "foo") if err != nil {} var output = "" if foo == "bar" { output = `{ "result": true }` } else { output = `{ "result": false, "error": { "message": "you're not authorized", "status": 401 } }` } mem := pdk.AllocateString(output) pdk.OutputMemory(mem) return 0 } func main() {}
The plugin receives the request context from Otoroshi (the matching route, the api key if present, the headers, etc) as WasmAccessValidatorContext
object. Then it applies a check on the headers, and responds with an error or success depending on the content of the foo header. Obviously, the previous snippet is an example and the editor allows you to write whatever you want as a check.
Let’s create a route that uses the previous wasm file as an access validator plugin :
curl -X POST "http://otoroshi-api.oto.tools:8080/api/routes" \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
"id": "demo-otoroshi",
"name": "demo-otoroshi",
"frontend": {
"domains": ["demo-otoroshi.oto.tools"]
},
"backend": {
"targets": [
{
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true
}
],
"load_balancing": {
"type": "RoundRobin"
}
},
"plugins": [
{
"plugin": "cp:otoroshi.next.plugins.OverrideHost",
"enabled": true
},
{
"plugin": "cp:otoroshi.next.plugins.WasmAccessValidator",
"enabled": true,
"config": {
"source": {
"kind": "http",
"path": "https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm",
"opts": {}
},
"memoryPages": 4,
"functionName": "execute"
}
}
]
}
EOF
This request will apply the following process:
- names the route demo-otoroshi
- creates a frontend exposed on the
demo-otoroshi.oto.tools
- forward requests on one target, reachable at
request.otoroshi.io
using TLS on port 443 - adds the WasmAccessValidator plugin to validate access based on the foo header to the route
You can validate the route creation by navigating to the dashboard
Test your validator
curl "http://demo-otoroshi.oto.tools:8080" -I
This should output the following error:
HTTP/1.1 401 Unauthorized
Let’s call again the route by adding the header foo with the bar value.
curl "http://demo-otoroshi.oto.tools:8080" -H "foo:bar" -I
This should output the successfull message:
HTTP/1.1 200 OK
Update the route by replacing the backend with a WASM file
The next step in this tutorial is to use a WASM file as backend of the route. We will use an existing WASM file, available in our wasm demos repository on github. The content of this plugin, called wasm-target.wasm
, looks like:
- target.rs
-
// (1) mod types; // (2) use extism_pdk::*; use std::collections::HashMap; #[plugin_fn] pub fn execute(Json(context): Json<types::WasmQueryContext>) -> FnResult<Json<types::WasmQueryResponse>> { // (3) let mut headers = HashMap::new(); headers.insert("foo".to_string(), "bar".to_string()); // (4) let response = types::WasmQueryResponse { headers: Some(headers.into_iter().chain(context.raw_request.headers).collect()), body: "{\"foo\": \"bar\"}".to_owned(), status: 200 }; Ok(Json(response)) }
- target.js
-
// (2) export function execute() { let str = Host.inputString(); let context = JSON.parse(str); // (3) let headers = { ...context.request.headers }; headers["foo"] = "bar"; // (4) let response = { headers, body: "{\"foo\": \"bar\"}", status: 200 }; Host.outputString(JSON.stringify(response)); return 0; }
- target.ts
-
// (1) import { WasmQueryContext, WasmQueryResponse } from './types'; export declare var Host: any; // (2) export function execute() { const context = JSON.parse(Host.inputString()) as WasmQueryContext; // (3) const headers = { "foo": "bar", ...(context.request.headers || {}) } // (4) const response: WasmQueryResponse = { headers, status: 200, body: "{\"foo\": \"bar\"}" }; Host.outputString(JSON.stringify(response)); return 0; }
- target.go
-
package main // (1) import ( "github.com/extism/go-pdk" "github.com/buger/jsonparser" ) // (2) //export execute func execute() int32 { input := pdk.Input() // (3) var headers, dataType, offset, err = jsonparser.Get(input, "request", "headers") _ = dataType _ = offset if err != nil {} // (4) output := `{"headers":` + string(headers) + `, "body": {\"foo\": \"bar\"}, "status": 200 }` mem := pdk.AllocateString(output) pdk.OutputMemory(mem) return 0 } func main() {}
Let’s explain this snippet. The purpose of this type of plugin is to respond an HTTP response with http status, body and headers map.
- Includes all public structures from
types.rs
file. This file contains predefined Otoroshi structures that plugins can manipulate. - Necessary imports. Extism’s goal is to make all software programmable by providing a plug-in system.
- Creates a map of new headers that will be merged with incoming request headers.
- Creates the response object with the map of merged headers, a simple JSON body and a successfull status code.
The file is downloadable here.
Let’s update the route using the this wasm file.
curl -X PUT "http://otoroshi-api.oto.tools:8080/api/routes/demo-otoroshi" \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
"id": "demo-otoroshi",
"name": "demo-otoroshi",
"frontend": {
"domains": ["demo-otoroshi.oto.tools"]
},
"backend": {
"targets": [
{
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true
}
],
"load_balancing": {
"type": "RoundRobin"
}
},
"plugins": [
{
"plugin": "cp:otoroshi.next.plugins.OverrideHost",
"enabled": true
},
{
"plugin": "cp:otoroshi.next.plugins.WasmAccessValidator",
"enabled": true,
"config": {
"source": {
"kind": "http",
"path": "https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/first-validator.wasm",
"opts": {}
},
"memoryPages": 4,
"functionName": "execute"
}
},
{
"plugin": "cp:otoroshi.next.plugins.WasmBackend",
"enabled": true,
"config": {
"source": {
"kind": "http",
"path": "https://raw.githubusercontent.com/MAIF/otoroshi/master/demos/wasm/wasm-target.wasm",
"opts": {}
},
"memoryPages": 4,
"functionName": "execute"
}
}
]
}
EOF
The response should contains the updated route content.
WASM backend test
Let’s call our route.
curl "http://demo-otoroshi.oto.tools:8080" -H "foo:bar" -H "fifi: foo" -v
This should output:
* Trying 127.0.0.1:8080...
* Connected to demo-otoroshi.oto.tools (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: demo-otoroshi.oto.tools:8080
> User-Agent: curl/7.79.1
> Accept: */*
> foo:bar
> fifi:foo
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< foo: bar
< Host: demo-otoroshi.oto.tools:8080
<
* Closing connection 0
{"foo": "bar"}
In this response, we can find our headers send in the curl command and those added by the wasm plugin.
Expose a single file as WASM backend
A WASM backend plugin can directly expose a file written in Wasmo. This is the simplest possibility to write HTML, Javascript, JSON or expose a simple PNG image.
Let’s expose a HTML page. In your Wasmo instance, execute the following instructions:
- Click the new plugin button
- Add a name and
validate
- Click the new plugin
- Create a new file named
index.html
- Copy and paste the following content
<!DOCTYPE html>
<html>
<head>
<title>Wasmo plugin</title>
</head>
<body>
<h1>Hello from Wasmo</h1>
</body>
</html>
This snippet is a short HTML template with a title to indicate that it comes from Wasmo.
Now we can write our javascript function to parse and return the content of our HTML to Otoroshi.
- Navigate to the
index.js
file - Replace the content with the following content
import IndexPage from "./index.html";
export function execute() {
let response = {
headers: {
"Content-Type": "text/html; charset=utf-8",
},
body: IndexPage,
status: 200,
};
Host.outputString(JSON.stringify(response));
return 0;
}
The code is pretty self-explanatory. We start by importing our HTML page and we build the response with the correct content type, the body and a 200 http status.
Just before testing, we need to change the esbuild configuration to specify how to bundle the HTML file.
The contents of your esbuild.js
file should look this:
const esbuild = require("esbuild");
esbuild.build({
entryPoints: ["index.js"],
outdir: "dist",
bundle: true,
loader: {
".html": "text",
},
sourcemap: true,
minify: false, // might want to use true for production build
format: "cjs", // needs to be CJS for now
target: ["es2020"], // don't go over es2020 because quickjs doesn't support it
});
Check your browser at http://demo-otoroshi.oto.tools:8080
and you should see your page content updated to the new text.
If you need to expose more than a HTML page, we highly recommend to use the Zip Backend plugin