# HTTP Plugins

> Octelium documentation. Canonical page: <https://octelium.com/docs/octelium/latest/management/core/service/http-plugins>.

## Overview

Octelium already supports native ways to manipulate HTTP request/response headers, validate request JSON body and send direct responses back to the downstreams (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/http.md)). However, in many real-world use cases, such native methods might be not flexible enough.

Plugins provide an additional, more advanced and dynamic way to manipulate HTTP requests and responses in arbitrarily complex ways. Such plugins can be especially useful for many use cases that can be applied to ZTNA/BeyondCorp, API gateways, AI gateways, MCP gateways, etc...

A plugin has a required unique name, a required condition that decides whether the plugin should be triggered on a per-request basis via policy-as-code, and the type-specific configuration (e.g. Lua, rate limiting, etc... as will be shown below). Uoi can have one or more plugins, including multiple plugins of the same type. For example, here is a list of plugins that includes one lua plugin that is triggered on all requests.

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: my-plugin
          # !mark(1:2)
          condition:
            matchAny: true
          lua:
            # The rest of your config
```

Here is another example where the plugin is only used for requests whose path prefix is `/api/v1`:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: my-plugin
          # !mark(1:2)
          condition:
            match: ctx.request.http.path.startsWith("/api/v1")
          lua:
            # The rest of your config
```

## Disabling a Plugin

You can also disable a specific plugin without having to change its condition or delete it as follows:

```yaml
kind: Service
metadata:
  name: svc1
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: my-plugin
          #!mark
          isDisabled: true
          condition:
            matchAny: true
          lua:
            # The rest of your config
```

## Phase

By default, plugins are invoked after the authentication and authorization processes are done. However, you might choose to invoke a plugin before authentication/authorization by explicity setting a `phase` value for that plugin. Currently the values available are: `POST_AUTH` which is the default behavior when not explicitly set, and `PRE_AUTH` to force a pre-auth invocation. `PRE_AUTH` plugins might be useful to do pre-authentication manipulation or even completely dropping the requests before proceeding further to the authentication/authorization process.

Note that `PRE_AUTH` plugins must be in the `default` *Service* configuration. In other words, you can not have a `PRE_AUTH` in a named dynamic config (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/dynamic-config.md)). Here is an example:

```yaml
kind: Service
metadata:
  name: svc1
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: check-body-size
        #!mark
          phase: PRE_AUTH
          condition:
            matchAny: true
          lua:
            # The rest of your config
```

## Types

### Lua

Lua plugin invokes a Lua script that includes two functions: `onRequest` that is invoked upon receiving the request before proxying it to the upstream, and `onResponse` that is invoked upon receiving the response from the upstream. Here is a detailed example:

```yaml
kind: Service
metadata:
  name: svc1
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: my-lua-script
          condition:
            matchAny: true
          lua:
            inline: |
              function onRequest(ctx)
                octelium.req.setRequestHeader("X-User-Uid", ctx.user.metadata.uid)
                octelium.req.deleteRequestHeader("X-Delete")
                octelium.req.setQueryParam("user", ctx.user.metadata.name)
                octelium.req.deleteQueryParam("param1")
                local body = json.decode(octelium.req.getRequestBody())
                if strings.contains(strings.toLower(strings.trim(base64.decode(body.strField))), "privileged") then
                  local res = {
                    error = "You are not allowed"
                  }
                  octelium.req.setResponseBody(json.encode(res))
                  octelium.req.exit(403)
                  return
                end

                if strings.hasPrefix(ctx.request.http.path, "/users") then
                  octelium.req.setPath("/users/"..ctx.user.metadata.uid)
                end

                if strings.hasSuffix(ctx.request.http.path, ".php") then
                  octelium.req.setPath("/users/"..ctx.user.metadata.uid)
                end

                if strings.len(body.someStrField) > 1000 then
                  local c = http.client()
                  c:setBaseURL("http://my-api.default.svc")
                  local req = c:request()
                  req:setBody(json.encode(ctx.user))
                  req:setHeader("X-User-Uid", ctx.user.metadata.uid)
                  local resp, err = req:post("/v1/check-user")
                  if err then
                    octelium.req.exit(500)
                    return
                  end

                  if resp:code() == 200 then
                    local apiResp = json.decode(resp:body())
                    if not apiResp.userIsAllowed then
                      octelium.req.exit(403)
                      return
                    end
                  end
                end
              end

              function onResponse(ctx)
                octelium.req.setResponseHeader("X-Session-Uid", ctx.session.metadata.uid)
                octelium.req.deleteResponseHeader("X-Delete")
                local resp = json.decode(octelium.req.getResponseBody())
                resp.email = ctx.user.spec.email
                octelium.req.setResponseBody(json.encode(resp))
                octelium.req.setStatusCode(200)
              end
```

