Skip to content

Commit

Permalink
feat: add idempotent-proxy-canister
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jul 7, 2024
1 parent 77f9c39 commit 308536d
Show file tree
Hide file tree
Showing 15 changed files with 707 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ Cargo.lock
debug
.wrangler
.dfx
.dfx_env

examples/**/.env
examples/**/.env
src/**/.env
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"src/idempotent-proxy-canister",
"src/idempotent-proxy-server",
"src/idempotent-proxy-types",
"examples/eth-canister",
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ build-linux:

# cargo install ic-wasm
build-wasm:
@cargo build --release --target wasm32-unknown-unknown --package idempotent-proxy-canister
@cargo build --release --target wasm32-unknown-unknown --package eth-canister

# cargo install candid-extractor
build-did:
@candid-extractor target/wasm32-unknown-unknown/release/idempotent_proxy_canister.wasm > src/idempotent-proxy-canister/idempotent-proxy-canister.did
@candid-extractor target/wasm32-unknown-unknown/release/eth_canister.wasm > examples/eth-canister/eth-canister.did
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The idempotent-proxy is a reverse proxy service written in Rust with built-in id

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](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-overview) for [ICP canisters](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/overview/introduction), enabling integration with any Web2 http service.
This service can be used to proxy [HTTPS outcalls](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works) for [ICP canisters](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/overview/introduction), enabling integration with any Web2 http service.

![Idempotent Proxy](./idempotent-proxy.png)

