Skip to content

Commit 591e941

Browse files
cong-orstevenjrafal-ch
authored
feat(hermes): progressive migration proxy (#494)
* feat: add HTTP proxy component for temporary endpoint mirroring * feat: add document API v2 endpoint and enhance proxy documentation - Add /api/gateway/v2/document/index endpoint mirroring - Enhance documentation with comprehensive testing examples - Update testing section to match test_parity.sh patterns - Distill documentation while maintaining technical accuracy * refactor: replace string matching with regex patterns in HTTP proxy Replace verbose string constants and manual path matching with regex patterns for cleaner, more maintainable routing logic. Patterns are compiled once at startup for performance. * docs(http-proxy): add missing v1 document endpoint to mirrored endpoints list The documentation was missing from the currently mirrored endpoints section, though it exists in the code implementation. * refactor: evolve HTTP proxy from mirror to configurable router - Rename mirror terminology to external routing for clarity - Update documentation to reflect long-term vision of configurable proxy - Add TODOs for future configuration system features - Expand route patterns to handle subpaths and parameters - Improve code organization and maintainability * ci dict * ci fmt * ci * Update hermes/apps/athena/modules/http-proxy/Cargo.toml Co-authored-by: Rafał Chabowski <[email protected]> * Refactor HTTP proxy routing with RegexSet and update CI to v3.5.11 - Replace individual regex compilation with unified RegexSet for better performance - Introduce RouteAction enum for cleaner routing logic - Remove duplicate static regex declarations causing compile errors - Update catalyst-ci version from v3.5.10 to v3.5.11 across all Earthfiles - Clean up dictionary and improve code formatting --------- Co-authored-by: Steven Johnson <[email protected]> Co-authored-by: Rafał Chabowski <[email protected]>
1 parent 58b6d8c commit 591e941

File tree

15 files changed

+272
-142
lines changed

15 files changed

+272
-142
lines changed

.config/dictionaries/project.dic

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ bootstrapper
1616
BROTLI
1717
cantopen
1818
cardano
19-
catfact
2019
cbor
2120
cbork
2221
cdylib
@@ -238,3 +237,14 @@ xpub
238237
yamux
239238
yoroi
240239
zstack
240+
reqwest
241+
projectcatalyst
242+
yamux
243+
justfile
244+
sandboxing
245+
rustup
246+
rollouts
247+
subpaths
248+
reloadable
249+
asat
250+

hermes/Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
VERSION 0.8
22

3-
IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.5.10 AS rust-ci
3+
IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.5.11 AS rust-ci
44
#IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:feat/disallow-debug-format AS rust-ci
55

66
# Use when debugging cat-ci locally.

hermes/apps/athena/modules/http-proxy/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ crate-type = ["cdylib"]
99
[dependencies]
1010
wit-bindgen = "0.43.0"
1111
serde = { version = "1.0", features = ["derive"] }
12-
tracing = "*"
12+
tracing = "0.1.41"
13+
regex = "1.10"

hermes/apps/athena/modules/http-proxy/src/lib.rs

Lines changed: 213 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,233 @@
1-
//! # HTTP Proxy Component - Hermes WASM Module Example
2-
//!
3-
//! **This is a toy example** showing basic HTTP routing in Hermes. Real applications
4-
//! would add authentication, error handling, configuration, and security features.
5-
//!
6-
//! ## Key Patterns Demonstrated
7-
//! - HTTP request routing with pattern matching
8-
//! - Direct HTTP responses vs internal redirects
9-
//! - Structured logging with appropriate levels
10-
//! - WASM component structure for Hermes
11-
//!
12-
//! ## Production Extensions
13-
//! Real applications could build on this to create:
14-
//! - API gateways routing to microservices
15-
//! - Load balancers with backend selection
16-
//! - Content management systems
17-
//! - Authentication/authorization layers
18-
19-
// Allow everything since this is generated code.
1+
//! HTTP Proxy Module - Configurable Request Router
2+
//!
3+
//! ## Vision
4+
//! This module is designed to be a fully configurable HTTP proxy system that can:
5+
//! - Route requests to different backends based on configurable rules
6+
//! - Support multiple routing strategies (path-based, header-based, etc.)
7+
//! - Handle load balancing and failover scenarios
8+
//! - Provide middleware capabilities for request/response transformation
9+
//! - Offer dynamic configuration updates without restarts
10+
//!
11+
//! ## Current State
12+
//! At present, the module serves as a temporary bridge to external Cat Voices endpoints
13+
//! while native implementations are under development. The current focus is on:
14+
//! - Maintaining API compatibility during the transition period
15+
//! - Ensuring reliable request forwarding to external services
16+
//! - Providing seamless user experience while backend services migrate
17+
//!
18+
//! ## Roadmap
19+
//! As native implementations are completed, this module will evolve into a sophisticated
20+
//! proxy system capable of routing between multiple backends, supporting A/B testing,
21+
//! gradual rollouts, and advanced traffic management scenarios.
22+
2023
#[allow(clippy::all, unused)]
2124
mod hermes;
2225
mod stub;
2326

24-
use crate::hermes::exports::hermes::http_gateway::event::{HttpGatewayResponse};
25-
use crate::hermes::hermes::binary::api::Bstr;
27+
use crate::hermes::exports::hermes::http_gateway::event::HttpGatewayResponse;
2628
use crate::hermes::exports::hermes::http_gateway::event::HttpResponse;
29+
use crate::hermes::hermes::binary::api::Bstr;
30+
31+
use regex::RegexSet;
32+
use std::sync::OnceLock;
33+
34+
/// What to do when a route pattern matches
35+
#[derive(Debug, Clone, Copy)]
36+
enum RouteAction {
37+
External, // Forward to Cat Voices
38+
Static, // Serve natively
39+
}
40+
41+
/// Compiled patterns for efficient matching
42+
static ROUTE_MATCHER: OnceLock<(RegexSet, Vec<RouteAction>)> = OnceLock::new();
43+
44+
/// External Cat Voices host for temporary external routing
45+
/// TODO: Make this configurable via environment variables or config file
46+
const EXTERNAL_HOST: &str = "https://app.dev.projectcatalyst.io";
47+
48+
/// Route patterns that should be forwarded to external Cat Voices system
49+
/// TODO: Convert to configurable rules engine supporting dynamic pattern updates
50+
const EXTERNAL_ROUTE_PATTERNS: &[&str] = &[
51+
r"^/api/gateway/v1/config/frontend$",
52+
r"^/api/gateway/v1/cardano/assets/.+$",
53+
r"^/api/gateway/v1/rbac/registration.*$",
54+
r"^/api/gateway/v1/document.*$",
55+
r"^/api/gateway/v2/document.*$", // can handle subpaths and query parameters
56+
];
57+
58+
/// Regex pattern for static content (handled natively)
59+
/// TODO: Make static content patterns configurable
60+
const STATIC_PATTERN: &str = r"^/static/.+$";
2761

28-
/// Simple HTTP proxy component for demonstration purposes.
62+
/// HTTP proxy component providing configurable request routing.
63+
///
64+
/// Currently serves as a temporary bridge to external Cat Voices endpoints
65+
/// while native implementations are developed. The long-term vision is to
66+
/// evolve this into a full-featured configurable proxy supporting:
67+
/// - Dynamic backend selection
68+
/// - Load balancing strategies
69+
/// - Circuit breakers and health checks
70+
/// - Request/response middleware chains
71+
/// - A/B testing and canary deployments
2972
struct HttpProxyComponent;
3073

74+
/// Initialize all route patterns as a single RegexSet
75+
fn init_route_matcher() -> &'static (RegexSet, Vec<RouteAction>) {
76+
ROUTE_MATCHER.get_or_init(|| {
77+
let mut patterns = Vec::new();
78+
let mut actions = Vec::new();
79+
80+
// External routes (redirect to Cat Voices)
81+
for pattern in EXTERNAL_ROUTE_PATTERNS {
82+
patterns.push(*pattern);
83+
actions.push(RouteAction::External);
84+
}
85+
86+
// Static content (serve natively)
87+
patterns.push(STATIC_PATTERN);
88+
actions.push(RouteAction::Static);
89+
90+
// Compile all patterns together for performance
91+
let regex_set = RegexSet::new(&patterns).unwrap_or_else(|e| {
92+
log_warn(&format!("Failed to compile patterns: {}", e));
93+
RegexSet::empty()
94+
});
95+
96+
(regex_set, actions)
97+
})
98+
}
99+
100+
/// Get the action for a given path
101+
fn get_route_action(path: &str) -> Option<RouteAction> {
102+
let (regex_set, actions) = init_route_matcher();
103+
regex_set.matches(path).iter().next().map(|i| actions[i])
104+
}
105+
106+
/// Check if path should route externally
107+
fn should_route_externally(path: &str) -> bool {
108+
matches!(get_route_action(path), Some(RouteAction::External))
109+
}
110+
111+
/// Check if path is static content
112+
fn is_static_content(path: &str) -> bool {
113+
matches!(get_route_action(path), Some(RouteAction::Static))
114+
}
115+
116+
/// Creates an external route redirect response
117+
/// Currently redirects to Cat Voices - will become configurable backend selection
118+
fn create_external_redirect(path: &str) -> HttpGatewayResponse {
119+
log_debug(&format!("Routing externally to Cat Voices: {}", path));
120+
HttpGatewayResponse::InternalRedirect(format!("{}{}", EXTERNAL_HOST, path))
121+
}
122+
123+
/// Creates a static content response (native handling)
124+
/// TODO: Integrate with configurable static content serving middleware
125+
fn create_static_response(path: &str) -> HttpGatewayResponse {
126+
log_debug(&format!("Serving static content natively: {}", path));
127+
HttpGatewayResponse::Http(HttpResponse {
128+
code: 200,
129+
headers: vec![("content-type".to_string(), vec!["text/plain".to_string()])],
130+
body: Bstr::from(format!("Static file content for: {}", path)),
131+
})
132+
}
133+
134+
/// Creates a 404 not found response
135+
/// TODO: Make error responses configurable (custom error pages, etc.)
136+
fn create_not_found_response(
137+
method: &str,
138+
path: &str,
139+
) -> HttpGatewayResponse {
140+
log_warn(&format!(
141+
"Route not found (no native implementation or external routing configured): {} {}",
142+
method, path
143+
));
144+
HttpGatewayResponse::Http(HttpResponse {
145+
code: 404,
146+
headers: vec![("content-type".to_string(), vec!["text/html".to_string()])],
147+
body: Bstr::from("<html><body><h1>404 - Page Not Found</h1></body></html>"),
148+
})
149+
}
150+
151+
/// Logs an info message
152+
fn log_info(message: &str) {
153+
hermes::hermes::logging::api::log(
154+
hermes::hermes::logging::api::Level::Info,
155+
Some("http-proxy"),
156+
None,
157+
None,
158+
None,
159+
None,
160+
message,
161+
None,
162+
);
163+
}
164+
165+
/// Logs a debug message
166+
fn log_debug(message: &str) {
167+
hermes::hermes::logging::api::log(
168+
hermes::hermes::logging::api::Level::Debug,
169+
Some("http-proxy"),
170+
None,
171+
None,
172+
None,
173+
None,
174+
message,
175+
None,
176+
);
177+
}
178+
179+
/// Logs a warning message
180+
fn log_warn(message: &str) {
181+
hermes::hermes::logging::api::log(
182+
hermes::hermes::logging::api::Level::Warn,
183+
Some("http-proxy"),
184+
None,
185+
None,
186+
None,
187+
None,
188+
message,
189+
None,
190+
);
191+
}
192+
193+
/// Formats the response type for logging
194+
fn format_response_type(response: &HttpGatewayResponse) -> String {
195+
match response {
196+
HttpGatewayResponse::Http(resp) => format!("HTTP {}", resp.code),
197+
HttpGatewayResponse::InternalRedirect(_) => {
198+
"EXTERNAL_REDIRECT (temporary bridge)".to_string()
199+
},
200+
}
201+
}
202+
31203
impl hermes::exports::hermes::http_gateway::event::Guest for HttpProxyComponent {
32-
/// Handle HTTP requests and return responses or redirects.
33-
///
34-
/// Production systems would add: request validation, authentication,
35-
/// caching, error handling, and security headers.
204+
/// Routes HTTP requests through configurable proxy logic.
205+
///
206+
/// Current implementation provides temporary bridging to external Cat Voices
207+
/// endpoints while native implementations are developed. Future versions will
208+
/// support sophisticated routing rules, backend selection, and middleware chains.
36209
fn reply(
37210
_body: Vec<u8>,
38211
_headers: hermes::exports::hermes::http_gateway::event::Headers,
39212
path: String,
40213
method: String,
41214
) -> Option<HttpGatewayResponse> {
42-
hermes::hermes::logging::api::log(
43-
hermes::hermes::logging::api::Level::Info,
44-
Some("http-proxy"),
45-
None,
46-
None,
47-
None,
48-
None,
49-
format!("Processing HTTP request: {} {}", method, path).as_str(),
50-
None,
51-
);
52-
53-
let response = match path.as_str() {
54-
"/api" | "/api/index" => {
55-
hermes::hermes::logging::api::log(
56-
hermes::hermes::logging::api::Level::Debug,
57-
Some("http-proxy"),
58-
None,
59-
None,
60-
None,
61-
None,
62-
"Serving homepage content",
63-
None,
64-
);
65-
HttpGatewayResponse::Http(HttpResponse {
66-
code: 200,
67-
headers: vec![("content-type".to_string(), vec!["text/html".to_string()])],
68-
body: Bstr::from("<html><body><h1>Welcome to the homepage</h1></body></html>"),
69-
})
70-
},
71-
"/api/dashboard" => {
72-
hermes::hermes::logging::api::log(
73-
hermes::hermes::logging::api::Level::Debug,
74-
Some("http-proxy"),
75-
None,
76-
None,
77-
None,
78-
None,
79-
"Redirecting to external API: https://catfact.ninja/fact",
80-
None,
81-
);
82-
HttpGatewayResponse::InternalRedirect("https://catfact.ninja/fact".to_string())
83-
},
84-
path if path.starts_with("/static/") => {
85-
hermes::hermes::logging::api::log(
86-
hermes::hermes::logging::api::Level::Debug,
87-
Some("http-proxy"),
88-
None,
89-
None,
90-
None,
91-
None,
92-
format!("Serving static content for: {}", path).as_str(),
93-
None,
94-
);
95-
HttpGatewayResponse::Http(HttpResponse {
96-
code: 200,
97-
headers: vec![("content-type".to_string(), vec!["text/plain".to_string()])],
98-
body: Bstr::from(format!("Static file content for: {}", path)),
99-
})
100-
},
101-
_ => {
102-
hermes::hermes::logging::api::log(
103-
hermes::hermes::logging::api::Level::Warn,
104-
Some("http-proxy"),
105-
None,
106-
None,
107-
None,
108-
None,
109-
format!("Route not found: {} {}", method, path).as_str(),
110-
None,
111-
);
112-
HttpGatewayResponse::Http(HttpResponse {
113-
code: 404,
114-
headers: vec![("content-type".to_string(), vec!["text/html".to_string()])],
115-
body: Bstr::from("<html><body><h1>404 - Page Not Found</h1></body></html>"),
116-
})
117-
},
215+
log_info(&format!("Processing HTTP request: {} {}", method, path));
216+
217+
let response = if should_route_externally(&path) {
218+
create_external_redirect(&path)
219+
} else if is_static_content(&path) {
220+
create_static_response(&path)
221+
} else {
222+
create_not_found_response(&method, &path)
118223
};
119224

120-
hermes::hermes::logging::api::log(
121-
hermes::hermes::logging::api::Level::Info,
122-
Some("http-proxy"),
123-
None,
124-
None,
125-
None,
126-
None,
127-
format!("Request completed: {} {} -> {}", method, path,
128-
match &response {
129-
HttpGatewayResponse::Http(resp) => format!("HTTP {}", resp.code),
130-
HttpGatewayResponse::InternalRedirect(_) => "REDIRECT".to_string(),
131-
}
132-
).as_str(),
133-
None,
134-
);
225+
log_info(&format!(
226+
"Request completed: {} {} -> {}",
227+
method,
228+
path,
229+
format_response_type(&response)
230+
));
135231

136232
Some(response)
137233
}

0 commit comments

Comments
 (0)