Note that you do not have to define both `onRequest` and `onResponse` in every Lua script. If any of these functions does not exist, then it is silently skipped.

### Direct Response

The direct plugin enables you to directly return a response without proceeding to the upstream. This acts as a much more performant, yet less flexible, way compared to Lua plugins to, for example, drop unwanted requests that contain undesirable request paths, body content, etc.... Here is an example:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: drop-php
          condition:
            match: ctx.request.http.path.toLower().endsWith(".php")
          direct:
            body:
              inline: This is not a PHP server!
            statusCode: 404
            headers:
              - key: X-Custom-Header
                value: some-value
              - key: X-Another-Header
                value: another-value
```

### JSON Schema Validation

The JSON schema validation plugin ise used to validate the request body content according to a predefined JSON schema. Here is an example:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: validate-users
          condition:
            match: ctx.request.http.path.startsWith("/users")
          jsonSchema:
            inline: |
              {
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "User",
                "description": "A user in the system",
                "type": "object",
                "properties": {
                  "id": {
                    "description": "The unique identifier for the user",
                    "type": "integer"
                  },
              // The rest of your JSON schema
```

In practice, however, you might want to use different JSON schemas that are used depending on the context (e.g. request path). Here is an example:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: validate-users
          condition:
            match: ctx.request.http.path.startsWith("/users")
          jsonSchema:
            inline: <USER_JSON_SCHEMA>
        - name: validate-groups
          condition:
            match: ctx.request.http.path.startsWith("/groups")
          jsonSchema:
            inline: <GROUP_JSON_SCHEMA>
```

By default, if the JSON schema validation fails, the *Service* returns a status code of `400`. You can, however, explicitly configure the returned status code and the body as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: validate-users
          condition:
            match: ctx.request.http.path.startsWith("/users")
          jsonSchema:
            inline: <USER_JSON_SCHEMA>
            statusCode: 403
            body:
              inline: You are not allowed!
```

### Rate Limiting

The rate limiting plugin provides a global sliding-window rate limiting mechanism that is backed by the *Cluster*'s Redis store. Here is an example where a *Session* is allowed to have up to 6 requests within a 30-second window:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: https://example.com
    http:
      plugins:
        - name: main-rate-limit
          condition:
            matchAny: true
          rateLimit:
            limit: 6
            window:
              seconds: 30
```

You can also define multiple rate limits. For example you might allow up to 10 requests per minute as well as up to 100 requests per hour per *Session* as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: https://example.com
    http:
      plugins:
        - name: minute-rate-limit
          condition:
            matchAny: true
          rateLimit:
            limit: 10
            window:
              minutes: 1
        - name: hour-rate-limit
          condition:
            matchAny: true
          rateLimit:
            limit: 100
            window:
              hours: 1
```

