Skip to content

πŸ’ˆ Reverse proxy server with built-in idempotency support, written in Rust & Cloudflare Worker.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

ldclabs/idempotent-proxy

Repository files navigation

Idempotent Proxy

πŸ’ˆ Reverse proxy server with built-in idempotency support, written in Rust & Cloudflare Worker.

πŸ’ This project received a $5k Developer Grant from the DFINITY Foundation.

Overview

The idempotent-proxy is a reverse proxy service written in Rust with built-in idempotency support.

When multiple requests with the same idempotency key arrive within a specific timeframe, only the first request is forwarded to the target service. The response is cached in Redis (or DurableObject in Cloudflare Worker), and subsequent requests retrieve the cached response, ensuring consistent results.

This service can be used to proxy HTTPS outcalls for ICP canisters, enabling integration with any Web2 http service.

Idempotent Proxy

Features

  • Reverse proxy with built-in idempotency support
  • Confidential information masking
  • JSON and CBOR response filtering
  • Response headers filtering
  • Access control using Secp256k1 and Ed25519
  • Deployable with Docker or Cloudflare Worker
  • On-chain Idempotent Proxy service on the ICP

Libraries

Library Description
idempotent-proxy-server Idempotent Proxy implemented in Rust.
idempotent-proxy-cf-worker Idempotent Proxy implemented as Cloudflare Worker.
idempotent-proxy-canister A ICP canister Make Idempotent Proxy service on-chain.
idempotent-proxy-types Idempotent Proxy types in Rust. Should not be used in ICP canister!
examples/eth-canister A ICP canister integration with Ethereum JSON-RPC API.
examples/eth-canister-lite A ICP canister integration with Ethereum JSON-RPC API through idempotent-proxy-canister

Who's using?

  • CK-Doge: An on-chain integration with the Dogecoin network on the Internet Computer.

If you plan to use this project and have any questions, feel free to open an issue. I will address it as soon as possible.

Usage

On-chain Idempotent Proxy

The idempotent-proxy-canister is an ICP smart contract that can connect to 1 to N Idempotent Proxy services deployed by idempotent-proxy-server or idempotent-proxy-cf-worker. It provides on-chain HTTPS outcalls with idempotency for other smart contracts.

Idempotent Proxy Canister

Go to the idempotent-proxy-canister directory for more information.

Online Demo: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=hpudd-yqaaa-aaaap-ahnbq-cai

ICP Canister Integration Examples

Run proxy in development mode

Run proxy:

cargo run -p idempotent-proxy-server

Make a request:

curl -v -X POST \
  --url http://YOUR_HOST/eth \
  --header 'content-type: application/json' \
  --header 'x-forwarded-host: rpc.ankr.com' \
  --header 'idempotency-key: key_001' \
  --data '{
	  "id": 1,
    "jsonrpc": "2.0",
    "method": "eth_getBlockByNumber",
    "params": ["latest", false]
}'

Building enclave image for Marlin Oyster

https://docs.marlin.org/user-guides/oyster/instances/quickstart/build

sudo docker run --rm --privileged --name nitro-cli -v `pwd`:/mnt/my-server marlinorg/nitro-cli

In a new terminal, run:

cd /mnt/my-server
sudo docker exec -it nitro-cli sh
nitro-cli build-enclave --docker-uri ghcr.io/ldclabs/idempotent-proxy_enclave_amd64:latest --output-file idempotent-proxy_enclave_amd64.eif

The image URL to deploy on Marlin Oyster:

https://pub-eea759c16b114748bd3b170eadbb2c30.r2.dev/idempotent-proxy_enclave_amd64.eif

Go to the idempotent-proxy-server directory for more information.

Running as Cloudflare Worker

Idempotent Proxy can be running as a Cloudflare Worker. In order to use Durable Objects, you must switch to a paid plan.

cd src/idempotent-proxy-cf-worker
npm i
npx wrangler deploy

A online version for testing is available at:

https://idempotent-proxy-cf-worker.zensh.workers.dev

Try it out:

curl -v -X GET 'https://idempotent-proxy-cf-worker.zensh.workers.dev/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'content-type: application/json'

More URL_ constants:

idempotent-proxy-cf-worker does not enable proxy-authorization, so it can be accessed.

Go to the idempotent-proxy-cf-worker directory for more information.

Run proxy with Docker

files in /mnt/idempotent-proxy directory:

/mnt/idempotent-proxy/.env
/mnt/idempotent-proxy/keys/doge-test-rpc.panda.fans.key
/mnt/idempotent-proxy/keys/doge-test-rpc.panda.fans.pem

.env file:

SERVER_ADDR=0.0.0.0:443
REDIS_URL=172.16.32.1:6379
POLL_INTERVAL=100 # in milliseconds
REQUEST_TIMEOUT=10000 # in milliseconds
LOG_LEVEL=info # debug, info, warn, error
# cert file path to enable https, for example: /etc/https/mydomain.crt
TLS_CERT_FILE = "keys/doge-test-rpc.panda.fans.pem"
# key file path to enable https, for example: /etc/https/mydomain.key
TLS_KEY_FILE = "keys/doge-test-rpc.panda.fans.key"

ECDSA_PUB_KEY_1="A44DZpzDwDvq9HwW3_dynOfDgkMJHKgOxUyCOrv5Pl3O"

# ECDSA_PUB_KEY_2="xxxxxx"

ALLOW_AGENTS="ICPanda"

URL_DOGE_TEST="http://172.16.32.1:44555/"
URL_DOGE="http://172.16.32.1:22555/"
# URL_XXX=...

