Deploy your own Wasmo
Installing Wasmo can be done by following the Getting Started in Wasmo documentation.
Tutorial
After completing these steps you will have a running Otoroshi instance and our owm Wasmo linked together.
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.19.0/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 a route to expose and protect Wasmo with authentication
We are going to use the admin API of Otoroshi to create the route. The configuration of the route is:
wasmo
as namewasmo.oto.tools
as exposed domainlocalhost:5001
as target without TLS option enabled
We need to add two more plugins to require the authentication from users and to pass the logged in user to Wasmo. These plugins are named Authentication
and Otoroshi Info. token
. The Authentication plugin will use an in-memory authentication with one default user (wasm@otoroshi.io/password). The second plugin will be configured with the value of the OTOROSHI_USER_HEADER
environment variable.
Let’s create the authentication module (if you are interested in how authentication module works, you should read the other tutorials about How to secure an app). The following command creates an in-memory authentication module with an user.
curl -X POST "http://otoroshi-api.oto.tools:8080/api/auths" \
-u "admin-api-apikey-id:admin-api-apikey-secret" \
-H 'Content-Type: application/json; charset=utf-8' \
-d @- <<'EOF'
{
"id": "wasmo_in_memory",
"type": "basic",
"name": "In memory authentication",
"desc": "Group of static users",
"users": [
{
"name": "User Otoroshi",
"password": "$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i",
"email": "wasm@otoroshi.io"
}
],
"sessionCookieValues": {
"httpOnly": true,
"secure": false
}
}
EOF
Once created, you can create our route to expose Wasmo.
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": "wasmo",
"name": "wasmo",
"frontend": {
"domains": ["wasmo.oto.tools"]
},
"backend": {
"targets": [
{
"hostname": "localhost",
"port": 5001,
"tls": false
}
],
"load_balancing": {
"type": "RoundRobin"
}
},
"plugins": [
{
"enabled": true,
"plugin": "cp:otoroshi.next.plugins.AuthModule",
"exclude": [
"/plugins",
"/wasm/.*"
],
"config": {
"pass_with_apikey": false,
"auth_module": null,
"module": "wasmo_in_memory"
}
},
{
"enabled": true,
"plugin": "cp:otoroshi.next.plugins.ApikeyCalls",
"include": [
"/plugins",
"/wasm/.*"
],
"config": {}
},
{
"enabled": true,
"plugin": "cp:otoroshi.next.plugins.OtoroshiInfos",
"config": {
"version": "Latest",
"ttl": 30,
"header_name": "Otoroshi-User",
"algo": {
"type": "HSAlgoSettings",
"size": 512,
"secret": "veryverysecret"
}
}
}
]
}
EOF
Try to access to Wasmo with the new domain: http://wasmo.oto.tools:8080. This should redirect you to the login page of Otoroshi. Enter the credentials of the user: wasm@otoroshi.io/password Congratulations, you now have secured Wasmo.
Create a first validator plugin using Wasmo
In the previous part, we secured the access to Wasmo. Now, is the time to create your first simple plugin, written in Rust. This plugin will apply a check on the request and ensure that the headers contains the key-value foo:bar.
- On the right top of the screen, click on the plus icon to create a new plugin
- Select the Rust language
- Call it
my-first-validator
and press the enter key - Click on the new plugin called
my-first-validator
Before continuing, let’s explain the different files already present in your plugin.
types.rs
: this file contains all Otoroshi structures that the plugin can receive and respondlib.rs
: this file is the core of your plugin. It must contain at least one function which will be called by Otoroshi when executing the plugin.Cargo.toml
: for each rust package, this file is called its manifest. It is written in the TOML format. It contains metadata that is needed to compile the package. You can read more information about it here
You can write a plugin for different uses cases in Otoroshi: validate an access, transform request or generate a target. In terms of plugin type, you need to change your plugin’s context and reponse types accordingly.
Let’s take the example of creating a validator plugin. If we search in the types.rs file, we can found the corresponding types named: WasmAccessValidatorContext
and WasmAccessValidatorResponse
. These types must be use in the declaration of the main function (named execute in our case).
...
pub fn execute(Json(context): Json<types::WasmAccessValidatorContext>) -> FnResult<Json<types::WasmAccessValidatorResponse>> {
}
With this code, we declare a function named execute
, which takes a context of type WasmAccessValidatorContext as parameter, and which returns an object of type WasmAccessValidatorResponse. Now, let’s add the check of the foo header.
...
pub fn execute(Json(context): Json<types::WasmAccessValidatorContext>) -> FnResult<Json<types::WasmAccessValidatorResponse>> {
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(types::WasmAccessValidatorResponse {
result: false,
error: Some(types::WasmAccessValidatorError {
message: "you're not authorized".to_owned(),
status: 401
})
}))
}
}
First, we checked if the foo header is present, otherwise we return an object of type WasmAccessValidatorError. In the other case, we continue by checking its value. In this example, we have used three types, already declared for you in the types.rs file: WasmAccessValidatorResponse
, WasmAccessValidatorError
and WasmAccessValidatorContext
.
At this time, the content of your lib.rs file should be:
mod types;
use extism_pdk::*;
#[plugin_fn]
pub fn execute(Json(context): Json<types::WasmAccessValidatorContext>) -> FnResult<Json<types::WasmAccessValidatorResponse>> {
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(types::WasmAccessValidatorResponse {
result: false,
error: Some(types::WasmAccessValidatorError {
message: "you're not authorized".to_owned(),
status: 401
})
}))
}
}
Let’s compile this plugin by clicking on the hammer icon at the right top of your screen. Once done, you can try your built plugin directly in the UI. Click on the play button at the right top of your screen, select your plugin and the correct type of the incoming fake context. Once done, click on the run button at the bottom of your screen. This should output an error.
{
"result": false,
"error": {
"message": "asd is not authorized",
"status": 401
}
}
Let’s edit the fake input context by adding the exepected foo Header.
{
"request": {
"id": 0,
"method": "",
"headers": {
"foo": "bar"
},
"cookies"
...
Resubmit the command. It should pass.
Pairing Otoroshi and Wasmo
Now that we have our compiled plugin, we have to connect Otoroshi with Wasmo. Let’s navigate to the danger zone, and add the following values in the Wasmo section:
URL
: http://localhost:5001 or http://wasmo.oto.tools:8080Apikey id
: admin-api-apikey-idApikey secret
: admin-api-apikey-secretUser(s)
: *Token secret
:
The User(s) property is used by Wasmo to filter the list of returned plugins (example: wasm@otoroshi.io will only return the list of plugins created by this user).
Don’t forget to save the configuration.
Create a route using the generated wasm file
The last step of our tutorial is to create the route using the validator. Let’s create the route with the following parameters:
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": "wasm-route",
"name": "wasm-route",
"frontend": {
"domains": ["wasm-route.oto.tools"]
},
"backend": {
"targets": [
{
"hostname": "localhost",
"port": 5001,
"tls": false
}
],
"load_balancing": {
"type": "RoundRobin"
}
},
"plugins": [
{
"plugin": "cp:otoroshi.next.plugins.WasmAccessValidator",
"enabled": true,
"config": {
"compiler_source": "my-first-validator",
"functionName": "execute"
}
}
]
}
EOF
You can validate the creation by navigating to the dashboard
Test your route
Run the two following commands. The first should show an unauthorized error and the second should conclude this tutorial.
curl "http://wasm-route.oto.tools:8080"
and
curl "http://wasm-route.oto.tools:8080" -H "foo:bar"
Congratulations, you have successfully written your first validator using your own Wasmo.