As mentioned above, by default the *Service* applies the rate limit on a per-*Session* basis. You can, however, override that behavior and dynamically evaluate your own key. Here is an example where the *User*'s uid is used as the key:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: https://example.com
    http:
      plugins:
        - name: main-rate-limit
          condition:
            matchAny: true
          rateLimit:
            limit: 6
            key:
              eval: ctx.user.metadata.uid
            window:
              seconds: 30
```

By default, when the rate limit is exceeded, the *Service* returns a status code of `429`. You can, however, override that behavior and set your own status code and body content as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: https://example.com
    http:
      plugins:
        - name: main-rate-limit
          condition:
            matchAny: true
          rateLimit:
            limit: 6
            window:
              seconds: 30
            statusCode: 400
            body:
              inline: "Invalid request!"
```

### Caching

The cache plugin returns globally cached responses that are stored in the *Cluster*'s Redis store. Here is an example where content in `/static` prefix path are cached for 2 hours:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: WEB
  isPublic: true
  config:
    upstream:
      url: https://example.com
    http:
      plugins:
        - name: cache-static-files
          condition:
            match: ctx.request.http.path.startsWith("/static")
          cache:
            ttl:
              hours: 2
```

By default, the *Service* uses the request URI (i.e. path + query parameters) on a per *Service* basis as the key for the cache entry. You can, however, override that default behavior and set your own key. Here is an example where the key is set to a combination of the *Service* uid, the request path and the *User* uid to cache the responses on a per-*User* basis:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: cache-static-files
          condition:
            match: ctx.request.http.path.startsWith("/static")
          cache:
            ttl:
              hours: 2
            # !mark(1:2)
            key:
              eval: ctx.service.metadata.uid + ctx.request.http.path + ctx.user.metadata.uid
```

By default, the *Service* only does cache GET and HEAD requests. You can, however, override that behavior and allow for caching other HTTP methods via the `allowUnsafeMethods` field as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: cache-static-files
          condition:
            match: ctx.request.http.path.startsWith("/static")
          cache:
            ttl:
              hours: 2
            # !mark
            allowUnsafeMethods: true
```

You can also define the max size, in bytes, of responses that can be cached via the `maxSize` field as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: cache-static-files
          condition:
            match: ctx.request.http.path.startsWith("/static")
          cache:
            ttl:
              hours: 2
            # !mark
            maxSize: 1000000
```

### Path Manipulation

The path plugin can remove or/and add prefixes of the request path. Here is an example of removing a prefix:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: remove-api-prefix
          condition:
            matchAny: true
          # !mark(1:2)
          path:
            removePrefix: /api/v1
```

You can also add a path prefix via `addPrefix` as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: remove-api-prefix
          condition:
            matchAny: true
          # !mark(1:2)
          path:
            addPrefix: /api/v1
```

You can also replace a prefix with another by combining both `removePrefix` and `addPrefix` as follows:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: remove-api-prefix
          condition:
            matchAny: true
          # !mark(1:3)
          path:
            removePrefix: /api/v1
            addPrefix: /api/v2
```

### Envoy Ext Proc

Octelium also supports Envoy's [ext\_proc](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_proc_filter). In this plugin type, Octelium's identity-aware proxy, [*Vigil*](https://octelium.com/docs/octelium/latest/reference/components.md#vigil), behaves like an Envoy instance and acts as an ext\_proc gRPC client that sends [ProcessingRequest](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ext_proc/v3/external_processor.proto#envoy-v3-api-msg-service-ext-proc-v3-processingrequest) to your ext\_proc gRPC compliant server and waits for [ProcessingResponse](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ext_proc/v3/external_processor.proto#envoy-v3-api-msg-service-ext-proc-v3-processingresponse) on every request. Here is an example:

```yaml
kind: Service
metadata:
  name: example
spec:
  mode: HTTP
  config:
    upstream:
      url: https://api.example.com
    http:
      plugins:
        - name: my-ext-proc-svc
          condition:
            match: ctx.request.http.path.startsWith("/api/v1")
          extProc:
            address: ext-proc.default.svc:8080
```