HEADER_API_TOKEN="Basic SUNQYW5kYTpJVEZDNlJjam56RkdEQnd0SzByYV9kS0swR29lSElqVUl3V2lEb3VrRWU0"
# HEADER_XXX=...

Run proxy with Docker:

docker run --restart=always -v /mnt/idempotent-proxy/.env:/app/.env -v /mnt/idempotent-proxy/keys:/app/keys --name proxy -d -p 443:443 ghcr.io/ldclabs/idempotent-proxy:latest

Request Examples

Regular Proxy Request Example

Make a request:

curl -v -X GET 'http://localhost:8080/get' \
  -H 'x-forwarded-host: httpbin.org' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'content-type: application/json'

Response:

< HTTP/1.1 200 OK
< date: Wed, 22 May 2024 11:03:33 GMT
< content-type: application/json
< content-length: 375
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
<
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Idempotency-Key": "idempotency_key_001",
    "User-Agent": "curl/8.6.0",
    "X-Amzn-Trace-Id": "Root=1-664dd105-7930bcc43ae6081a4508d114"
  },
  "origin": "120.204.60.218",
  "url": "https://httpbin.org/get"
}

Request again with the same idempotency key will return the same response.

Proxy Request Example with URL_ Constant Defined

Setting in .env file:

URL_HTTPBIN="https://httpbin.org/get?api-key=abc123"

Make a request with URL_HTTPBIN constant in url path:

curl -v -X GET 'http://localhost:8080/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'content-type: application/json'

Response:

< HTTP/1.1 200 OK
< date: Wed, 22 May 2024 11:07:05 GMT
< content-type: application/json
< content-length: 417
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
<
{
  "args": {
    "api-key": "abc123"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Idempotency-Key": "idempotency_key_001",
    "User-Agent": "curl/8.6.0",
    "X-Amzn-Trace-Id": "Root=1-664dd1d9-6612bfd076e95b814dd9329d"
  },
  "origin": "120.204.60.218",
  "url": "https://httpbin.org/get?api-key=abc123"
}

Proxy Request Example with HEADER_ Constant Defined

Setting in .env file:

URL_HTTPBIN="https://httpbin.org/get?api-key=abc123"
HEADER_TOKEN="Bearer xyz123456"

Make a request with HEADER_TOKEN constant in header:

curl -v -X GET 'http://localhost:8080/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'authorization: HEADER_TOKEN' \
  -H 'content-type: application/json'

Response:

< HTTP/1.1 200 OK
< date: Wed, 22 May 2024 11:11:17 GMT
< content-type: application/json
< content-length: 459
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
<
{
  "args": {
    "api-key": "abc123"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Authorization": "Bearer xyz123456",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Idempotency-Key": "idempotency_key_001",
    "User-Agent": "curl/8.6.0",
    "X-Amzn-Trace-Id": "Root=1-664dd2d5-15b233f974a01ca34bd9a8ab"
  },
  "origin": "120.204.60.218",
  "url": "https://httpbin.org/get?api-key=abc123"
}

Proxy Request Example with Response Headers Filtered

Make a request with response-headers header:

curl -v -X GET 'http://localhost:8080/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'authorization: HEADER_TOKEN' \
  -H 'response-headers: content-type,content-length' \
  -H 'content-type: application/json'

Response:

< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 515
< date: Wed, 22 May 2024 11:13:39 GMT
<
{
  "args": {
    "api-key": "abc123"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip",
    "Authorization": "Bearer xyz123456",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "Idempotency-Key": "idempotency_key_001",
    "Response-Headers": "content-type,content-length",
    "User-Agent": "curl/8.6.0",
    "X-Amzn-Trace-Id": "Root=1-664dd363-2bbae4420bf9add8512f5930"
  },
  "origin": "120.204.60.218",
  "url": "https://httpbin.org/get?api-key=abc123"
}

Proxy Request Example with JSON Response Filtered

Make a request with x-json-mask header:

curl -v -X GET 'http://localhost:8080/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'authorization: HEADER_TOKEN' \
  -H 'response-headers: content-type,content-length' \
  -H 'x-json-mask: args,url' \
  -H 'content-type: application/json'

Response:

< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 76
< date: Wed, 22 May 2024 12:19:03 GMT
<
* Connection #0 to host localhost left intact
{"args":{"api-key":"abc123"},"url":"https://httpbin.org/get?api-key=abc123"}

Proxy Request Example with Access Control Added

Setting in .env file:

ECDSA_PUB_KEY_1="A6t1U8kc10AbLJ3-V1avU4rYvmAsYjXuzY0kPublttot"

You can add other public keys by adding ECDSA_PUB_KEY_2, ECDSA_PUB_KEY_abc for key rotation.

Make a request with proxy-authorization header, the bearer token is signed with the private key:

curl -v -X GET 'http://localhost:8080/URL_HTTPBIN' \
  -H 'idempotency-key: idempotency_key_001' \
  -H 'proxy-authorization: Bearer 6LduPbIpAAAAANSOUfb-8bU45eilZFSmlSguN5TO' \
  -H 'authorization: HEADER_TOKEN' \
  -H 'response-headers: content-type,content-length' \
  -H 'x-json-mask: args,url' \
  -H 'content-type: application/json'

A 407 response:

< HTTP/1.1 407 Proxy Authentication Required
< content-type: text/plain; charset=utf-8
< content-length: 34
< date: Wed, 22 May 2024 12:24:40 GMT
<
* Connection #0 to host localhost left intact
proxy authentication verify failed: failed to decode CBOR data

License

Copyright Β© 2024 LDC Labs.

ldclabs/idempotent-proxy is licensed under the MIT License. See LICENSE for the full license text.