From 84e5696436d5f920a2623306c229a53c186b1a13 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Tue, 5 May 2026 11:12:50 +0100 Subject: [PATCH 1/5] Adding TypeBuilder pattern to mcp_service to enable passing in the request client Signed-off-by: Dawid Nowak --- Cargo.lock | 21 ++++++++++++++++ Cargo.toml | 2 +- crates/contextforge-gateway-rs-lib/Cargo.toml | 2 +- .../src/gateway/mcp_gateway.rs | 24 ++++++------------- crates/contextforge-gateway-rs-lib/src/lib.rs | 9 ++++++- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c2570b2..5bc3af88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,6 +588,7 @@ dependencies = [ "tracing-appender", "tracing-opentelemetry", "tracing-subscriber", + "typed-builder", "url", "uuid", ] @@ -3996,6 +3997,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-builder" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "typenum" version = "1.20.0" diff --git a/Cargo.toml b/Cargo.toml index 054f56e6..e450e582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ http-body-util = "0.1.3" bytes = "1.11.1" reqwest = "0.13" itertools = "0.14" - +typed-builder = "0.23.2" [profile.release] codegen-units = 1 diff --git a/crates/contextforge-gateway-rs-lib/Cargo.toml b/crates/contextforge-gateway-rs-lib/Cargo.toml index 99408ee7..b0ab15b9 100644 --- a/crates/contextforge-gateway-rs-lib/Cargo.toml +++ b/crates/contextforge-gateway-rs-lib/Cargo.toml @@ -47,7 +47,7 @@ hyper = { version = "1.4.0" } rustls = "0.23" rustls-pki-types = { version = "1.14.1", features = ["std"] } tokio-rustls = "0.26.4" - +typed-builder.workspace = true [features] default = [] with_tools = [] diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs index 7dee390f..01202b1d 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; - use std::{collections::HashSet, sync::Arc}; use super::mcp_call_validator::AuthorizedCallValidator; + use http::request::Parts; use itertools::Itertools; use rmcp::RoleClient; @@ -25,6 +25,7 @@ use rmcp::{ use tokio::sync::Mutex; use tracing::{debug, info, warn}; +use typed_builder::TypedBuilder; use crate::gateway::mcp_call_validator::InitializeCallValidator; use crate::gateway::session_manager::SessionManager; @@ -33,33 +34,22 @@ pub use crate::gateway::session_store::LocalUserSessionStore; use crate::gateway::session_store::{UserSession, UserSessionStore}; use crate::{SessionId, user_config_store::UserConfig}; -#[derive(Clone)] +#[derive(Clone, TypedBuilder)] +#[builder(field_defaults(setter(prefix = "with_")))] pub struct McpService where T: UserSessionStore, { + #[builder(default = Arc::new(Mutex::new(HashSet::new())))] subscriptions: Arc>>, + #[builder(default = Arc::new(Mutex::new(HashMap::new())))] transports: Arc>>, + #[builder(default = Arc::new(Mutex::new(LoggingLevel::Debug)))] log_level: Arc>, http_client: reqwest::Client, user_session_store: T, } -impl McpService -where - T: UserSessionStore, -{ - pub fn with_stores(user_session_store: T) -> Self { - Self { - subscriptions: Arc::new(Mutex::new(HashSet::new())), - transports: Arc::new(Mutex::new(HashMap::new())), - log_level: Arc::new(Mutex::new(LoggingLevel::Debug)), - http_client: reqwest::Client::new(), - user_session_store, - } - } -} - #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BackendTransportKey { backend_name: String, diff --git a/crates/contextforge-gateway-rs-lib/src/lib.rs b/crates/contextforge-gateway-rs-lib/src/lib.rs index 399bd601..e5367565 100644 --- a/crates/contextforge-gateway-rs-lib/src/lib.rs +++ b/crates/contextforge-gateway-rs-lib/src/lib.rs @@ -49,10 +49,17 @@ pub async fn run_gateway( let streamable_config = StreamableHttpServerConfig::default().disable_allowed_hosts(); + let reqwest_backend_client = reqwest::Client::default(); + // Create streamable HTTP service let mcp_service: StreamableHttpService, LocalSessionManager> = StreamableHttpService::new( - move || Ok(McpService::with_stores(user_session_store.clone())), + move || { + Ok(McpService::builder() + .with_user_session_store(user_session_store.clone()) + .with_http_client(reqwest_backend_client.clone()) + .build()) + }, local_session_manager, streamable_config, ); From 8d523d7cd2f34e8224403f26192d7e689310c0b8 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 6 May 2026 11:29:57 +0100 Subject: [PATCH 2/5] Framework for mocking infrastructure Signed-off-by: Dawid Nowak --- Cargo.lock | 318 ++++++++++++++++++ crates/contextforge-gateway-rs-lib/Cargo.toml | 6 + .../contextforge-gateway-rs-lib/src/common.rs | 83 ++++- .../src/gateway/session_store/mod.rs | 7 +- crates/contextforge-gateway-rs-lib/src/lib.rs | 159 +++++---- .../src/tests/gateway_end_to_end.rs | 227 +++++++++++++ .../src/tests/mock_counter.rs | 259 ++++++++++++++ .../src/tests/mocked_user_config_store.rs | 69 ++++ .../src/tests/mod.rs | 3 + .../contextforge-gateway-rs-lib/src/tools.rs | 34 +- .../src/user_config_store/mod.rs | 6 + crates/contextforge-gateway-rs/src/main.rs | 13 +- crates/contextforge-gateway-rs/src/runtime.rs | 31 +- 13 files changed, 1081 insertions(+), 134 deletions(-) create mode 100644 crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs create mode 100644 crates/contextforge-gateway-rs-lib/src/tests/mock_counter.rs create mode 100644 crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs create mode 100644 crates/contextforge-gateway-rs-lib/src/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5bc3af88..eb279097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -282,6 +292,34 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum-test" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a86bfe2ef15bee102ac34912f7f4542b0bb37dc464fa55461763999c4d625e7" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "expect-json", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower", + "url", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -373,6 +411,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cc" version = "1.2.61" @@ -477,6 +521,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "combine" version = "4.6.7" @@ -556,6 +609,7 @@ dependencies = [ "async-trait", "axum", "axum-jwt-auth", + "axum-test", "bytes", "chrono", "clap", @@ -568,7 +622,9 @@ dependencies = [ "itertools", "jsonwebtoken", "lru_time_cache", + "mockito", "openid", + "openport", "redis", "reqwest 0.13.3", "rmcp", @@ -577,6 +633,7 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", + "test-log", "thiserror 2.0.18", "tokio", "tokio-rustls", @@ -871,6 +928,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -998,6 +1061,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1007,12 +1079,44 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1044,6 +1148,35 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "expect-json" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "regex", + "serde", + "serde_json", + "thiserror 2.0.18", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1734,6 +1867,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1995,6 +2137,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mockito" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "pin-project-lite", + "rand 0.9.4", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -2033,6 +2200,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2059,6 +2240,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" @@ -2095,6 +2285,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2190,6 +2391,15 @@ dependencies = [ "validator", ] +[[package]] +name = "openport" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365c699f76305b3e62588961a288be10f0819ef1391f25870a69b35a213577cc" +dependencies = [ + "rand 0.8.6", +] + [[package]] name = "openssl" version = "0.10.78" @@ -2471,6 +2681,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2888,6 +3108,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reserve-port" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -2996,6 +3225,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdaa068902270ca7fa8619775e1838e23a63620abac0947ce0f715819b8cec" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.10.1", + "thiserror 2.0.18", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3369,6 +3613,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simple_asn1" version = "0.6.4" @@ -3573,6 +3823,38 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-log" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" +dependencies = [ + "env_logger", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-core" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn 2.0.117", + "test-log-core", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4017,12 +4299,42 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "typetag" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "unicase" version = "2.9.0" @@ -4661,6 +4973,12 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.2" diff --git a/crates/contextforge-gateway-rs-lib/Cargo.toml b/crates/contextforge-gateway-rs-lib/Cargo.toml index b0ab15b9..83102b15 100644 --- a/crates/contextforge-gateway-rs-lib/Cargo.toml +++ b/crates/contextforge-gateway-rs-lib/Cargo.toml @@ -53,5 +53,11 @@ default = [] with_tools = [] +[dev-dependencies] +openport = { version = "0.1.1", features = ["rand"] } +mockito = "1.7.2" +axum-test = "20.0.0" +test-log = "0.2.20" + [lints] workspace = true diff --git a/crates/contextforge-gateway-rs-lib/src/common.rs b/crates/contextforge-gateway-rs-lib/src/common.rs index 3f9b5684..39f37015 100644 --- a/crates/contextforge-gateway-rs-lib/src/common.rs +++ b/crates/contextforge-gateway-rs-lib/src/common.rs @@ -1,16 +1,18 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{fs, path::PathBuf, sync::Arc}; use axum_jwt_auth::JwtDecoder; -use clap::Parser; +use chrono::{Duration, Utc}; +use clap::{Parser, ValueEnum}; use http::uri::Authority; use openid::{CompactJson, CustomClaims, StandardClaims}; use redis::{ConnectionAddr, IntoConnectionInfo}; use serde::{Deserialize, Serialize}; +use url::Url; use std::net::SocketAddr; use thiserror::Error; -use crate::user_config_store::UserConfigStore; +use crate::{const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, user_config_store::UserConfigStore}; #[derive(Clone)] pub struct ContextForgeGatewayAppState { @@ -48,7 +50,15 @@ impl IntoConnectionInfo for RedisConfig { } } -#[derive(Debug, Clone, Parser)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum UpstreamConnectionMode { + PlainTextAndTls, + PlainTextAndMTls, + TlsOnly, + MtlsOnly, +} + +#[derive(Debug, Clone, Parser, Default)] #[command(name = "contextforge-gateway-rs")] #[command(about = "Minimal, fast and experimental Gateway/Dataplane for ContextForge")] pub struct Config { @@ -83,6 +93,15 @@ pub struct Config { #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_SERVER_CERTIFICATE")] pub server_certificate: Option, + + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE")] + pub upstream_connection_mode: Option, + + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM_PRIVATE_KEY")] + pub upstream_private_key: Option, + + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM__CERTIFICATE")] + pub upstream_certificate: Option, } #[derive(Error, Debug)] @@ -101,3 +120,59 @@ impl TryFrom<&Config> for RedisConfig { type Error = ConfigValidationError; } + +impl TryFrom<&Config> for reqwest::Client { + type Error = Box; + + fn try_from(config: &Config) -> Result { + let builder = reqwest::Client::builder(); + let builder = match config.upstream_connection_mode.as_ref() { + None | Some(UpstreamConnectionMode::TlsOnly) => builder.https_only(true), + Some(UpstreamConnectionMode::PlainTextAndTls) => builder.https_only(false), + Some(UpstreamConnectionMode::PlainTextAndMTls) => { + builder.https_only(false).identity(extract_identity(config)?) + }, + Some(UpstreamConnectionMode::MtlsOnly) => builder.https_only(true).identity(extract_identity(config)?), + }; + Ok(builder.build()?) + } +} + +fn extract_identity(config: &Config) -> Result> { + match (config.upstream_private_key.as_ref(), config.upstream_certificate.as_ref()) { + (Some(private_key), Some(certificate)) => { + let cert = fs::read(certificate)?; + let key = fs::read(private_key)?; + Ok(reqwest::Identity::from_pkcs8_pem(&cert, &key)?) + }, + + _ => Err("Invalid/missing configuration".into()), + } +} + +#[derive(Deserialize, Serialize)] +pub struct DefaultClaims { + iss: Url, + sub: String, + aud: String, + exp: i64, + iat: Option, + userinfo: openid::Userinfo, +} + +impl DefaultClaims { + pub fn new(user_id: String) -> Self { + let url = "http://contextforge-gateway-rs".parse().expect("Expecting this to work"); + let audience = CONEXT_FORGE_GATEWAY_AUDIENCE.to_owned(); + let user_info = openid::Userinfo { sub: user_id.clone(), ..Default::default() }; + Self { + iss: url, + sub: user_id, + aud: audience, + exp: (Utc::now() + Duration::hours(1)).timestamp(), + iat: Some(Utc::now().timestamp()), + + userinfo: user_info, + } + } +} diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs index e988b7ed..5e83cbef 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs @@ -17,7 +17,7 @@ pub struct SessionMap { } impl SessionMap { - #[expect(dead_code, reason = "session mapping access is used by store implementations as they evolve")] + #[allow(dead_code, reason = "session mapping access is used by store implementations as they evolve")] pub fn session(&self) -> Option> { self.upstream_session_id.clone() } @@ -29,17 +29,16 @@ pub struct SessionMapping { pub session_mapping: Vec, } +#[allow(dead_code, reason = "session mapping access is used by store implementations as they evolve")] impl SessionMapping { - #[expect(dead_code, reason = "session mapping helpers are used by alternate session stores")] pub fn new() -> Self { Self { session_mapping: vec![] } } - #[expect(dead_code, reason = "session mapping helpers are used by alternate session stores")] + pub fn push(&mut self, host: String, upstream_session: Option<&Arc>) { self.session_mapping.push(SessionMap { upstream_session_id: upstream_session.cloned(), backend_name: host }); } - #[expect(dead_code, reason = "session mapping helpers are used by alternate session stores")] pub fn get<'a>(&'a self, host: &'a str) -> Option<&'a SessionMap> { self.session_mapping.iter().find(|m| m.backend_name == host) } diff --git a/crates/contextforge-gateway-rs-lib/src/lib.rs b/crates/contextforge-gateway-rs-lib/src/lib.rs index e5367565..d6797475 100644 --- a/crates/contextforge-gateway-rs-lib/src/lib.rs +++ b/crates/contextforge-gateway-rs-lib/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fs, sync::Arc}; +use std::{env, fs, sync::Arc}; use axum::middleware; use axum_jwt_auth::LocalDecoder; @@ -15,6 +15,9 @@ mod gateway; mod layers; mod transports; +#[cfg(test)] +mod tests; + #[cfg(feature = "with_tools")] mod tools; @@ -25,85 +28,107 @@ use layers::session_id::SessionId; use tower_http::cors::{Any, CorsLayer}; use transports::{DownstreamTls, Tcp}; +use typed_builder::TypedBuilder; use crate::{ - common::{ContextForgeGatewayAppState, RedisClient, RedisConfig}, + common::ContextForgeGatewayAppState, const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, gateway::LocalUserSessionStore, layers::{ claims_id::claims_layer, session_id::SessionIdLayer, user_config_store::user_config_store_layer, virtual_host_id::virtual_host_id_layer, }, - user_config_store::RedisUserConfigStore, + user_config_store::UserConfigStore, }; pub use crate::common::Config; +pub use common::{RedisClient, RedisConfig}; +pub use user_config_store::RedisUserConfigStore; -pub async fn run_gateway( +#[derive(Clone, TypedBuilder)] +#[builder(field_defaults(setter(prefix = "with_")))] +pub struct Gateway { config: Config, - local_session_manager: Arc, -) -> Result<(), Box> { - let redis_config = RedisConfig::try_from(&config)?; - let redis_client = RedisClient::open(redis_config)?; - let user_session_store = LocalUserSessionStore::new(); - - let streamable_config = StreamableHttpServerConfig::default().disable_allowed_hosts(); - - let reqwest_backend_client = reqwest::Client::default(); - - // Create streamable HTTP service - let mcp_service: StreamableHttpService, LocalSessionManager> = - StreamableHttpService::new( - move || { - Ok(McpService::builder() - .with_user_session_store(user_session_store.clone()) - .with_http_client(reqwest_backend_client.clone()) - .build()) - }, - local_session_manager, - streamable_config, - ); - - let cors_layer = CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any).expose_headers(Any); - - let mut validation = Validation::new(Algorithm::RS256); - validation.set_audience(&[CONEXT_FORGE_GATEWAY_AUDIENCE]); - - let local_docoder = LocalDecoder::builder() - .keys(vec![DecodingKey::from_rsa_pem(&fs::read(&config.token_verification_public_key)?)?]) - .validation(validation) - .build()?; - let mcp_add_state = ContextForgeGatewayAppState { - jwt_token_decoder: Arc::new(local_docoder), - config_store: Arc::new(RedisUserConfigStore::new(redis_client)), - config: config.clone(), - }; - - let app = axum::Router::new() - .nest_service("/servers/{virtual_host_name}/mcp", mcp_service) - .layer(middleware::from_fn_with_state(mcp_add_state.clone(), user_config_store_layer)) - .layer(middleware::from_fn_with_state(mcp_add_state.clone(), claims_layer)) - .layer(SessionIdLayer) - .layer(middleware::from_fn(virtual_host_id_layer)) - .layer(cors_layer); - - #[cfg(feature = "with_tools")] - let app = tools::add_tools(app); - - let app = app.with_state(mcp_add_state); - let app = axum::Router::new().nest("/contextforge-rs", app); - - let mut handlers = vec![]; - - if let Some(tcp) = Option::::try_from(&config)? { - handlers.push(tcp.handle_tcp(app.clone()).boxed()); - } + session_manager: Arc, + user_config_store: Arc, +} - if let Some(tls) = Option::::try_from(&config)? { - handlers.push(tls.handle_tls(app.clone()).boxed()); +impl Gateway { + pub async fn run_gateway(self) -> Result<(), Box> { + let path = env::current_dir()?; + println!("Current path {path:?}"); + let config = &self.config; + let session_manager = self.session_manager; + let user_config_store = self.user_config_store; + + let user_session_store = LocalUserSessionStore::new(); + + let streamable_config = StreamableHttpServerConfig::default().disable_allowed_hosts(); + + let reqwest_backend_client = reqwest::Client::builder().build()?; + + // Create streamable HTTP service + let mcp_service: StreamableHttpService, LocalSessionManager> = + StreamableHttpService::new( + move || { + Ok(McpService::builder() + .with_user_session_store(user_session_store.clone()) + .with_http_client(reqwest_backend_client.clone()) + .build()) + }, + session_manager, + streamable_config, + ); + + let cors_layer = CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any).expose_headers(Any); + + let mut validation = Validation::new(Algorithm::RS256); + validation.set_audience(&[CONEXT_FORGE_GATEWAY_AUDIENCE]); + + let local_docoder = LocalDecoder::builder() + .keys(vec![ + DecodingKey::from_rsa_pem(&fs::read(&config.token_verification_public_key).map_err(|e| { + format!("Error when creating local decoder {e:?} {:?}", config.token_verification_public_key) + })?) + .map_err(|e| { + format!("Error when creating local decoder {e:?} {:?}", config.token_verification_public_key) + })?, + ]) + .validation(validation) + .build() + .map_err(|e| format!("Error when creating local decoder {e:?}"))?; + let mcp_add_state: ContextForgeGatewayAppState = ContextForgeGatewayAppState { + jwt_token_decoder: Arc::new(local_docoder), + config_store: Arc::clone(&user_config_store), + config: config.clone(), + }; + + let app = axum::Router::new() + .nest_service("/servers/{virtual_host_name}/mcp", mcp_service) + .layer(middleware::from_fn_with_state(mcp_add_state.clone(), user_config_store_layer)) + .layer(middleware::from_fn_with_state(mcp_add_state.clone(), claims_layer)) + .layer(SessionIdLayer) + .layer(middleware::from_fn(virtual_host_id_layer)) + .layer(cors_layer); + + #[cfg(feature = "with_tools")] + let app = tools::add_tools(app); + + let app = app.with_state(mcp_add_state); + let app = axum::Router::new().nest("/contextforge-rs", app); + + let mut handlers = vec![]; + + if let Some(tcp) = Option::::try_from(config)? { + handlers.push(tcp.handle_tcp(app.clone()).boxed()); + } + + if let Some(tls) = Option::::try_from(config)? { + handlers.push(tls.handle_tls(app.clone()).boxed()); + } + + let _ = futures::future::join_all(handlers).await; + + Ok(()) } - - let _ = futures::future::join_all(handlers).await; - - Ok(()) } diff --git a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs new file mode 100644 index 00000000..9a3243fa --- /dev/null +++ b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs @@ -0,0 +1,227 @@ +use futures::{FutureExt, future::BoxFuture}; +use http::{HeaderMap, HeaderValue}; +use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; +use openport; +use rmcp::{ + ServiceExt, + model::InitializeRequestParams, + transport::{ + StreamableHttpClientTransport, StreamableHttpServerConfig, StreamableHttpService, + streamable_http_client::StreamableHttpClientTransportConfig, + streamable_http_server::session::local::LocalSessionManager, + }, +}; +use std::{collections::HashMap, fs, sync::Arc}; +use tokio::sync::Semaphore; +use tracing::{info, warn}; + +use crate::{ + Config, Gateway, + common::DefaultClaims, + tests::{mock_counter, mocked_user_config_store::MockedUserConfigStore}, + user_config_store::{BackendMCPGateway, User, UserConfig, UserConfigStore, VirtualHost}, +}; + +const MOCK_COUNTER_TOOL_NAMES: &[&str] = + &["decrement", "echo", "get_session_id", "get_value", "increment", "long_task", "say_hello", "sum"]; + +fn create_ports(ports: usize) -> Vec { + (0..ports).into_iter().map(|_| openport::pick_random_unused_port().expect("Expecting to find port")).collect() +} + +fn create_backends(ports: &[u16]) -> HashMap { + ports + .iter() + .filter_map(|port| { + let url = format!("http://127.0.0.1:{port}/mcp").parse().expect("This should work"); + Some((format!("backend-{port}"), BackendMCPGateway { url })) + }) + .collect::>() +} + +fn create_tool_names(ports: &[u16]) -> Vec { + ports + .iter() + .flat_map(|port| { + MOCK_COUNTER_TOOL_NAMES.iter().map(|name| format!("backend-{port}-{name}")).collect::>() + }) + .collect::>() +} + +fn create_axum_servers( + ports: &[u16], + router: axum::Router, +) -> Vec>>> { + ports + .iter() + .map(|port| { + let addr = format!("127.0.0.1:{port}"); + let router = router.clone(); + async { + let listener = tokio::net::TcpListener::bind(addr).await.expect("Expect this to work"); + axum::serve(listener, router).await.unwrap(); + Ok(()) + } + .boxed() + }) + .collect() +} + +pub fn get_token(user_id: String) -> String { + let key = EncodingKey::from_rsa_pem(&fs::read("../../assets/jwt.key").expect("Expecting this to work")) + .expect("Expecting this to work"); + let mut header = Header::new(Algorithm::RS256); + header.kid = Some("test".to_owned()); + + let claims = DefaultClaims::new(user_id); + + encode::(&header, &claims, &key).expect("Expecting this to work") +} + +struct TestSettings { + handle: tokio::task::JoinHandle>>>, + gateway_url: String, + expected_tool_names: Vec, +} + +async fn create_gateway_with_four_counters(user: &str, test_semaphore: Arc) -> TestSettings { + let gateway_port = create_ports(1)[0]; + + let mut config = Config::default(); + config.address = Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")); + config.token_verification_public_key = "../../assets/jwt.key.pub".into(); + + let mocked_user_config_store = MockedUserConfigStore::default(); + + let gateway_one_ports = create_ports(2); + let gateway_two_ports = create_ports(2); + + let service = StreamableHttpService::new( + || Ok(mock_counter::Counter::new()), + LocalSessionManager::default().into(), + StreamableHttpServerConfig::default(), + ); + + let router = axum::Router::new().route_service("/mcp", service); + + let servers_one = create_axum_servers(&gateway_one_ports, router.clone()); + let servers_two = create_axum_servers(&gateway_two_ports, router.clone()); + + assert_ne!(gateway_one_ports, gateway_two_ports); + + let gateway_one_backends = create_backends(&gateway_one_ports); + let gateway_two_backends = create_backends(&gateway_two_ports); + + let mut virtual_host_one_tool_names = create_tool_names(&gateway_one_ports); + let mut virtual_host_two_tool_names = create_tool_names(&gateway_two_ports); + virtual_host_one_tool_names.sort(); + virtual_host_two_tool_names.sort(); + + let user_key = User::new(user); + + let virtual_host_one_id = uuid::Uuid::new_v4().to_string(); + let virtual_host_two_id = uuid::Uuid::new_v4().to_string(); + + let virtual_hosts = HashMap::from([ + (virtual_host_one_id.clone(), VirtualHost { backends: gateway_one_backends }), + (virtual_host_two_id.clone(), VirtualHost { backends: gateway_two_backends }), + ]); + + let user_config = UserConfig { virtual_hosts }; + + mocked_user_config_store.set_config(&user_key, &user_config).await.expect("This should work"); + + let gateway = Gateway::builder() + .with_config(config) + .with_user_config_store(Arc::new(mocked_user_config_store)) + .with_session_manager(Arc::new(LocalSessionManager::default())) + .build(); + + let gateway: std::pin::Pin< + Box>> + Send>, + > = async move { + let res = gateway.run_gateway().await; + warn!("Gateway exited with result {res:?}"); + test_semaphore.forget_permits(1); + Ok(()) + } + .boxed(); + + let handle: tokio::task::JoinHandle>>> = + tokio::spawn(futures::future::join_all( + vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), + )); + + let gateway_url = format!("http://127.0.0.1:{gateway_port}/contextforge-rs/servers/{}/mcp", virtual_host_one_id); + TestSettings { handle, gateway_url, expected_tool_names: virtual_host_one_tool_names } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn list_tools_end_to_end_test() -> Result<(), Box> { + let semaphore = Arc::new(Semaphore::new(1)); + let _ = semaphore.acquire().await.unwrap(); + let user = "admin@example.com"; + let test_semaphore = Arc::new(Semaphore::new(1)); + let _ = semaphore.acquire().await.unwrap(); + + let TestSettings { handle, gateway_url, expected_tool_names } = + create_gateway_with_four_counters(user, test_semaphore.clone()).await; + + let test_future: std::pin::Pin< + Box>> + Send>, + > = async { + let _ = test_semaphore.acquire().await; + //tokio::time::sleep(Duration::from_millis(100)).await; + let mut default_headers = HeaderMap::new(); + let token = get_token(user.to_owned()); + default_headers.insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(format!("Bearer {token}").as_str()).expect("This should work"), + ); + let client = reqwest::Client::builder().default_headers(default_headers).build().expect("This should work"); + + info!("Seding request to {gateway_url}"); + + let config = StreamableHttpClientTransportConfig::with_uri(gateway_url); + let transport = StreamableHttpClientTransport::with_client(client, config); + let request = InitializeRequestParams::default(); + + let maybe_service = request.serve(transport).await; + let Ok(running_service) = maybe_service else { + warn!("No Service {maybe_service:?}"); + return Err("Couldn't get a service".into()); + }; + + let list_tools = running_service.list_tools(None).await; + let Ok(list_tools) = list_tools else { + let msg = format!("List tools returned error {list_tools:?}"); + warn!(msg); + return Err(msg.into()); + }; + + let mut names: Vec = list_tools.tools.iter().map(|t| t.name.to_string()).collect(); + names.sort(); + + info!("Tool names {names:#?}"); + if expected_tool_names != names { + warn!("Actual {names:#?} Expected {expected_tool_names:#?}"); + return Err("Expected tool names don't match actual".into()); + } + + Ok(()) + } + .boxed(); + + let maybe_passed = test_future.await; + + handle.abort(); + if let Ok(_) = maybe_passed { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} diff --git a/crates/contextforge-gateway-rs-lib/src/tests/mock_counter.rs b/crates/contextforge-gateway-rs-lib/src/tests/mock_counter.rs new file mode 100644 index 00000000..76a7d0f0 --- /dev/null +++ b/crates/contextforge-gateway-rs-lib/src/tests/mock_counter.rs @@ -0,0 +1,259 @@ +#![allow(dead_code)] +use std::{any::Any, sync::Arc}; + +use rmcp::{ + ErrorData as McpError, RoleServer, ServerHandler, + handler::server::{ + router::{prompt::PromptRouter, tool::ToolRouter}, + wrapper::Parameters, + }, + model::*, + prompt, prompt_handler, prompt_router, schemars, + service::RequestContext, + task_handler, + task_manager::{OperationProcessor, OperationResultTransport}, + tool, tool_handler, tool_router, +}; +use serde_json::json; +use tokio::sync::Mutex; + +struct ToolCallOperationResult { + id: String, + result: Result, +} + +impl OperationResultTransport for ToolCallOperationResult { + fn operation_id(&self) -> &String { + &self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct StructRequest { + pub a: i32, + pub b: i32, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +pub struct ExamplePromptArgs { + /// A message to put in the prompt + pub message: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +pub struct CounterAnalysisArgs { + /// The target value you're trying to reach + pub goal: i32, + /// Preferred strategy: 'fast' or 'careful' + #[serde(skip_serializing_if = "Option::is_none")] + pub strategy: Option, +} + +#[derive(Clone)] +pub struct Counter { + counter: Arc>, + tool_router: ToolRouter, + prompt_router: PromptRouter, + processor: Arc>, +} + +#[tool_router] +impl Counter { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + counter: Arc::new(Mutex::new(0)), + tool_router: Self::tool_router(), + prompt_router: Self::prompt_router(), + processor: Arc::new(Mutex::new(OperationProcessor::new())), + } + } + + fn _create_resource_text(&self, uri: &str, name: &str) -> Resource { + RawResource::new(uri, name.to_string()).no_annotation() + } + + #[tool(description = "Increment the counter by 1")] + async fn increment(&self) -> Result { + let mut counter = self.counter.lock().await; + *counter += 1; + Ok(CallToolResult::success(vec![Content::text(counter.to_string())])) + } + + #[tool(description = "Decrement the counter by 1")] + async fn decrement(&self) -> Result { + let mut counter = self.counter.lock().await; + *counter -= 1; + Ok(CallToolResult::success(vec![Content::text(counter.to_string())])) + } + + #[tool(description = "Get the current counter value")] + async fn get_value(&self) -> Result { + let counter = self.counter.lock().await; + Ok(CallToolResult::success(vec![Content::text(counter.to_string())])) + } + + #[tool(description = "Long running task example")] + async fn long_task(&self) -> Result { + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + Ok(CallToolResult::success(vec![Content::text("Long task completed")])) + } + + #[tool(description = "Say hello to the client")] + fn say_hello(&self) -> Result { + Ok(CallToolResult::success(vec![Content::text("hello")])) + } + + #[tool(description = "Repeat what you say")] + fn echo(&self, Parameters(object): Parameters) -> Result { + Ok(CallToolResult::success(vec![Content::text(serde_json::Value::Object(object).to_string())])) + } + + #[tool(description = "Calculate the sum of two numbers")] + fn sum(&self, Parameters(StructRequest { a, b }): Parameters) -> Result { + Ok(CallToolResult::success(vec![Content::text((a + b).to_string())])) + } + + /// Returns the `Mcp-Session-Id` of the current session (streamable HTTP only). + #[tool(description = "Get the session ID for this connection")] + fn get_session_id(&self, ctx: RequestContext) -> Result { + let session_id = ctx + .extensions + .get::() + .and_then(|parts| parts.headers.get("mcp-session-id")) + .map(|v| v.to_str().unwrap_or("(non-ascii)").to_owned()); + + match session_id { + Some(id) => Ok(CallToolResult::success(vec![Content::text(id)])), + None => Ok(CallToolResult::success(vec![Content::text("no session (not running over streamable HTTP?)")])), + } + } +} + +#[prompt_router] +impl Counter { + /// This is an example prompt that takes one required argument, message + #[prompt( + name = "example_prompt", + meta = Meta(rmcp::object!({"meta_key": "meta_value"})) + )] + async fn example_prompt( + &self, + Parameters(args): Parameters, + _ctx: RequestContext, + ) -> Result, McpError> { + let prompt = format!("This is an example prompt with your message here: '{}'", args.message); + Ok(vec![PromptMessage::new_text(PromptMessageRole::User, prompt)]) + } + + /// Analyze the current counter value and suggest next steps + #[prompt(name = "counter_analysis")] + async fn counter_analysis( + &self, + Parameters(args): Parameters, + _ctx: RequestContext, + ) -> Result { + let strategy = args.strategy.unwrap_or_else(|| "careful".to_string()); + let current_value = *self.counter.lock().await; + let difference = args.goal - current_value; + + let messages = vec![ + PromptMessage::new_text( + PromptMessageRole::Assistant, + "I'll analyze the counter situation and suggest the best approach.", + ), + PromptMessage::new_text( + PromptMessageRole::User, + format!( + "Current counter value: {}\nGoal value: {}\nDifference: {}\nStrategy preference: {}\n\nPlease analyze the situation and suggest the best approach to reach the goal.", + current_value, args.goal, difference, strategy + ), + ), + ]; + + Ok(GetPromptResult::new(messages) + .with_description(format!("Counter analysis for reaching {} from {}", args.goal, current_value))) + } +} + +#[tool_handler(meta = Meta(rmcp::object!({"tool_meta_key": "tool_meta_value"})))] +#[prompt_handler(meta = Meta(rmcp::object!({"router_meta_key": "router_meta_value"})))] +#[task_handler] +impl ServerHandler for Counter { + fn get_info(&self) -> ServerInfo { + ServerInfo::new( + ServerCapabilities::builder() + .enable_prompts() + .enable_resources() + .enable_tools() + .build(), + ) + .with_server_info(Implementation::from_build_env()) + .with_protocol_version(ProtocolVersion::V_2024_11_05) + .with_instructions("This server provides counter tools and prompts. Tools: increment, decrement, get_value, say_hello, echo, sum. Prompts: example_prompt (takes a message), counter_analysis (analyzes counter state with a goal).".to_string()) + } + + async fn list_resources( + &self, + _request: Option, + _: RequestContext, + ) -> Result { + Ok(ListResourcesResult { + resources: vec![ + self._create_resource_text("str:////Users/to/some/path/", "cwd"), + self._create_resource_text("memo://insights", "memo-name"), + ], + next_cursor: None, + meta: None, + }) + } + + async fn read_resource( + &self, + request: ReadResourceRequestParams, + _: RequestContext, + ) -> Result { + let uri = &request.uri; + match uri.as_str() { + "str:////Users/to/some/path/" => { + let cwd = "/Users/to/some/path/"; + Ok(ReadResourceResult::new(vec![ResourceContents::text(cwd, uri.clone())])) + }, + "memo://insights" => { + let memo = "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ..."; + Ok(ReadResourceResult::new(vec![ResourceContents::text(memo, uri.clone())])) + }, + _ => Err(McpError::resource_not_found( + "resource_not_found", + Some(json!({ + "uri": uri + })), + )), + } + } + + async fn list_resource_templates( + &self, + _request: Option, + _: RequestContext, + ) -> Result { + Ok(ListResourceTemplatesResult { next_cursor: None, resource_templates: Vec::new(), meta: None }) + } + + async fn initialize( + &self, + _request: InitializeRequestParams, + context: RequestContext, + ) -> Result { + if let Some(http_request_part) = context.extensions.get::() { + let initialize_headers = &http_request_part.headers; + let initialize_uri = &http_request_part.uri; + tracing::info!(?initialize_headers, %initialize_uri, "initialize from http server"); + } + Ok(self.get_info()) + } +} diff --git a/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs b/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs new file mode 100644 index 00000000..c6f9497b --- /dev/null +++ b/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs @@ -0,0 +1,69 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use lru_time_cache::LruCache; +use tokio::sync::Mutex; + +use crate::user_config_store::{ConfigStoreError, User, UserConfig, UserConfigStore}; + +#[derive(Clone)] +pub struct MockedUserConfigStore { + pub user_map: Arc, Vec>>>, + pub cache: Arc>>, +} + +impl Default for MockedUserConfigStore { + fn default() -> Self { + Self { + user_map: Arc::new(Mutex::new(HashMap::new())), + cache: Arc::new(Mutex::new(LruCache::with_capacity(10))), + } + } +} + +#[async_trait] +impl UserConfigStore for MockedUserConfigStore { + async fn get_config<'a>(&self, user_key: &'a User) -> Result { + let has_key = { self.cache.lock().await.contains_key(user_key.key()) }; + if has_key { + if let Some(user_config) = self.cache.lock().await.get_mut(user_key.key()) { + Ok(user_config.clone()) + } else { + return Err(ConfigStoreError::NoDataForKey); + } + } else { + let Ok(key) = rmp_serde::encode::to_vec::(user_key) else { + return Err(ConfigStoreError::DataEncoding); + }; + + let user_map = self.user_map.lock().await; + let maybe_user_config = user_map.get(&key); + + let Some(user_config) = maybe_user_config else { + return Err(ConfigStoreError::NoDataForKey); + }; + + let Ok(user_config) = rmp_serde::decode::from_slice::(&user_config) else { + return Err(ConfigStoreError::DataWrongFormat); + }; + + self.cache.lock().await.insert(user_key.key().to_owned(), user_config.clone()); + Ok(user_config) + } + } + + async fn set_config<'a>(&self, user_key: &'a User, config: &'a UserConfig) -> Result<(), ConfigStoreError> { + let Ok(key) = rmp_serde::encode::to_vec::(user_key) else { + return Err(ConfigStoreError::DataEncoding); + }; + + let Ok(encoded) = rmp_serde::encode::to_vec::(config) else { + return Err(ConfigStoreError::DataEncoding); + }; + + let mut user_map = self.user_map.lock().await; + user_map.insert(key.clone(), encoded); + self.cache.lock().await.insert(user_key.key().to_owned(), config.clone()); + Ok(()) + } +} diff --git a/crates/contextforge-gateway-rs-lib/src/tests/mod.rs b/crates/contextforge-gateway-rs-lib/src/tests/mod.rs new file mode 100644 index 00000000..bde29179 --- /dev/null +++ b/crates/contextforge-gateway-rs-lib/src/tests/mod.rs @@ -0,0 +1,3 @@ +mod gateway_end_to_end; +mod mock_counter; +mod mocked_user_config_store; diff --git a/crates/contextforge-gateway-rs-lib/src/tools.rs b/crates/contextforge-gateway-rs-lib/src/tools.rs index 934d251c..10bd5ea9 100644 --- a/crates/contextforge-gateway-rs-lib/src/tools.rs +++ b/crates/contextforge-gateway-rs-lib/src/tools.rs @@ -5,50 +5,18 @@ use axum::{ response::{IntoResponse, Response}, routing::{Router, get, post}, }; -use chrono::{Duration, Utc}; use http::{StatusCode, header}; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; -use serde::{Deserialize, Serialize}; use std::fs; //use tracing::debug; -use url::Url; - use crate::{ - common::ContextForgeGatewayAppState, - const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, + common::{ContextForgeGatewayAppState, DefaultClaims}, user_config_store::{User, UserConfig}, }; -#[derive(Deserialize, Serialize)] -struct DefaultClaims { - iss: Url, - sub: String, - aud: String, - exp: i64, - iat: Option, - userinfo: openid::Userinfo, -} - -impl DefaultClaims { - fn new(user_id: String) -> Self { - let url = "http://contextforge-gateway-rs".parse().expect("Expecting this to work"); - let audience = CONEXT_FORGE_GATEWAY_AUDIENCE.to_owned(); - let user_info = openid::Userinfo { sub: user_id.clone(), ..Default::default() }; - Self { - iss: url, - sub: user_id, - aud: audience, - exp: (Utc::now() + Duration::hours(1)).timestamp(), - iat: Some(Utc::now().timestamp()), - - userinfo: user_info, - } - } -} - pub fn add_tools(router: Router) -> Router { router .route("/admin/tokens/{user_id}", get(get_token)) diff --git a/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs b/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs index 2e35345c..b3b218ad 100644 --- a/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs +++ b/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs @@ -48,6 +48,12 @@ pub struct User<'a> { key: &'a str, } +impl User<'_> { + pub fn key(&self) -> &str { + self.key + } +} + impl<'a> User<'a> { pub fn new(key: &'a str) -> Self { Self { name: "UserConfig", key } diff --git a/crates/contextforge-gateway-rs/src/main.rs b/crates/contextforge-gateway-rs/src/main.rs index ea6f8400..e05155db 100644 --- a/crates/contextforge-gateway-rs/src/main.rs +++ b/crates/contextforge-gateway-rs/src/main.rs @@ -4,7 +4,7 @@ mod runtime; use std::sync::Arc; use clap::Parser; -use contextforge_gateway_rs_lib::Config; +use contextforge_gateway_rs_lib::{Config, Gateway, RedisClient, RedisConfig, RedisUserConfigStore}; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; @@ -18,11 +18,16 @@ fn main() -> Result<(), Box> { println!("contextforge-gateway-rs {config:?}"); let _guard = logging::init_tracing_logging(&config); - let local_session_manager = Arc::new(LocalSessionManager::default()); - let runtime = runtime::Runtime::from(&config); - _ = runtime.execute(config, local_session_manager); + let user_config_store = RedisUserConfigStore::new(RedisClient::open(RedisConfig::try_from(&config)?)?); + let gateway = Gateway::builder() + .with_config(config) + .with_user_config_store(Arc::new(user_config_store)) + .with_session_manager(Arc::new(LocalSessionManager::default())) + .build(); + + _ = runtime.execute(gateway); Ok(()) } diff --git a/crates/contextforge-gateway-rs/src/runtime.rs b/crates/contextforge-gateway-rs/src/runtime.rs index d6fb6369..b42bd58b 100644 --- a/crates/contextforge-gateway-rs/src/runtime.rs +++ b/crates/contextforge-gateway-rs/src/runtime.rs @@ -1,11 +1,6 @@ -use contextforge_gateway_rs_lib::Config; -use futures::{FutureExt, future::BoxFuture}; -use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; -use std::{pin::Pin, sync::Arc, thread}; -use tokio::{ - io, - runtime::{Builder, LocalOptions, Runtime as TokioRuntime}, -}; +use contextforge_gateway_rs_lib::{Config, Gateway}; +use std::thread; +use tokio::runtime::{Builder, LocalOptions}; use tracing::{debug, error, info, warn}; #[derive(Debug, Clone)] @@ -28,11 +23,6 @@ impl<'b> From<&'b Config> for Runtime { } } -pub enum RuntimeType { - MultipleRuntimes(Vec>), - SingleRuntime(io::Result), -} - impl Default for Runtime { fn default() -> Self { Self { @@ -68,18 +58,14 @@ impl Runtime { builder.enable_all().name(thread_name).global_queue_interval(1024).max_io_events_per_tick(4); } - pub fn execute( - self, - config: Config, - session_manager: Arc, - ) -> Result<(), Box> { + pub fn execute(self, gateway: Gateway) -> Result<(), Box> { if self.single_runtime { let mut builder = Builder::new_multi_thread(); self.configure_builder(&mut builder, self.thread_name.to_owned()); let runtime = builder.build()?; runtime.block_on(async { tokio::select! { - res = contextforge_gateway_rs_lib::run_gateway(config, session_manager) => + res = gateway.run_gateway() => if res.is_ok(){ debug!("Gateway process terminated"); }else{ @@ -93,8 +79,9 @@ impl Runtime { .map(|i| { let thread_name = self.thread_name.clone(); let runtime = self.clone(); - let config = config.clone(); - let session_manager = session_manager.clone(); + let gateway = gateway.clone(); + // let config = config.clone(); + // let session_manager = session_manager.clone(); thread::Builder::new().name("contextforge-gateway-rs-{i}".to_owned()).spawn(move || { let mut builder = Builder::new_current_thread(); @@ -107,7 +94,7 @@ impl Runtime { runtime.block_on(async { tokio::select! { - res = contextforge_gateway_rs_lib::run_gateway(config, session_manager) => + res = gateway.run_gateway() => if res.is_ok(){ debug!("Gateway process terminated"); }else{ From 238d4822d419252bbbe598f7795bb657f8e2314f Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 6 May 2026 14:06:57 +0100 Subject: [PATCH 3/5] Adding tests for downstream tls Signed-off-by: Dawid Nowak --- assets/tls_certificate.pem | 17 ++ assets/tls_key.pem | 28 ++++ .../src/tests/gateway_end_to_end.rs | 153 +++++++++++++++--- 3 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 assets/tls_certificate.pem create mode 100644 assets/tls_key.pem diff --git a/assets/tls_certificate.pem b/assets/tls_certificate.pem new file mode 100644 index 00000000..fc236a02 --- /dev/null +++ b/assets/tls_certificate.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICwTCCAamgAwIBAgIJAOvk/cOYx7QRMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV +BAoMBGJvZ28wHhcNMTcwODI4MTQzNTIzWhcNMjcwODI2MTQzNTIzWjAPMQ0wCwYD +VQQKDARib2dvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZ9e9VGd +jJ4z1B7D8RcBhiBCU9qSfYrP2hFLBEEPBGXfFx6PGTLbKoNz8SpX4qvsZSF5pLV8 +f2/MU42YSv28KfSpYVP38ewWeY/J3mwe/6kGyDIRrRaIsjCEOXm7grUSOgVaqEmI +5gZ28Za5j9hOWU6RPLCgVZzvesRxcS7bh+DTcdJ74A+dED7EV39VqSmu9H5/B0Xq +AD7bgFHN4DFURU9J01F0f2KPxVLFHa+PbyjQzZXhDmw+Zl6hesJCvFFc4raFt5b0 +aRsEepoWwUb8KgApI5/cOEwmGhpLugTC8LALW6ZTWimpN5D92GvPdN1w9w9VPc3C +ZDfD7PLK9MjRFQIDAQABoyAwHjAcBgNVHREEFTATggR0ZXN0ggtleGFtcGxlLmNv +bTANBgkqhkiG9w0BAQsFAAOCAQEANDSklYrPCpv3Tr5sT2nr09LlwRivGUCLgIVv +fqpfVE/ij3h33QthhJ/CR6x9e9q2WXB+UfNq6fIS5Kw6bE6dx4AljTUocng9IqNA +y+CPxfq7Xl2uq6RqUFFnaVDZUmZGt0EofHvzpQKU29vGOjKoalIujoOj0sVyH7qK +k26z9teDlU/wHHOElLHZaGwRPv7M5pWo5x2y5EnfTxwpi5Ic5mu0gaE3Xa6qlcu5 +WcFVEd8NVFDYfxN1A52JRjyfpYrjcaPCOAzFrJERGMPtHSkyfd0djihvW75cpf64 +CWeNQYqqf13rr1sg727cTXd65BRkhKFSb7A8QNf2e1m2vips2Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/assets/tls_key.pem b/assets/tls_key.pem new file mode 100644 index 00000000..06bf2d43 --- /dev/null +++ b/assets/tls_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNn171UZ2MnjPU +HsPxFwGGIEJT2pJ9is/aEUsEQQ8EZd8XHo8ZMtsqg3PxKlfiq+xlIXmktXx/b8xT +jZhK/bwp9KlhU/fx7BZ5j8nebB7/qQbIMhGtFoiyMIQ5ebuCtRI6BVqoSYjmBnbx +lrmP2E5ZTpE8sKBVnO96xHFxLtuH4NNx0nvgD50QPsRXf1WpKa70fn8HReoAPtuA +Uc3gMVRFT0nTUXR/Yo/FUsUdr49vKNDNleEObD5mXqF6wkK8UVzitoW3lvRpGwR6 +mhbBRvwqACkjn9w4TCYaGku6BMLwsAtbplNaKak3kP3Ya8903XD3D1U9zcJkN8Ps +8sr0yNEVAgMBAAECggEAFLuNB49DI3qQH0M63oRDUxLNnUbcnmvwqTw1JCirKvZa +mCoso43NK2w1tItgcSqEm23UphbmhrLvFz2frXAIGPLiYT3mMi5r1bX51MNyHLUe +SfFCdwFepxjdPC5aCcPYIqGHkpBXjVn5hEJ+U1KsE7UoT8Y/ZGJ+gxJrnc3rtLd4 +TFxF5p6KKX3qluFnsjwjlcP7tMXd+fY3YWecAt+nUuz1eoQ5btieYW37/HV/LUhU +Ew1aQ65nWx2Tb3XIRiEOB+KadI6BYYgfiNQuEfvDHy0MUToTTxl/P+Z/RIpuMWQz +PysCGY8REM4+5bD+R8PUw2BszR6+3mIhpxROc3J5wQKBgQDu3i0kJ3XgkgSQjHYD +ev2TzLP4U1GVITIOPm2si5rsuL3OP4fEBgurnY++L/SQzAztyV0ZYhrXpi1OzkJQ +4HKdOux9L21FBgpZuIW3FVAJ45nTgicrIxPG/4fXsepXA/+YVFCZE/6YcBHbMaAj +ZNRgd6/oPUkEqA7d0z2+39lOFwKBgQDcXsf3iBRv8JZnDiLKSol6ig4IiIDumhVt +gSfLYmRrycSa5nj0if76s1618WRiU3oNWHd9QCCgB0ju/faoQSo1s4q3rGMaACPX +WczSudci0dxID47UrdQE3F0tj3nF9yJSqmyc9sgY16O34k5OvsCtCqOaWLXddXUj +uqN6HZPhswKBgQDXDYpAoZI709puNTdOnN1Nwp9I8+JgTBmfv07IaIvbkdu4o3Pc +5MB/CoTOaqhZ8Iu3TXIXFz8pZcAm0gXcgKZPriwZ7KgI245YBovEMFj1/kaQqP4Q +lS0KHSa058Yd/0iPYWGK3/h4T3WUDVKqau3VyAvEH+DsY023IqbVgP1IkwKBgDfs +GYS4VK9fd1tpm+yH48Fj/VGvCkECewOR7f5P1rn/ttO0PueXiUwnbpZvTpEhK+zt +EU2Ik37oulpjuk9SUhrUmBQqO+/iLzY8BJ1JKc4dQXBL+mwAPLiLD147daSGJYCi +3PMsMPUU6+gDFuomwBBpjcDiWCx93R8XAts/XEK/AoGAJrh0wA4SEkFV//FcS0k8 +7J3ussV0xJf7QK156oZ0vTtgGQtKgJV7RQ6YoqlNoypTEcYy9pe1ygoab9PlGMDr +h9oW2/ULzcdvFhQrN9LIZYtZUftbFOsnbC0VCUxM9X3jKqveuWdSUkanVxzhIm1o +5jNe8cr8Wy4nn6BCc0CShyE= +-----END PRIVATE KEY----- diff --git a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs index 9a3243fa..956e09ce 100644 --- a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs +++ b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs @@ -11,8 +11,14 @@ use rmcp::{ streamable_http_server::session::local::LocalSessionManager, }, }; -use std::{collections::HashMap, fs, sync::Arc}; -use tokio::sync::Semaphore; +use rustls::crypto::{self}; +use std::{ + collections::HashMap, + fs::{self, File}, + sync::Arc, +}; +use std::{io::Read, time::Duration}; + use tracing::{info, warn}; use crate::{ @@ -84,13 +90,10 @@ struct TestSettings { expected_tool_names: Vec, } -async fn create_gateway_with_four_counters(user: &str, test_semaphore: Arc) -> TestSettings { - let gateway_port = create_ports(1)[0]; - - let mut config = Config::default(); - config.address = Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")); - config.token_verification_public_key = "../../assets/jwt.key.pub".into(); - +async fn create_gateway_with_four_counters( + user: &str, + config: Config, +) -> Result> { let mocked_user_config_store = MockedUserConfigStore::default(); let gateway_one_ports = create_ports(2); @@ -132,7 +135,7 @@ async fn create_gateway_with_four_counters(user: &str, test_semaphore: Arc = async move { let res = gateway.run_gateway().await; warn!("Gateway exited with result {res:?}"); - test_semaphore.forget_permits(1); Ok(()) } .boxed(); @@ -152,27 +154,40 @@ async fn create_gateway_with_four_counters(user: &str, test_semaphore: Arc Result<(), Box> { - let semaphore = Arc::new(Semaphore::new(1)); - let _ = semaphore.acquire().await.unwrap(); +async fn plaintext_list_tools_end_to_end_test() -> Result<(), Box> { + let gateway_port = create_ports(1)[0]; + + let mut config = Config::default(); + config.address = Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")); + config.token_verification_public_key = "../../assets/jwt.key.pub".into(); + let user = "admin@example.com"; - let test_semaphore = Arc::new(Semaphore::new(1)); - let _ = semaphore.acquire().await.unwrap(); - let TestSettings { handle, gateway_url, expected_tool_names } = - create_gateway_with_four_counters(user, test_semaphore.clone()).await; + let Ok(TestSettings { handle, gateway_url, expected_tool_names }) = + create_gateway_with_four_counters(user, config).await + else { + panic!("Invalid configuration "); + }; let test_future: std::pin::Pin< Box>> + Send>, > = async { - let _ = test_semaphore.acquire().await; - //tokio::time::sleep(Duration::from_millis(100)).await; + //let _ = test_semaphore.acquire().await; + tokio::time::sleep(Duration::from_millis(100)).await; let mut default_headers = HeaderMap::new(); let token = get_token(user.to_owned()); default_headers.insert( @@ -225,3 +240,97 @@ async fn list_tools_end_to_end_test() -> Result<(), Box Result<(), Box> { + let provider = crypto::ring::default_provider(); + _ = provider.install_default(); + let gateway_port = create_ports(1)[0]; + + let mut config = Config::default(); + + config.token_verification_public_key = "../../assets/jwt.key.pub".into(); + + let server_socket_addr: std::net::SocketAddr = + format!("127.0.0.1:{gateway_port}").parse().expect("This should work"); + config.tls_address = Some(server_socket_addr.clone()); + config.server_certificate = Some("../../assets/tls_certificate.pem".into()); + config.server_private_key = Some("../../assets/tls_key.pem".into()); + + let user = "admin@example.com"; + + let Ok(TestSettings { handle, gateway_url, expected_tool_names }) = + create_gateway_with_four_counters(user, config).await + else { + panic!("Invalid configuration "); + }; + + let test_future: std::pin::Pin< + Box>> + Send>, + > = async { + let mut buf = Vec::new(); + File::open("../../assets/tls_certificate.pem")?.read_to_end(&mut buf)?; + let cert = reqwest::Certificate::from_pem(&buf)?; + + tokio::time::sleep(Duration::from_millis(100)).await; + let mut default_headers = HeaderMap::new(); + let token = get_token(user.to_owned()); + default_headers.insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(format!("Bearer {token}").as_str()).expect("This should work"), + ); + let client = reqwest::Client::builder() + .https_only(true) + .add_root_certificate(cert) + .default_headers(default_headers) + .resolve_to_addrs("example.com", &[server_socket_addr]) + .build() + .expect("This should work"); + + let gateway_url = gateway_url.replace("127.0.0.1", "example.com"); + + info!("Seding request to {gateway_url}"); + + let config = StreamableHttpClientTransportConfig::with_uri(gateway_url); + let transport = StreamableHttpClientTransport::with_client(client, config); + let request = InitializeRequestParams::default(); + + let maybe_service = request.serve(transport).await; + let Ok(running_service) = maybe_service else { + warn!("No Service {maybe_service:?}"); + return Err("Couldn't get a service".into()); + }; + + let list_tools = running_service.list_tools(None).await; + let Ok(list_tools) = list_tools else { + let msg = format!("List tools returned error {list_tools:?}"); + warn!(msg); + return Err(msg.into()); + }; + + let mut names: Vec = list_tools.tools.iter().map(|t| t.name.to_string()).collect(); + names.sort(); + + info!("Tool names {names:#?}"); + if expected_tool_names != names { + warn!("Actual {names:#?} Expected {expected_tool_names:#?}"); + return Err("Expected tool names don't match actual".into()); + } + + Ok(()) + } + .boxed(); + + let maybe_passed = test_future.await; + + handle.abort(); + if let Ok(_) = maybe_passed { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} From 14ed44d441c4b5c42818af4a070d36508adff961 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 6 May 2026 21:34:55 +0100 Subject: [PATCH 4/5] More tests and reformatting Signed-off-by: Dawid Nowak --- Cargo.lock | 43 +++++ Cargo.toml | 1 + .../contextforge-client.cert.pem | 31 ++++ .../contextforge-client.key.pem | 28 +++ .../contextforge-server.cert.pem | 31 ++++ .../contextforge-server.key.pem | 28 +++ .../contextforgeCA/contextforge.ca.cert.pem | 34 ++++ assets/contextforgeCA/contextforge.ca.key.pem | 52 ++++++ ...ontextforge.intermediate.ca-chain.cert.pem | 68 ++++++++ .../contextforge.intermediate.cert.pem | 33 ++++ .../contextforge.intermediate.key.pem | 52 ++++++ crates/contextforge-gateway-rs-lib/Cargo.toml | 3 +- .../contextforge-gateway-rs-lib/src/common.rs | 30 +++- .../src/gateway/mcp_call_validator.rs | 1 - .../src/gateway/mcp_gateway.rs | 58 ++++--- .../session_store/local_session_store.rs | 5 +- .../src/gateway/session_store/mod.rs | 4 +- .../session_store/redis_session_store.rs | 1 - .../src/layers/session_id.rs | 1 - .../src/layers/user_config_store.rs | 1 - crates/contextforge-gateway-rs-lib/src/lib.rs | 12 +- .../src/tests/gateway_end_to_end.rs | 162 ++++++++++++++---- .../contextforge-gateway-rs-lib/src/tools.rs | 5 +- .../src/transports/tcp.rs | 3 +- .../src/transports/tls.rs | 20 +-- .../src/user_config_store/mod.rs | 4 +- crates/contextforge-gateway-rs/Cargo.toml | 1 + crates/contextforge-gateway-rs/src/logging.rs | 3 +- crates/contextforge-gateway-rs/src/main.rs | 6 +- crates/contextforge-gateway-rs/src/runtime.rs | 3 +- 30 files changed, 623 insertions(+), 101 deletions(-) create mode 100644 assets/contextforgeCA/contextforge-client.cert.pem create mode 100644 assets/contextforgeCA/contextforge-client.key.pem create mode 100644 assets/contextforgeCA/contextforge-server.cert.pem create mode 100644 assets/contextforgeCA/contextforge-server.key.pem create mode 100644 assets/contextforgeCA/contextforge.ca.cert.pem create mode 100644 assets/contextforgeCA/contextforge.ca.key.pem create mode 100644 assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem create mode 100644 assets/contextforgeCA/contextforge.intermediate.cert.pem create mode 100644 assets/contextforgeCA/contextforge.intermediate.key.pem diff --git a/Cargo.lock b/Cargo.lock index eb279097..5e006d02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -292,6 +301,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "axum-test" version = "20.0.0" @@ -592,6 +623,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "rmcp", + "rustls", "thiserror 2.0.18", "tikv-jemallocator", "tokio", @@ -609,6 +641,7 @@ dependencies = [ "async-trait", "axum", "axum-jwt-auth", + "axum-server", "axum-test", "bytes", "chrono", @@ -1266,6 +1299,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index e450e582..d77c8e08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ bytes = "1.11.1" reqwest = "0.13" itertools = "0.14" typed-builder = "0.23.2" +rustls = "0.23" [profile.release] codegen-units = 1 diff --git a/assets/contextforgeCA/contextforge-client.cert.pem b/assets/contextforgeCA/contextforge-client.cert.pem new file mode 100644 index 00000000..497b7bb6 --- /dev/null +++ b/assets/contextforgeCA/contextforge-client.cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRTCCAy2gAwIBAgICIAEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSUUx +ETAPBgNVBAgMCExlaW5zdGVyMQ4wDAYDVQQKDAVSdWdieTEVMBMGA1UECwwMY29u +dGV4dGZvcmdlMSIwIAYDVQQDDBljb250ZXh0Zm9yZ2UtaW50ZXJtZWRpYXRlMB4X +DTI2MDUwNjE1MTIzN1oXDTI3MDUxNjE1MTIzN1owHjEcMBoGA1UEAwwTY29udGV4 +dGZvcmdlLWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMub +OvkxFGqehq0V/tAFn6hQLyUeEd1RwXzYmTX7LxLIODtPuW3jGL1lXC+b+++JLSIs +7dQvQrMT7py2h7u1FCmjajPZ/5GEdGrhEhnas4Rl0baI7z+9U3yfN5LgR2srkJwI +tiAmiS33rbUIby9ccEA5/dqBXRPg1cZndFR2QiuB0PePLy+nUFs9gkH/C0mwcQCn +iIgzsyUhaGhjJ2r6QHSQa2UlxIM9cKSsfDCt4oF/QYm07nOIOYCO2w3+GG3bBCiH +RXZxCULYNf3KMhr6kpapVMcyCYHNQwuLWp5pCgIUFh5XFA3bKitAEacYXxvBOziy +PL0TtctzjyOBGWWzSkECAwEAAaOCAT4wggE6MAkGA1UdEwQCMAAwHQYDVR0OBBYE +FBM3mYhUh3Ss9ZEuPMQs9JxeK1C2MIGaBgNVHSMEgZIwgY+AFKr4pMjVJ1ni4281 +TzGyKBuk4lZkoXOkcTBvMQswCQYDVQQGEwJJRTERMA8GA1UECAwITGVpbnN0ZXIx +DzANBgNVBAcMBkR1YmxpbjEOMAwGA1UECgwFUnVnYnkxFTATBgNVBAsMDGNvbnRl +eHRmb3JnZTEVMBMGA1UEAwwMY29udGV4dGZvcmdlggIQADAOBgNVHQ8BAf8EBAMC +BeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEIGA1UdEQQ7MDmCCWxv +Y2FsaG9zdIIUY29udGV4dGZvcmdlLmV4YW1wbGWHBH8AAAGHEAAAAAAAAAAAAAAA +AAAAAAEwDQYJKoZIhvcNAQELBQADggIBAIX57BeFiHKIRYm8bn9NsfYx4qu+NFYw +b5qPR55UulB5XSSFT7dwyewXYnmoU+rQtEYizCVK0IcPhFE016HPxlQQa2Q3xpdJ +NmKswbynddrkKiCAQepoivz1J8fhliakDI4uBZ1sbVnc0m0fov88in03FQ8bHsyt +bDpYAwm38Jt8DErbBRUS5z/SwAYj5ixmsd5bPf4B4fvsU6hhc94gWYQEqNC1u+z8 +A26fr0h1Lqe7rkutoX9XF1ZwbNRRJ3Nf0ejw1hjkaZWGzwRw9WIBRhnbVoG4Tttp +82fq6lwOxKDWseYDQMrG2d5F3Z+zgmQ2p/aidTwftfZzT5Yc4mb0WhXVhRRx75gt +aIOb+7yxfWJRQKRNpnQWlXiuTNq0idaTYt6BpYxCVkVSo/GSesHvgj4sssadTFlc +4qg3xscb4PAFtXTwjAm+52TtHVILMndTJ0+Oi0P6h5hqfZ7/Ugmne4KynsbDGMzK +EzdSpPeMHYrUWq1z7aLSHo9uNelE+w2c5oINxrCZ5ppaN4zPA0enynnrHJtPf1Se +QUflV/e0QxKxD/agL0uyRnwGJnGbYRMpT9msP+yDb9m+8qN2+mAvTn2/xTSyLgRO +Rx/4IfqrlOCag55TV/wrxjXzNW6eW1dbNMmCrrCyR09fxWq2LJaWmMBN85rY6qAp +uwZxAGi1chuQ +-----END CERTIFICATE----- diff --git a/assets/contextforgeCA/contextforge-client.key.pem b/assets/contextforgeCA/contextforge-client.key.pem new file mode 100644 index 00000000..de3a2750 --- /dev/null +++ b/assets/contextforgeCA/contextforge-client.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLmzr5MRRqnoat +Ff7QBZ+oUC8lHhHdUcF82Jk1+y8SyDg7T7lt4xi9ZVwvm/vviS0iLO3UL0KzE+6c +toe7tRQpo2oz2f+RhHRq4RIZ2rOEZdG2iO8/vVN8nzeS4EdrK5CcCLYgJokt9621 +CG8vXHBAOf3agV0T4NXGZ3RUdkIrgdD3jy8vp1BbPYJB/wtJsHEAp4iIM7MlIWho +Yydq+kB0kGtlJcSDPXCkrHwwreKBf0GJtO5ziDmAjtsN/hht2wQoh0V2cQlC2DX9 +yjIa+pKWqVTHMgmBzUMLi1qeaQoCFBYeVxQN2yorQBGnGF8bwTs4sjy9E7XLc48j +gRlls0pBAgMBAAECggEAAQ4DTrIp5GmA0hm4kbRNwO31aJBQ0GAxi+eFV6m+8y51 +WB1QHRkdkjGZuidpNTL1OGWaL8y04D+cr1QkvNyxx4twq+cXs1XH0pvq35Zp8qgi +X3szHhXpfk5G1+xwgkYeACjtZJo+gK31SOhLxyeH71U9kumzpTw3b0ku6ZcZPady +onwHvsEs5Fce/SVyIiiTfrHBujXuaN7oiTPeje2E18yCAiQ4zvNijCvpAH5f4tUC +6EAvfv+Fahezoh1cShUN2+11anvNnEXBwP+YRqWZsse+ttHS5HgokbqX/HwBGxPv +rsusHO5OjLMACKnGP6LdL3fPt3QGCL+X3N3B80kr5wKBgQDy0T2E7QLTzUFdrXSL +WLCSACReuXxN9vdPn0OvSABtdN1ZGMbG2HgITJUkvH+FVAG7mwaV8QME8ENo6swH +AJ0uY9F5kK7jw48crX1dDmT44SYf9CcCDz/p7L1SdWoPM4BKtifUiiwQ1E4nnpzv +rPrOW8lqKk41lSLcdDI1n4ALWwKBgQDWqQW1u7o+VWvfAYztk7dd2v9DeEIMuo4U +LXuliFcfS5gSL4sj/lMrmbm1Z9UZiM7Hts+3ATAQSJKIhToO1KoV4F7SWbeE09bX +JE1hHsZadXYDwy3uuENsDnhI8xaqD8VdhJIY+uJjm+cuh06ixol7UQkbhENvy4Vl +zWEgEMJfkwKBgQCJTxF+zZg/tV41XxT8h5axuSrX9gP5AqMvf1yDDjBPtTpGW+Bs +KZUW/FeKgp2KA8tHD49V0whmDofQGJZvj0VTKlcWa166paeUC/dMXAt1QbyRbTtx +yrXVzm3w5zymg/UUSpWTdt8cVTIs7WDJmAPsFbN8OZgobMFd8MdD04JQ6QKBgQDN +LmRx0I32FQp17/J4CHEGOlUydZmUtyElclA06nx0QnqKL32tGuT+0QKsviH4NUeJ +qFklUPJTLjs6WTYkhOxK2ttn7y+2vBIoNN/tzE/GmW4DrKWT//caKz6YZBsu1MJP +YG+RhwWsNpIkbFsixekVwWCWN7eJ/Zx1sXl7/+j7FwKBgQCIciUzfeLOMwcMXhLB +qakkz5sq52c+PC2G/vzdkB1n7xeUi21ieSwGFDOwpQiAi0kyqKyCv2Q8kARw41M2 +7wQnqNHagjyVBwIm70vF3D+yXL+zNXn7aFjUZAScjWlACR0yr5x+Cb5s2el09v2W +cbvnELXiBEMgSY9SJJtdwJn7oA== +-----END PRIVATE KEY----- diff --git a/assets/contextforgeCA/contextforge-server.cert.pem b/assets/contextforgeCA/contextforge-server.cert.pem new file mode 100644 index 00000000..d56942d7 --- /dev/null +++ b/assets/contextforgeCA/contextforge-server.cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRTCCAy2gAwIBAgICIAAwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSUUx +ETAPBgNVBAgMCExlaW5zdGVyMQ4wDAYDVQQKDAVSdWdieTEVMBMGA1UECwwMY29u +dGV4dGZvcmdlMSIwIAYDVQQDDBljb250ZXh0Zm9yZ2UtaW50ZXJtZWRpYXRlMB4X +DTI2MDUwNjE1MTIzNloXDTI3MDUxNjE1MTIzNlowHjEcMBoGA1UEAwwTY29udGV4 +dGZvcmdlLXNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKn7 +6+YQJY9LfLZ0pfe+XwqyY8PZHMiub3H737OrkUUhX3GAhs4yvcFRsP7wa4wGHTn+ +9qz3/9FmR59rOpC9MsOclth1zD7DN2OrvQRHyzYjZqXTT5qYMeHRp7WBTlMbROgI +cj9KBgrTR/+SVHLzaOoP4KZt/H08uTUeTEb9kvQarpX0NfExnhW6Rgu9UeyL1tO4 +2BJCc58o2rqwk80655F/Ilv6Yozu92EzqUP8i72vUHajwHAumATfFyRhubul8+pR +clMKr0EGirLL8eHuB9PlMGPiJ8L8qKyevOEB+IWeDObm82SA2vcE0l33l8+IYs3h +29e6EgND2vcPERPLlncCAwEAAaOCAT4wggE6MAkGA1UdEwQCMAAwHQYDVR0OBBYE +FF24prrcCxYyuspJQLSX2uH+Rc1+MIGaBgNVHSMEgZIwgY+AFKr4pMjVJ1ni4281 +TzGyKBuk4lZkoXOkcTBvMQswCQYDVQQGEwJJRTERMA8GA1UECAwITGVpbnN0ZXIx +DzANBgNVBAcMBkR1YmxpbjEOMAwGA1UECgwFUnVnYnkxFTATBgNVBAsMDGNvbnRl +eHRmb3JnZTEVMBMGA1UEAwwMY29udGV4dGZvcmdlggIQADAOBgNVHQ8BAf8EBAMC +BeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEIGA1UdEQQ7MDmCCWxv +Y2FsaG9zdIIUY29udGV4dGZvcmdlLmV4YW1wbGWHBH8AAAGHEAAAAAAAAAAAAAAA +AAAAAAEwDQYJKoZIhvcNAQELBQADggIBAKlOjCH966+oiXFwfEecHD3OHJDj4P+B +adqTy4ZvOwXAK00lQ+/j/UeOHls16Po0m/YC/aqpBl5zPHzGdGOOE45FMRhzlmWo +yNjELN/SlKAwo6Fwsd+8a4RR4vjuBR/Wuk24hcfOIvZ3V+9FPIWrn0DP8wu25IgI +oJFkVz/SUegrBgN/7wltoh0QumH/a9/gV6fVfrF84b93J/xjDaP5IYepkbzgQRtY +FlEwYV58bgvH/Ichy9bHu9oBYyUYEgJsQupb2S19z8S46Ssccy2uUmHoc9U1Fgp3 +bzK1orCBDEsA/OeAkXw7SfFc3fD373s4NMJYktCeiBAZvkmKEofCvK8EvQWctr/A +1h7UGGtRJJM2J9Ew5G+JIaEa07ZQpAULe2N9lhHnaFrxQjE1mwirg+jKZBo0/JYo +yeLPJkjj6l+cSSi1wefZTGv96c/MJdbP1XqLgv1bNzv/C/yY8hqUP4TWs/yd2772 +rxIPEEU0dsrAbSrqA/qu26+ePnHnBkUUjmK6hOVScuqWQMc3pOElpeHR/uZ9s2CK +JXuc1SfmyuXNmqaeGHFW7z11XyEZZKa7ohE9uGEqDhgrX9Ye0Y4LYe58TCeu+2Ck +CP+O7CuG5p48pMhfOlUA1I5lbHItrqcN/8kjggao41OzIpfoSWtFiDzA6BxJUvh+ +OKg3uUOzfJJq +-----END CERTIFICATE----- diff --git a/assets/contextforgeCA/contextforge-server.key.pem b/assets/contextforgeCA/contextforge-server.key.pem new file mode 100644 index 00000000..468c9932 --- /dev/null +++ b/assets/contextforgeCA/contextforge-server.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCp++vmECWPS3y2 +dKX3vl8KsmPD2RzIrm9x+9+zq5FFIV9xgIbOMr3BUbD+8GuMBh05/vas9//RZkef +azqQvTLDnJbYdcw+wzdjq70ER8s2I2al00+amDHh0ae1gU5TG0ToCHI/SgYK00f/ +klRy82jqD+Cmbfx9PLk1HkxG/ZL0Gq6V9DXxMZ4VukYLvVHsi9bTuNgSQnOfKNq6 +sJPNOueRfyJb+mKM7vdhM6lD/Iu9r1B2o8BwLpgE3xckYbm7pfPqUXJTCq9BBoqy +y/Hh7gfT5TBj4ifC/KisnrzhAfiFngzm5vNkgNr3BNJd95fPiGLN4dvXuhIDQ9r3 +DxETy5Z3AgMBAAECggEADTfqQUASMPwgHO7MlTZi8lRVo743byJOx6CxkFQ602hj +9qdAVGU9ElJwSXLtZLx4+1s+Asy07QC0zcsrM7mIFWQQ6uZGKr9IMw0A1ttftxG2 +WBYq7jeq8VzksMhMYfQQk1v3c7/QNnlK868vt/yrtOqP1Va04rxHyWoAvn3WNqXY +BYgx2e0I9WHWptfXobdsob+Lx/LhiaEKnEyQVDrU5QVu3hAAenQAs9TxtdSQ0iOZ +QXVP2TdflI4XdHAcEPDUpqkSGc3PoHzn89HzZ9JTuMaxNGy3kgiBXJ18ExHj6lG1 +TeddMSzqc3KJv5+wB16yYegW28KGbWaACxocuT/VUQKBgQDdokIFKKYeNNMbs/Ju +kAQ3Fh0Z7kENv68NyRNL+fzziv4XABC+/oBrMqo5acUEL9iV9YxiDmHPRlMkVd1w +jcY61PIRl2U0xh6WWb2pY/mv6GjNlq/k3RxJpCRjg7vJL/KBJcmmNoabMfEXsvVJ +kxZfQAX/0+sxj4DZ75xV6Ta7SQKBgQDEV235BQNxbJeYjTb7D0leadQxohJeZW7F +W78J3aHKtVzVIjPSSG8ox9RsUGH1sdFEUmFcrlwBoKQ6E4p4+yC6KfcKdQY8y/Q9 +P6BdIdNHtNiQHzIpPem2GKB9q847Lc39mZkvnbBCVIYl9LHIu5jJ8OSclEX8ESBH +l4GbzaQDvwKBgChRZhUuKdoA3g6CE64NQNcMjMq0ztzDbALj+0Cs/1kGheaFOoak +IZReqRy8ovx5/7p36svDtgNQ1bsca6YYBGGbb1XH5r9M8y+Cr7/q0fcwHjCYIvNN +TeIPnBcGVdjpggIAb8huztnYofUftwNlYIJ/URgS4wwnANekgRoXzL8RAoGAPgPP +fIQU3lKKX2jbINlnNyb+Gt8yJharsjKUWK1kWP6H39n5vEWctqjHc57AEjaj/ox2 +rCt2bB0tQhrB9gx7/dEbcnYcDj/tWRsrr77rWQ7KCFuSIGyp0RZeOtN1REPneF53 +gA6yiYDhDkQHk3uNettXmg1LPZ67L2GvUmLVl0kCgYEAv9j/Rsld5HO4KoguIWkE +/KGt9RG6YQlw/5zvO+sWJfCiTEMXH+Ou+DxvgrvY5PrOBotzUwweUQmhemskp08W +sh7jD5wzA7dfLc62wjNOF7j8TAZydtgUXYOHBpbNuQGaVFaPe414t11/0aGpK1Up +aXdtU3TdqYDQVXrOE8Qviok= +-----END PRIVATE KEY----- diff --git a/assets/contextforgeCA/contextforge.ca.cert.pem b/assets/contextforgeCA/contextforge.ca.cert.pem new file mode 100644 index 00000000..be7aeef4 --- /dev/null +++ b/assets/contextforgeCA/contextforge.ca.cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUXULBEyXfb1qeTYcCxjzBc+yE6TAwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSUUxETAPBgNVBAgMCExlaW5zdGVyMQ8wDQYDVQQHDAZE +dWJsaW4xDjAMBgNVBAoMBVJ1Z2J5MRUwEwYDVQQLDAxjb250ZXh0Zm9yZ2UxFTAT +BgNVBAMMDGNvbnRleHRmb3JnZTAeFw0yNjA1MDYxNTEyMzZaFw00NjA1MDExNTEy +MzZaMG8xCzAJBgNVBAYTAklFMREwDwYDVQQIDAhMZWluc3RlcjEPMA0GA1UEBwwG +RHVibGluMQ4wDAYDVQQKDAVSdWdieTEVMBMGA1UECwwMY29udGV4dGZvcmdlMRUw +EwYDVQQDDAxjb250ZXh0Zm9yZ2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQC4yzhBBBERiCMQI0SUWmncGw+aUhsy4MT1QiG5zrqWSf97Jp2ocAgsCVNM +Bkk38DeUR4NPXMPoIG143H6CvaHV2HDGxUKlKR8I4b9vIh3Bdng8kr8SeVMSpYN1 +SkqscOgFOPVzfbScZiRuaM+iTm/tzoHgwnIXhMmuRfhC3rlGGbAyd/v6Em5kDkxE +BfX9kQxx8v2CC3mEGdui2lzRTc7K4VM+Wu6hJCRoWAnKj0CH6lXEJtGVDMP8INrS +Md1vuei0m7j0Zdgbpz7WmaQPDsl2EMZ4G5bv/gJ8Ot8dn1gKt3aWDd95Z6i+YI0z +B20RrVZaFzmyjsU3W3LX0zmAn/qQV1tKB62YA4OfLjOLghYZTR940VaydI1zmxVJ +0GPD1LYBgBFq1LckBHRvA4M12ZghaYjIv/5Td42qquoIO0EPibnunH5r+/Sxlhzt +LrTWTwjosWTcdVlJcEFO201z3BQH8O/wfU/rJm9E3UnkM036pNECwduQzn+kjdRv +83QGyaFDAfiFdbKcztopXaRgjTlM9MBGh9BqV0QM+EjaXsPiqGVJ2EW9y6GO+bHo +cK9d6ooaV68GN+7EFKQ7rwFHgfvLfab3LV+Bg9R7H0rNkFOaAYa39DNPD1M3pJGl +6h232BYrD2PxpmR/OEVr/9s7OTVoWmxAzV/Lyweggesqmh7wawIDAQABo2MwYTAd +BgNVHQ4EFgQUZppcOLmprnMH3M68R96/5euq0jMwHwYDVR0jBBgwFoAUZppcOLmp +rnMH3M68R96/5euq0jMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAKLs3Yr3QTjd7FNizevcjYzrfJ/9RiFl6blpTz3B +2DTI29p/918ef+8h3taJGUEh6YL2mSegYNF9evyV0SQaQUlLOkEv+lSGTE5JQ9Ez +2463yH58HcdsgZ40JLnIPRRRspEhyw8ScmPniKQuTQOa+XsEs24ohLMjzJow5UiQ +7z6vAVsIIcYLi7NeZ7Zhyi/My9basOdTzaUwUqYsNtXOhP0MeIeo4wv2ifTeFrF3 +h1DYH6w8tC2mYIu3KPZ2RGi0wAF+5BkpIv1FCrsNUeg6N8KGXulyo+yoL3iR9aiM +cu0leJGBDVZ7mdzOGAAiMcfNDkDK87oywqo3ZdZwOj69zGYznYDqGxBeJFuz5B6n +DrpuwiX6SJ5BbYnVyC4bHnRFwkaSGZBVX/OconVueRHa1iyDGR4xPrYR96ZOanib +ZxcsA/efScjtpdnQU1whTL8PmxAFCJwdC2XGoO6dfovWjvYIMccmTbkqdq56Lkdw +mqSYjzBy1ud/3LOoiU0+kmcyVXpvvKwGyocM7Spx8xu+BE8oaiUi9nPPurEIYK7D +kDRFmamVGQ1Bom49dxFZC60Kh+Em7MbDNuM4r/F5cvuib2eeAVAf0UMDQ8oYs3x6 +FovjZ9jLX+4KwZSsRL5ZiNgNxHWNVK3uoocKa/OlIb5+SWJoL08vE2AEXZg/4v47 +pwsN +-----END CERTIFICATE----- diff --git a/assets/contextforgeCA/contextforge.ca.key.pem b/assets/contextforgeCA/contextforge.ca.key.pem new file mode 100644 index 00000000..1832f7a2 --- /dev/null +++ b/assets/contextforgeCA/contextforge.ca.key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +MDWdW5/swI8afQ1cz8WCCOvP7QrAIz0= +-----END PRIVATE KEY----- diff --git a/assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem b/assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem new file mode 100644 index 00000000..500c32b9 --- /dev/null +++ b/assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUXULBEyXfb1qeTYcCxjzBc+yE6TAwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSUUxETAPBgNVBAgMCExlaW5zdGVyMQ8wDQYDVQQHDAZE +dWJsaW4xDjAMBgNVBAoMBVJ1Z2J5MRUwEwYDVQQLDAxjb250ZXh0Zm9yZ2UxFTAT +BgNVBAMMDGNvbnRleHRmb3JnZTAeFw0yNjA1MDYxNTEyMzZaFw00NjA1MDExNTEy +MzZaMG8xCzAJBgNVBAYTAklFMREwDwYDVQQIDAhMZWluc3RlcjEPMA0GA1UEBwwG +RHVibGluMQ4wDAYDVQQKDAVSdWdieTEVMBMGA1UECwwMY29udGV4dGZvcmdlMRUw +EwYDVQQDDAxjb250ZXh0Zm9yZ2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQC4yzhBBBERiCMQI0SUWmncGw+aUhsy4MT1QiG5zrqWSf97Jp2ocAgsCVNM +Bkk38DeUR4NPXMPoIG143H6CvaHV2HDGxUKlKR8I4b9vIh3Bdng8kr8SeVMSpYN1 +SkqscOgFOPVzfbScZiRuaM+iTm/tzoHgwnIXhMmuRfhC3rlGGbAyd/v6Em5kDkxE +BfX9kQxx8v2CC3mEGdui2lzRTc7K4VM+Wu6hJCRoWAnKj0CH6lXEJtGVDMP8INrS +Md1vuei0m7j0Zdgbpz7WmaQPDsl2EMZ4G5bv/gJ8Ot8dn1gKt3aWDd95Z6i+YI0z +B20RrVZaFzmyjsU3W3LX0zmAn/qQV1tKB62YA4OfLjOLghYZTR940VaydI1zmxVJ +0GPD1LYBgBFq1LckBHRvA4M12ZghaYjIv/5Td42qquoIO0EPibnunH5r+/Sxlhzt +LrTWTwjosWTcdVlJcEFO201z3BQH8O/wfU/rJm9E3UnkM036pNECwduQzn+kjdRv +83QGyaFDAfiFdbKcztopXaRgjTlM9MBGh9BqV0QM+EjaXsPiqGVJ2EW9y6GO+bHo +cK9d6ooaV68GN+7EFKQ7rwFHgfvLfab3LV+Bg9R7H0rNkFOaAYa39DNPD1M3pJGl +6h232BYrD2PxpmR/OEVr/9s7OTVoWmxAzV/Lyweggesqmh7wawIDAQABo2MwYTAd +BgNVHQ4EFgQUZppcOLmprnMH3M68R96/5euq0jMwHwYDVR0jBBgwFoAUZppcOLmp +rnMH3M68R96/5euq0jMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAKLs3Yr3QTjd7FNizevcjYzrfJ/9RiFl6blpTz3B +2DTI29p/918ef+8h3taJGUEh6YL2mSegYNF9evyV0SQaQUlLOkEv+lSGTE5JQ9Ez +2463yH58HcdsgZ40JLnIPRRRspEhyw8ScmPniKQuTQOa+XsEs24ohLMjzJow5UiQ +7z6vAVsIIcYLi7NeZ7Zhyi/My9basOdTzaUwUqYsNtXOhP0MeIeo4wv2ifTeFrF3 +h1DYH6w8tC2mYIu3KPZ2RGi0wAF+5BkpIv1FCrsNUeg6N8KGXulyo+yoL3iR9aiM +cu0leJGBDVZ7mdzOGAAiMcfNDkDK87oywqo3ZdZwOj69zGYznYDqGxBeJFuz5B6n +DrpuwiX6SJ5BbYnVyC4bHnRFwkaSGZBVX/OconVueRHa1iyDGR4xPrYR96ZOanib +ZxcsA/efScjtpdnQU1whTL8PmxAFCJwdC2XGoO6dfovWjvYIMccmTbkqdq56Lkdw +mqSYjzBy1ud/3LOoiU0+kmcyVXpvvKwGyocM7Spx8xu+BE8oaiUi9nPPurEIYK7D +kDRFmamVGQ1Bom49dxFZC60Kh+Em7MbDNuM4r/F5cvuib2eeAVAf0UMDQ8oYs3x6 +FovjZ9jLX+4KwZSsRL5ZiNgNxHWNVK3uoocKa/OlIb5+SWJoL08vE2AEXZg/4v47 +pwsN +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFvDCCA6SgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCSUUx +ETAPBgNVBAgMCExlaW5zdGVyMQ8wDQYDVQQHDAZEdWJsaW4xDjAMBgNVBAoMBVJ1 +Z2J5MRUwEwYDVQQLDAxjb250ZXh0Zm9yZ2UxFTATBgNVBAMMDGNvbnRleHRmb3Jn +ZTAeFw0yNjA1MDYxNTEyMzZaFw0zNjA1MDMxNTEyMzZaMGsxCzAJBgNVBAYTAklF +MREwDwYDVQQIDAhMZWluc3RlcjEOMAwGA1UECgwFUnVnYnkxFTATBgNVBAsMDGNv +bnRleHRmb3JnZTEiMCAGA1UEAwwZY29udGV4dGZvcmdlLWludGVybWVkaWF0ZTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALsr6aNPLZSiRToivn+S0iVM +/RpHERMPlIaVdJt7CwwsGKRK8lzaX7/2eGLb9plK34Pm3wbSx0i6oo4mUSHxJ2zs +9hS7dIgHEmITcqlWoUhwRbUNUS+lwdfxypi1LaT8OLTpcj1BoREIwJLmcywAebDH +4NfeokLqs/XPUZsiCGo7c89DZxwzq9WKLlQNd6ADL2itcAfQ2zn8n7g8q5Bjx7oN +b7FIk9DzVdDyKiHY+LCLj6qocEsNNPtOHMjQzXVq2X+oqCUgcB6GgfJJJ2TUUCS8 +nw9WCyVrX2TVqpAYK953GQrluV7FnKXNQCxJ2Qq8AIebd+osAO+GzcPA+s1sjeXN +muS2Gyrm8p36WhG/SdJHk7rjntSGmpGVeCAafiS5BTbKFcH79NspcSyQdBOoRLSy +g8WNkQGx0L29fYOuvVB2/1XlSo1zhcT8hWzDFUeuSSqO3ZxcnMueCBJ6vQfXLtw+ +l2kzsMPNxeGgYyGtPXStI/b5LlPAju/Lx+GY19znHcYKfjsdbX2c68LIj6OuUfvI +Iz9wRjrPEUvW7NWohwtSSJtUZz/HmQXJJ4DH1PGf5dBUdA8vhORUlH/10lIX2+7n +XnWinpy3qAN3ryYWvpF91K7hFltIZW4N3OJsFQ7IFKIYMY4EJJQlvFt1Pk3Ti1bW +VfC64d5JBC+wAjsqh3xzAgMBAAGjZjBkMB0GA1UdDgQWBBSq+KTI1SdZ4uNvNU8x +sigbpOJWZDAfBgNVHSMEGDAWgBRmmlw4uamucwfczrxH3r/l66rSMzASBgNVHRMB +Af8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA +Jw6MKh/Zvk1rvi5WBfeJ2MOOA/qGHO3Dy5pAfb0HRFR7r7mNGjWBzVTq/ScczGtb +dWPegr8LuI2H3840lmc0xuwnArzdaBJLJujzuhvPODvfKqKCW2Z2HY0twRL8qyBt +Sca+NHFAua9KHkW5K+TSqZzsENIhhhyENGRSHyX+QysK8lpgsXC8P00kj2TwHIuO +WFrC8RKtKJcgMuRcOw+IFgFqMM9IkhpsUWFcYd2JvMpw2YvI/hioTBa4G8kHziEy +f8Grr7nyPl5FQajETrktNnjQNuPFJNMNo9y+9ynK92REhijiIF7brVG9T/LOG25s +gc9STfBksZw9MHkGq6dvoeSsj6Uq0wU+eKXIQfUlqtlYNRqdR7zJZYxnunLUaZOH +tluL7xjIcgmIqsxqDdyguOcG66igtox/jziPNkAZbxCI8n8RV04s/U4yg5ik0gbr +zrs6TNAX0d4abkdih24z5LhDB2nGsMOZrt+J3lTk1jDl+zVxePnQjDjdoDZfVMsc +FUBoyJBRzE9DKZ/PLrqKQcXCTzYOJjToxLiwCzBZLyQD8dPoIrkT9avWfX6/rFKs +3gD9U4UCBXdJdE2qnKCE1Sj86Cr2/rHf742gEMqraOHko8zzQ1YaBUObNNdqs0U0 +7Be9DRVVNItXDV0SuCuZdCw6YCZtoI8+0l/NWpg61i0= +-----END CERTIFICATE----- + diff --git a/assets/contextforgeCA/contextforge.intermediate.cert.pem b/assets/contextforgeCA/contextforge.intermediate.cert.pem new file mode 100644 index 00000000..c13156ce --- /dev/null +++ b/assets/contextforgeCA/contextforge.intermediate.cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFvDCCA6SgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMCSUUx +ETAPBgNVBAgMCExlaW5zdGVyMQ8wDQYDVQQHDAZEdWJsaW4xDjAMBgNVBAoMBVJ1 +Z2J5MRUwEwYDVQQLDAxjb250ZXh0Zm9yZ2UxFTATBgNVBAMMDGNvbnRleHRmb3Jn +ZTAeFw0yNjA1MDYxNTEyMzZaFw0zNjA1MDMxNTEyMzZaMGsxCzAJBgNVBAYTAklF +MREwDwYDVQQIDAhMZWluc3RlcjEOMAwGA1UECgwFUnVnYnkxFTATBgNVBAsMDGNv +bnRleHRmb3JnZTEiMCAGA1UEAwwZY29udGV4dGZvcmdlLWludGVybWVkaWF0ZTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALsr6aNPLZSiRToivn+S0iVM +/RpHERMPlIaVdJt7CwwsGKRK8lzaX7/2eGLb9plK34Pm3wbSx0i6oo4mUSHxJ2zs +9hS7dIgHEmITcqlWoUhwRbUNUS+lwdfxypi1LaT8OLTpcj1BoREIwJLmcywAebDH +4NfeokLqs/XPUZsiCGo7c89DZxwzq9WKLlQNd6ADL2itcAfQ2zn8n7g8q5Bjx7oN +b7FIk9DzVdDyKiHY+LCLj6qocEsNNPtOHMjQzXVq2X+oqCUgcB6GgfJJJ2TUUCS8 +nw9WCyVrX2TVqpAYK953GQrluV7FnKXNQCxJ2Qq8AIebd+osAO+GzcPA+s1sjeXN +muS2Gyrm8p36WhG/SdJHk7rjntSGmpGVeCAafiS5BTbKFcH79NspcSyQdBOoRLSy +g8WNkQGx0L29fYOuvVB2/1XlSo1zhcT8hWzDFUeuSSqO3ZxcnMueCBJ6vQfXLtw+ +l2kzsMPNxeGgYyGtPXStI/b5LlPAju/Lx+GY19znHcYKfjsdbX2c68LIj6OuUfvI +Iz9wRjrPEUvW7NWohwtSSJtUZz/HmQXJJ4DH1PGf5dBUdA8vhORUlH/10lIX2+7n +XnWinpy3qAN3ryYWvpF91K7hFltIZW4N3OJsFQ7IFKIYMY4EJJQlvFt1Pk3Ti1bW +VfC64d5JBC+wAjsqh3xzAgMBAAGjZjBkMB0GA1UdDgQWBBSq+KTI1SdZ4uNvNU8x +sigbpOJWZDAfBgNVHSMEGDAWgBRmmlw4uamucwfczrxH3r/l66rSMzASBgNVHRMB +Af8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA +Jw6MKh/Zvk1rvi5WBfeJ2MOOA/qGHO3Dy5pAfb0HRFR7r7mNGjWBzVTq/ScczGtb +dWPegr8LuI2H3840lmc0xuwnArzdaBJLJujzuhvPODvfKqKCW2Z2HY0twRL8qyBt +Sca+NHFAua9KHkW5K+TSqZzsENIhhhyENGRSHyX+QysK8lpgsXC8P00kj2TwHIuO +WFrC8RKtKJcgMuRcOw+IFgFqMM9IkhpsUWFcYd2JvMpw2YvI/hioTBa4G8kHziEy +f8Grr7nyPl5FQajETrktNnjQNuPFJNMNo9y+9ynK92REhijiIF7brVG9T/LOG25s +gc9STfBksZw9MHkGq6dvoeSsj6Uq0wU+eKXIQfUlqtlYNRqdR7zJZYxnunLUaZOH +tluL7xjIcgmIqsxqDdyguOcG66igtox/jziPNkAZbxCI8n8RV04s/U4yg5ik0gbr +zrs6TNAX0d4abkdih24z5LhDB2nGsMOZrt+J3lTk1jDl+zVxePnQjDjdoDZfVMsc +FUBoyJBRzE9DKZ/PLrqKQcXCTzYOJjToxLiwCzBZLyQD8dPoIrkT9avWfX6/rFKs +3gD9U4UCBXdJdE2qnKCE1Sj86Cr2/rHf742gEMqraOHko8zzQ1YaBUObNNdqs0U0 +7Be9DRVVNItXDV0SuCuZdCw6YCZtoI8+0l/NWpg61i0= +-----END CERTIFICATE----- diff --git a/assets/contextforgeCA/contextforge.intermediate.key.pem b/assets/contextforgeCA/contextforge.intermediate.key.pem new file mode 100644 index 00000000..0cf4bf48 --- /dev/null +++ b/assets/contextforgeCA/contextforge.intermediate.key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +yuouRuvMb7eGYLhuLFlugbkbMiiAZw0= +-----END PRIVATE KEY----- diff --git a/crates/contextforge-gateway-rs-lib/Cargo.toml b/crates/contextforge-gateway-rs-lib/Cargo.toml index 83102b15..d68c88b6 100644 --- a/crates/contextforge-gateway-rs-lib/Cargo.toml +++ b/crates/contextforge-gateway-rs-lib/Cargo.toml @@ -44,7 +44,7 @@ uuid = { version = "1.23", features = ["v4"] } lru_time_cache = "0.11.11" hyper-util = "0.1.20" hyper = { version = "1.4.0" } -rustls = "0.23" +rustls.workspace = true rustls-pki-types = { version = "1.14.1", features = ["std"] } tokio-rustls = "0.26.4" typed-builder.workspace = true @@ -58,6 +58,7 @@ openport = { version = "0.1.1", features = ["rand"] } mockito = "1.7.2" axum-test = "20.0.0" test-log = "0.2.20" +axum-server = { version = "0.8.0", features = ["tls-rustls"] } [lints] workspace = true diff --git a/crates/contextforge-gateway-rs-lib/src/common.rs b/crates/contextforge-gateway-rs-lib/src/common.rs index 39f37015..ea672f28 100644 --- a/crates/contextforge-gateway-rs-lib/src/common.rs +++ b/crates/contextforge-gateway-rs-lib/src/common.rs @@ -1,4 +1,10 @@ -use std::{fs, path::PathBuf, sync::Arc}; +use std::{ + fs::{self, File}, + io::Read, + net::SocketAddr, + path::PathBuf, + sync::Arc, +}; use axum_jwt_auth::JwtDecoder; use chrono::{Duration, Utc}; @@ -7,10 +13,8 @@ use http::uri::Authority; use openid::{CompactJson, CustomClaims, StandardClaims}; use redis::{ConnectionAddr, IntoConnectionInfo}; use serde::{Deserialize, Serialize}; -use url::Url; - -use std::net::SocketAddr; use thiserror::Error; +use url::Url; use crate::{const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, user_config_store::UserConfigStore}; @@ -100,8 +104,11 @@ pub struct Config { #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM_PRIVATE_KEY")] pub upstream_private_key: Option, - #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM__CERTIFICATE")] + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM_CERTIFICATE")] pub upstream_certificate: Option, + + #[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TLS_UPSTREAM_TRUST_BUNDLE")] + pub upstream_trust_bundle: Option, } #[derive(Error, Debug)] @@ -134,6 +141,19 @@ impl TryFrom<&Config> for reqwest::Client { }, Some(UpstreamConnectionMode::MtlsOnly) => builder.https_only(true).identity(extract_identity(config)?), }; + + let builder = if let Some(trust_bundle) = config.upstream_trust_bundle.as_ref() { + let mut buf = Vec::new(); + File::open(trust_bundle)?.read_to_end(&mut buf)?; + let certificates = reqwest::Certificate::from_pem_bundle(&mut buf)?; + builder.tls_certs_merge(certificates) + } else { + builder + }; + // let mut header_map = HeaderMap::new(); + // //header_map.insert(http::header::HOST, HeaderValue::from_static("127.0.0.1")); + //let builder = builder.indefault_headers(header_map); + Ok(builder.build()?) } } diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs index 9812e296..3f470319 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs @@ -4,7 +4,6 @@ use rmcp::{ ErrorData, RoleServer, model::ErrorCode, service::RequestContext, transport::streamable_http_server::tower::DownstreamSessionId, }; - use tracing::info; use crate::{ diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs index 3800b503..a554e5fe 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs @@ -1,38 +1,38 @@ -use std::collections::HashMap; -use std::{collections::HashSet, sync::Arc}; - -use super::mcp_call_validator::AuthorizedCallValidator; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use http::request::Parts; use itertools::Itertools; -use rmcp::RoleClient; -use rmcp::model::{ErrorCode, Resource}; -use rmcp::service::RunningService; -use rmcp::transport::StreamableHttpClientTransport; -use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; use rmcp::{ - ErrorData, RoleServer, ServerHandler, ServiceExt, + ErrorData, RoleClient, RoleServer, ServerHandler, ServiceExt, model::{ AnnotateAble, CallToolRequestParams, CallToolResult, CompleteRequestParams, CompleteResult, CompletionInfo, - GetPromptRequestParams, GetPromptResult, Implementation, InitializeRequestParams, InitializeResult, + ErrorCode, GetPromptRequestParams, GetPromptResult, Implementation, InitializeRequestParams, InitializeResult, ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, ListToolsResult, LoggingLevel, PaginatedRequestParams, Prompt, PromptArgument, PromptMessage, PromptMessageContent, PromptMessageRole, - RawImageContent, RawResourceTemplate, ReadResourceRequestParams, ReadResourceResult, Reference, + RawImageContent, RawResourceTemplate, ReadResourceRequestParams, ReadResourceResult, Reference, Resource, ServerCapabilities, SetLevelRequestParams, SubscribeRequestParams, Tool, UnsubscribeRequestParams, }, - service::RequestContext, + service::{RequestContext, RunningService}, + transport::{StreamableHttpClientTransport, streamable_http_client::StreamableHttpClientTransportConfig}, }; - use tokio::sync::Mutex; use tracing::{debug, info, warn}; use typed_builder::TypedBuilder; -use crate::gateway::mcp_call_validator::InitializeCallValidator; -use crate::gateway::session_manager::SessionManager; +use super::mcp_call_validator::AuthorizedCallValidator; pub use crate::gateway::session_store::LocalUserSessionStore; - -use crate::gateway::session_store::{UserSession, UserSessionStore}; -use crate::{SessionId, user_config_store::UserConfig}; +use crate::{ + SessionId, + gateway::{ + mcp_call_validator::InitializeCallValidator, + session_manager::SessionManager, + session_store::{UserSession, UserSessionStore}, + }, + user_config_store::UserConfig, +}; #[derive(Clone, TypedBuilder)] #[builder(field_defaults(setter(prefix = "with_")))] @@ -137,7 +137,17 @@ where let downstream_session_id = downstream_session_id.clone(); Box::pin(async move { - let config = StreamableHttpClientTransportConfig::with_uri(backend_url.to_string()); + let mut headers = HashMap::new(); + if let Some(host) = backend_url.host_str() && backend_url.scheme() == "https"{ + if let Ok(value) = http::HeaderValue::from_str(host){ + headers.insert(http::header::HOST, value); + }else{ + warn!("Really can't set the host header for {:?}",backend_url.host_str()); + } + }; + + let config = StreamableHttpClientTransportConfig::with_uri(backend_url.to_string()) + .custom_headers(headers); let transport = StreamableHttpClientTransport::with_client(client, config); let maybe_running_service = request.serve(transport).await; if let Ok(running_service) = maybe_running_service { @@ -148,7 +158,6 @@ where (name, None) } }) - //) }).collect(); let initialization_results: Vec<(&String, Option>)> = @@ -160,7 +169,12 @@ where info!("initialize: Adding transport: session_id {downstream_session_id:#?} backend {name} {running_service:?}"); let server_capabilities = - running_service.as_ref().and_then(|rs| rs.peer().peer_info().as_ref().map(|pi| pi.capabilities.clone())); + running_service.as_ref() + .and_then(|rs| + rs.peer() + .peer_info() + .as_ref() + .map(|pi| pi.capabilities.clone())); ( (name.clone(), server_capabilities.clone()), (name.clone(), BackendTransportService::from((server_capabilities, running_service))), diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/local_session_store.rs b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/local_session_store.rs index 9c26aeee..56cb69a8 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/local_session_store.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/local_session_store.rs @@ -1,14 +1,11 @@ use std::sync::Arc; use async_trait::async_trait; - use lru_time_cache::LruCache; - use tokio::sync::Mutex; -use crate::const_values::{LRU_CACHE_ENTRIES, LRU_CACHE_EXPIRY_DURATION}; - use super::{SessionMapping, SessionStoreError, UserSession, UserSessionStore}; +use crate::const_values::{LRU_CACHE_ENTRIES, LRU_CACHE_EXPIRY_DURATION}; #[derive(Clone)] pub struct LocalUserSessionStore { diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs index 5e83cbef..4b6b94f6 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/mod.rs @@ -4,11 +4,9 @@ mod redis_session_store; use std::sync::Arc; use async_trait::async_trait; - -use serde::{Deserialize, Serialize}; - //pub use inmemory_config_store::InMemoryUserSessionStore; pub use local_session_store::LocalUserSessionStore; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SessionMap { diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/redis_session_store.rs b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/redis_session_store.rs index 6c8b9b95..d6c65a1b 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/session_store/redis_session_store.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/session_store/redis_session_store.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use async_trait::async_trait; - use lru_time_cache::LruCache; use redis::{AsyncCommands, RedisError, cmd}; use tokio::sync::Mutex; diff --git a/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs b/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs index cc0c97fa..b5b6fb58 100644 --- a/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs +++ b/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs @@ -2,7 +2,6 @@ use std::task::{Context, Poll}; use axum::http::Request; use tower::Service; - use tower_layer::Layer; use tracing::info; diff --git a/crates/contextforge-gateway-rs-lib/src/layers/user_config_store.rs b/crates/contextforge-gateway-rs-lib/src/layers/user_config_store.rs index 29674d5d..3815b751 100644 --- a/crates/contextforge-gateway-rs-lib/src/layers/user_config_store.rs +++ b/crates/contextforge-gateway-rs-lib/src/layers/user_config_store.rs @@ -1,7 +1,6 @@ use axum::{body::Body, extract::State, middleware::Next, response::Response}; use http::{StatusCode, header}; use openid::Claims; - use tracing::{debug, info, warn}; use crate::{ diff --git a/crates/contextforge-gateway-rs-lib/src/lib.rs b/crates/contextforge-gateway-rs-lib/src/lib.rs index d6797475..a4f3f7f2 100644 --- a/crates/contextforge-gateway-rs-lib/src/lib.rs +++ b/crates/contextforge-gateway-rs-lib/src/lib.rs @@ -2,7 +2,6 @@ use std::{env, fs, sync::Arc}; use axum::middleware; use axum_jwt_auth::LocalDecoder; - use futures::FutureExt; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use rmcp::transport::{ @@ -22,14 +21,15 @@ mod tests; mod tools; mod user_config_store; +pub use common::{RedisClient, RedisConfig}; use gateway::McpService; use layers::session_id::SessionId; - use tower_http::cors::{Any, CorsLayer}; - use transports::{DownstreamTls, Tcp}; use typed_builder::TypedBuilder; +pub use user_config_store::RedisUserConfigStore; +pub use crate::common::Config; use crate::{ common::ContextForgeGatewayAppState, const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, @@ -41,10 +41,6 @@ use crate::{ user_config_store::UserConfigStore, }; -pub use crate::common::Config; -pub use common::{RedisClient, RedisConfig}; -pub use user_config_store::RedisUserConfigStore; - #[derive(Clone, TypedBuilder)] #[builder(field_defaults(setter(prefix = "with_")))] pub struct Gateway { @@ -65,7 +61,7 @@ impl Gateway { let streamable_config = StreamableHttpServerConfig::default().disable_allowed_hosts(); - let reqwest_backend_client = reqwest::Client::builder().build()?; + let reqwest_backend_client = reqwest::Client::try_from(config)?; // Create streamable HTTP service let mcp_service: StreamableHttpService, LocalSessionManager> = diff --git a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs index 956e09ce..890db1b0 100644 --- a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs +++ b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs @@ -1,3 +1,13 @@ +use std::{ + collections::HashMap, + fs::{self, File}, + io::Read, + net::SocketAddr, + sync::Arc, + time::Duration, +}; + +use axum_server; use futures::{FutureExt, future::BoxFuture}; use http::{HeaderMap, HeaderValue}; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; @@ -12,13 +22,6 @@ use rmcp::{ }, }; use rustls::crypto::{self}; -use std::{ - collections::HashMap, - fs::{self, File}, - sync::Arc, -}; -use std::{io::Read, time::Duration}; - use tracing::{info, warn}; use crate::{ @@ -35,11 +38,16 @@ fn create_ports(ports: usize) -> Vec { (0..ports).into_iter().map(|_| openport::pick_random_unused_port().expect("Expecting to find port")).collect() } -fn create_backends(ports: &[u16]) -> HashMap { +fn create_backends(ports: &[u16], with_tls: bool) -> HashMap { ports .iter() .filter_map(|port| { - let url = format!("http://127.0.0.1:{port}/mcp").parse().expect("This should work"); + let url = if with_tls { + format!("https://127.0.0.1:{port}/mcp").parse().expect("This should work") + } else { + format!("http://127.0.0.1:{port}/mcp").parse().expect("This should work") + }; + Some((format!("backend-{port}"), BackendMCPGateway { url })) }) .collect::>() @@ -73,6 +81,33 @@ fn create_axum_servers( .collect() } +async fn create_axum_tls_servers( + ports: &[u16], + router: axum::Router, +) -> Vec>>> { + let config = axum_server::tls_rustls::RustlsConfig::from_pem_file( + "../../assets/contextforgeCA/contextforge-server.cert.pem", + "../../assets/contextforgeCA/contextforge-server.key.pem", + ) + .await + .expect("Expect this to work"); + + ports + .iter() + .map(|port| { + let router = router.clone(); + let addr: SocketAddr = format!("127.0.0.1:{port}").parse().expect("Expect this to work"); + let config = config.clone(); + async move { + //let listener = tokio::net::TcpListener::bind(addr).await.expect("Expect this to work"); + _ = axum_server::bind_rustls(addr, config).serve(router.into_make_service()).await; + Ok(()) + } + .boxed() + }) + .collect() +} + pub fn get_token(user_id: String) -> String { let key = EncodingKey::from_rsa_pem(&fs::read("../../assets/jwt.key").expect("Expecting this to work")) .expect("Expecting this to work"); @@ -107,13 +142,10 @@ async fn create_gateway_with_four_counters( let router = axum::Router::new().route_service("/mcp", service); - let servers_one = create_axum_servers(&gateway_one_ports, router.clone()); - let servers_two = create_axum_servers(&gateway_two_ports, router.clone()); - assert_ne!(gateway_one_ports, gateway_two_ports); - let gateway_one_backends = create_backends(&gateway_one_ports); - let gateway_two_backends = create_backends(&gateway_two_ports); + let gateway_one_backends = create_backends(&gateway_one_ports, false); + let gateway_two_backends = create_backends(&gateway_two_ports, false); let mut virtual_host_one_tool_names = create_tool_names(&gateway_one_ports); let mut virtual_host_two_tool_names = create_tool_names(&gateway_two_ports); @@ -149,17 +181,89 @@ async fn create_gateway_with_four_counters( } .boxed(); - let handle: tokio::task::JoinHandle>>> = - tokio::spawn(futures::future::join_all( - vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), - )); - if let Some(address) = config.address.as_ref() { let gateway_url = format!("http://{}/contextforge-rs/servers/{}/mcp", address.to_string(), virtual_host_one_id); + + let servers_one = create_axum_servers(&gateway_one_ports, router.clone()); + let servers_two = create_axum_servers(&gateway_two_ports, router.clone()); + let handle: tokio::task::JoinHandle>>> = + tokio::spawn(futures::future::join_all( + vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), + )); + Ok(TestSettings { handle, gateway_url, expected_tool_names: virtual_host_one_tool_names }) - } else if let Some(address) = config.tls_address.as_ref() { + } else { + Err("Invalid configuration".into()) + } +} + +async fn create_tls_gateway_with_four_tls_counters( + user: &str, + config: Config, +) -> Result> { + let mocked_user_config_store = MockedUserConfigStore::default(); + + let gateway_one_ports = create_ports(2); + let gateway_two_ports = create_ports(2); + + let service = StreamableHttpService::new( + || Ok(mock_counter::Counter::new()), + LocalSessionManager::default().into(), + StreamableHttpServerConfig::default().disable_allowed_hosts().disable_allowed_origins(), + ); + + let router = axum::Router::new().route_service("/mcp", service); + + assert_ne!(gateway_one_ports, gateway_two_ports); + + let gateway_one_backends = create_backends(&gateway_one_ports, true); + let gateway_two_backends = create_backends(&gateway_two_ports, true); + + let mut virtual_host_one_tool_names = create_tool_names(&gateway_one_ports); + let mut virtual_host_two_tool_names = create_tool_names(&gateway_two_ports); + virtual_host_one_tool_names.sort(); + virtual_host_two_tool_names.sort(); + + let user_key = User::new(user); + + let virtual_host_one_id = uuid::Uuid::new_v4().to_string(); + let virtual_host_two_id = uuid::Uuid::new_v4().to_string(); + + let virtual_hosts = HashMap::from([ + (virtual_host_one_id.clone(), VirtualHost { backends: gateway_one_backends }), + (virtual_host_two_id.clone(), VirtualHost { backends: gateway_two_backends }), + ]); + + let user_config = UserConfig { virtual_hosts }; + + mocked_user_config_store.set_config(&user_key, &user_config).await.expect("This should work"); + + let gateway = Gateway::builder() + .with_config(config.clone()) + .with_user_config_store(Arc::new(mocked_user_config_store)) + .with_session_manager(Arc::new(LocalSessionManager::default())) + .build(); + + let gateway: std::pin::Pin< + Box>> + Send>, + > = async move { + let res = gateway.run_gateway().await; + warn!("Gateway exited with result {res:?}"); + Ok(()) + } + .boxed(); + + if let Some(address) = config.tls_address.as_ref() { let gateway_url = format!("https://{}/contextforge-rs/servers/{}/mcp", address.to_string(), virtual_host_one_id); + + let servers_one = create_axum_tls_servers(&gateway_one_ports, router.clone()).await; + let servers_two = create_axum_tls_servers(&gateway_two_ports, router.clone()).await; + let handle: tokio::task::JoinHandle>>> = + tokio::spawn(futures::future::join_all( + vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), + )); + Ok(TestSettings { handle, gateway_url, expected_tool_names: virtual_host_one_tool_names }) } else { Err("Invalid configuration".into()) @@ -174,6 +278,7 @@ async fn plaintext_list_tools_end_to_end_test() -> Result<(), Box Result<(), Box Result<(), Box>> + Send>, > = async { let mut buf = Vec::new(); - File::open("../../assets/tls_certificate.pem")?.read_to_end(&mut buf)?; - let cert = reqwest::Certificate::from_pem(&buf)?; + File::open("../../assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem")?.read_to_end(&mut buf)?; + let certificates = reqwest::Certificate::from_pem_bundle(&mut buf)?; tokio::time::sleep(Duration::from_millis(100)).await; let mut default_headers = HeaderMap::new(); @@ -282,14 +389,11 @@ async fn tls_list_tools_end_to_end_test() -> Result<(), Box for Option { fn try_from(config: &Config) -> Result { match (config.tls_address.clone(), config.server_certificate.clone(), config.server_private_key.clone()) { (Some(address), Some(certificate), Some(private_key)) => { - let certificate = CertificateDer::from_pem_file(&certificate)?; + let certificates = CertificateDer::pem_file_iter(&certificate)?.flatten().collect::>(); let private_key = PrivateKeyDer::from_pem_file(&private_key)?; let server_config = ServerConfig::builder_with_protocol_versions(rustls::ALL_VERSIONS) .with_no_client_auth() - .with_single_cert(vec![certificate], private_key)?; + .with_single_cert(certificates, private_key)?; if let Some(tcp_address) = config.address && tcp_address == address @@ -40,8 +39,8 @@ impl TryFrom<&Config> for Option { let tcp = Tcp::new(address); Ok(Some(DownstreamTls { tcp, server_config })) }, - (None, _, _) => Ok(None), - (Some(_), _, _) => Err("Invalid tls config... configuration missing ".into()), + (None, ..) => Ok(None), + (Some(_), ..) => Err("Invalid tls config... configuration missing ".into()), } } } @@ -54,13 +53,6 @@ impl DownstreamTls { let tls_acceptor = TlsAcceptor::from(Arc::new(server_config)); - // Ok(axum::serve_tls(tls_acceptor, service) - // .with_graceful_shutdown(async { - // tokio::signal::ctrl_c().await.ok(); - // info!("Shutting down..."); - // }) - // .await?) - loop { tokio::select! { // here we accept a connection, and then start processing it. @@ -69,7 +61,6 @@ impl DownstreamTls { let tower_service = service.clone(); let tls_acceptor = tls_acceptor.clone(); - if let Ok((tcp_stream, addr)) = maybe_stream { tokio::spawn(async move { let Ok(stream) = tls_acceptor.accept(tcp_stream).await else { @@ -96,6 +87,9 @@ impl DownstreamTls { continue; }; } + _= tokio::signal::ctrl_c()=>{ + return Ok(()) + } } } diff --git a/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs b/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs index b3b218ad..74eff270 100644 --- a/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs +++ b/crates/contextforge-gateway-rs-lib/src/user_config_store/mod.rs @@ -4,11 +4,9 @@ mod redis_config_store; use std::collections::HashMap; use async_trait::async_trait; - -use serde::{Deserialize, Serialize}; - //pub use inmemory_config_store::InMemoryUserConfigStore; pub use redis_config_store::RedisUserConfigStore; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/contextforge-gateway-rs/Cargo.toml b/crates/contextforge-gateway-rs/Cargo.toml index 8b826add..ff1a3015 100644 --- a/crates/contextforge-gateway-rs/Cargo.toml +++ b/crates/contextforge-gateway-rs/Cargo.toml @@ -24,6 +24,7 @@ num_cpus = "1.17.0" rmcp.workspace = true tikv-jemallocator = "0.6.1" futures.workspace = true +rustls.workspace = true [lints] workspace = true diff --git a/crates/contextforge-gateway-rs/src/logging.rs b/crates/contextforge-gateway-rs/src/logging.rs index 55ef87e2..71f5e7f2 100644 --- a/crates/contextforge-gateway-rs/src/logging.rs +++ b/crates/contextforge-gateway-rs/src/logging.rs @@ -1,3 +1,4 @@ +use contextforge_gateway_rs_lib::Config; use opentelemetry::trace::TracerProvider; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler}; @@ -9,8 +10,6 @@ use tracing_subscriber::{ util::SubscriberInitExt, }; -use contextforge_gateway_rs_lib::Config; - #[allow(dead_code)] pub enum Guard { Appender(WorkerGuard), diff --git a/crates/contextforge-gateway-rs/src/main.rs b/crates/contextforge-gateway-rs/src/main.rs index e05155db..85be9566 100644 --- a/crates/contextforge-gateway-rs/src/main.rs +++ b/crates/contextforge-gateway-rs/src/main.rs @@ -5,15 +5,17 @@ use std::sync::Arc; use clap::Parser; use contextforge_gateway_rs_lib::{Config, Gateway, RedisClient, RedisConfig, RedisUserConfigStore}; - use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; - +use rustls::crypto; use tikv_jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; #[allow(clippy::print_stdout)] fn main() -> Result<(), Box> { + let provider = crypto::ring::default_provider(); + _ = provider.install_default(); + let config = Config::parse(); println!("contextforge-gateway-rs {config:?}"); let _guard = logging::init_tracing_logging(&config); diff --git a/crates/contextforge-gateway-rs/src/runtime.rs b/crates/contextforge-gateway-rs/src/runtime.rs index b42bd58b..3b915ff8 100644 --- a/crates/contextforge-gateway-rs/src/runtime.rs +++ b/crates/contextforge-gateway-rs/src/runtime.rs @@ -1,5 +1,6 @@ -use contextforge_gateway_rs_lib::{Config, Gateway}; use std::thread; + +use contextforge_gateway_rs_lib::{Config, Gateway}; use tokio::runtime::{Builder, LocalOptions}; use tracing::{debug, error, info, warn}; From 403834f9b0877002caa9424680704f9ca887316a Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Thu, 7 May 2026 10:25:30 +0100 Subject: [PATCH 5/5] Fixing clippy warnings+code review Signed-off-by: Dawid Nowak --- Cargo.lock | 85 +++++++-------- .../contextforge-gateway-rs-lib/src/common.rs | 9 +- .../src/gateway/mcp_gateway.rs | 10 +- crates/contextforge-gateway-rs-lib/src/lib.rs | 20 ++-- .../src/tests/gateway_end_to_end.rs | 102 +++++++----------- .../src/tests/mock_counter.rs | 8 +- .../src/tests/mocked_user_config_store.rs | 2 +- .../src/transports/tcp.rs | 8 +- .../src/transports/tls.rs | 12 +-- crates/contextforge-gateway-rs/src/runtime.rs | 16 ++- 10 files changed, 123 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e006d02..70951b5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -1518,9 +1518,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1925,16 +1925,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2017,9 +2007,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -2445,15 +2435,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.78" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2477,9 +2466,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.114" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -2649,18 +2638,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", @@ -2978,9 +2967,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redis" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44e94c96d8870a387d88ce3de3fdd608cbfc0705f03cb343cdde91509d3e49a" +checksum = "72d32a1ac9123f0d84fda64bfc02a271d9868483162dd2d9099b5c362ece064c" dependencies = [ "arcstr", "async-lock", @@ -4027,9 +4016,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -4111,9 +4100,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "base64", @@ -4137,9 +4126,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -4167,9 +4156,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "async-compression", "base64", @@ -4182,7 +4171,6 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", - "iri-string", "mime", "mime_guess", "percent-encoding", @@ -4193,6 +4181,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -4537,9 +4526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -4550,9 +4539,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ "js-sys", "wasm-bindgen", @@ -4560,9 +4549,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4570,9 +4559,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -4583,9 +4572,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -4639,9 +4628,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/contextforge-gateway-rs-lib/src/common.rs b/crates/contextforge-gateway-rs-lib/src/common.rs index ea672f28..09a8ba25 100644 --- a/crates/contextforge-gateway-rs-lib/src/common.rs +++ b/crates/contextforge-gateway-rs-lib/src/common.rs @@ -129,7 +129,7 @@ impl TryFrom<&Config> for RedisConfig { } impl TryFrom<&Config> for reqwest::Client { - type Error = Box; + type Error = crate::Error; fn try_from(config: &Config) -> Result { let builder = reqwest::Client::builder(); @@ -145,20 +145,17 @@ impl TryFrom<&Config> for reqwest::Client { let builder = if let Some(trust_bundle) = config.upstream_trust_bundle.as_ref() { let mut buf = Vec::new(); File::open(trust_bundle)?.read_to_end(&mut buf)?; - let certificates = reqwest::Certificate::from_pem_bundle(&mut buf)?; + let certificates = reqwest::Certificate::from_pem_bundle(&buf)?; builder.tls_certs_merge(certificates) } else { builder }; - // let mut header_map = HeaderMap::new(); - // //header_map.insert(http::header::HOST, HeaderValue::from_static("127.0.0.1")); - //let builder = builder.indefault_headers(header_map); Ok(builder.build()?) } } -fn extract_identity(config: &Config) -> Result> { +fn extract_identity(config: &Config) -> crate::Result { match (config.upstream_private_key.as_ref(), config.upstream_certificate.as_ref()) { (Some(private_key), Some(certificate)) => { let cert = fs::read(certificate)?; diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs index a554e5fe..dda462ba 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs @@ -139,12 +139,18 @@ where Box::pin(async move { let mut headers = HashMap::new(); if let Some(host) = backend_url.host_str() && backend_url.scheme() == "https"{ - if let Ok(value) = http::HeaderValue::from_str(host){ + let host = if let Some(port) = backend_url.port(){ + format!("{host}:{port}") + }else{ + host.to_owned() + }; + + if let Ok(value) = http::HeaderValue::from_str(&host){ headers.insert(http::header::HOST, value); }else{ warn!("Really can't set the host header for {:?}",backend_url.host_str()); } - }; + } let config = StreamableHttpClientTransportConfig::with_uri(backend_url.to_string()) .custom_headers(headers); diff --git a/crates/contextforge-gateway-rs-lib/src/lib.rs b/crates/contextforge-gateway-rs-lib/src/lib.rs index a4f3f7f2..8e319b09 100644 --- a/crates/contextforge-gateway-rs-lib/src/lib.rs +++ b/crates/contextforge-gateway-rs-lib/src/lib.rs @@ -1,4 +1,4 @@ -use std::{env, fs, sync::Arc}; +use std::{fs, sync::Arc}; use axum::middleware; use axum_jwt_auth::LocalDecoder; @@ -30,6 +30,10 @@ use typed_builder::TypedBuilder; pub use user_config_store::RedisUserConfigStore; pub use crate::common::Config; + +pub type Error = Box; +pub type Result = std::result::Result; + use crate::{ common::ContextForgeGatewayAppState, const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, @@ -50,9 +54,7 @@ pub struct Gateway { } impl Gateway { - pub async fn run_gateway(self) -> Result<(), Box> { - let path = env::current_dir()?; - println!("Current path {path:?}"); + pub async fn run_gateway(self) -> Result<()> { let config = &self.config; let session_manager = self.session_manager; let user_config_store = self.user_config_store; @@ -84,10 +86,16 @@ impl Gateway { let local_docoder = LocalDecoder::builder() .keys(vec![ DecodingKey::from_rsa_pem(&fs::read(&config.token_verification_public_key).map_err(|e| { - format!("Error when creating local decoder {e:?} {:?}", config.token_verification_public_key) + format!( + "Error when creating local decoder {e:?} {}", + config.token_verification_public_key.display() + ) })?) .map_err(|e| { - format!("Error when creating local decoder {e:?} {:?}", config.token_verification_public_key) + format!( + "Error when creating local decoder {e:?} {}", + config.token_verification_public_key.display() + ) })?, ]) .validation(validation) diff --git a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs index 890db1b0..dd10cee2 100644 --- a/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs +++ b/crates/contextforge-gateway-rs-lib/src/tests/gateway_end_to_end.rs @@ -7,11 +7,9 @@ use std::{ time::Duration, }; -use axum_server; use futures::{FutureExt, future::BoxFuture}; use http::{HeaderMap, HeaderValue}; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; -use openport; use rmcp::{ ServiceExt, model::InitializeRequestParams, @@ -35,20 +33,20 @@ const MOCK_COUNTER_TOOL_NAMES: &[&str] = &["decrement", "echo", "get_session_id", "get_value", "increment", "long_task", "say_hello", "sum"]; fn create_ports(ports: usize) -> Vec { - (0..ports).into_iter().map(|_| openport::pick_random_unused_port().expect("Expecting to find port")).collect() + (0..ports).map(|_| openport::pick_random_unused_port().expect("Expecting to find port")).collect() } fn create_backends(ports: &[u16], with_tls: bool) -> HashMap { ports .iter() - .filter_map(|port| { + .map(|port| { let url = if with_tls { format!("https://127.0.0.1:{port}/mcp").parse().expect("This should work") } else { format!("http://127.0.0.1:{port}/mcp").parse().expect("This should work") }; - Some((format!("backend-{port}"), BackendMCPGateway { url })) + (format!("backend-{port}"), BackendMCPGateway { url }) }) .collect::>() } @@ -62,10 +60,7 @@ fn create_tool_names(ports: &[u16]) -> Vec { .collect::>() } -fn create_axum_servers( - ports: &[u16], - router: axum::Router, -) -> Vec>>> { +fn create_axum_servers(ports: &[u16], router: &axum::Router) -> Vec>> { ports .iter() .map(|port| { @@ -81,10 +76,7 @@ fn create_axum_servers( .collect() } -async fn create_axum_tls_servers( - ports: &[u16], - router: axum::Router, -) -> Vec>>> { +async fn create_axum_tls_servers(ports: &[u16], router: axum::Router) -> Vec>> { let config = axum_server::tls_rustls::RustlsConfig::from_pem_file( "../../assets/contextforgeCA/contextforge-server.cert.pem", "../../assets/contextforgeCA/contextforge-server.key.pem", @@ -120,15 +112,12 @@ pub fn get_token(user_id: String) -> String { } struct TestSettings { - handle: tokio::task::JoinHandle>>>, + handle: tokio::task::JoinHandle>>, gateway_url: String, expected_tool_names: Vec, } -async fn create_gateway_with_four_counters( - user: &str, - config: Config, -) -> Result> { +async fn create_gateway_with_four_counters(user: &str, config: Config) -> crate::Result { let mocked_user_config_store = MockedUserConfigStore::default(); let gateway_one_ports = create_ports(2); @@ -172,9 +161,7 @@ async fn create_gateway_with_four_counters( .with_session_manager(Arc::new(LocalSessionManager::default())) .build(); - let gateway: std::pin::Pin< - Box>> + Send>, - > = async move { + let gateway = async move { let res = gateway.run_gateway().await; warn!("Gateway exited with result {res:?}"); Ok(()) @@ -182,13 +169,13 @@ async fn create_gateway_with_four_counters( .boxed(); if let Some(address) = config.address.as_ref() { - let gateway_url = format!("http://{}/contextforge-rs/servers/{}/mcp", address.to_string(), virtual_host_one_id); + let gateway_url = format!("http://{address}/contextforge-rs/servers/{virtual_host_one_id}/mcp"); - let servers_one = create_axum_servers(&gateway_one_ports, router.clone()); - let servers_two = create_axum_servers(&gateway_two_ports, router.clone()); + let servers_one = create_axum_servers(&gateway_one_ports, &router); + let servers_two = create_axum_servers(&gateway_two_ports, &router); let handle: tokio::task::JoinHandle>>> = tokio::spawn(futures::future::join_all( - vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), + vec![gateway].into_iter().chain(servers_one).chain(servers_two), //.chain(vec![test_future].into_iter()), )); Ok(TestSettings { handle, gateway_url, expected_tool_names: virtual_host_one_tool_names }) @@ -197,10 +184,7 @@ async fn create_gateway_with_four_counters( } } -async fn create_tls_gateway_with_four_tls_counters( - user: &str, - config: Config, -) -> Result> { +async fn create_tls_gateway_with_four_tls_counters(user: &str, config: Config) -> crate::Result { let mocked_user_config_store = MockedUserConfigStore::default(); let gateway_one_ports = create_ports(2); @@ -244,9 +228,7 @@ async fn create_tls_gateway_with_four_tls_counters( .with_session_manager(Arc::new(LocalSessionManager::default())) .build(); - let gateway: std::pin::Pin< - Box>> + Send>, - > = async move { + let gateway = async move { let res = gateway.run_gateway().await; warn!("Gateway exited with result {res:?}"); Ok(()) @@ -254,15 +236,12 @@ async fn create_tls_gateway_with_four_tls_counters( .boxed(); if let Some(address) = config.tls_address.as_ref() { - let gateway_url = - format!("https://{}/contextforge-rs/servers/{}/mcp", address.to_string(), virtual_host_one_id); + let gateway_url = format!("https://{address}/contextforge-rs/servers/{virtual_host_one_id}/mcp"); let servers_one = create_axum_tls_servers(&gateway_one_ports, router.clone()).await; let servers_two = create_axum_tls_servers(&gateway_two_ports, router.clone()).await; let handle: tokio::task::JoinHandle>>> = - tokio::spawn(futures::future::join_all( - vec![gateway].into_iter().chain(servers_one.into_iter()).chain(servers_two.into_iter()), //.chain(vec![test_future].into_iter()), - )); + tokio::spawn(futures::future::join_all(vec![gateway].into_iter().chain(servers_one).chain(servers_two))); Ok(TestSettings { handle, gateway_url, expected_tool_names: virtual_host_one_tool_names }) } else { @@ -272,13 +251,15 @@ async fn create_tls_gateway_with_four_tls_counters( #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] -async fn plaintext_list_tools_end_to_end_test() -> Result<(), Box> { +async fn plaintext_list_tools_end_to_end_test() -> crate::Result<()> { let gateway_port = create_ports(1)[0]; - let mut config = Config::default(); - config.address = Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")); - config.token_verification_public_key = "../../assets/jwt.key.pub".into(); - config.upstream_connection_mode = Some(crate::common::UpstreamConnectionMode::PlainTextAndTls); + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: "../../assets/jwt.key.pub".into(), + upstream_connection_mode: Some(crate::common::UpstreamConnectionMode::PlainTextAndTls), + ..Default::default() + }; let user = "admin@example.com"; @@ -288,10 +269,7 @@ async fn plaintext_list_tools_end_to_end_test() -> Result<(), Box>> + Send>, - > = async { - //let _ = test_semaphore.acquire().await; + let test_future: BoxFuture<'_, crate::Result<()>> = async { tokio::time::sleep(Duration::from_millis(100)).await; let mut default_headers = HeaderMap::new(); let token = get_token(user.to_owned()); @@ -336,7 +314,7 @@ async fn plaintext_list_tools_end_to_end_test() -> Result<(), Box Result<(), Box Result<(), Box> { +async fn tls_list_tools_end_to_end_test() -> crate::Result<()> { let provider = crypto::ring::default_provider(); _ = provider.install_default(); let gateway_port = create_ports(1)[0]; - - let mut config = Config::default(); - - config.token_verification_public_key = "../../assets/jwt.key.pub".into(); - let server_socket_addr: std::net::SocketAddr = format!("127.0.0.1:{gateway_port}").parse().expect("This should work"); - config.tls_address = Some(server_socket_addr.clone()); - config.server_certificate = Some("../../assets/contextforgeCA/contextforge-server.cert.pem".into()); - config.server_private_key = Some("../../assets/contextforgeCA/contextforge-server.key.pem".into()); - config.upstream_trust_bundle = - Some("../../assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem".into()); + + let config = Config { + token_verification_public_key: "../../assets/jwt.key.pub".into(), + upstream_connection_mode: Some(crate::common::UpstreamConnectionMode::PlainTextAndTls), + tls_address: Some(server_socket_addr), + server_private_key: Some("../../assets/contextforgeCA/contextforge-server.key.pem".into()), + server_certificate: Some("../../assets/contextforgeCA/contextforge-server.cert.pem".into()), + upstream_trust_bundle: Some("../../assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem".into()), + ..Default::default() + }; let user = "admin@example.com"; @@ -373,12 +351,10 @@ async fn tls_list_tools_end_to_end_test() -> Result<(), Box>> + Send>, - > = async { + let test_future: BoxFuture> = async { let mut buf = Vec::new(); File::open("../../assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem")?.read_to_end(&mut buf)?; - let certificates = reqwest::Certificate::from_pem_bundle(&mut buf)?; + let certificates = reqwest::Certificate::from_pem_bundle(&buf)?; tokio::time::sleep(Duration::from_millis(100)).await; let mut default_headers = HeaderMap::new(); @@ -429,7 +405,7 @@ async fn tls_list_tools_end_to_end_test() -> Result<(), Box Resource { - RawResource::new(uri, name.to_string()).no_annotation() + RawResource::new(uri, name.to_owned()).no_annotation() } #[tool(description = "Increment the counter by 1")] @@ -157,7 +159,7 @@ impl Counter { Parameters(args): Parameters, _ctx: RequestContext, ) -> Result { - let strategy = args.strategy.unwrap_or_else(|| "careful".to_string()); + let strategy = args.strategy.unwrap_or_else(|| "careful".to_owned()); let current_value = *self.counter.lock().await; let difference = args.goal - current_value; @@ -194,7 +196,7 @@ impl ServerHandler for Counter { ) .with_server_info(Implementation::from_build_env()) .with_protocol_version(ProtocolVersion::V_2024_11_05) - .with_instructions("This server provides counter tools and prompts. Tools: increment, decrement, get_value, say_hello, echo, sum. Prompts: example_prompt (takes a message), counter_analysis (analyzes counter state with a goal).".to_string()) + .with_instructions("This server provides counter tools and prompts. Tools: increment, decrement, get_value, say_hello, echo, sum. Prompts: example_prompt (takes a message), counter_analysis (analyzes counter state with a goal).".to_owned()) } async fn list_resources( diff --git a/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs b/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs index c6f9497b..68d7fb4b 100644 --- a/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs +++ b/crates/contextforge-gateway-rs-lib/src/tests/mocked_user_config_store.rs @@ -43,7 +43,7 @@ impl UserConfigStore for MockedUserConfigStore { return Err(ConfigStoreError::NoDataForKey); }; - let Ok(user_config) = rmp_serde::decode::from_slice::(&user_config) else { + let Ok(user_config) = rmp_serde::decode::from_slice::(user_config) else { return Err(ConfigStoreError::DataWrongFormat); }; diff --git a/crates/contextforge-gateway-rs-lib/src/transports/tcp.rs b/crates/contextforge-gateway-rs-lib/src/transports/tcp.rs index 5e48ed72..cc468bbd 100644 --- a/crates/contextforge-gateway-rs-lib/src/transports/tcp.rs +++ b/crates/contextforge-gateway-rs-lib/src/transports/tcp.rs @@ -15,7 +15,7 @@ impl Tcp { Self { address } } - pub async fn handle_tcp(self, service: Router) -> Result<(), Box> { + pub async fn handle_tcp(self, service: Router) -> crate::Result<()> { info!("Starting TCP listener at {}", self.address); let tcp_listener: TcpListener = self.try_into()?; @@ -29,10 +29,10 @@ impl Tcp { } impl TryFrom<&Config> for Option { - type Error = Box; + type Error = crate::Error; fn try_from(config: &Config) -> Result { - match config.address.clone() { + match config.address { Some(address) => Ok(Some(Tcp { address })), None => Ok(None), } @@ -40,7 +40,7 @@ impl TryFrom<&Config> for Option { } impl TryInto for Tcp { - type Error = Box; + type Error = crate::Error; fn try_into(self) -> Result { let address = self.address; diff --git a/crates/contextforge-gateway-rs-lib/src/transports/tls.rs b/crates/contextforge-gateway-rs-lib/src/transports/tls.rs index 59b7aac1..0f227e5b 100644 --- a/crates/contextforge-gateway-rs-lib/src/transports/tls.rs +++ b/crates/contextforge-gateway-rs-lib/src/transports/tls.rs @@ -11,7 +11,7 @@ use tokio_rustls::TlsAcceptor; use tower::Service; use tracing::{error, info, warn}; -use crate::{Config, transports::tcp::Tcp}; +use crate::{Config, Error, transports::tcp::Tcp}; pub struct DownstreamTls { tcp: Tcp, @@ -19,10 +19,10 @@ pub struct DownstreamTls { } impl TryFrom<&Config> for Option { - type Error = Box; + type Error = Error; fn try_from(config: &Config) -> Result { - match (config.tls_address.clone(), config.server_certificate.clone(), config.server_private_key.clone()) { + match (config.tls_address, config.server_certificate.clone(), config.server_private_key.clone()) { (Some(address), Some(certificate), Some(private_key)) => { let certificates = CertificateDer::pem_file_iter(&certificate)?.flatten().collect::>(); let private_key = PrivateKeyDer::from_pem_file(&private_key)?; @@ -46,7 +46,7 @@ impl TryFrom<&Config> for Option { } impl DownstreamTls { - pub async fn handle_tls(self, service: Router) -> Result<(), Box> { + pub async fn handle_tls(self, service: Router) -> crate::Result<()> { let DownstreamTls { tcp, server_config } = self; info!("Starting TLS listener at {}", tcp.address); let tcp_listener: TcpListener = tcp.try_into()?; @@ -55,8 +55,6 @@ impl DownstreamTls { loop { tokio::select! { - // here we accept a connection, and then start processing it. - // we spawn early so that we don't block other connections from being accepted due to a slow client maybe_stream = tcp_listener.accept() => { let tower_service = service.clone(); let tls_acceptor = tls_acceptor.clone(); @@ -84,7 +82,7 @@ impl DownstreamTls { }) } else { warn!("Problem during TCP handshake {maybe_stream:?}"); - continue; + return Err(maybe_stream.expect_err("Expect this to work").into()); }; } _= tokio::signal::ctrl_c()=>{ diff --git a/crates/contextforge-gateway-rs/src/runtime.rs b/crates/contextforge-gateway-rs/src/runtime.rs index 3b915ff8..ad476d16 100644 --- a/crates/contextforge-gateway-rs/src/runtime.rs +++ b/crates/contextforge-gateway-rs/src/runtime.rs @@ -5,6 +5,7 @@ use tokio::runtime::{Builder, LocalOptions}; use tracing::{debug, error, info, warn}; #[derive(Debug, Clone)] +#[allow(clippy::struct_field_names)] pub struct Runtime { single_runtime: bool, number_of_threads: usize, @@ -55,14 +56,14 @@ impl Runtime { } } - fn configure_single_thread_builder(&self, builder: &mut Builder, thread_name: String) { + fn configure_single_thread_builder(builder: &mut Builder, thread_name: String) { builder.enable_all().name(thread_name).global_queue_interval(1024).max_io_events_per_tick(4); } - pub fn execute(self, gateway: Gateway) -> Result<(), Box> { + pub fn execute(self, gateway: Gateway) -> contextforge_gateway_rs_lib::Result<()> { if self.single_runtime { let mut builder = Builder::new_multi_thread(); - self.configure_builder(&mut builder, self.thread_name.to_owned()); + self.configure_builder(&mut builder, self.thread_name.clone()); let runtime = builder.build()?; runtime.block_on(async { tokio::select! { @@ -79,14 +80,11 @@ impl Runtime { let handles = (0..self.number_of_threads) .map(|i| { let thread_name = self.thread_name.clone(); - let runtime = self.clone(); let gateway = gateway.clone(); - // let config = config.clone(); - // let session_manager = session_manager.clone(); thread::Builder::new().name("contextforge-gateway-rs-{i}".to_owned()).spawn(move || { let mut builder = Builder::new_current_thread(); - runtime.configure_single_thread_builder(&mut builder, format!("{}{}", thread_name, i)); + Self::configure_single_thread_builder(&mut builder, format!("{thread_name}{i}")); let maybe_runtime = builder.build_local(LocalOptions::default()); let Ok(runtime) = maybe_runtime else { warn!("Can't build thread {maybe_runtime:?}"); @@ -108,14 +106,14 @@ impl Runtime { }) .collect::>(); - handles.into_iter().for_each(|maybe_handle| { + for maybe_handle in handles { if let Ok(handle) = maybe_handle { let res = handle.join(); info!("Thread terminated with {res:?}"); } else { warn!("Thread terminated at start with {maybe_handle:?}"); } - }); + } Ok(()) } }