diff --git a/Cargo.lock b/Cargo.lock index 54fb86a2b..2d48c6f53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" dependencies = [ "backtrace", ] @@ -247,7 +247,7 @@ dependencies = [ "js-sys", "thiserror 1.0.69", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tokio-socks", "tokio-tungstenite 0.24.0", "url", @@ -710,8 +710,10 @@ dependencies = [ "futures", "moka", "paste", + "redis", "serde", "serde_json", + "sha2", "tokio", "tracing", "utoipa", @@ -1042,9 +1044,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -1052,9 +1054,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -1076,9 +1078,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cln-rpc" @@ -1122,6 +1124,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1150,6 +1166,16 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -1837,9 +1863,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1864,7 +1890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1875,7 +1901,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] @@ -1931,7 +1957,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "httparse", "itoa", @@ -1962,14 +1988,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 1.1.0", + "http 1.2.0", "hyper 1.5.1", "hyper-util", "rustls 0.23.19", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tower-service", "webpki-roots 0.26.7", ] @@ -1995,7 +2021,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "hyper 1.5.1", "pin-project-lite", @@ -2524,9 +2550,9 @@ dependencies = [ [[package]] name = "minreq" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" +checksum = "36a8e50e917e18a37d500d27d40b7bc7d127e71c0c94fb2d83f43b4afd308390" dependencies = [ "log", "serde", @@ -3204,7 +3230,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.19", "socket2 0.5.8", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tracing", ] @@ -3223,7 +3249,7 @@ dependencies = [ "rustls 0.23.19", "rustls-pki-types", "slab", - "thiserror 2.0.3", + "thiserror 2.0.4", "tinyvec", "tracing", "web-time", @@ -3320,6 +3346,30 @@ dependencies = [ "libc", ] +[[package]] +name = "redis" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "ryu", + "sha1_smol", + "socket2 0.4.10", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3403,7 +3453,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "http-body-util", "hyper 1.5.1", @@ -3418,7 +3468,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.19", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -3426,7 +3476,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tokio-socks", "tower-service", "url", @@ -3620,6 +3670,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -3629,7 +3691,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.0.1", ] [[package]] @@ -3797,6 +3859,19 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.0.1" @@ -3804,7 +3879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3942,6 +4017,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -4278,11 +4359,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.4", ] [[package]] @@ -4298,9 +4379,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" dependencies = [ "proc-macro2", "quote", @@ -4319,9 +4400,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -4340,9 +4421,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -4385,9 +4466,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -4445,12 +4526,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls 0.23.19", - "rustls-pki-types", "tokio", ] @@ -4468,9 +4548,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -4500,16 +4580,16 @@ dependencies = [ "rustls 0.23.19", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tungstenite 0.24.0", "webpki-roots 0.26.7", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -4723,7 +4803,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.2.0", "httparse", "log", "rand", diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index 861a028cd..e049bfad7 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/crates/cdk-axum/Cargo.toml @@ -30,6 +30,8 @@ serde_json = "1" paste = "1.0.15" serde = { version = "1.0.210", features = ["derive"] } uuid = { version = "1", features = ["v4", "serde"] } +sha2 = "0.10.8" +redis = { version = "0.23.3", features = ["tokio-rustls-comp"] } [features] swagger = ["cdk/swagger", "dep:utoipa"] diff --git a/crates/cdk-axum/src/cache/backend/memory.rs b/crates/cdk-axum/src/cache/backend/memory.rs new file mode 100644 index 000000000..997df4928 --- /dev/null +++ b/crates/cdk-axum/src/cache/backend/memory.rs @@ -0,0 +1,38 @@ +use std::time::Duration; + +use moka::future::Cache; + +use crate::cache::{HttpCacheKey, HttpCacheStorage}; + +/// In memory cache storage for the HTTP cache. +/// +/// This is the default cache storage backend, which is used if no other storage +/// backend is provided, or if the provided storage backend is `None`. +/// +/// The cache is limited to 10,000 entries and it is not shared between +/// instances nor persisted. +pub struct InMemoryHttpCache(pub Cache>); + +#[async_trait::async_trait] +impl HttpCacheStorage for InMemoryHttpCache { + fn new(cache_ttl: Duration, cache_tti: Duration) -> Self + where + Self: Sized, + { + InMemoryHttpCache( + Cache::builder() + .max_capacity(10_000) + .time_to_live(cache_ttl) + .time_to_idle(cache_tti) + .build(), + ) + } + + async fn get(&self, key: &HttpCacheKey) -> Option> { + self.0.get(key) + } + + async fn set(&self, key: HttpCacheKey, value: Vec) { + self.0.insert(key, value).await; + } +} diff --git a/crates/cdk-axum/src/cache/backend/mod.rs b/crates/cdk-axum/src/cache/backend/mod.rs new file mode 100644 index 000000000..9fd883018 --- /dev/null +++ b/crates/cdk-axum/src/cache/backend/mod.rs @@ -0,0 +1,5 @@ +mod memory; +mod redis; + +pub use self::memory::InMemoryHttpCache; +pub use self::redis::{Config as RedisConfig, HttpCacheRedis}; diff --git a/crates/cdk-axum/src/cache/backend/redis.rs b/crates/cdk-axum/src/cache/backend/redis.rs new file mode 100644 index 000000000..34fc840a1 --- /dev/null +++ b/crates/cdk-axum/src/cache/backend/redis.rs @@ -0,0 +1,103 @@ +use std::time::Duration; + +use redis::AsyncCommands; +use serde::{Deserialize, Serialize}; + +use crate::cache::{HttpCacheKey, HttpCacheStorage}; + +/// Redis cache storage for the HTTP cache. +/// +/// This cache storage backend uses Redis to store the cache. +pub struct HttpCacheRedis { + cache_ttl: Duration, + prefix: Option>, + client: Option, +} + +/// Configuration for the Redis cache storage. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + /// Commong key prefix + pub key_prefix: Option, + + /// Connection string to the Redis server. + pub connection_string: String, +} + +impl HttpCacheRedis { + /// Create a new Redis cache. + pub fn set_client(mut self, client: redis::Client) -> Self { + self.client = Some(client); + self + } + + /// Set a prefix for the cache keys. + /// + /// This is useful to have all the HTTP cache keys under a common prefix, + /// some sort of namespace, to make management of the database easier. + pub fn set_prefix(mut self, prefix: Vec) -> Self { + self.prefix = Some(prefix); + self + } +} + +#[async_trait::async_trait] +impl HttpCacheStorage for HttpCacheRedis { + fn new(cache_ttl: Duration, _cache_tti: Duration) -> Self { + Self { + cache_ttl, + prefix: None, + client: None, + } + } + + async fn get(&self, key: &HttpCacheKey) -> Option> { + let mut con = match self + .client + .as_ref() + .expect("A client must be set with set_client()") + .get_multiplexed_tokio_connection() + .await + { + Ok(con) => con, + Err(err) => { + tracing::error!("Failed to get redis connection: {:?}", err); + return None; + } + }; + + let mut db_key = self.prefix.clone().unwrap_or_default(); + db_key.extend(&**key); + + con.get(db_key) + .await + .map_err(|err| { + tracing::error!("Failed to get value from redis: {:?}", err); + err + }) + .ok() + } + + async fn set(&self, key: HttpCacheKey, value: Vec) { + let mut db_key = self.prefix.clone().unwrap_or_default(); + db_key.extend(&*key); + + let mut con = match self + .client + .as_ref() + .expect("A client must be set with set_client()") + .get_multiplexed_tokio_connection() + .await + { + Ok(con) => con, + Err(err) => { + tracing::error!("Failed to get redis connection: {:?}", err); + return; + } + }; + + let _: Result<(), _> = con + .set_ex(db_key, value, self.cache_ttl.as_secs() as usize) + .await; + } +} diff --git a/crates/cdk-axum/src/cache/config.rs b/crates/cdk-axum/src/cache/config.rs new file mode 100644 index 000000000..7bc875ad7 --- /dev/null +++ b/crates/cdk-axum/src/cache/config.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(tag = "backend")] +#[serde(rename_all = "lowercase")] +pub enum Backend { + #[default] + Memory, + Redis(super::backend::RedisConfig), +} + +/// Cache configuration. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + /// Cache backend. + #[serde(default)] + #[serde(flatten)] + pub backend: Backend, + + /// Time to live for the cache entries. + pub ttl: Option, + + /// Time for the cache entries to be idle. + pub tti: Option, +} diff --git a/crates/cdk-axum/src/cache/mod.rs b/crates/cdk-axum/src/cache/mod.rs new file mode 100644 index 000000000..aa79d59fb --- /dev/null +++ b/crates/cdk-axum/src/cache/mod.rs @@ -0,0 +1,176 @@ +//! HTTP cache. +//! +//! This is mod defines a common trait to define custom backends for the HTTP cache. +//! +//! The HTTP cache is a layer to cache responses from HTTP requests, to avoid hitting +//! the same endpoint multiple times, which can be expensive and slow, or to provide +//! idempotent operations. +//! +//! This mod also provides common backend implementations as well, such as In +//! Memory (default) and Redis. +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; + +use serde::de::DeserializeOwned; +use serde::Serialize; +use sha2::{Digest, Sha256}; + +mod backend; +mod config; + +pub use self::backend::*; +pub use self::config::Config; + +#[async_trait::async_trait] +/// Cache storage for the HTTP cache. +pub trait HttpCacheStorage { + /// Create a new cache storage instance + fn new(cache_ttl: Duration, cache_tti: Duration) -> Self + where + Self: Sized; + + /// Get a value from the cache. + async fn get(&self, key: &HttpCacheKey) -> Option>; + + /// Set a value in the cache. + async fn set(&self, key: HttpCacheKey, value: Vec); +} + +/// Http cache with a pluggable storage backend. +pub struct HttpCache { + /// Time to live for the cache. + pub ttl: Duration, + /// Time to idle for the cache. + pub tti: Duration, + storage: Arc, +} + +impl Default for HttpCache { + fn default() -> Self { + Self::new( + Duration::from_secs(DEFAULT_TTL_SECS), + Duration::from_secs(DEFAULT_TTI_SECS), + None, + ) + } +} + +/// Max payload size for the cache key. +/// +/// This is a trade-off between security and performance. A large payload can be used to +/// perform a CPU attack. +const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; + +/// Default TTL for the cache. +const DEFAULT_TTL_SECS: u64 = 60; + +/// Default TTI for the cache. +const DEFAULT_TTI_SECS: u64 = 60; + +/// Http cache key. +/// +/// This type ensures no Vec is used as a key, which is error-prone. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct HttpCacheKey([u8; 32]); + +impl Deref for HttpCacheKey { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for HttpCache { + fn from(config: config::Config) -> Self { + match config.backend { + config::Backend::Memory => Self::new( + Duration::from_secs(config.ttl.unwrap_or(DEFAULT_TTL_SECS)), + Duration::from_secs(config.tti.unwrap_or(DEFAULT_TTI_SECS)), + None, + ), + config::Backend::Redis(redis_config) => { + let client = redis::Client::open(redis_config.connection_string) + .expect("Failed to create Redis client"); + let storage = HttpCacheRedis::new( + Duration::from_secs(config.ttl.unwrap_or(60)), + Duration::from_secs(config.tti.unwrap_or(60)), + ) + .set_client(client) + .set_prefix( + redis_config + .key_prefix + .unwrap_or_default() + .as_bytes() + .to_vec(), + ); + Self::new( + Duration::from_secs(config.ttl.unwrap_or(DEFAULT_TTL_SECS)), + Duration::from_secs(config.tti.unwrap_or(DEFAULT_TTI_SECS)), + Some(Arc::new(storage)), + ) + } + } + } +} + +impl HttpCache { + /// Create a new HTTP cache. + pub fn new( + ttl: Duration, + tti: Duration, + storage: Option>, + ) -> Self { + Self { + ttl, + tti, + storage: storage.unwrap_or_else(|| Arc::new(InMemoryHttpCache::new(ttl, tti))), + } + } + + /// Calculate a cache key from a serializable value. + /// + /// Usually the input is the request body or query parameters. + /// + /// The result is an optional cache key. If the key cannot be calculated, it + /// will be None, meaning the value cannot be cached, therefore the entire + /// caching mechanism should be skipped. + /// + /// Instead of using the entire serialized input as the key, the key is a + /// double hash to have a predictable key size, although it may open the + /// window for CPU attacks with large payloads, but it is a trade-off. + /// Perhaps upper layer have a protection against large payloads. + pub fn calculate_key(&self, key: &K) -> Option { + let json_value = match serde_json::to_vec(key) { + Ok(value) => value, + Err(err) => { + tracing::warn!("Failed to serialize key: {:?}", err); + return None; + } + }; + + if json_value.len() > MAX_PAYLOAD_SIZE { + tracing::warn!("Key size is too large: {}", json_value.len()); + return None; + } + + let first_hash = Sha256::digest(json_value); + let second_hash = Sha256::digest(first_hash); + Some(HttpCacheKey(second_hash.into())) + } + + /// Get a value from the cache. + pub async fn get(self: &Arc, key: &HttpCacheKey) -> Option { + self.storage + .get(key) + .await + .map(|value| serde_json::from_slice(&value).unwrap()) + } + + /// Set a value in the cache. + pub async fn set(self: &Arc, key: HttpCacheKey, value: &V) { + let value = serde_json::to_vec(value).unwrap(); + self.storage.set(key, value).await; + } +} diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index 1acc9370c..b5b2ed24a 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -4,15 +4,15 @@ #![warn(rustdoc::bare_urls)] use std::sync::Arc; -use std::time::Duration; use anyhow::Result; use axum::routing::{get, post}; use axum::Router; +use cache::HttpCache; use cdk::mint::Mint; -use moka::future::Cache; use router_handlers::*; +pub mod cache; mod router_handlers; mod ws; @@ -52,7 +52,7 @@ use uuid::Uuid; #[derive(Clone)] pub struct MintState { mint: Arc, - cache: Cache, + cache: Arc, } #[cfg(feature = "swagger")] @@ -131,15 +131,20 @@ pub struct MintState { /// OpenAPI spec for the mint's v1 APIs pub struct ApiDocV1; -/// Create mint [`Router`] with required endpoints for cashu mint -pub async fn create_mint_router(mint: Arc, cache_ttl: u64, cache_tti: u64) -> Result { +/// Create mint [`Router`] with required endpoints for cashu mint with the default cache +pub async fn create_mint_router(mint: Arc) -> Result { + create_mint_router_with_custom_cache(mint, Default::default()).await +} + +/// Create mint [`Router`] with required endpoints for cashu mint with a custom +/// backend for cache +pub async fn create_mint_router_with_custom_cache( + mint: Arc, + cache: HttpCache, +) -> Result { let state = MintState { mint, - cache: Cache::builder() - .max_capacity(10_000) - .time_to_live(Duration::from_secs(cache_ttl)) - .time_to_idle(Duration::from_secs(cache_tti)) - .build(), + cache: Arc::new(cache), }; let v1_router = Router::new() diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 0d2bfac44..5c5710598 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -11,7 +11,6 @@ use cdk::nuts::{ SwapRequest, SwapResponse, }; use cdk::util::unix_time; -use cdk::Error; use paste::paste; use uuid::Uuid; @@ -31,19 +30,20 @@ macro_rules! post_cache_wrapper { let json_extracted_payload = payload.deref(); let State(mint_state) = state.clone(); - let cache_key = serde_json::to_string(&json_extracted_payload).map_err(|err| { - into_response(Error::from(err)) - })?; - - if let Some(cached_response) = mint_state.cache.get(&cache_key) { - return Ok(Json(serde_json::from_str(&cached_response) - .expect("Shouldn't panic: response is json-deserializable."))); + let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) { + Some(key) => key, + None => { + // Could not calculate key, just return the handler result + return $handler(state, payload).await; + } + }; + + if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await { + return Ok(Json(cached_response)); } let response = $handler(state, payload).await?; - mint_state.cache.insert(cache_key, serde_json::to_string(response.deref()) - .expect("Shouldn't panic: response is json-serializable.") - ).await; + mint_state.cache.set(cache_key, &response.deref()).await; Ok(response) } } diff --git a/crates/cdk-integration-tests/src/init_fake_wallet.rs b/crates/cdk-integration-tests/src/init_fake_wallet.rs index d3216b820..b24f5b522 100644 --- a/crates/cdk-integration-tests/src/init_fake_wallet.rs +++ b/crates/cdk-integration-tests/src/init_fake_wallet.rs @@ -50,11 +50,9 @@ where ); let mint = create_mint(database, ln_backends.clone()).await?; - let cache_ttl = 3600; - let cache_tti = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti) + let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)) .await .unwrap(); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 525f8e4ed..e1fa4f750 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -216,17 +216,11 @@ where ); let mint = create_mint(database, ln_backends.clone()).await?; - let cache_time_to_live = 3600; - let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router( - Arc::clone(&mint_arc), - cache_time_to_live, - cache_time_to_idle, - ) - .await - .unwrap(); + let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)) + .await + .unwrap(); let mint_service = Router::new() .merge(v1_service) diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 2e52b0345..60887607f 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -83,17 +83,10 @@ pub async fn start_mint( HashMap::new(), ) .await?; - let cache_time_to_live = 3600; - let cache_time_to_idle = 3600; let mint_arc = Arc::new(mint); - let v1_service = cdk_axum::create_mint_router( - Arc::clone(&mint_arc), - cache_time_to_live, - cache_time_to_idle, - ) - .await?; + let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)).await?; let mint_service = Router::new() .merge(v1_service) diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 7946fd5ba..975991f6f 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -6,6 +6,12 @@ mnemonic = "" # input_fee_ppk = 0 # enable_swagger_ui = false +[info.http_cache] +backend = "redis" +ttl = 60 +tti = 60 +key_prefix = "mintd" +connection_string = "redis://localhost" [mint_info] @@ -32,9 +38,12 @@ ln_backend = "cln" # fee_percent=0.04 # reserve_fee_min=4 -# [cln] -# Required if using cln backend path to rpc -# cln_path = "" +[cln] +#Required if using cln backend path to rpc +cln_path = "" +rpc_path = "" +fee_percent = 0.02 +reserve_fee_min = 1 # [strike] # For the Webhook subscription, the url under [info] must be a valid, absolute, non-local, https url diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index fc579b17a..e2f38a45e 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use cdk::nuts::{CurrencyUnit, PublicKey}; use cdk::Amount; +use cdk_axum::cache; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; @@ -11,11 +12,10 @@ pub struct Info { pub listen_host: String, pub listen_port: u16, pub mnemonic: String, - pub seconds_quote_is_valid_for: Option, - pub seconds_to_cache_requests_for: Option, - pub seconds_to_extend_cache_by: Option, pub input_fee_ppk: Option, + pub http_cache: cache::Config, + /// When this is set to true, the mint exposes a Swagger UI for it's API at /// `[listen_host]:[listen_port]/swagger-ui` /// @@ -93,6 +93,7 @@ pub struct LNbits { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Cln { pub rpc_path: PathBuf, + #[serde(default)] pub bolt12: bool, pub fee_percent: f32, pub reserve_fee_min: Amount, @@ -210,7 +211,7 @@ pub struct MintInfo { impl Settings { #[must_use] - pub fn new(config_file_name: &Option) -> Self { + pub fn new>(config_file_name: Option

) -> Self { let default_settings = Self::default(); // attempt to construct settings with file let from_file = Self::new_from_default(&default_settings, config_file_name); @@ -223,9 +224,9 @@ impl Settings { } } - fn new_from_default( + fn new_from_default>( default: &Settings, - config_file_name: &Option, + config_file_name: Option

, ) -> Result { let mut default_config_file_name = home::home_dir() .ok_or(ConfigError::NotFound("Config Path".to_string()))? @@ -233,7 +234,7 @@ impl Settings { default_config_file_name.push("config.toml"); let config: String = match config_file_name { - Some(value) => value.clone().to_string_lossy().to_string(), + Some(value) => value.into().to_string_lossy().to_string(), None => default_config_file_name.to_string_lossy().to_string(), }; let builder = Config::builder(); diff --git a/crates/cdk-mintd/src/env_vars.rs b/crates/cdk-mintd/src/env_vars.rs index eae4de4d2..8150858d2 100644 --- a/crates/cdk-mintd/src/env_vars.rs +++ b/crates/cdk-mintd/src/env_vars.rs @@ -127,22 +127,15 @@ impl Info { self.mnemonic = mnemonic; } - // Optional fields - if let Ok(seconds_str) = env::var(ENV_SECONDS_QUOTE_VALID) { - if let Ok(seconds) = seconds_str.parse() { - self.seconds_quote_is_valid_for = Some(seconds); - } - } - if let Ok(cache_seconds_str) = env::var(ENV_CACHE_SECONDS) { if let Ok(seconds) = cache_seconds_str.parse() { - self.seconds_to_cache_requests_for = Some(seconds); + self.http_cache.ttl = Some(seconds); } } if let Ok(extend_cache_str) = env::var(ENV_EXTEND_CACHE_SECONDS) { if let Ok(seconds) = extend_cache_str.parse() { - self.seconds_to_extend_cache_by = Some(seconds); + self.http_cache.tti = Some(seconds); } } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index f088ba3b5..1919dcf22 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -21,3 +21,22 @@ fn expand_path(path: &str) -> Option { Some(PathBuf::from(path)) } } + +#[cfg(test)] +mod test { + use std::env::current_dir; + + use super::*; + + #[test] + fn example_is_parsed() { + let config = config::Settings::new(Some(format!( + "{}/example.config.toml", + current_dir().expect("cwd").to_string_lossy() + ))); + let cache = config.info.http_cache; + + assert_eq!(cache.ttl, Some(60)); + assert_eq!(cache.tti, Some(60)); + } +} diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index ac61daa63..8e45660cc 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -19,6 +19,7 @@ use cdk::nuts::nut17::SupportedMethods; use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; use cdk::types::LnKey; +use cdk_axum::cache::HttpCache; use cdk_mintd::cli::CLIArgs; use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; use cdk_mintd::setup::LnBackendSetup; @@ -32,9 +33,6 @@ use tracing_subscriber::EnvFilter; use utoipa::OpenApi; const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); -const DEFAULT_QUOTE_TTL_SECS: u64 = 1800; -const DEFAULT_CACHE_TTL_SECS: u64 = 1800; -const DEFAULT_CACHE_TTI_SECS: u64 = 1800; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -68,7 +66,7 @@ async fn main() -> anyhow::Result<()> { let mut mint_builder = MintBuilder::new(); let mut settings = if config_file_arg.exists() { - config::Settings::new(&Some(config_file_arg)) + config::Settings::new(Some(config_file_arg)) } else { tracing::info!("Config file does not exist. Attempting to read env vars"); config::Settings::default() @@ -300,18 +298,15 @@ async fn main() -> anyhow::Result<()> { .with_quote_ttl(10000, 10000) .with_seed(mnemonic.to_seed_normalized("").to_vec()); - let cache_ttl = settings - .info - .seconds_to_cache_requests_for - .unwrap_or(DEFAULT_CACHE_TTL_SECS); - let cached_endpoints = vec![ CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap), ]; - mint_builder = mint_builder.add_cache(Some(cache_ttl), cached_endpoints); + let cache: HttpCache = settings.info.http_cache.into(); + + mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints); let mint = mint_builder.build().await?; @@ -330,16 +325,9 @@ async fn main() -> anyhow::Result<()> { let listen_addr = settings.info.listen_host; let listen_port = settings.info.listen_port; - let _quote_ttl = settings - .info - .seconds_quote_is_valid_for - .unwrap_or(DEFAULT_QUOTE_TTL_SECS); - let cache_tti = settings - .info - .seconds_to_extend_cache_by - .unwrap_or(DEFAULT_CACHE_TTI_SECS); - - let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?; + + let v1_service = + cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache).await?; let mut mint_service = Router::new() .merge(v1_service)