JSPM

  • Created
  • Published
  • Downloads 17350
  • Score
    100M100P100Q124923F
  • License MIT

An api gateway for cloudflare workers

Package Exports

  • cloudworker-proxy

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (cloudworker-proxy) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

cloudworker-proxy

An api gateway for cloudflare workers with configurable handlers for:

  • Routing
    • Load balancing of http endpoints
    • Invoking AWS lambdas and google cloud functions
    • Static responses
  • Logging (http, kinesis)
  • Authentication (basic, oauth2)
  • Rate limiting
  • Rewrite
    • Modifying headers
    • Adding cors headers

Concept

The proxy is a pipeline of different handlers that processes each request. The handlers in the pipeline could be:

  • Middleware. Such as logging or authentication that typically passes on the request further down the pipeline
  • Origins. Fetches content from other services, for instance using http.
  • Tranforms. Modifies the content before passing it back to the client

Each handler can specify rules for which hosts and paths it should apply to, so it's possible to for instance only apply authentication to certain requests.

Usage

A proxy is instanciated with a set of middlewares, origins and transforms that are matched against each request based on hostname, method and path. Each rule is configured to execute one of the predefined handlers. The handlers could either terminate the request and send the response to the client or pass on the request to the following handlers matching the request.

A simple hello world proxy:

const Proxy = require('cloudworker-proxy');

const config = [{
    handlerName: "static",
    options: {
    body: "Hello world"
    }
}];

const proxy = new Proxy(config);

async function fetchAndApply(event) {
    return await proxy.resolve(event);
}

addEventListener('fetch', (event) => {
  event.respondWith(fetchAndApply(event));
});

Handlers

Ratelimit

Ratelimit the matching requests per minute per IP or for all clients.

The ratelimit keeps the counters in memory so different edge nodes will have separate counters. For IP-based ratelimits it should work just fine as the requests from a client will hit the same edge node.

The ratelimit can have different scopes, so a proxy can have multiple rate-limits for different endpoints.

The ratelimit adds the following headers to the response object:

  • X-Ratelimit-Limit. This is the current limit being enforced
  • X-Ratelimit-Count. The current count of requests being made within the window
  • X-Ratelimit-Reset. The timeperiod in seconds until the rate limit is reset.

HEAD and OPTIONS requests are not counted against the limit.

An example of the configuration for ratelimit handler:

rules = [{
    handlerName: 'ratelimit',
    options: {
        limit: 1000, // The default allowed calls
        scope: 'default',
        type: 'IP', // Anything except IP will sum up all calls
    }
}];

Logging

The logging handler supports logging of requests and errors to http endpoints such as logz.io and AWS Kinesis.

The logs are sent in chunks to the server. The chunks are sent when the predefined limit of messages are reached or after a certain time, whatever comes first.

An example of configuration for a http logger:

config = [{
    handlerName: 'logger',
    options: {
        type: 'http',
        url: process.env.LOGZ_URL,
        contentType: 'text/plain',
        delimiter: '_',
    },
}];

Basic Auth

Uses basic auth to protect matching rules. The username and authTokens (base64-encoded versions of the passwords) are stored straight in the config which works fine for simple scenarios, but makes adding and removing users hard.

An example of the configuration for the basic auth middleware:

config = [{
    handlerName: 'basicAuth',
    path: '/basic',
    options: {
        users: [
            {
                username: 'test',
                authToken: 'dGVzdDpwYXNzd29yZA==', // "password" Base64 encoded
            }
        ],
    },
}];

Apikeys

The api keys handler reads the X-Api-Key header, with a fallback to ?apikey=.. querystring, and adds a jwt token to the authorizion header if there's a match. The jwt access and refresh tokens are stored in a cloudflare Key Value Storage.

The handler renews the access tokens once they expire using the refresh token. It doesn't validate the access token but only ads it to the request headers so that it can be validated using the jwt handler.

There's a separate api-key-api handler to add, remove or list a api keys.

An example of the configuration for the apikey handler:

config = [{
    handlerName: 'apikeys',
    options: {
        oauthClientId: <OAUTH2_CLIENT_ID>,
        oauth2ClientSecret: <OAUTH2_CLIENT_SECRET>,
        oauth2AuthDomain: <OAUTH2_AUTH_DOMAIN>,
        kvAccountId: <KV_ACCOUNT_ID>,
        kvNamespace: <KV_NAMESPACE>,
        kvAuthEmail: <KV_AUTH_EMAIL>,
        kvAuthKey: <KV_AUTH_KEY>,
    },
}];

