Calling Otoroshi from a Frontend

Route plugins:
CORS plugin

Tutorial

  1. Before your start
  2. Create an archive with HTML and CSS files
  3. Use the Zip Backend Plugin

After completing these steps, you will be able to call Otoroshi with CORS rules applied using Otoroshi.

Before your start

If you already have an up and running otoroshi instance, you can skip the following instructions

Set up an Otoroshi

Let’s start by downloading the latest Otoroshi.

curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v17.0.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

Let’s start by creating an index.html file from where we will call our Otoroshi Route.

The contents of your index.html file should be likes this:

<!DOCTYPE html>
<html>

<head>
    <title>CORS tutorial</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/roboto-font@0.1.0/css/fonts.min.css">
</head>
<style>
    * {
        font-family: Roboto;
    }

    .flexcol {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        flex: 1;
    }

    body {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        min-height: 100vh;
    }

    button {
        padding: 1rem;
        border-radius: 1rem;
        border: none;
        cursor: pointer;
    }

    pre {
        display: flex;
        align-items: center;
        justify-content: center;
        word-wrap: break-word;
        overflow-wrap: break-word;
        white-space: pre-wrap;
        border: 1px solid #000;
        height: 100%;
        min-width: 10rem;
        min-height: 5rem;
        padding: 2rem;
        border-radius: .5rem;
    }

    h3 {
        margin: 0;
    }

    .result {
        display: flex;
        gap: 1rem;
    }

    .container {
        background-color: #eee;
        padding: 1rem;
        border-radius: 1rem;
        margin-top: 2rem;
    }

    .header {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        width: 100%;
    }

    .header span:first-child {
        font-weight: bold;
    }

    #headers {
        display: flex;
        flex-direction: column;
        align-items: start;
    }
</style>

<script>
    function callDemoRoute() {
        fetch('http://cors.oto.tools:9999', {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ foo: "bar" })
        })
            .then(r => {
                let headers = ""
                for (let entry of r.headers.entries()) {
                    headers += `<div class="header"><span>${entry[0]}</span> <span>${entry[1]}</span></div>`
                }
                document.getElementById('headers').innerHTML = headers
                return r.json()
            })
            .then(result => {
                document.getElementById('result').textContent = JSON.stringify(result, null, 4)
            })
            .catch(err => {
                let error = err.message;
                if (err.name === "TypeError" && err.message === "Failed to fetch")
                    error = "CORS error: The server is not allowing cross-origin requests."

                document.getElementById('headers').innerHTML = ''
                document.getElementById('result').textContent = error
            })
    }
</script>

<body>
    <h1>Welcome to the CORS tutorial</h1>

    <button onclick="callDemoRoute()">Call demo Otoroshi Route</button>

    <div class="container">
        <h2 style="font-style: italic;">Result of http://cors.oto.tools calls</h2>
        <div class="result">
            <div class="flexcol">
                <h3>Result</h3>
                <pre id="result"></pre>
            </div>
            <div class="flexcol">
                <h3>Headers</h3>
                <pre id="headers"></pre>
            </div>
        </div>
    </div>
</body>

</html>

Once created, you can expose it using http-server tool or any other HTTP server.

http-server .
# Available on:
  # http://127.0.0.1:8080

Now, click on the main button on the simple UI. Since nothing is configured yet, the call should result in a “failed to fetch” error, and you should see the following error in your browser’s developer tools:

Access to fetch at 'http://cors.oto.tools:9999/' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, 
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Use the CORS

Let’s create the route using the Otoroshi admin API. The route configuration is straightforward, with a few fields specifying the name, frontend, and CORS plugin in the list of plugins.

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": "cors route",
  "frontend": {
    "domains": ["cors.oto.tools"]
  },
  "backend": {
    "targets": [
      {
        "hostname": "request.otoroshi.io",
        "port": 443,
        "tls": true
      }
    ]
  },
  "plugins": [
    {
      "plugin": "cp:otoroshi.next.plugins.Cors",
      "enabled": true,
      "config": {
        "allow_origin": "*"
      }
    },
    {
      "plugin" : "cp:otoroshi.next.plugins.OverrideHost",
      "enabled": true
    }
  ]
}
EOF

Try calling your route again using the button.

@@@ div

This time, the response will be received, and the CORS rules will be checked by your browser. To see the headers added by Otoroshi in the response, expose these headers by specifying them in the plugin configuration.

Now open your browser to http://otoroshi.oto.tools:8080/, log in with the credential generated in the logs. and navigate to the CORS route previously created.

Edit the CORS plugin by adding an new entry * in the Expose headers input, and save your route. Try calling your route again using the button.

@@@ div { .centered-img }

You should now be able to see the headers sent back by Otoroshi. Let’s reduce the scope of allowed origins by restricting access to the route to your own domain only.

Edit the CORS plugin by replacing the * entry of the Allow origins input by ’http://localhost:8080, and save your route. Try calling your route again using the button.

Try calling your route again using the button.

You can filter the HTTP methods allowed to be used on your route. Edit the CORS plugin and add GET, POST, OPTIONS, HEAD methods to the list of Allow methods. Save your route and try calling your route again using the button.

Try calling your route again using the button.

One important parameter of the CORS Plugin is the Allow credentials flag. It allows the browser to send or not send credentials (such as cookies, authorization headers, and TLS client certificates) to the server.

Let’s test this scenario. Edit the index.html and add the following line to the script:

fetch(...., {
  headers ....,
  credentials: 'include'
})

Call the route again and toggle the Allow credentials flag in your CORS plugin to show the difference.