Expand All @@ -23,12 +23,13 @@ This service can be used to proxy [HTTPS outcalls](https://internetcomputer.org/

## Packages

| Package | Description |
| :----------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ |
| [idempotent-proxy-server](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-server) | Idempotent Proxy implemented in Rust. |
| [idempotent-proxy-cf-worker](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-cf-worker) | Idempotent Proxy implemented as Cloudflare Worker. |
| [idempotent-proxy-types](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-types) | Idempotent Proxy types in Rust. Should not be used in ICP canister! |
| [examples/eth-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/examples/eth-canister) | A ICP canister integration with Ethereum JSON-RPC API. |
| Package | Description |
| :----------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- |
| [idempotent-proxy-server](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-server) | Idempotent Proxy implemented in Rust. |
| [idempotent-proxy-cf-worker](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-cf-worker) | Idempotent Proxy implemented as Cloudflare Worker. |
| [idempotent-proxy-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-canister) | Make `idempotent-proxy-server` or `idempotent-proxy-cf-worker` as a canister. |
| [idempotent-proxy-types](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-types) | Idempotent Proxy types in Rust. Should not be used in ICP canister! |
| [examples/eth-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/examples/eth-canister) | A ICP canister integration with Ethereum JSON-RPC API. |

## Who's using?

Expand Down
21 changes: 21 additions & 0 deletions dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"canisters": {
"idempotent-proxy-canister": {
"candid": "src/idempotent-proxy-canister/idempotent-proxy-canister.did",
"declarations": {
"node_compatibility": true
},
"package": "idempotent-proxy-canister",
"optimize": "cycles",
"type": "rust"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".dfx_env",
"version": 1
}
37 changes: 37 additions & 0 deletions src/idempotent-proxy-canister/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "idempotent-proxy-canister"
description = "idempotent-proxy canister"
repository = "https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-canister"
publish = false

version.workspace = true
edition.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[profile.release]
debug = false
lto = true
strip = true
opt-level = 's'

[dependencies]
async-trait = "0.1"
bytes = "1.6"
candid = "0.10"
ic-cdk = "0.14"
ic-cdk-timers = "0.8"
ic-stable-structures = "0.6"
url = "2.5"
base64 = { workspace = true }
ciborium = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_bytes = { workspace = true }
sha3 = { workspace = true }
39 changes: 39 additions & 0 deletions src/idempotent-proxy-canister/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# `idempotent-proxy-canister`

## Running the project locally

If you want to test your project locally, you can use the following commands:

```bash
# Starts the replica, running in the background
dfx start --background

# deploy the canister
dfx deploy idempotent-proxy-canister --argument "(opt variant {Init =
record {
ecdsa_key_name = \"dfx_test_key\";
proxy_token_refresh_interval = 3600;
}
})"

dfx canister call idempotent-proxy-canister get_state '()'

dfx canister call idempotent-proxy-canister admin_set_agent '
(vec {
record {
name = "LDCLabs";
endpoint = "https://idempotent-proxy-cf-worker.zensh.workers.dev";
max_cycles = 100000000000;
proxy_token = null;
}
})
'

```

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

## License
Copyright © 2024 [LDC Labs](https://github.com/ldclabs).

`ldclabs/idempotent-proxy` is licensed under the MIT License. See [LICENSE](../../LICENSE-MIT) for the full license text.
52 changes: 52 additions & 0 deletions src/idempotent-proxy-canister/idempotent-proxy-canister.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
type Agent = record {
proxy_token : opt text;
endpoint : text;
name : text;
max_cycles : nat64;
};
type CanisterHttpRequestArgument = record {
url : text;
method : HttpMethod;
max_response_bytes : opt nat64;
body : opt blob;
transform : opt TransformContext;
headers : vec HttpHeader;
};
type ChainArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs };
type HttpHeader = record { value : text; name : text };
type HttpMethod = variant { get; head; post };
type HttpResponse = record {
status : nat;
body : blob;
headers : vec HttpHeader;
};
type InitArgs = record {
ecdsa_key_name : text;
proxy_token_refresh_interval : nat64;
};
type Result = variant { Ok : bool; Err : text };
type Result_1 = variant { Ok; Err : text };
type Result_2 = variant { Ok : State; Err };
type State = record {
proxy_token_public_key : text;
ecdsa_key_name : text;
managers : vec principal;
agents : vec Agent;
allowed_canisters : vec principal;
proxy_token_refresh_interval : nat64;
};
type TransformArgs = record { context : blob; response : HttpResponse };
type TransformContext = record {
function : func (TransformArgs) -> (HttpResponse) query;
context : blob;
};
type UpgradeArgs = record { proxy_token_refresh_interval : opt nat64 };
service : (opt ChainArgs) -> {
admin_add_canister : (principal) -> (Result);
admin_remove_canister : (principal) -> (Result);
admin_set_agent : (vec Agent) -> (Result_1);
admin_set_managers : (vec principal) -> (Result_1);
get_state : () -> (Result_2) query;
proxy_http_request : (CanisterHttpRequestArgument) -> (HttpResponse);
validate_admin_set_managers : (vec principal) -> (Result_1) query;
}
89 changes: 89 additions & 0 deletions src/idempotent-proxy-canister/src/agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use candid::{CandidType, Nat};
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpHeader, HttpResponse, TransformArgs,
TransformContext,
};
use serde::{Deserialize, Serialize};
use url::Url;

#[derive(CandidType, Default, Clone, Deserialize, Serialize)]
pub struct Agent {
pub name: String, // used as a prefix for idempotency_key and message in sign_proxy_token to separate different business processes.
pub endpoint: String,
pub max_cycles: u64,
pub proxy_token: Option<String>,
}

impl Agent {
fn build_request(&self, req: &mut CanisterHttpRequestArgument) -> Result<(), String> {
if !req.headers.iter().any(|h| h.name == "idempotency-key") {
Err("idempotency-key header is missing".to_string())?;
}

if req.url.starts_with("URL_") {
req.url = format!("{}/{}", self.endpoint, req.url);
} else {
let url = Url::parse(&req.url)
.map_err(|err| format!("parse url {} error: {}", req.url, err))?;
let host = url
.host_str()
.ok_or_else(|| format!("url host is empty: {}", req.url))?;
req.headers.push(HttpHeader {
name: "x-forwarded-host".to_string(),
value: host.to_string(),
});
req.url.clone_from(&self.endpoint);
}

if !req.headers.iter().any(|h| h.name == "response-headers") {
req.headers.push(HttpHeader {
name: "response-headers".to_string(),
value: "date".to_string(),
});
}

if let Some(proxy_token) = &self.proxy_token {
req.headers.push(HttpHeader {
name: "proxy-authorization".to_string(),
value: format!("Bearer {}", proxy_token),
});
}

req.transform = Some(TransformContext::from_name(
"inner_transform_response".to_string(),
vec![],
));

Ok(())
}

pub async fn call(&self, mut req: CanisterHttpRequestArgument) -> HttpResponse {
if let Err(err) = self.build_request(&mut req) {
return HttpResponse {
status: Nat::from(400u64),
body: err.into_bytes(),
headers: vec![],
};
}

match http_request(req, self.max_cycles as u128).await {
Ok((res,)) => res,
Err((code, message)) => HttpResponse {
status: Nat::from(503u64),
body: format!("http_request resulted into error. code: {code:?}, error: {message}")
.into_bytes(),
headers: vec![],
},
}
}
}

#[ic_cdk::query(hidden = true)]
fn inner_transform_response(args: TransformArgs) -> HttpResponse {
HttpResponse {
status: args.response.status,
body: args.response.body,
// Remove headers (which may contain a timestamp) for consensus
headers: vec![],
}
}
Loading

0 comments on commit 308536d

Please sign in to comment.