Apikeys api

Exposes an POST endpoint to create a new api key based on the current oauth2 session and hence the handler must be added after the jwt handler. The handler stores a document for each user in cloudflare Key Value Storage with the api keys and their corresponding access/refesh tokens.

An example of the configuration for the apikey api handler:

config = [{
    handlerName: 'apikeysApi',
    options: {
        createPath: '/apikeys',
        kvAccountId: <KV_ACCOUNT_ID>,
        kvNamespace: <KV_NAMESPACE>,
        kvAuthEmail: <KV_AUTH_EMAIL>,
        kvAuthKey: <KV_AUTH_KEY>,
    },
}];

Split

Splits the request in two separate requests. The duplicated request will not return any results to the client, but can for instance be used to sample the traffic on a live website or to get webhooks to post to multiple endpoints.

The split handler takes a host parameter that lets you route the requests to a different origin.

An example of the configuration for the split handler:

config = [{
    handlerName: 'split',
    options: {
        host: 'test.example.com',
    },
}];

Response

Returns a static response to the request.

An example of configuration for a static handler:

const rules = [
  {
    handlerName: "response",
    options: {
      body: "Hello world"
    }
  }
];

Cors

Adds cross origin request headers for a path. The cors handler can optionally take an array of allowed origins to enable cors for.

An example of the configuration for cors handler:

config = [{
    handlerName: 'cors',
    options: {
        allowedOrigins: ['http://domain.com'],
    }
}];

Load balancer

Load balances requests between one or many endpoints.

Currently the load balancer distributes the load between the endpoints randomly. Use cases for this handler are:

  • Load balance between multiple ingress servers in kubernetes
  • Route trafic to providers that doesn't support custom domains easliy, such as google cloud functions
  • Route trafic to cloud services with nested paths such as AWS Api Gateway or google cloud functions.
  • Route trafic to different endpoints and having flexibility to do updates without changing the origin servers.

In some cases it is necessary to resolve the IP of the endpoint based on a different url than the host header. One example of this is when the load is distributed over multiple load balancer nodes that host multiple domains or subdomains. In these cases it's possible to set the resolveOverride on the load balancer handler. This way it will resolve the IP according to url property of the source, but use the resolveOverride as host header. NOTE: this is only possible if the host is hosted via the cloudflare cdn.

An example of the configuration for loadbalancer with a single source on google cloud functions:

config = [{
    handlerName: 'loadbalancer',
    options: {
        sources: [
            {
                url: 'https://europe-west1-ahlstrand.cloudfunctions.net/hello/{file}'
            }
        ]
    }
}];

An example of the configuration for loadbalancing traffic between two ingresses for multiple hosts, with an override of the host header:

config = [{
    handlerName: 'loadbalancer',
    path: '/:file*',
    options: {
        "resolveOverride": "www.ahlstrand.es",
        "sources": [
            {
                "url": "https://rancher-ingress-1.ahlstrand.es/{file}"
            },
            {
                "url": "https://rancher-ingress-2.ahlstrand.es/{file}"
            },
        ]
    }
}];

Using path and host parameters the handler can be more generic:

config = [{
    handlerName: 'loadbalancer',
    path: '/:file*',
    host: ':host.ahlstrand.es',
    options: {
        "resolveOverride": "{host}.ahlstrand.es",
        "sources": [
            {
                "url": "https://rancher-ingress-1.ahlstrand.es/{file}"
            },
            {
                "url": "https://rancher-ingress-2.ahlstrand.es/{file}"
            },
        ]
    }
}];

Origin

Passed the request to the origin for the cdn. This is typically used as a catch all handler to pass all requests that the worker shouldn't handle to origin.

As this wouldn't work when running locall it's possible to specify another host name that will be used for debugging locally.

An example of the configuration for the origin handler:

config = [{
    handlerName: 'origin',
    options: {
        localOriginOverride: 'https://some.origin.com',
    }
}];

Security

The hanlders api-keys and oauth2 only stores AES encrypted access and refresh-tokens in storage. The encryption keys are sent as part of the request from the user, so in order to get a valid token you need to have access both to the storage and a request from a user.

The tokens entries have a ttl of one month by default, so any token that hasn't been accessed in a month will automatically be removed.

Examples

For more examples of usage see the example folder which contains a complete solution deployed using serverless