From efce9fa8df6d5733dd98fc75f38c5fb473c25c73 Mon Sep 17 00:00:00 2001 From: laruh Date: Fri, 28 Jun 2024 14:40:12 +0700 Subject: [PATCH] WIP: impl `get_proxy_route_by_uri_inbound` func and `test_get_proxy_route_by_uri_inbound` test, update modify_request_uri logic --- assets/.config_test | 28 +++++++++++ src/ctx.rs | 111 +++++++++++++++++++++++++++++++++++++++++++ src/net/http.rs | 41 ++++++++++++++++ src/proxy/moralis.rs | 44 +++-------------- src/security/jwt.rs | 1 + 5 files changed, 188 insertions(+), 37 deletions(-) diff --git a/assets/.config_test b/assets/.config_test index b746995..c8b140f 100644 --- a/assets/.config_test +++ b/assets/.config_test @@ -34,6 +34,34 @@ "rp_30_min": 1000, "rp_60_min": 2000 } + }, + { + "inbound_route": "/nft-test/special", + "outbound_route": "https://nft.special", + "proxy_type": "moralis", + "authorized": false, + "allowed_rpc_methods": [], + "rate_limiter": { + "rp_1_min": 60, + "rp_5_min": 200, + "rp_15_min": 700, + "rp_30_min": 1000, + "rp_60_min": 2000 + } + }, + { + "inbound_route": "/", + "outbound_route": "https://adex.io", + "proxy_type": "moralis", + "authorized": false, + "allowed_rpc_methods": [], + "rate_limiter": { + "rp_1_min": 60, + "rp_5_min": 200, + "rp_15_min": 700, + "rp_30_min": 1000, + "rp_60_min": 2000 + } } ], "rate_limiter": { diff --git a/src/ctx.rs b/src/ctx.rs index 6d5192c..67236be 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,3 +1,4 @@ +use hyper::Uri; use once_cell::sync::OnceCell; use proxy::ProxyType; use serde::{Deserialize, Serialize}; @@ -90,6 +91,60 @@ impl AppConfig { route_index.map(|index| &self.proxy_routes[index]) } + + #[allow(dead_code)] + pub(crate) fn get_proxy_route_by_uri_inbound( + &self, + uri: &mut Uri, + ) -> GenericResult> { + let path_segments: Vec<&str> = uri.path().split('/').filter(|s| !s.is_empty()).collect(); + + let mut best_match: Option<(&ProxyRoute, usize)> = None; + + for r in &self.proxy_routes { + let route_segments: Vec<&str> = r + .inbound_route + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + let matched_segments = route_segments + .iter() + .zip(&path_segments) + .take_while(|(route_seg, path_seg)| route_seg == path_seg) + .count(); + if matched_segments == route_segments.len() + && (best_match.is_none() || matched_segments > best_match.unwrap().1) + { + best_match = Some((r, matched_segments)); + } + } + + if let Some((route, matched_segments)) = best_match { + let remaining_path: String = path_segments.iter().skip(matched_segments).fold( + String::new(), + |mut acc, segment| { + acc.push('/'); + acc.push_str(segment); + acc + }, + ); + + // Construct the new path and query + let new_path_and_query = match uri.query() { + Some(query) => format!("{}?{}", remaining_path, query), + None => remaining_path, + }; + + let mut parts = uri.clone().into_parts(); + parts.path_and_query = Some(new_path_and_query.parse()?); + let new_uri = Uri::from_parts(parts)?; + *uri = new_uri; + + return Ok(Some(route)); + } + + Ok(None) + } } #[cfg(test)] @@ -131,6 +186,34 @@ pub(crate) fn get_app_config_test_instance() -> AppConfig { rp_60_min: 2000, }), }, + ProxyRoute { + inbound_route: String::from("/nft-test/special"), + outbound_route: String::from("https://nft.special"), + proxy_type: ProxyType::Moralis, + authorized: false, + allowed_rpc_methods: Vec::default(), + rate_limiter: Some(RateLimiter { + rp_1_min: 60, + rp_5_min: 200, + rp_15_min: 700, + rp_30_min: 1000, + rp_60_min: 2000, + }), + }, + ProxyRoute { + inbound_route: String::from("/"), + outbound_route: String::from("https://adex.io"), + proxy_type: ProxyType::Moralis, + authorized: false, + allowed_rpc_methods: Vec::default(), + rate_limiter: Some(RateLimiter { + rp_1_min: 60, + rp_5_min: 200, + rp_15_min: 700, + rp_30_min: 1000, + rp_60_min: 2000, + }), + }, ]), rate_limiter: RateLimiter { rp_1_min: 555, @@ -180,6 +263,34 @@ fn test_app_config_serialzation_and_deserialization() { "rp_30_min": 1000, "rp_60_min": 2000 } + }, + { + "inbound_route": "/nft-test/special", + "outbound_route": "https://nft.special", + "proxy_type":"moralis", + "authorized": false, + "allowed_rpc_methods": [], + "rate_limiter": { + "rp_1_min": 60, + "rp_5_min": 200, + "rp_15_min": 700, + "rp_30_min": 1000, + "rp_60_min": 2000 + } + }, + { + "inbound_route": "/", + "outbound_route": "https://adex.io", + "proxy_type":"moralis", + "authorized": false, + "allowed_rpc_methods": [], + "rate_limiter": { + "rp_1_min": 60, + "rp_5_min": 200, + "rp_15_min": 700, + "rp_30_min": 1000, + "rp_60_min": 2000 + } } ], "rate_limiter": { diff --git a/src/net/http.rs b/src/net/http.rs index 437667d..388d5d1 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -93,6 +93,9 @@ pub(crate) async fn http_handler( return handle_preflight(); } + // TODO instead of let inbound_route = match req.method() use get_proxy_route_by_uri_inbound fnc for GET method + // for other methods use get_proxy_route_by_inbound + let inbound_route = match req.method() { // should take the second element as path string starts with a delimiter &Method::GET => req.uri().path().split('/').nth(1).unwrap_or("").to_string(), @@ -209,6 +212,44 @@ fn test_get_proxy_route_by_inbound() { assert_eq!(proxy_route.outbound_route, "https://nft.proxy"); } +#[test] +fn test_get_proxy_route_by_uri_inbound() { + use hyper::Uri; + use std::str::FromStr; + + let cfg = ctx::get_app_config_test_instance(); + + // test "/nft-test" inbound case + let mut url = Uri::from_str("https://komodo.proxy:5535/nft-test/nft/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + let proxy_route = cfg + .get_proxy_route_by_uri_inbound(&mut url) + .unwrap() + .unwrap(); + assert_eq!(proxy_route.outbound_route, "https://nft.proxy"); + let expected = Uri::from_str("https://komodo.proxy:5535/nft/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + assert_eq!(expected, url); + + // test "/nft-test/special" inbound case + let mut url = Uri::from_str("https://komodo.proxy:3333/nft-test/special/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + let proxy_route = cfg + .get_proxy_route_by_uri_inbound(&mut url) + .unwrap() + .unwrap(); + assert_eq!(proxy_route.outbound_route, "https://nft.special"); + let expected = Uri::from_str("https://komodo.proxy:3333/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + assert_eq!(expected, url); + + // test "/" inbound case + let mut url = Uri::from_str("https://komodo.proxy:0333/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + let proxy_route = cfg + .get_proxy_route_by_uri_inbound(&mut url) + .unwrap() + .unwrap(); + assert_eq!(proxy_route.outbound_route, "https://adex.io"); + let expected = Uri::from_str("https://komodo.proxy:0333/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC").unwrap(); + assert_eq!(expected, url); +} + #[test] fn test_respond_by_status() { let all_supported_status_codes = [ diff --git a/src/proxy/moralis.rs b/src/proxy/moralis.rs index 99c4a80..33fe99a 100644 --- a/src/proxy/moralis.rs +++ b/src/proxy/moralis.rs @@ -9,11 +9,9 @@ use crate::rate_limiter::RateLimitOperations; use crate::sign::{SignOps, SignedMessage}; use crate::{log_format, GenericResult}; use hyper::header::{HeaderName, HeaderValue}; -use hyper::http::uri::PathAndQuery; use hyper::{header, Body, Request, Response, StatusCode, Uri}; use hyper_tls::HttpsConnector; use std::net::SocketAddr; -use std::str::FromStr; pub(crate) async fn proxy_moralis( cfg: &AppConfig, @@ -87,43 +85,14 @@ pub(crate) async fn proxy_moralis( Ok(res) } -/// Modifies the URI of an HTTP request by replacing its base URI with the outbound URI specified in `ProxyRoute`, -/// while incorporating the path and query parameters from the original request URI. Additionally, this function -/// removes the first path segment from the original URI. +/// Modifies the request URI to use the proxy base URI from `outbound_route`. fn modify_request_uri(req: &mut Request, proxy_route: &ProxyRoute) -> GenericResult<()> { let proxy_base_uri = proxy_route.outbound_route.parse::()?; - - let req_uri = req.uri().clone(); - - // Remove the first path segment - let mut path_segments: Vec<&str> = req_uri - .path() - .split('/') - .filter(|s| !s.is_empty()) - .collect(); - if !path_segments.is_empty() { - path_segments.remove(0); - } - let new_path = format!("/{}", path_segments.join("/")); - - // Construct the new path and query - let path_and_query_str = match req_uri.query() { - Some(query) => format!("{}?{}", new_path, query), - None => new_path, - }; - - let path_and_query = PathAndQuery::from_str(&path_and_query_str)?; - - // Update the proxy URI with the new path and query - let mut proxy_outbound_parts = proxy_base_uri.into_parts(); - proxy_outbound_parts.path_and_query = Some(path_and_query); - - // Reconstruct the full URI with the updated parts - let new_uri = Uri::from_parts(proxy_outbound_parts)?; - - // Update the request URI + let original_uri = req.uri(); + let mut base_uri_parts = proxy_base_uri.into_parts(); + base_uri_parts.path_and_query = original_uri.path_and_query().cloned(); + let new_uri = Uri::from_parts(base_uri_parts)?; *req.uri_mut() = new_uri; - Ok(()) } @@ -283,9 +252,10 @@ async fn test_parse_moralis_payload() { #[tokio::test] async fn test_modify_request_uri() { use super::ProxyType; + use std::str::FromStr; let mut req = Request::builder() - .uri("https://komodoproxy.com/nft-proxy/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC") + .uri("https://komodoproxy.com/api/v2.2/0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326/nft/transfers?chain=eth&format=decimal&order=DESC") .body(Body::empty()) .unwrap(); diff --git a/src/security/jwt.rs b/src/security/jwt.rs index c2622a2..3d67909 100644 --- a/src/security/jwt.rs +++ b/src/security/jwt.rs @@ -38,6 +38,7 @@ impl JwtClaims { } static AUTH_DECODING_KEY: OnceCell = OnceCell::new(); + #[allow(dead_code)] pub(crate) fn get_decoding_key(cfg: &AppConfig) -> &'static DecodingKey { let buffer_closure = || -> Vec { read_file_buffer(&cfg.pubkey_path) };