From f2c6d738b6cad83c4c19c2c28ac878c1eac28b15 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Mon, 7 Aug 2023 22:18:54 +0900 Subject: [PATCH 01/30] todo: add http2-only client for h2c case --- rpxy-lib/src/backend/upstream_opts.rs | 8 ++++---- rpxy-lib/src/globals.rs | 3 ++- rpxy-lib/src/handler/handler_main.rs | 20 +++++++++++++------- rpxy-lib/src/handler/utils_request.rs | 8 ++++++-- rpxy-lib/src/lib.rs | 1 + 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/rpxy-lib/src/backend/upstream_opts.rs b/rpxy-lib/src/backend/upstream_opts.rs index 1cdb2a5..a96bb58 100644 --- a/rpxy-lib/src/backend/upstream_opts.rs +++ b/rpxy-lib/src/backend/upstream_opts.rs @@ -4,8 +4,8 @@ use crate::error::*; pub enum UpstreamOption { OverrideHost, UpgradeInsecureRequests, - ConvertHttpsTo11, - ConvertHttpsTo2, + ForceHttp11Upstream, + ForceHttp2Upstream, // TODO: Adds more options for heder override } impl TryFrom<&str> for UpstreamOption { @@ -14,8 +14,8 @@ impl TryFrom<&str> for UpstreamOption { match val { "override_host" => Ok(Self::OverrideHost), "upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests), - "convert_https_to_11" => Ok(Self::ConvertHttpsTo11), - "convert_https_to_2" => Ok(Self::ConvertHttpsTo2), + "force_http11_upstream" => Ok(Self::ForceHttp11Upstream), + "force_http2_upstream" => Ok(Self::ForceHttp2Upstream), _ => Err(RpxyError::Other(anyhow!("Unsupported header option"))), } } diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 6186d84..0bed623 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -225,7 +225,8 @@ where } if !(upstream.iter().all(|(_, elem)| { - !(elem.opts.contains(&UpstreamOption::ConvertHttpsTo11) && elem.opts.contains(&UpstreamOption::ConvertHttpsTo2)) + !(elem.opts.contains(&UpstreamOption::ForceHttp11Upstream) + && elem.opts.contains(&UpstreamOption::ForceHttp2Upstream)) })) { error!("Either one of force_http11 or force_http2 can be enabled"); return Err(RpxyError::ConfigBuild("Invalid upstream option setting")); diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index 0b554ae..bb5b22e 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -356,14 +356,20 @@ where } // If not specified (force_httpXX_upstream) and https, version is preserved except for http/3 - apply_upstream_options_to_request_line(req, upstream_group)?; - // Maybe workaround: Change version to http/1.1 when destination scheme is http - if req.version() != Version::HTTP_11 && upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { - *req.version_mut() = Version::HTTP_11; - } else if req.version() == Version::HTTP_3 { - debug!("HTTP/3 is currently unsupported for request to upstream. Use HTTP/2."); - *req.version_mut() = Version::HTTP_2; + match req.version() { + Version::HTTP_3 => { + debug!("HTTP/3 is currently unsupported for request to upstream."); + *req.version_mut() = Version::HTTP_2; + } + _ => { + if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { + // Change version to http/1.1 when destination scheme is http + debug!("Change version to http/1.1 when destination scheme is http."); + *req.version_mut() = Version::HTTP_11; + } + } } + apply_upstream_options_to_request_line(req, upstream_group)?; Ok(context) } diff --git a/rpxy-lib/src/handler/utils_request.rs b/rpxy-lib/src/handler/utils_request.rs index 03e36a1..6204f41 100644 --- a/rpxy-lib/src/handler/utils_request.rs +++ b/rpxy-lib/src/handler/utils_request.rs @@ -11,8 +11,12 @@ use hyper::{header, Request}; pub(super) fn apply_upstream_options_to_request_line(req: &mut Request, upstream: &UpstreamGroup) -> Result<()> { for opt in upstream.opts.iter() { match opt { - UpstreamOption::ConvertHttpsTo11 => *req.version_mut() = hyper::Version::HTTP_11, - UpstreamOption::ConvertHttpsTo2 => *req.version_mut() = hyper::Version::HTTP_2, + UpstreamOption::ForceHttp11Upstream => *req.version_mut() = hyper::Version::HTTP_11, + UpstreamOption::ForceHttp2Upstream => { + // case: h2c -> https://www.rfc-editor.org/rfc/rfc9113.txt + // Upgrade from HTTP/1.1 to HTTP/2 is deprecated. So, http-2 prior knowledge is required. + *req.version_mut() = hyper::Version::HTTP_2; + } _ => (), } } diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index c472b05..b2a777c 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -70,6 +70,7 @@ where .enable_http2() .build(); + // TODO: HTTP2 only client is needed for http2 cleartext case let msg_handler = HttpMessageHandlerBuilder::default() .forwarder(Arc::new(Client::builder().build::<_, hyper::Body>(connector))) .globals(globals.clone()) From 265cc025b05bd394075bb08436966124b5f09481 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Mon, 7 Aug 2023 22:27:18 +0900 Subject: [PATCH 02/30] refactor --- rpxy-lib/src/handler/utils_headers.rs | 14 +++++++------- rpxy-lib/src/log.rs | 11 ++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/rpxy-lib/src/handler/utils_headers.rs b/rpxy-lib/src/handler/utils_headers.rs index d09df79..6a09c1d 100644 --- a/rpxy-lib/src/handler/utils_headers.rs +++ b/rpxy-lib/src/handler/utils_headers.rs @@ -23,7 +23,7 @@ pub(super) fn takeout_sticky_cookie_lb_context( ) -> Result> { let mut headers_clone = headers.clone(); - match headers_clone.entry(hyper::header::COOKIE) { + match headers_clone.entry(header::COOKIE) { header::Entry::Vacant(_) => Ok(None), header::Entry::Occupied(entry) => { let cookies_iter = entry @@ -43,8 +43,8 @@ pub(super) fn takeout_sticky_cookie_lb_context( } let cookies_passed_to_upstream = without_sticky_cookies.join("; "); let cookie_passed_to_lb = sticky_cookies.first().unwrap(); - headers.remove(hyper::header::COOKIE); - headers.insert(hyper::header::COOKIE, cookies_passed_to_upstream.parse()?); + headers.remove(header::COOKIE); + headers.insert(header::COOKIE, cookies_passed_to_upstream.parse()?); let sticky_cookie = StickyCookie { value: StickyCookieValue::try_from(cookie_passed_to_lb, expected_cookie_name)?, @@ -63,7 +63,7 @@ pub(super) fn set_sticky_cookie_lb_context(headers: &mut HeaderMap, context_from let sticky_cookie_string: String = context_from_lb.sticky_cookie.clone().try_into()?; let new_header_val: HeaderValue = sticky_cookie_string.parse()?; let expected_cookie_name = &context_from_lb.sticky_cookie.value.name; - match headers.entry(hyper::header::SET_COOKIE) { + match headers.entry(header::SET_COOKIE) { header::Entry::Vacant(entry) => { entry.insert(new_header_val); } @@ -173,13 +173,13 @@ pub(super) fn add_header_entry_overwrite_if_exist( pub(super) fn make_cookie_single_line(headers: &mut HeaderMap) -> Result<()> { let cookies = headers .iter() - .filter(|(k, _)| **k == hyper::header::COOKIE) + .filter(|(k, _)| **k == header::COOKIE) .map(|(_, v)| v.to_str().unwrap_or("")) .collect::>() .join("; "); if !cookies.is_empty() { - headers.remove(hyper::header::COOKIE); - headers.insert(hyper::header::COOKIE, HeaderValue::from_bytes(cookies.as_bytes())?); + headers.remove(header::COOKIE); + headers.insert(header::COOKIE, HeaderValue::from_bytes(cookies.as_bytes())?); } Ok(()) } diff --git a/rpxy-lib/src/log.rs b/rpxy-lib/src/log.rs index 0fb7812..6b8afbe 100644 --- a/rpxy-lib/src/log.rs +++ b/rpxy-lib/src/log.rs @@ -1,4 +1,5 @@ use crate::utils::ToCanonical; +use hyper::header; use std::net::SocketAddr; pub use tracing::{debug, error, info, warn}; @@ -20,7 +21,7 @@ pub struct MessageLog { impl From<&hyper::Request> for MessageLog { fn from(req: &hyper::Request) -> Self { - let header_mapper = |v: hyper::header::HeaderName| { + let header_mapper = |v: header::HeaderName| { req .headers() .get(v) @@ -31,7 +32,7 @@ impl From<&hyper::Request> for MessageLog { // tls_server_name: "".to_string(), client_addr: "".to_string(), method: req.method().to_string(), - host: header_mapper(hyper::header::HOST), + host: header_mapper(header::HOST), p_and_q: req .uri() .path_and_query() @@ -40,8 +41,8 @@ impl From<&hyper::Request> for MessageLog { version: req.version(), uri_scheme: req.uri().scheme_str().unwrap_or("").to_string(), uri_host: req.uri().host().unwrap_or("").to_string(), - ua: header_mapper(hyper::header::USER_AGENT), - xff: header_mapper(hyper::header::HeaderName::from_static("x-forwarded-for")), + ua: header_mapper(header::USER_AGENT), + xff: header_mapper(header::HeaderName::from_static("x-forwarded-for")), status: "".to_string(), upstream: "".to_string(), } @@ -61,7 +62,7 @@ impl MessageLog { self.status = status_code.to_string(); self } - pub fn xff(&mut self, xff: &Option<&hyper::header::HeaderValue>) -> &mut Self { + pub fn xff(&mut self, xff: &Option<&header::HeaderValue>) -> &mut Self { self.xff = xff.map_or_else(|| "", |v| v.to_str().unwrap_or("")).to_string(); self } From 3dbe9c72178eaa17a73aaec8a9b2f3956eec9b87 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 8 Aug 2023 16:27:21 +0900 Subject: [PATCH 03/30] refactor: update rustls and fix response header server name --- rpxy-bin/Cargo.toml | 6 +++--- rpxy-lib/Cargo.toml | 4 ++-- rpxy-lib/src/constants.rs | 1 + rpxy-lib/src/handler/handler_main.rs | 5 +++-- rpxy-lib/src/proxy/crypto_service.rs | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index d7f5808..85f99a7 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -23,7 +23,7 @@ rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ anyhow = "1.0.72" rustc-hash = "1.1.0" -serde = { version = "1.0.180", default-features = false, features = ["derive"] } +serde = { version = "1.0.183", default-features = false, features = ["derive"] } derive_builder = "0.12.0" tokio = { version = "1.29.1", default-features = false, features = [ "net", @@ -36,7 +36,7 @@ async-trait = "0.1.72" rustls-pemfile = "1.0.3" # config -clap = { version = "4.3.19", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.3.21", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.7.6", default-features = false, features = ["parse"] } hot_reload = "0.1.4" @@ -46,7 +46,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [target.'cfg(not(target_env = "msvc"))'.dependencies] -tikv-jemallocator = "0.5.4" +tikv-jemallocator = "0.5.0" [dev-dependencies] diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index e1327f7..3f80fb0 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -51,9 +51,9 @@ hyper-rustls = { version = "0.24.1", default-features = false, features = [ "http2", ] } tokio-rustls = { version = "0.24.1", features = ["early-data"] } -rustls = { version = "0.21.5", default-features = false } +rustls = { version = "0.21.6", default-features = false } webpki = "0.22.0" -x509-parser = "0.15.0" +x509-parser = "0.15.1" # logging tracing = { version = "0.1.37" } diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index 39a93e7..b7b0bff 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -1,3 +1,4 @@ +pub const RESPONSE_HEADER_SERVER: &str = "rpxy"; // pub const LISTEN_ADDRESSES_V4: &[&str] = &["0.0.0.0"]; // pub const LISTEN_ADDRESSES_V6: &[&str] = &["[::]"]; pub const TCP_LISTEN_BACKLOG: u32 = 1024; diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index 0b554ae..29f0296 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -3,6 +3,7 @@ use super::{utils_headers::*, utils_request::*, utils_synth_response::*, Handler use crate::{ backend::{Backend, UpstreamGroup}, certs::CryptoSource, + constants::RESPONSE_HEADER_SERVER, error::*, globals::Globals, log::*, @@ -15,7 +16,7 @@ use hyper::{ http::uri::Scheme, Body, Client, Request, Response, StatusCode, Uri, Version, }; -use std::{env, net::SocketAddr, sync::Arc}; +use std::{net::SocketAddr, sync::Arc}; use tokio::{io::copy_bidirectional, time::timeout}; #[derive(Clone, Builder)] @@ -208,7 +209,7 @@ where let headers = response.headers_mut(); remove_connection_header(headers); remove_hop_header(headers); - add_header_entry_overwrite_if_exist(headers, "server", env!("CARGO_PKG_NAME"))?; + add_header_entry_overwrite_if_exist(headers, "server", RESPONSE_HEADER_SERVER)?; #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] { diff --git a/rpxy-lib/src/proxy/crypto_service.rs b/rpxy-lib/src/proxy/crypto_service.rs index d6191e6..ae0f993 100644 --- a/rpxy-lib/src/proxy/crypto_service.rs +++ b/rpxy-lib/src/proxy/crypto_service.rs @@ -115,7 +115,7 @@ impl ServerCryptoBase { // add client certificate if specified match certs_and_keys.parse_client_ca_certs() { Ok((owned_trust_anchors, _subject_key_ids)) => { - client_ca_roots_local.add_server_trust_anchors(owned_trust_anchors.into_iter()); + client_ca_roots_local.add_trust_anchors(owned_trust_anchors.into_iter()); } Err(e) => { warn!( From 43b004cf6ec096a91248049d604b3a918482c766 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 8 Aug 2023 17:59:20 +0900 Subject: [PATCH 04/30] refactor: separeted forwarder definition for more flexibility --- config-example.toml | 5 ++- rpxy-lib/src/handler/forwarder.rs | 58 ++++++++++++++++++++++++++++ rpxy-lib/src/handler/handler_main.rs | 12 ++++-- rpxy-lib/src/handler/mod.rs | 6 ++- rpxy-lib/src/lib.rs | 18 ++++----- 5 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 rpxy-lib/src/handler/forwarder.rs diff --git a/config-example.toml b/config-example.toml index 605067c..3d90761 100644 --- a/config-example.toml +++ b/config-example.toml @@ -56,7 +56,10 @@ upstream = [ { location = 'www.yahoo.co.jp', tls = true }, ] load_balance = "round_robin" # or "random" or "sticky" (sticky session) or "none" (fix to the first one, default) -upstream_options = ["override_host", "convert_https_to_2"] +upstream_options = [ + "override_host", + "force_http2_upstream", # mutually exclusive with "force_http11_upstream" +] # Non-default destination in "localhost" app, which is routed by "path" [[apps.localhost.reverse_proxy]] diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs new file mode 100644 index 0000000..bbcebca --- /dev/null +++ b/rpxy-lib/src/handler/forwarder.rs @@ -0,0 +1,58 @@ +use crate::error::RpxyError; +use async_trait::async_trait; +use derive_builder::Builder; +use hyper::{ + body::{Body, HttpBody}, + client::{connect::Connect, HttpConnector}, + Client, Request, Response, +}; +use hyper_rustls::HttpsConnector; + +#[async_trait] +/// Definition of the forwarder that simply forward requests from downstream client to upstream app servers. +pub trait ForwardRequest { + type Error; + async fn request(&self, req: Request) -> Result, Self::Error>; +} + +#[derive(Builder, Clone)] +/// Forwarder struct +pub struct Forwarder +where + C: Connect + Clone + Sync + Send + 'static, +{ + // TODO: need `h2c` or http/2-only client separately + inner: Client, +} + +#[async_trait] +impl ForwardRequest for Forwarder +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into>, + C: Connect + Clone + Sync + Send + 'static, +{ + type Error = RpxyError; + async fn request(&self, req: Request) -> Result, Self::Error> { + // TODO: + // TODO: Implement here a client that handles `h2c` requests + // TODO: + self.inner.request(req).await.map_err(RpxyError::Hyper) + } +} + +impl Forwarder, Body> { + pub async fn new() -> Self { + // let connector = TrustDnsResolver::default().into_rustls_webpki_https_connector(); + let connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + let inner = Client::builder().build::<_, Body>(connector); + Self { inner } + } +} diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index c450dea..22e05ca 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -1,5 +1,11 @@ // Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy -use super::{utils_headers::*, utils_request::*, utils_synth_response::*, HandlerContext}; +use super::{ + forwarder::{ForwardRequest, Forwarder}, + utils_headers::*, + utils_request::*, + utils_synth_response::*, + HandlerContext, +}; use crate::{ backend::{Backend, UpstreamGroup}, certs::CryptoSource, @@ -14,7 +20,7 @@ use hyper::{ client::connect::Connect, header::{self, HeaderValue}, http::uri::Scheme, - Body, Client, Request, Response, StatusCode, Uri, Version, + Body, Request, Response, StatusCode, Uri, Version, }; use std::{net::SocketAddr, sync::Arc}; use tokio::{io::copy_bidirectional, time::timeout}; @@ -27,7 +33,7 @@ where T: Connect + Clone + Sync + Send + 'static, U: CryptoSource + Clone, { - forwarder: Arc>, + forwarder: Arc>, globals: Arc>, } diff --git a/rpxy-lib/src/handler/mod.rs b/rpxy-lib/src/handler/mod.rs index aed9831..854bd8f 100644 --- a/rpxy-lib/src/handler/mod.rs +++ b/rpxy-lib/src/handler/mod.rs @@ -1,3 +1,4 @@ +mod forwarder; mod handler_main; mod utils_headers; mod utils_request; @@ -5,7 +6,10 @@ mod utils_synth_response; #[cfg(feature = "sticky-cookie")] use crate::backend::LbContext; -pub use handler_main::{HttpMessageHandler, HttpMessageHandlerBuilder, HttpMessageHandlerBuilderError}; +pub use { + forwarder::Forwarder, + handler_main::{HttpMessageHandler, HttpMessageHandlerBuilder, HttpMessageHandlerBuilderError}, +}; #[allow(dead_code)] #[derive(Debug)] diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index b2a777c..c2b8f0e 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -8,9 +8,14 @@ mod log; mod proxy; mod utils; -use crate::{error::*, globals::Globals, handler::HttpMessageHandlerBuilder, log::*, proxy::ProxyBuilder}; +use crate::{ + error::*, + globals::Globals, + handler::{Forwarder, HttpMessageHandlerBuilder}, + log::*, + proxy::ProxyBuilder, +}; use futures::future::select_all; -use hyper::Client; // use hyper_trust_dns::TrustDnsResolver; use std::sync::Arc; @@ -62,17 +67,10 @@ where request_count: Default::default(), runtime_handle: runtime_handle.clone(), }); - // let connector = TrustDnsResolver::default().into_rustls_webpki_https_connector(); - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_webpki_roots() - .https_or_http() - .enable_http1() - .enable_http2() - .build(); // TODO: HTTP2 only client is needed for http2 cleartext case let msg_handler = HttpMessageHandlerBuilder::default() - .forwarder(Arc::new(Client::builder().build::<_, hyper::Body>(connector))) + .forwarder(Arc::new(Forwarder::new().await)) .globals(globals.clone()) .build()?; From 2edc8eb79f7dd5605d768c0413e85389d30b29f5 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 8 Aug 2023 18:02:37 +0900 Subject: [PATCH 05/30] docs: add comments in forwarder and todo --- TODO.md | 1 + rpxy-lib/src/handler/forwarder.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index bf783c7..a069ecc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ # TODO List +- [Try in v0.5.1 or 0.6.0] Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task. - [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** - Improvement of path matcher - More flexible option for rewriting path diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index bbcebca..4fb2382 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -23,6 +23,7 @@ where { // TODO: need `h2c` or http/2-only client separately inner: Client, + // TODO: maybe this forwarder definition is suitable for cache handling. } #[async_trait] From 02c333905f0f0a6e536e62d80b6401319c5437ad Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 9 Aug 2023 02:13:04 +0900 Subject: [PATCH 06/30] feat: changed options for http version of requests to upstream app servers --- TODO.md | 1 + config-example.toml | 2 +- quinn | 2 +- rpxy-lib/src/error.rs | 26 +++++++++++++------------- rpxy-lib/src/handler/forwarder.rs | 25 ++++++++++++++++++------- s2n-quic | 2 +- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/TODO.md b/TODO.md index bf783c7..a069ecc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ # TODO List +- [Try in v0.5.1 or 0.6.0] Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task. - [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** - Improvement of path matcher - More flexible option for rewriting path diff --git a/config-example.toml b/config-example.toml index 3d90761..561ebc2 100644 --- a/config-example.toml +++ b/config-example.toml @@ -78,7 +78,7 @@ load_balance = "random" # or "round_robin" or "sticky" (sticky session) or "none upstream_options = [ "override_host", "upgrade_insecure_requests", - "convert_https_to_11", + "force_http11_upstream", ] ###################################################################### diff --git a/quinn b/quinn index 70e14b5..8076ffe 160000 --- a/quinn +++ b/quinn @@ -1 +1 @@ -Subproject commit 70e14b5c26b45ee1e3d5dd64b2a184e2d6376880 +Subproject commit 8076ffe94d38813ce0220af9d3438e7bfb5e8429 diff --git a/rpxy-lib/src/error.rs b/rpxy-lib/src/error.rs index dd88a9a..da56dac 100644 --- a/rpxy-lib/src/error.rs +++ b/rpxy-lib/src/error.rs @@ -7,13 +7,13 @@ pub type Result = std::result::Result; /// Describes things that can go wrong in the Rpxy #[derive(Debug, Error)] pub enum RpxyError { - #[error("Proxy build error")] + #[error("Proxy build error: {0}")] ProxyBuild(#[from] crate::proxy::ProxyBuilderError), - #[error("Backend build error")] + #[error("Backend build error: {0}")] BackendBuild(#[from] crate::backend::BackendBuilderError), - #[error("MessageHandler build error")] + #[error("MessageHandler build error: {0}")] HandlerBuild(#[from] crate::handler::HttpMessageHandlerBuilderError), #[error("Config builder error: {0}")] @@ -32,40 +32,40 @@ pub enum RpxyError { #[error("LoadBalance Layer Error: {0}")] LoadBalance(String), - #[error("I/O Error")] + #[error("I/O Error: {0}")] Io(#[from] io::Error), // #[error("Toml Deserialization Error")] // TomlDe(#[from] toml::de::Error), #[cfg(feature = "http3-quinn")] - #[error("Quic Connection Error")] + #[error("Quic Connection Error [quinn]: {0}")] QuicConn(#[from] quinn::ConnectionError), #[cfg(feature = "http3-s2n")] - #[error("Quic Connection Error [s2n-quic]")] + #[error("Quic Connection Error [s2n-quic]: {0}")] QUicConn(#[from] s2n_quic::connection::Error), #[cfg(feature = "http3-quinn")] - #[error("H3 Error")] + #[error("H3 Error [quinn]: {0}")] H3(#[from] h3::Error), #[cfg(feature = "http3-s2n")] - #[error("H3 Error [s2n-quic]")] + #[error("H3 Error [s2n-quic]: {0}")] H3(#[from] s2n_quic_h3::h3::Error), - #[error("rustls Connection Error")] + #[error("rustls Connection Error: {0}")] Rustls(#[from] rustls::Error), - #[error("Hyper Error")] + #[error("Hyper Error: {0}")] Hyper(#[from] hyper::Error), - #[error("Hyper Http Error")] + #[error("Hyper Http Error: {0}")] HyperHttp(#[from] hyper::http::Error), - #[error("Hyper Http HeaderValue Error")] + #[error("Hyper Http HeaderValue Error: {0}")] HyperHeaderValue(#[from] hyper::header::InvalidHeaderValue), - #[error("Hyper Http HeaderName Error")] + #[error("Hyper Http HeaderName Error: {0}")] HyperHeaderName(#[from] hyper::header::InvalidHeaderName), #[error(transparent)] diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index bbcebca..f1ba5e3 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -4,6 +4,7 @@ use derive_builder::Builder; use hyper::{ body::{Body, HttpBody}, client::{connect::Connect, HttpConnector}, + http::Version, Client, Request, Response, }; use hyper_rustls::HttpsConnector; @@ -21,24 +22,28 @@ pub struct Forwarder where C: Connect + Clone + Sync + Send + 'static, { - // TODO: need `h2c` or http/2-only client separately + // TODO: maybe this forwarder definition is suitable for cache handling. inner: Client, + inner_h2: Client, // `h2c` or http/2-only client is defined separately } #[async_trait] impl ForwardRequest for Forwarder where - B: HttpBody + Send + 'static, + B: HttpBody + Send + Sync + 'static, B::Data: Send, B::Error: Into>, C: Connect + Clone + Sync + Send + 'static, { type Error = RpxyError; async fn request(&self, req: Request) -> Result, Self::Error> { - // TODO: - // TODO: Implement here a client that handles `h2c` requests - // TODO: - self.inner.request(req).await.map_err(RpxyError::Hyper) + // TODO: This 'match' condition is always evaluated at every 'request' invocation. So, it is inefficient. + // Needs to be reconsidered. Currently, this is a kind of work around. + // This possibly relates to https://github.com/hyperium/hyper/issues/2417. + match req.version() { + Version::HTTP_2 => self.inner_h2.request(req).await.map_err(RpxyError::Hyper), // handles `h2c` requests + _ => self.inner.request(req).await.map_err(RpxyError::Hyper), + } } } @@ -51,8 +56,14 @@ impl Forwarder, Body> { .enable_http1() .enable_http2() .build(); + let connector_h2 = hyper_rustls::HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .build(); let inner = Client::builder().build::<_, Body>(connector); - Self { inner } + let inner_h2 = Client::builder().http2_only(true).build::<_, Body>(connector_h2); + Self { inner, inner_h2 } } } diff --git a/s2n-quic b/s2n-quic index 8ef0a6b..1ff2cd2 160000 --- a/s2n-quic +++ b/s2n-quic @@ -1 +1 @@ -Subproject commit 8ef0a6b66a856dc9f34ce18159c617ac29154cc7 +Subproject commit 1ff2cd230fdf46596fe77830966857c438a8b31a From eea4f28c56c809075159d382a77755db90820d5f Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 9 Aug 2023 03:16:04 +0900 Subject: [PATCH 07/30] fix: fix conversion flow of http version in requests to upstream app servers --- rpxy-lib/src/handler/handler_main.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index 22e05ca..98d563b 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -363,19 +363,16 @@ where } // If not specified (force_httpXX_upstream) and https, version is preserved except for http/3 - match req.version() { - Version::HTTP_3 => { - debug!("HTTP/3 is currently unsupported for request to upstream."); - *req.version_mut() = Version::HTTP_2; - } - _ => { - if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { - // Change version to http/1.1 when destination scheme is http - debug!("Change version to http/1.1 when destination scheme is http."); - *req.version_mut() = Version::HTTP_11; - } - } + if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { + // Change version to http/1.1 when destination scheme is http + debug!("Change version to http/1.1 when destination scheme is http."); + *req.version_mut() = Version::HTTP_11; + } else if req.version() == Version::HTTP_3 { + // HTTP/3 is always https + debug!("HTTP/3 is currently unsupported for request to upstream."); + *req.version_mut() = Version::HTTP_2; } + apply_upstream_options_to_request_line(req, upstream_group)?; Ok(context) From 6a616b160320bd104936ba49062b5cc00a38527d Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 9 Aug 2023 03:20:32 +0900 Subject: [PATCH 08/30] docs: update todo and changelog --- CHANGELOG.md | 9 +++++++++ TODO.md | 2 +- rpxy-bin/Cargo.toml | 2 +- rpxy-lib/Cargo.toml | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df06ae..df3a364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## 0.6.0 (unreleased) +### Improvement + +- Feat: Enabled `h2c` (HTTP/2 cleartext) requests to upstream app servers (in the previous versions, only HTTP/1.1 is allowed for cleartext requests) +- Refactor: logs of minor improvements + +### Bugfix + +- Fix: fix `server` in the response header (`rpxy_lib` -> `rpxy`) + ## 0.5.0 ### Improvement diff --git a/TODO.md b/TODO.md index a069ecc..abd51de 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO List -- [Try in v0.5.1 or 0.6.0] Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task. +- [Done in 0.6.0] ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ - [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** - Improvement of path matcher - More flexible option for rewriting path diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 85f99a7..e11df63 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rpxy" -version = "0.5.0" +version = "0.6.0" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/rust-rpxy" repository = "https://github.com/junkurihara/rust-rpxy" diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 3f80fb0..bcb56e3 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rpxy-lib" -version = "0.5.0" +version = "0.6.0" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/rust-rpxy" repository = "https://github.com/junkurihara/rust-rpxy" From 3d60175c11a5c4479a6f9e6f78b0449c1ef35ffd Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 9 Aug 2023 11:31:38 +0900 Subject: [PATCH 09/30] fix: fix message handler (changed this inside Arc) --- TODO.md | 1 + rpxy-bin/Cargo.toml | 2 +- rpxy-lib/src/handler/handler_main.rs | 2 +- rpxy-lib/src/lib.rs | 10 ++++++---- rpxy-lib/src/proxy/proxy_h3.rs | 4 ++-- rpxy-lib/src/proxy/proxy_main.rs | 23 ++++++++++++++++++++--- rpxy-lib/src/proxy/socket.rs | 2 +- 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index abd51de..ad4f6b2 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ - [Done in 0.6.0] ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ - [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** +- Fix dynamic reloading of configuration file - Improvement of path matcher - More flexible option for rewriting path - Refactoring diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index e11df63..65fe24d 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -46,7 +46,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [target.'cfg(not(target_env = "msvc"))'.dependencies] -tikv-jemallocator = "0.5.0" +tikv-jemallocator = "0.5.4" [dev-dependencies] diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index 98d563b..fb83d69 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -50,7 +50,7 @@ where /// Handle incoming request message from a client pub async fn handle_request( - self, + &self, mut req: Request, client_addr: SocketAddr, // アクセス制御用 listen_addr: SocketAddr, diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index c2b8f0e..5369cc2 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -69,10 +69,12 @@ where }); // TODO: HTTP2 only client is needed for http2 cleartext case - let msg_handler = HttpMessageHandlerBuilder::default() - .forwarder(Arc::new(Forwarder::new().await)) - .globals(globals.clone()) - .build()?; + let msg_handler = Arc::new( + HttpMessageHandlerBuilder::default() + .forwarder(Arc::new(Forwarder::new().await)) + .globals(globals.clone()) + .build()?, + ); let addresses = globals.proxy_config.listen_sockets.clone(); let futures = select_all(addresses.into_iter().map(|addr| { diff --git a/rpxy-lib/src/proxy/proxy_h3.rs b/rpxy-lib/src/proxy/proxy_h3.rs index 7773ad9..fd07521 100644 --- a/rpxy-lib/src/proxy/proxy_h3.rs +++ b/rpxy-lib/src/proxy/proxy_h3.rs @@ -15,7 +15,7 @@ where U: CryptoSource + Clone + Sync + Send + 'static, { pub(super) async fn connection_serve_h3( - self, + &self, quic_connection: C, tls_server_name: ServerNameBytesExp, client_addr: SocketAddr, @@ -79,7 +79,7 @@ where } async fn stream_serve_h3( - self, + &self, req: Request<()>, stream: RequestStream, client_addr: SocketAddr, diff --git a/rpxy-lib/src/proxy/proxy_main.rs b/rpxy-lib/src/proxy/proxy_main.rs index 166f048..9bfcde9 100644 --- a/rpxy-lib/src/proxy/proxy_main.rs +++ b/rpxy-lib/src/proxy/proxy_main.rs @@ -40,7 +40,7 @@ where { pub listening_on: SocketAddr, pub tls_enabled: bool, // TCP待受がTLSかどうか - pub msg_handler: HttpMessageHandler, + pub msg_handler: Arc>, pub globals: Arc>, } @@ -49,6 +49,21 @@ where T: Connect + Clone + Sync + Send + 'static, U: CryptoSource + Clone + Sync + Send, { + /// Wrapper function to handle request + async fn serve( + handler: Arc>, + req: Request, + client_addr: SocketAddr, + listen_addr: SocketAddr, + tls_enabled: bool, + tls_server_name: Option, + ) -> Result> { + handler + .handle_request(req, client_addr, listen_addr, tls_enabled, tls_server_name) + .await + } + + /// Serves requests from clients pub(super) fn client_serve( self, stream: I, @@ -72,7 +87,8 @@ where .serve_connection( stream, service_fn(move |req: Request| { - self.msg_handler.clone().handle_request( + Self::serve( + self.msg_handler.clone(), req, peer_addr, self.listening_on, @@ -91,11 +107,11 @@ where }); } + /// Start without TLS (HTTP cleartext) async fn start_without_tls(self, server: Http) -> Result<()> { let listener_service = async { let tcp_socket = bind_tcp_socket(&self.listening_on)?; let tcp_listener = tcp_socket.listen(self.globals.proxy_config.tcp_listen_backlog)?; - // let tcp_listener = TcpListener::bind(&self.listening_on).await?; info!("Start TCP proxy serving with HTTP request for configured host names"); while let Ok((stream, _client_addr)) = tcp_listener.accept().await { self.clone().client_serve(stream, server.clone(), _client_addr, None); @@ -106,6 +122,7 @@ where Ok(()) } + /// Entrypoint for HTTP/1.1 and HTTP/2 servers pub async fn start(self) -> Result<()> { let mut server = Http::new(); server.http1_keep_alive(self.globals.proxy_config.keepalive); diff --git a/rpxy-lib/src/proxy/socket.rs b/rpxy-lib/src/proxy/socket.rs index a8b9f01..9e4c8f9 100644 --- a/rpxy-lib/src/proxy/socket.rs +++ b/rpxy-lib/src/proxy/socket.rs @@ -32,7 +32,7 @@ pub(super) fn bind_udp_socket(listening_on: &SocketAddr) -> Result { } else { Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) }?; - // socket.set_reuse_address(true)?; // This isn't necessary + socket.set_reuse_address(true)?; // This isn't necessary? socket.set_reuse_port(true)?; if let Err(e) = socket.bind(&(*listening_on).into()) { From 7c2205f275221aaaaa93400769c9b7a68b52b337 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 9 Aug 2023 14:07:40 +0900 Subject: [PATCH 10/30] fix: bug for dynamic reloading of config files --- CHANGELOG.md | 1 + TODO.md | 5 +++-- rpxy-bin/src/main.rs | 11 ++++++++--- rpxy-lib/src/lib.rs | 5 +++-- rpxy-lib/src/proxy/proxy_main.rs | 34 +++++++++++++++++++++++++++----- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3a364..ff5cd18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Bugfix - Fix: fix `server` in the response header (`rpxy_lib` -> `rpxy`) +- Fix: fix bug for hot-reloading configuration file (Add termination notification receiver in proxy services) ## 0.5.0 diff --git a/TODO.md b/TODO.md index ad4f6b2..8ec6d5d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,7 @@ # TODO List -- [Done in 0.6.0] ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ +- [Done in 0.6.0] But we need more sophistication on `Forwarder` struct. ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ - [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** -- Fix dynamic reloading of configuration file - Improvement of path matcher - More flexible option for rewriting path - Refactoring @@ -33,5 +32,7 @@ ~~Benchmark with other reverse proxy implementations like Sozu ([#58](https://github.com/junkurihara/rust-rpxy/issues/58)) Currently, Sozu can work only on `amd64` format due to its HTTP message parser limitation... Since the main developer have only `arm64` (Apple M1) laptops, so we should do that on VPS?~~ - Done in v0.4.0: ~~Split `rpxy` source codes into `rpxy-lib` and `rpxy-bin` to make the core part (reverse proxy) isolated from the misc part like toml file loader. This is in order to make the configuration-related part more flexible (related to [#33](https://github.com/junkurihara/rust-rpxy/issues/33))~~ +- Done in 0.6.0: + ~~Fix dynamic reloading of configuration file~~ - etc. diff --git a/rpxy-bin/src/main.rs b/rpxy-bin/src/main.rs index 861c3d5..474eff1 100644 --- a/rpxy-bin/src/main.rs +++ b/rpxy-bin/src/main.rs @@ -84,7 +84,7 @@ async fn rpxy_service_without_watcher( return Err(anyhow::anyhow!(e)); } }; - entrypoint(&proxy_conf, &app_conf, &runtime_handle) + entrypoint(&proxy_conf, &app_conf, &runtime_handle, None) .await .map_err(|e| anyhow::anyhow!(e)) } @@ -105,10 +105,13 @@ async fn rpxy_service_with_watcher( } }; + // Notifier for proxy service termination + let term_notify = std::sync::Arc::new(tokio::sync::Notify::new()); + // Continuous monitoring loop { tokio::select! { - _ = entrypoint(&proxy_conf, &app_conf, &runtime_handle) => { + _ = entrypoint(&proxy_conf, &app_conf, &runtime_handle, Some(term_notify.clone())) => { error!("rpxy entrypoint exited"); break; } @@ -127,7 +130,9 @@ async fn rpxy_service_with_watcher( continue; } }; - info!("Configuration updated. Force to re-bind TCP/UDP sockets"); + info!("Configuration updated. Terminate all spawned proxy services and force to re-bind TCP/UDP sockets"); + term_notify.notify_waiters(); + // tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } else => break } diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index 5369cc2..b3af2a8 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -36,6 +36,7 @@ pub async fn entrypoint( proxy_config: &ProxyConfig, app_config_list: &AppConfigList, runtime_handle: &tokio::runtime::Handle, + term_notify: Option>, ) -> Result<()> where T: CryptoSource + Clone + Send + Sync + 'static, @@ -68,7 +69,7 @@ where runtime_handle: runtime_handle.clone(), }); - // TODO: HTTP2 only client is needed for http2 cleartext case + // build message handler including a request forwarder let msg_handler = Arc::new( HttpMessageHandlerBuilder::default() .forwarder(Arc::new(Forwarder::new().await)) @@ -91,7 +92,7 @@ where .build() .unwrap(); - globals.runtime_handle.spawn(proxy.start()) + globals.runtime_handle.spawn(proxy.start(term_notify.clone())) })); // wait for all future diff --git a/rpxy-lib/src/proxy/proxy_main.rs b/rpxy-lib/src/proxy/proxy_main.rs index 9bfcde9..bd52ea9 100644 --- a/rpxy-lib/src/proxy/proxy_main.rs +++ b/rpxy-lib/src/proxy/proxy_main.rs @@ -8,6 +8,7 @@ use std::{net::SocketAddr, sync::Arc}; use tokio::{ io::{AsyncRead, AsyncWrite}, runtime::Handle, + sync::Notify, time::{timeout, Duration}, }; @@ -123,7 +124,7 @@ where } /// Entrypoint for HTTP/1.1 and HTTP/2 servers - pub async fn start(self) -> Result<()> { + pub async fn start(self, term_notify: Option>) -> Result<()> { let mut server = Http::new(); server.http1_keep_alive(self.globals.proxy_config.keepalive); server.http2_max_concurrent_streams(self.globals.proxy_config.max_concurrent_streams); @@ -131,12 +132,35 @@ where let executor = LocalExecutor::new(self.globals.runtime_handle.clone()); let server = server.with_executor(executor); - if self.tls_enabled { - self.start_with_tls(server).await?; - } else { - self.start_without_tls(server).await?; + let listening_on = self.listening_on; + + let proxy_service = async { + if self.tls_enabled { + self.start_with_tls(server).await + } else { + self.start_without_tls(server).await + } + }; + + match term_notify { + Some(term) => { + tokio::select! { + _ = proxy_service => { + warn!("Proxy service got down"); + } + _ = term.notified() => { + info!("Proxy service listening on {} receives term signal", listening_on); + } + } + } + None => { + proxy_service.await?; + warn!("Proxy service got down"); + } } + // proxy_service.await?; + Ok(()) } } From 719e845d5e6f88b5fe14e7234b12a8fcc8245300 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 10 Aug 2023 17:34:29 +0900 Subject: [PATCH 11/30] fix: typo --- rpxy-lib/src/handler/forwarder.rs | 2 +- rpxy-lib/src/handler/handler_main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index f1ba5e3..4bfd15b 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -59,7 +59,7 @@ impl Forwarder, Body> { let connector_h2 = hyper_rustls::HttpsConnectorBuilder::new() .with_webpki_roots() .https_or_http() - .enable_http1() + .enable_http2() .build(); let inner = Client::builder().build::<_, Body>(connector); diff --git a/rpxy-lib/src/handler/handler_main.rs b/rpxy-lib/src/handler/handler_main.rs index fb83d69..8b13dc7 100644 --- a/rpxy-lib/src/handler/handler_main.rs +++ b/rpxy-lib/src/handler/handler_main.rs @@ -365,7 +365,7 @@ where // If not specified (force_httpXX_upstream) and https, version is preserved except for http/3 if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) { // Change version to http/1.1 when destination scheme is http - debug!("Change version to http/1.1 when destination scheme is http."); + debug!("Change version to http/1.1 when destination scheme is http unless upstream option enabled."); *req.version_mut() = Version::HTTP_11; } else if req.version() == Version::HTTP_3 { // HTTP/3 is always https From c20e80b64c53ee3096deccd083a5f4982229fb29 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 05:10:42 +0900 Subject: [PATCH 12/30] feat: very unstalbe implementation of initial file cache --- .gitmodules | 15 +- Cargo.toml | 2 +- config-example.toml | 4 + quinn | 1 - rpxy-bin/Cargo.toml | 9 +- rpxy-bin/src/config/toml.rs | 17 +- rpxy-bin/src/constants.rs | 1 + rpxy-lib/Cargo.toml | 25 ++- rpxy-lib/src/constants.rs | 2 + rpxy-lib/src/error.rs | 3 + rpxy-lib/src/globals.rs | 9 +- rpxy-lib/src/handler/cache.rs | 196 +++++++++++++++++++ rpxy-lib/src/handler/forwarder.rs | 73 ++++++- rpxy-lib/src/handler/mod.rs | 2 + rpxy-lib/src/lib.rs | 9 +- s2n-quic | 1 - h3 => submodules/h3 | 0 {h3-quinn => submodules/h3-quinn}/Cargo.toml | 0 {h3-quinn => submodules/h3-quinn}/src/lib.rs | 0 submodules/quinn | 1 + submodules/rusty-http-cache-semantics | 1 + submodules/s2n-quic | 1 + 22 files changed, 340 insertions(+), 32 deletions(-) delete mode 160000 quinn create mode 100644 rpxy-lib/src/handler/cache.rs delete mode 160000 s2n-quic rename h3 => submodules/h3 (100%) rename {h3-quinn => submodules/h3-quinn}/Cargo.toml (100%) rename {h3-quinn => submodules/h3-quinn}/src/lib.rs (100%) create mode 160000 submodules/quinn create mode 160000 submodules/rusty-http-cache-semantics create mode 160000 submodules/s2n-quic diff --git a/.gitmodules b/.gitmodules index 59b7ea8..65fcd3b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,12 @@ -[submodule "h3"] - path = h3 +[submodule "submodules/h3"] + path = submodules/h3 url = git@github.com:junkurihara/h3.git -[submodule "quinn"] - path = quinn +[submodule "submodules/quinn"] + path = submodules/quinn url = git@github.com:junkurihara/quinn.git -[submodule "s2n-quic"] - path = s2n-quic +[submodule "submodules/s2n-quic"] + path = submodules/s2n-quic url = git@github.com:junkurihara/s2n-quic.git +[submodule "submodules/rusty-http-cache-semantics"] + path = submodules/rusty-http-cache-semantics + url = git@github.com:junkurihara/rusty-http-cache-semantics.git diff --git a/Cargo.toml b/Cargo.toml index aa65657..0a32c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = ["rpxy-bin", "rpxy-lib"] -exclude = ["quinn", "h3-quinn", "h3", "s2n-quic"] +exclude = ["submodules", "h3-quinn"] [profile.release] codegen-units = 1 diff --git a/config-example.toml b/config-example.toml index 561ebc2..41d70e7 100644 --- a/config-example.toml +++ b/config-example.toml @@ -107,3 +107,7 @@ max_concurrent_bidistream = 100 max_concurrent_unistream = 100 max_idle_timeout = 10 # secs. 0 represents an infinite timeout. # WARNING: If a peer or its network path malfunctions or acts maliciously, an infinite idle timeout can result in permanently hung futures! + +# If this specified, file cache feature is enabled +[experimental.cache] +cache_dir = './cache' # optional. default is "./cache" relative to the current working directory diff --git a/quinn b/quinn deleted file mode 160000 index 8076ffe..0000000 --- a/quinn +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8076ffe94d38813ce0220af9d3438e7bfb5e8429 diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 65fe24d..96771c1 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -12,27 +12,28 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["http3-quinn"] +default = ["http3-quinn", "cache"] http3-quinn = ["rpxy-lib/http3-quinn"] http3-s2n = ["rpxy-lib/http3-s2n"] +cache = [] [dependencies] rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ "sticky-cookie", ] } -anyhow = "1.0.72" +anyhow = "1.0.73" rustc-hash = "1.1.0" serde = { version = "1.0.183", default-features = false, features = ["derive"] } derive_builder = "0.12.0" -tokio = { version = "1.29.1", default-features = false, features = [ +tokio = { version = "1.31.0", default-features = false, features = [ "net", "rt-multi-thread", "time", "sync", "macros", ] } -async-trait = "0.1.72" +async-trait = "0.1.73" rustls-pemfile = "1.0.3" # config diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index 5f6ab4a..60f13b3 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -6,7 +6,7 @@ use crate::{ use rpxy_lib::{reexports::Uri, AppConfig, ProxyConfig, ReverseProxyConfig, TlsConfig, UpstreamUri}; use rustc_hash::FxHashMap as HashMap; use serde::Deserialize; -use std::{fs, net::SocketAddr}; +use std::{fs, net::SocketAddr, path::PathBuf}; #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct ConfigToml { @@ -32,10 +32,17 @@ pub struct Http3Option { pub max_idle_timeout: Option, } +#[cfg(feature = "cache")] +#[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] +pub struct CacheOption { + pub cache_dir: Option, +} + #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct Experimental { #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] pub h3: Option, + pub cache: Option, pub ignore_sni_consistency: Option, } @@ -160,6 +167,14 @@ impl TryInto for &ConfigToml { if let Some(ignore) = exp.ignore_sni_consistency { proxy_config.sni_consistency = !ignore; } + + if let Some(cache_option) = &exp.cache { + proxy_config.cache_enabled = true; + proxy_config.cache_dir = match &cache_option.cache_dir { + Some(cache_dir) => Some(PathBuf::from(cache_dir)), + None => Some(PathBuf::from(CACHE_DIR)), + } + } } Ok(proxy_config) diff --git a/rpxy-bin/src/constants.rs b/rpxy-bin/src/constants.rs index 323615f..a7e811a 100644 --- a/rpxy-bin/src/constants.rs +++ b/rpxy-bin/src/constants.rs @@ -1,3 +1,4 @@ pub const LISTEN_ADDRESSES_V4: &[&str] = &["0.0.0.0"]; pub const LISTEN_ADDRESSES_V6: &[&str] = &["[::]"]; pub const CONFIG_WATCH_DELAY_SECS: u32 = 20; +pub const CACHE_DIR: &str = "./cache"; diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index bcb56e3..a859131 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -23,19 +23,20 @@ rustc-hash = "1.1.0" bytes = "1.4.0" derive_builder = "0.12.0" futures = { version = "0.3.28", features = ["alloc", "async-await"] } -tokio = { version = "1.29.1", default-features = false, features = [ +tokio = { version = "1.31.0", default-features = false, features = [ "net", "rt-multi-thread", "time", "sync", "macros", + "fs", ] } -async-trait = "0.1.72" +async-trait = "0.1.73" hot_reload = "0.1.4" # reloading certs # Error handling -anyhow = "1.0.72" -thiserror = "1.0.44" +anyhow = "1.0.73" +thiserror = "1.0.45" # http and tls hyper = { version = "0.14.27", default-features = false, features = [ @@ -60,17 +61,21 @@ tracing = { version = "0.1.37" } # http/3 # quinn = { version = "0.9.3", optional = true } -quinn = { path = "../quinn/quinn", optional = true } # Tentative to support rustls-0.21 -h3 = { path = "../h3/h3/", optional = true } +quinn = { path = "../submodules/quinn/quinn", optional = true } # Tentative to support rustls-0.21 +h3 = { path = "../submodules/h3/h3/", optional = true } # h3-quinn = { path = "./h3/h3-quinn/", optional = true } -h3-quinn = { path = "../h3-quinn/", optional = true } # Tentative to support rustls-0.21 +h3-quinn = { path = "../submodules/h3-quinn/", optional = true } # Tentative to support rustls-0.21 # for UDP socket wit SO_REUSEADDR when h3 with quinn socket2 = { version = "0.5.3", features = ["all"], optional = true } -s2n-quic = { path = "../s2n-quic/quic/s2n-quic/", default-features = false, features = [ +s2n-quic = { path = "../submodules/s2n-quic/quic/s2n-quic/", default-features = false, features = [ "provider-tls-rustls", ], optional = true } -s2n-quic-h3 = { path = "../s2n-quic/quic/s2n-quic-h3/", optional = true } -s2n-quic-rustls = { path = "../s2n-quic/quic/s2n-quic-rustls/", optional = true } +s2n-quic-h3 = { path = "../submodules/s2n-quic/quic/s2n-quic-h3/", optional = true } +s2n-quic-rustls = { path = "../submodules/s2n-quic/quic/s2n-quic-rustls/", optional = true } + +# cache +http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/" } +moka = { version = "0.11.3", features = ["future"] } # cookie handling for sticky cookie chrono = { version = "0.4.26", default-features = false, features = [ diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index b7b0bff..93b54bf 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -31,3 +31,5 @@ pub mod H3 { #[cfg(feature = "sticky-cookie")] /// For load-balancing with sticky cookie pub const STICKY_COOKIE_NAME: &str = "rpxy_srv_id"; + +pub const MAX_CACHE_ENTRY: u64 = 10_000; diff --git a/rpxy-lib/src/error.rs b/rpxy-lib/src/error.rs index da56dac..c672682 100644 --- a/rpxy-lib/src/error.rs +++ b/rpxy-lib/src/error.rs @@ -22,6 +22,9 @@ pub enum RpxyError { #[error("Http Message Handler Error: {0}")] Handler(&'static str), + #[error("Cache Error: {0}")] + Cache(&'static str), + #[error("Http Request Message Error: {0}")] Request(&'static str), diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 0bed623..9442507 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -9,11 +9,11 @@ use crate::{ utils::{BytesName, PathNameBytesExp}, }; use rustc_hash::FxHashMap as HashMap; -use std::net::SocketAddr; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; +use std::{net::SocketAddr, path::PathBuf}; use tokio::time::Duration; /// Global object containing proxy configurations and shared object like counters. @@ -52,6 +52,10 @@ pub struct ProxyConfig { // experimentals pub sni_consistency: bool, // Handler + + pub cache_enabled: bool, + pub cache_dir: Option, + // All need to make packet acceptor #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] pub http3: bool, @@ -87,6 +91,9 @@ impl Default for ProxyConfig { sni_consistency: true, + cache_enabled: false, + cache_dir: None, + #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] http3: false, #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs new file mode 100644 index 0000000..6b672d3 --- /dev/null +++ b/rpxy-lib/src/handler/cache.rs @@ -0,0 +1,196 @@ +use crate::{constants::MAX_CACHE_ENTRY, error::*, globals::Globals, log::*, CryptoSource}; +use base64::{engine::general_purpose, Engine as _}; +use bytes::{Buf, Bytes, BytesMut}; +use http_cache_semantics::CachePolicy; +use hyper::{ + http::{Request, Response}, + Body, +}; +use moka::future::Cache as MokaCache; +use sha2::{Digest, Sha256}; +use std::{fmt::Debug, path::PathBuf, time::SystemTime}; +use tokio::{ + fs::{self, File}, + io::{AsyncReadExt, AsyncWriteExt}, +}; + +// #[async_trait] +// pub trait CacheTarget { +// type TargetInput; +// type TargetOutput; +// type Error; +// /// Get target object from somewhere +// async fn get(&self) -> Self::TargetOutput; +// /// Write target object into somewhere +// async fn put(&self, taget: Self::TargetOutput) -> Result<(), Self::Error>; +// /// Remove target object from somewhere (when evicted self) +// async fn remove(&self) -> Result<(), Self::Error>; +// } + +fn derive_filename_from_uri(uri: &hyper::Uri) -> String { + let mut hasher = Sha256::new(); + hasher.update(uri.to_string()); + let digest = hasher.finalize(); + general_purpose::URL_SAFE_NO_PAD.encode(digest) +} + +fn derive_moka_key_from_uri(uri: &hyper::Uri) -> String { + uri.to_string() +} + +#[derive(Clone, Debug)] +pub struct CacheObject { + pub policy: CachePolicy, + pub target: Option, +} + +#[derive(Clone, Debug)] +pub struct RpxyCache { + cache_dir: PathBuf, + inner: MokaCache, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう + runtime_handle: tokio::runtime::Handle, +} + +impl RpxyCache { + /// Generate cache storage + pub async fn new(globals: &Globals) -> Option { + if !globals.proxy_config.cache_enabled { + return None; + } + let runtime_handle = globals.runtime_handle.clone(); + let runtime_handle_clone = globals.runtime_handle.clone(); + let eviction_listener = move |k, v: CacheObject, cause| { + debug!("Cache entry is being evicted : {k} {:?}", cause); + runtime_handle.block_on(async { + if let Some(filepath) = v.target { + debug!("Evict file object: {k}"); + if let Err(e) = fs::remove_file(filepath).await { + warn!("Eviction failed during file object removal: {:?}", e); + }; + } + }) + }; + + // Create cache file dir + // Clean up the file dir before init + // TODO: Persistent cache is really difficult. maybe SQLite is needed. + let path = globals.proxy_config.cache_dir.as_ref().unwrap(); + if let Err(e) = fs::remove_dir_all(path).await { + warn!("Failed to clean up the cache dir: {e}"); + }; + fs::create_dir_all(path).await.unwrap(); + + Some(Self { + cache_dir: path.clone(), + inner: MokaCache::builder() + .max_capacity(MAX_CACHE_ENTRY) + .eviction_listener_with_queued_delivery_mode(eviction_listener) + .build(), // TODO: make this configurable, and along with size + runtime_handle: runtime_handle_clone, + }) + } + + /// Get cached response + pub async fn get(&self, req: &Request) -> Option> { + let moka_key = req.uri().to_string(); + + // First check cache chance + let Some(cached_object) = self.inner.get(&moka_key) else { + return None; + }; + + let now = SystemTime::now(); + if let http_cache_semantics::BeforeRequest::Fresh(res_parts) = cached_object.policy.before_request(req, now) { + let Some(filepath) = cached_object.target else { + return None; + }; + + let Ok(mut file) = File::open(&filepath.clone()).await else { + warn!("Cache file doesn't exist. Remove pointer cache."); + let my_cache = self.inner.clone(); + self.runtime_handle.spawn(async move { + my_cache.invalidate(&moka_key).await; + }); + return None; + }; + let (body_sender, res_body) = Body::channel(); + self.runtime_handle.spawn(async move { + let mut sender = body_sender; + // let mut size = 0usize; + let mut buf = BytesMut::new(); + loop { + match file.read_buf(&mut buf).await { + Ok(0) => break, + Ok(_) => sender.send_data(buf.copy_to_bytes(buf.remaining())).await?, + Err(_) => break, + }; + } + Ok(()) as Result<()> + }); + + let res = Response::from_parts(res_parts, res_body); + debug!("Cache hit: {moka_key}"); + Some(res) + } else { + // Evict stale cache entry here + debug!("Evict stale cache entry and file object: {moka_key}"); + let my_cache = self.inner.clone(); + self.runtime_handle.spawn(async move { + // eviction listener will be activated during invalidation. + my_cache.invalidate(&moka_key).await; + }); + None + } + } + + pub fn is_cacheable(&self, req: Option<&Request>, res: Option<&Response>) -> Result> + where + R: Debug, + { + // deduce cache policy from req and res + let (Some(req), Some(res)) = (req, res) else { + return Err(RpxyError::Cache("Invalid null request and/or response")); + }; + + let new_policy = CachePolicy::new(req, res); + if new_policy.is_storable() { + debug!("Response is cacheable: {:?}\n{:?}", req, res.headers()); + Ok(Some(new_policy)) + } else { + Ok(None) + } + } + + pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: CachePolicy) -> Result<()> { + let my_cache = self.inner.clone(); + let uri = uri.clone(); + let cache_dir = self.cache_dir.clone(); + let mut bytes_clone = body_bytes.clone(); + + self.runtime_handle.spawn(async move { + let moka_key = derive_moka_key_from_uri(&uri); + let cache_filename = derive_filename_from_uri(&uri); + let cache_filepath = cache_dir.join(cache_filename); + + let _x = my_cache + .get_with(moka_key, async { + let mut file = File::create(&cache_filepath).await.unwrap(); + while bytes_clone.has_remaining() { + if let Err(e) = file.write_buf(&mut bytes_clone).await { + error!("Failed to write file cache: {e}"); + return CacheObject { policy, target: None }; + }; + } + CacheObject { + policy, + target: Some(cache_filepath), + } + }) + .await; + + debug!("Current cache entries: {}", my_cache.entry_count()); + }); + + Ok(()) + } +} diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index 4bfd15b..fab1342 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -1,5 +1,7 @@ -use crate::error::RpxyError; +use super::cache::RpxyCache; +use crate::{error::RpxyError, globals::Globals, log::*, CryptoSource}; use async_trait::async_trait; +use bytes::Buf; use derive_builder::Builder; use hyper::{ body::{Body, HttpBody}, @@ -9,6 +11,18 @@ use hyper::{ }; use hyper_rustls::HttpsConnector; +fn build_synth_req_for_cache(req: &Request) -> Request<()> { + let mut builder = Request::builder() + .method(req.method()) + .uri(req.uri()) + .version(req.version()); + // TODO: omit extensions. is this approach correct? + for (header_key, header_value) in req.headers() { + builder = builder.header(header_key, header_value); + } + builder.body(()).unwrap() +} + #[async_trait] /// Definition of the forwarder that simply forward requests from downstream client to upstream app servers. pub trait ForwardRequest { @@ -17,12 +31,12 @@ pub trait ForwardRequest { } #[derive(Builder, Clone)] -/// Forwarder struct +/// Forwarder struct responsible to cache handling pub struct Forwarder where C: Connect + Clone + Sync + Send + 'static, { - // TODO: maybe this forwarder definition is suitable for cache handling. + cache: Option, inner: Client, inner_h2: Client, // `h2c` or http/2-only client is defined separately } @@ -37,18 +51,63 @@ where { type Error = RpxyError; async fn request(&self, req: Request) -> Result, Self::Error> { + let mut synth_req = None; + if self.cache.is_some() { + debug!("Search cache first"); + if let Some(cached_response) = self.cache.as_ref().unwrap().get(&req).await { + // if found, return it as response. + debug!("Cache hit - Return from cache"); + return Ok(cached_response); + }; + + // Synthetic request copy used just for caching (cannot clone request object...) + synth_req = Some(build_synth_req_for_cache(&req)); + } + // TODO: This 'match' condition is always evaluated at every 'request' invocation. So, it is inefficient. // Needs to be reconsidered. Currently, this is a kind of work around. // This possibly relates to https://github.com/hyperium/hyper/issues/2417. - match req.version() { + let res = match req.version() { Version::HTTP_2 => self.inner_h2.request(req).await.map_err(RpxyError::Hyper), // handles `h2c` requests _ => self.inner.request(req).await.map_err(RpxyError::Hyper), + }; + + if self.cache.is_none() { + return res; } + + // check cacheability and store it if cacheable + let Ok(Some(cache_policy)) = self + .cache + .as_ref() + .unwrap() + .is_cacheable(synth_req.as_ref(), res.as_ref().ok()) else { + return res; + }; + let (parts, body) = res.unwrap().into_parts(); + // TODO: Inefficient? + let Ok(mut bytes) = hyper::body::aggregate(body).await else { + return Err(RpxyError::Cache("Failed to write byte buffer")); + }; + let aggregated = bytes.copy_to_bytes(bytes.remaining()); + + if let Err(cache_err) = self + .cache + .as_ref() + .unwrap() + .put(synth_req.unwrap().uri(), &aggregated, cache_policy) + .await + { + error!("{:?}", cache_err); + }; + + // res + Ok(Response::from_parts(parts, Body::from(aggregated))) } } impl Forwarder, Body> { - pub async fn new() -> Self { + pub async fn new(globals: &std::sync::Arc>) -> Self { // let connector = TrustDnsResolver::default().into_rustls_webpki_https_connector(); let connector = hyper_rustls::HttpsConnectorBuilder::new() .with_webpki_roots() @@ -64,6 +123,8 @@ impl Forwarder, Body> { let inner = Client::builder().build::<_, Body>(connector); let inner_h2 = Client::builder().http2_only(true).build::<_, Body>(connector_h2); - Self { inner, inner_h2 } + + let cache = RpxyCache::new(globals).await; + Self { inner, inner_h2, cache } } } diff --git a/rpxy-lib/src/handler/mod.rs b/rpxy-lib/src/handler/mod.rs index 854bd8f..beea073 100644 --- a/rpxy-lib/src/handler/mod.rs +++ b/rpxy-lib/src/handler/mod.rs @@ -1,3 +1,4 @@ +mod cache; mod forwarder; mod handler_main; mod utils_headers; @@ -6,6 +7,7 @@ mod utils_synth_response; #[cfg(feature = "sticky-cookie")] use crate::backend::LbContext; +pub use cache::CacheObject; pub use { forwarder::Forwarder, handler_main::{HttpMessageHandler, HttpMessageHandlerBuilder, HttpMessageHandlerBuilderError}, diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index b3af2a8..d3f7bca 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -22,6 +22,7 @@ use std::sync::Arc; pub use crate::{ certs::{CertsAndKeys, CryptoSource}, globals::{AppConfig, AppConfigList, ProxyConfig, ReverseProxyConfig, TlsConfig, UpstreamUri}, + handler::CacheObject, }; pub mod reexports { pub use hyper::Uri; @@ -60,6 +61,12 @@ where if !proxy_config.sni_consistency { info!("Ignore consistency between TLS SNI and Host header (or Request line). Note it violates RFC."); } + if proxy_config.cache_enabled { + info!( + "Cache is enabled: cache dir = {:?}", + proxy_config.cache_dir.as_ref().unwrap() + ); + } // build global let globals = Arc::new(Globals { @@ -72,7 +79,7 @@ where // build message handler including a request forwarder let msg_handler = Arc::new( HttpMessageHandlerBuilder::default() - .forwarder(Arc::new(Forwarder::new().await)) + .forwarder(Arc::new(Forwarder::new(&globals).await)) .globals(globals.clone()) .build()?, ); diff --git a/s2n-quic b/s2n-quic deleted file mode 160000 index 1ff2cd2..0000000 --- a/s2n-quic +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1ff2cd230fdf46596fe77830966857c438a8b31a diff --git a/h3 b/submodules/h3 similarity index 100% rename from h3 rename to submodules/h3 diff --git a/h3-quinn/Cargo.toml b/submodules/h3-quinn/Cargo.toml similarity index 100% rename from h3-quinn/Cargo.toml rename to submodules/h3-quinn/Cargo.toml diff --git a/h3-quinn/src/lib.rs b/submodules/h3-quinn/src/lib.rs similarity index 100% rename from h3-quinn/src/lib.rs rename to submodules/h3-quinn/src/lib.rs diff --git a/submodules/quinn b/submodules/quinn new file mode 160000 index 0000000..4f25f50 --- /dev/null +++ b/submodules/quinn @@ -0,0 +1 @@ +Subproject commit 4f25f501ef4d009af9d3bef44d322c09c327b2df diff --git a/submodules/rusty-http-cache-semantics b/submodules/rusty-http-cache-semantics new file mode 160000 index 0000000..3cd0917 --- /dev/null +++ b/submodules/rusty-http-cache-semantics @@ -0,0 +1 @@ +Subproject commit 3cd09170305753309d86e88b9427827cca0de0dd diff --git a/submodules/s2n-quic b/submodules/s2n-quic new file mode 160000 index 0000000..c7e3f51 --- /dev/null +++ b/submodules/s2n-quic @@ -0,0 +1 @@ +Subproject commit c7e3f517f3267f2e19b605cb53e6bf265e70e5af From 460562183ecac14550efdccfb504734553b26b35 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 13:16:10 +0900 Subject: [PATCH 13/30] fix: fix caching routine about eviction --- rpxy-lib/Cargo.toml | 2 +- rpxy-lib/src/handler/cache.rs | 22 ++++++++++++---------- rpxy-lib/src/handler/forwarder.rs | 1 - 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index a859131..9cd6265 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -75,7 +75,7 @@ s2n-quic-rustls = { path = "../submodules/s2n-quic/quic/s2n-quic-rustls/", optio # cache http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/" } -moka = { version = "0.11.3", features = ["future"] } +moka = { version = "0.11.3", features = ["future", "sync"] } # cookie handling for sticky cookie chrono = { version = "0.4.26", default-features = false, features = [ diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index 6b672d3..8bc14f8 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -106,11 +106,12 @@ impl RpxyCache { }; let Ok(mut file) = File::open(&filepath.clone()).await else { - warn!("Cache file doesn't exist. Remove pointer cache."); - let my_cache = self.inner.clone(); - self.runtime_handle.spawn(async move { - my_cache.invalidate(&moka_key).await; - }); + warn!("Cache file object doesn't exist. Remove cache entry."); + self.inner.invalidate(&moka_key).await; + // let my_cache = self.inner.clone(); + // self.runtime_handle.spawn(async move { + // my_cache.invalidate(&moka_key).await; + // }); return None; }; let (body_sender, res_body) = Body::channel(); @@ -134,11 +135,12 @@ impl RpxyCache { } else { // Evict stale cache entry here debug!("Evict stale cache entry and file object: {moka_key}"); - let my_cache = self.inner.clone(); - self.runtime_handle.spawn(async move { - // eviction listener will be activated during invalidation. - my_cache.invalidate(&moka_key).await; - }); + self.inner.invalidate(&moka_key).await; + // let my_cache = self.inner.clone(); + // self.runtime_handle.spawn(async move { + // eviction listener will be activated during invalidation. + // my_cache.invalidate(&moka_key).await; + // }); None } } diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index fab1342..713ba06 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -53,7 +53,6 @@ where async fn request(&self, req: Request) -> Result, Self::Error> { let mut synth_req = None; if self.cache.is_some() { - debug!("Search cache first"); if let Some(cached_response) = self.cache.as_ref().unwrap().get(&req).await { // if found, return it as response. debug!("Cache hit - Return from cache"); From cc6b78feb3ddc5bd0a787733a1551b27abd5283d Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 19:12:39 +0900 Subject: [PATCH 14/30] feat: update implementation of cache. still unstable --- Cargo.toml | 2 +- rpxy-lib/Cargo.toml | 1 + rpxy-lib/src/constants.rs | 2 + rpxy-lib/src/handler/cache.rs | 191 ++++++++++++++++++++---------- rpxy-lib/src/handler/forwarder.rs | 2 +- rpxy-lib/src/handler/mod.rs | 1 - rpxy-lib/src/lib.rs | 1 - 7 files changed, 134 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a32c32..29e2277 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = ["rpxy-bin", "rpxy-lib"] -exclude = ["submodules", "h3-quinn"] +exclude = ["submodules"] [profile.release] codegen-units = 1 diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 9cd6265..3b90659 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -76,6 +76,7 @@ s2n-quic-rustls = { path = "../submodules/s2n-quic/quic/s2n-quic-rustls/", optio # cache http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/" } moka = { version = "0.11.3", features = ["future", "sync"] } +fs4 = { version = "0.6.6", features = ["tokio", "tokio-async"] } # cookie handling for sticky cookie chrono = { version = "0.4.26", default-features = false, features = [ diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index 93b54bf..4abf403 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -33,3 +33,5 @@ pub mod H3 { pub const STICKY_COOKIE_NAME: &str = "rpxy_srv_id"; pub const MAX_CACHE_ENTRY: u64 = 10_000; +// TODO: max cache size per entry +// TODO: max cache size in total diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index 8bc14f8..5e09efa 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -1,6 +1,7 @@ use crate::{constants::MAX_CACHE_ENTRY, error::*, globals::Globals, log::*, CryptoSource}; use base64::{engine::general_purpose, Engine as _}; use bytes::{Buf, Bytes, BytesMut}; +use fs4::tokio::AsyncFileExt; use http_cache_semantics::CachePolicy; use hyper::{ http::{Request, Response}, @@ -8,10 +9,16 @@ use hyper::{ }; use moka::future::Cache as MokaCache; use sha2::{Digest, Sha256}; -use std::{fmt::Debug, path::PathBuf, time::SystemTime}; +use std::{ + fmt::Debug, + path::{Path, PathBuf}, + sync::Arc, + time::SystemTime, +}; use tokio::{ fs::{self, File}, io::{AsyncReadExt, AsyncWriteExt}, + sync::RwLock, }; // #[async_trait] @@ -39,15 +46,94 @@ fn derive_moka_key_from_uri(uri: &hyper::Uri) -> String { } #[derive(Clone, Debug)] -pub struct CacheObject { +struct CacheObject { pub policy: CachePolicy, pub target: Option, } +#[derive(Debug)] +struct CacheFileManager { + cache_dir: PathBuf, + cnt: usize, + runtime_handle: tokio::runtime::Handle, +} + +impl CacheFileManager { + async fn new(path: &PathBuf, runtime_handle: &tokio::runtime::Handle) -> Self { + // Create cache file dir + // Clean up the file dir before init + // TODO: Persistent cache is really difficult. maybe SQLite is needed. + if let Err(e) = fs::remove_dir_all(path).await { + warn!("Failed to clean up the cache dir: {e}"); + }; + fs::create_dir_all(path).await.unwrap(); + Self { + cache_dir: path.clone(), + cnt: 0, + runtime_handle: runtime_handle.clone(), + } + } + + async fn write(&mut self, cache_filename: &str, body_bytes: &Bytes, policy: &CachePolicy) -> Result { + let cache_filepath = self.cache_dir.join(cache_filename); + let Ok(mut file) = File::create(&cache_filepath).await else { + return Err(RpxyError::Cache("Failed to create file")); + }; + // TODO: ここでちゃんと書けないパターンっぽい?あるいは書いた後消されるパターンが起きている模様。 + // evictしたときファイルは消えてentryが残ってるっぽい + let mut bytes_clone = body_bytes.clone(); + while bytes_clone.has_remaining() { + warn!("remaining {}", bytes_clone.remaining()); + if let Err(e) = file.write_buf(&mut bytes_clone).await { + error!("Failed to write file cache: {e}"); + return Err(RpxyError::Cache("Failed to write file cache: {e}")); + }; + } + self.cnt += 1; + Ok(CacheObject { + policy: policy.clone(), + target: Some(cache_filepath), + }) + } + + async fn read(&self, path: impl AsRef) -> Result { + let Ok(mut file) = File::open(&path).await else { + warn!("Cache file object cannot be opened"); + return Err(RpxyError::Cache("Cache file object cannot be opened")); + }; + let (body_sender, res_body) = Body::channel(); + self.runtime_handle.spawn(async move { + let mut sender = body_sender; + let mut buf = BytesMut::new(); + loop { + match file.read_buf(&mut buf).await { + Ok(0) => break, + Ok(_) => sender.send_data(buf.copy_to_bytes(buf.remaining())).await?, + Err(_) => break, + }; + } + Ok(()) as Result<()> + }); + + Ok(res_body) + } + + async fn remove(&mut self, path: impl AsRef) -> Result<()> { + fs::remove_file(path.as_ref()).await?; + self.cnt -= 1; + debug!("Removed a cache file at {:?} (file count: {})", path.as_ref(), self.cnt); + + Ok(()) + } +} + #[derive(Clone, Debug)] pub struct RpxyCache { - cache_dir: PathBuf, + /// Managing cache file objects through RwLock's lock mechanism for file lock + cache_file_manager: Arc>, + /// Moka's cache storing http message caching policy inner: MokaCache, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう + /// Async runtime runtime_handle: tokio::runtime::Handle, } @@ -57,41 +143,39 @@ impl RpxyCache { if !globals.proxy_config.cache_enabled { return None; } + + let path = globals.proxy_config.cache_dir.as_ref().unwrap(); + let cache_file_manager = Arc::new(RwLock::new(CacheFileManager::new(path, &globals.runtime_handle).await)); + let mgr_clone = cache_file_manager.clone(); + let runtime_handle = globals.runtime_handle.clone(); - let runtime_handle_clone = globals.runtime_handle.clone(); let eviction_listener = move |k, v: CacheObject, cause| { debug!("Cache entry is being evicted : {k} {:?}", cause); runtime_handle.block_on(async { if let Some(filepath) = v.target { debug!("Evict file object: {k}"); - if let Err(e) = fs::remove_file(filepath).await { + // Acquire the write lock + let mut mgr = mgr_clone.write().await; + if let Err(e) = mgr.remove(filepath).await { warn!("Eviction failed during file object removal: {:?}", e); }; } }) }; - // Create cache file dir - // Clean up the file dir before init - // TODO: Persistent cache is really difficult. maybe SQLite is needed. - let path = globals.proxy_config.cache_dir.as_ref().unwrap(); - if let Err(e) = fs::remove_dir_all(path).await { - warn!("Failed to clean up the cache dir: {e}"); - }; - fs::create_dir_all(path).await.unwrap(); - Some(Self { - cache_dir: path.clone(), + cache_file_manager, inner: MokaCache::builder() .max_capacity(MAX_CACHE_ENTRY) .eviction_listener_with_queued_delivery_mode(eviction_listener) .build(), // TODO: make this configurable, and along with size - runtime_handle: runtime_handle_clone, + runtime_handle: globals.runtime_handle.clone(), }) } /// Get cached response pub async fn get(&self, req: &Request) -> Option> { + debug!("Current cache entries: {:?}", self.inner); let moka_key = req.uri().to_string(); // First check cache chance @@ -105,36 +189,24 @@ impl RpxyCache { return None; }; - let Ok(mut file) = File::open(&filepath.clone()).await else { - warn!("Cache file object doesn't exist. Remove cache entry."); - self.inner.invalidate(&moka_key).await; - // let my_cache = self.inner.clone(); - // self.runtime_handle.spawn(async move { - // my_cache.invalidate(&moka_key).await; - // }); - return None; - }; - let (body_sender, res_body) = Body::channel(); - self.runtime_handle.spawn(async move { - let mut sender = body_sender; - // let mut size = 0usize; - let mut buf = BytesMut::new(); - loop { - match file.read_buf(&mut buf).await { - Ok(0) => break, - Ok(_) => sender.send_data(buf.copy_to_bytes(buf.remaining())).await?, - Err(_) => break, - }; + let mgr = self.cache_file_manager.read().await; + let res_body = match mgr.read(&filepath).await { + Ok(res_body) => res_body, + Err(e) => { + warn!("Failed to read from cache: {e}"); + self.inner.invalidate(&moka_key).await; + return None; } - Ok(()) as Result<()> - }); - - let res = Response::from_parts(res_parts, res_body); + }; debug!("Cache hit: {moka_key}"); - Some(res) + + Some(Response::from_parts(res_parts, res_body)) } else { - // Evict stale cache entry here - debug!("Evict stale cache entry and file object: {moka_key}"); + // Evict stale cache entry. + // This might be okay to keep as is since it would be updated later. + // However, there is no guarantee that newly got objects will be still cacheable. + // So, we have to evict stale cache entries and cache file objects if found. + debug!("Stale cache entry and file object: {moka_key}"); self.inner.invalidate(&moka_key).await; // let my_cache = self.inner.clone(); // self.runtime_handle.spawn(async move { @@ -163,34 +235,29 @@ impl RpxyCache { } } - pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: CachePolicy) -> Result<()> { + pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: &CachePolicy) -> Result<()> { let my_cache = self.inner.clone(); let uri = uri.clone(); - let cache_dir = self.cache_dir.clone(); - let mut bytes_clone = body_bytes.clone(); + let bytes_clone = body_bytes.clone(); + let policy_clone = policy.clone(); + let mgr_clone = self.cache_file_manager.clone(); self.runtime_handle.spawn(async move { let moka_key = derive_moka_key_from_uri(&uri); let cache_filename = derive_filename_from_uri(&uri); - let cache_filepath = cache_dir.join(cache_filename); - let _x = my_cache - .get_with(moka_key, async { - let mut file = File::create(&cache_filepath).await.unwrap(); - while bytes_clone.has_remaining() { - if let Err(e) = file.write_buf(&mut bytes_clone).await { - error!("Failed to write file cache: {e}"); - return CacheObject { policy, target: None }; - }; - } - CacheObject { - policy, - target: Some(cache_filepath), - } + warn!("{:?} bytes to be written", bytes_clone.len()); + if let Err(e) = my_cache + .try_get_with(moka_key, async { + let mut mgr = mgr_clone.write().await; + mgr.write(&cache_filename, &bytes_clone, &policy_clone).await }) - .await; + .await + { + error!("Failed to put the body into the file object or cache entry: {e}"); + }; - debug!("Current cache entries: {}", my_cache.entry_count()); + debug!("Current cache entries: {:?}", my_cache); }); Ok(()) diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index 713ba06..28fc263 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -94,7 +94,7 @@ where .cache .as_ref() .unwrap() - .put(synth_req.unwrap().uri(), &aggregated, cache_policy) + .put(synth_req.unwrap().uri(), &aggregated, &cache_policy) .await { error!("{:?}", cache_err); diff --git a/rpxy-lib/src/handler/mod.rs b/rpxy-lib/src/handler/mod.rs index beea073..f2d3e43 100644 --- a/rpxy-lib/src/handler/mod.rs +++ b/rpxy-lib/src/handler/mod.rs @@ -7,7 +7,6 @@ mod utils_synth_response; #[cfg(feature = "sticky-cookie")] use crate::backend::LbContext; -pub use cache::CacheObject; pub use { forwarder::Forwarder, handler_main::{HttpMessageHandler, HttpMessageHandlerBuilder, HttpMessageHandlerBuilderError}, diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index d3f7bca..0596625 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -22,7 +22,6 @@ use std::sync::Arc; pub use crate::{ certs::{CertsAndKeys, CryptoSource}, globals::{AppConfig, AppConfigList, ProxyConfig, ReverseProxyConfig, TlsConfig, UpstreamUri}, - handler::CacheObject, }; pub mod reexports { pub use hyper::Uri; From 2477c6bf1b48f099646be0c18b62b94891a97aa8 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 19:21:22 +0900 Subject: [PATCH 15/30] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f14668d..a4c71ed 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Other than them, all you need is to mount your `config.toml` as `/etc/rpxy.toml` ### HTTP/3 -`rpxy` can serves HTTP/3 requests thanks to `quinn` and `hyperium/h3`. To enable this experimental feature, add an entry `experimental.h3` in your `config.toml` like follows. Any values in the entry like `alt_svc_max_age` are optional. +`rpxy` can serves HTTP/3 requests thanks to `quinn`, `s2n-quic` and `hyperium/h3`. To enable this experimental feature, add an entry `experimental.h3` in your `config.toml` like follows. Any values in the entry like `alt_svc_max_age` are optional. ```toml [experimental.h3] From 07d3accb912a0a0aca72a41e47c09b64a8bcce6b Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 23:04:04 +0900 Subject: [PATCH 16/30] feat: totally updated cache structure using lru crate instead of moka (i.e., using simpler crate) --- CHANGELOG.md | 1 + TODO.md | 5 +- config-example.toml | 4 +- rpxy-bin/src/config/toml.rs | 12 +- rpxy-bin/src/constants.rs | 8 + rpxy-lib/Cargo.toml | 3 +- rpxy-lib/src/constants.rs | 4 - rpxy-lib/src/globals.rs | 4 + rpxy-lib/src/handler/cache.rs | 234 +++++++++++++++--------------- rpxy-lib/src/handler/forwarder.rs | 14 +- 10 files changed, 157 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5cd18..492db57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Improvement - Feat: Enabled `h2c` (HTTP/2 cleartext) requests to upstream app servers (in the previous versions, only HTTP/1.1 is allowed for cleartext requests) +- Feat: Initial implementation of caching feature using file + on memory cache. (Caveats: No persistance of the cache. Once config is updated, the cache is totally eliminated.) - Refactor: logs of minor improvements ### Bugfix diff --git a/TODO.md b/TODO.md index 8ec6d5d..78a364e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,10 @@ # TODO List - [Done in 0.6.0] But we need more sophistication on `Forwarder` struct. ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ -- [Try in v0.6.0] **Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))** +- [Initial implementation in v0.6.0] ~~**Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))**~~ Using `lru` crate might be inefficient in terms of the speed. Also, this cache feature should be a separated `feature` (But I think okay to be included in `default`). + - Consider more sophisticated architecture for cache + - Persistent cache (if possible). + - etc etc - Improvement of path matcher - More flexible option for rewriting path - Refactoring diff --git a/config-example.toml b/config-example.toml index 41d70e7..694ff7c 100644 --- a/config-example.toml +++ b/config-example.toml @@ -110,4 +110,6 @@ max_idle_timeout = 10 # secs. 0 represents an infinite timeout. # If this specified, file cache feature is enabled [experimental.cache] -cache_dir = './cache' # optional. default is "./cache" relative to the current working directory +cache_dir = './cache' # optional. default is "./cache" relative to the current working directory +max_cache_entry = 1000 # optional. default is 1k +max_cache_each_size = 65535 # optional. default is 64k diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index 60f13b3..984553a 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -36,6 +36,8 @@ pub struct Http3Option { #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct CacheOption { pub cache_dir: Option, + pub max_cache_entry: Option, + pub max_cache_each_size: Option, } #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] @@ -173,7 +175,15 @@ impl TryInto for &ConfigToml { proxy_config.cache_dir = match &cache_option.cache_dir { Some(cache_dir) => Some(PathBuf::from(cache_dir)), None => Some(PathBuf::from(CACHE_DIR)), - } + }; + proxy_config.cache_max_entry = match &cache_option.max_cache_entry { + Some(num) => Some(*num), + None => Some(MAX_CACHE_ENTRY), + }; + proxy_config.cache_max_each_size = match &cache_option.max_cache_each_size { + Some(num) => Some(*num), + None => Some(MAX_CACHE_EACH_SIZE), + }; } } diff --git a/rpxy-bin/src/constants.rs b/rpxy-bin/src/constants.rs index a7e811a..54fa7dd 100644 --- a/rpxy-bin/src/constants.rs +++ b/rpxy-bin/src/constants.rs @@ -1,4 +1,12 @@ pub const LISTEN_ADDRESSES_V4: &[&str] = &["0.0.0.0"]; pub const LISTEN_ADDRESSES_V6: &[&str] = &["[::]"]; pub const CONFIG_WATCH_DELAY_SECS: u32 = 20; + +// Cache directory pub const CACHE_DIR: &str = "./cache"; +// # of entries in cache +pub const MAX_CACHE_ENTRY: usize = 1_000; +// max size for each file in bytes +pub const MAX_CACHE_EACH_SIZE: usize = 65_535; + +// TODO: max cache size in total diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 3b90659..7d48b44 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -75,8 +75,7 @@ s2n-quic-rustls = { path = "../submodules/s2n-quic/quic/s2n-quic-rustls/", optio # cache http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/" } -moka = { version = "0.11.3", features = ["future", "sync"] } -fs4 = { version = "0.6.6", features = ["tokio", "tokio-async"] } +lru = { version = "0.11.0" } # cookie handling for sticky cookie chrono = { version = "0.4.26", default-features = false, features = [ diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index 4abf403..b7b0bff 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -31,7 +31,3 @@ pub mod H3 { #[cfg(feature = "sticky-cookie")] /// For load-balancing with sticky cookie pub const STICKY_COOKIE_NAME: &str = "rpxy_srv_id"; - -pub const MAX_CACHE_ENTRY: u64 = 10_000; -// TODO: max cache size per entry -// TODO: max cache size in total diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 9442507..8a782b6 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -55,6 +55,8 @@ pub struct ProxyConfig { pub cache_enabled: bool, pub cache_dir: Option, + pub cache_max_entry: Option, + pub cache_max_each_size: Option, // All need to make packet acceptor #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] @@ -93,6 +95,8 @@ impl Default for ProxyConfig { cache_enabled: false, cache_dir: None, + cache_max_entry: None, + cache_max_each_size: None, #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] http3: false, diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index 5e09efa..c676c45 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -1,18 +1,17 @@ -use crate::{constants::MAX_CACHE_ENTRY, error::*, globals::Globals, log::*, CryptoSource}; +use crate::{error::*, globals::Globals, log::*, CryptoSource}; use base64::{engine::general_purpose, Engine as _}; use bytes::{Buf, Bytes, BytesMut}; -use fs4::tokio::AsyncFileExt; use http_cache_semantics::CachePolicy; use hyper::{ http::{Request, Response}, Body, }; -use moka::future::Cache as MokaCache; +use lru::LruCache; use sha2::{Digest, Sha256}; use std::{ fmt::Debug, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Mutex}, time::SystemTime, }; use tokio::{ @@ -21,34 +20,10 @@ use tokio::{ sync::RwLock, }; -// #[async_trait] -// pub trait CacheTarget { -// type TargetInput; -// type TargetOutput; -// type Error; -// /// Get target object from somewhere -// async fn get(&self) -> Self::TargetOutput; -// /// Write target object into somewhere -// async fn put(&self, taget: Self::TargetOutput) -> Result<(), Self::Error>; -// /// Remove target object from somewhere (when evicted self) -// async fn remove(&self) -> Result<(), Self::Error>; -// } - -fn derive_filename_from_uri(uri: &hyper::Uri) -> String { - let mut hasher = Sha256::new(); - hasher.update(uri.to_string()); - let digest = hasher.finalize(); - general_purpose::URL_SAFE_NO_PAD.encode(digest) -} - -fn derive_moka_key_from_uri(uri: &hyper::Uri) -> String { - uri.to_string() -} - #[derive(Clone, Debug)] struct CacheObject { pub policy: CachePolicy, - pub target: Option, + pub target: PathBuf, } #[derive(Debug)] @@ -74,16 +49,13 @@ impl CacheFileManager { } } - async fn write(&mut self, cache_filename: &str, body_bytes: &Bytes, policy: &CachePolicy) -> Result { + async fn create(&mut self, cache_filename: &str, body_bytes: &Bytes, policy: &CachePolicy) -> Result { let cache_filepath = self.cache_dir.join(cache_filename); let Ok(mut file) = File::create(&cache_filepath).await else { return Err(RpxyError::Cache("Failed to create file")); }; - // TODO: ここでちゃんと書けないパターンっぽい?あるいは書いた後消されるパターンが起きている模様。 - // evictしたときファイルは消えてentryが残ってるっぽい let mut bytes_clone = body_bytes.clone(); while bytes_clone.has_remaining() { - warn!("remaining {}", bytes_clone.remaining()); if let Err(e) = file.write_buf(&mut bytes_clone).await { error!("Failed to write file cache: {e}"); return Err(RpxyError::Cache("Failed to write file cache: {e}")); @@ -92,7 +64,7 @@ impl CacheFileManager { self.cnt += 1; Ok(CacheObject { policy: policy.clone(), - target: Some(cache_filepath), + target: cache_filepath, }) } @@ -131,10 +103,12 @@ impl CacheFileManager { pub struct RpxyCache { /// Managing cache file objects through RwLock's lock mechanism for file lock cache_file_manager: Arc>, - /// Moka's cache storing http message caching policy - inner: MokaCache, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう + /// Lru cache storing http message caching policy + inner: Arc>>, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう /// Async runtime runtime_handle: tokio::runtime::Handle, + /// Maximum size of each cache file object + max_each_size: usize, } impl RpxyCache { @@ -146,120 +120,152 @@ impl RpxyCache { let path = globals.proxy_config.cache_dir.as_ref().unwrap(); let cache_file_manager = Arc::new(RwLock::new(CacheFileManager::new(path, &globals.runtime_handle).await)); - let mgr_clone = cache_file_manager.clone(); - - let runtime_handle = globals.runtime_handle.clone(); - let eviction_listener = move |k, v: CacheObject, cause| { - debug!("Cache entry is being evicted : {k} {:?}", cause); - runtime_handle.block_on(async { - if let Some(filepath) = v.target { - debug!("Evict file object: {k}"); - // Acquire the write lock - let mut mgr = mgr_clone.write().await; - if let Err(e) = mgr.remove(filepath).await { - warn!("Eviction failed during file object removal: {:?}", e); - }; - } - }) - }; + let inner = Arc::new(Mutex::new(LruCache::new( + std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry.unwrap()).unwrap(), + ))); Some(Self { cache_file_manager, - inner: MokaCache::builder() - .max_capacity(MAX_CACHE_ENTRY) - .eviction_listener_with_queued_delivery_mode(eviction_listener) - .build(), // TODO: make this configurable, and along with size + inner, runtime_handle: globals.runtime_handle.clone(), + max_each_size: globals.proxy_config.cache_max_each_size.unwrap(), }) } + fn evict_cache_entry(&self, cache_key: &str) -> Option<(String, CacheObject)> { + let Ok(mut lock) = self.inner.lock() else { + error!("Mutex can't be locked to evict a cache entry"); + return None; + }; + lock.pop_entry(cache_key) + } + + async fn evict_cache_file(&self, filepath: impl AsRef) { + // Acquire the write lock + let mut mgr = self.cache_file_manager.write().await; + if let Err(e) = mgr.remove(filepath).await { + warn!("Eviction failed during file object removal: {:?}", e); + }; + } + /// Get cached response pub async fn get(&self, req: &Request) -> Option> { debug!("Current cache entries: {:?}", self.inner); - let moka_key = req.uri().to_string(); + let cache_key = req.uri().to_string(); // First check cache chance - let Some(cached_object) = self.inner.get(&moka_key) else { - return None; - }; - - let now = SystemTime::now(); - if let http_cache_semantics::BeforeRequest::Fresh(res_parts) = cached_object.policy.before_request(req, now) { - let Some(filepath) = cached_object.target else { + let cached_object = { + let Ok(mut lock) = self.inner.lock() else { + error!("Mutex can't be locked for checking cache entry"); return None; }; - - let mgr = self.cache_file_manager.read().await; - let res_body = match mgr.read(&filepath).await { - Ok(res_body) => res_body, - Err(e) => { - warn!("Failed to read from cache: {e}"); - self.inner.invalidate(&moka_key).await; - return None; - } + let Some(cached_object) = lock.get(&cache_key) else { + return None; }; - debug!("Cache hit: {moka_key}"); + cached_object.clone() + }; - Some(Response::from_parts(res_parts, res_body)) - } else { + // Secondly check the cache freshness as an HTTP message + let now = SystemTime::now(); + let http_cache_semantics::BeforeRequest::Fresh(res_parts) = cached_object.policy.before_request(req, now) else { // Evict stale cache entry. // This might be okay to keep as is since it would be updated later. // However, there is no guarantee that newly got objects will be still cacheable. // So, we have to evict stale cache entries and cache file objects if found. - debug!("Stale cache entry and file object: {moka_key}"); - self.inner.invalidate(&moka_key).await; - // let my_cache = self.inner.clone(); - // self.runtime_handle.spawn(async move { - // eviction listener will be activated during invalidation. - // my_cache.invalidate(&moka_key).await; - // }); - None - } - } - - pub fn is_cacheable(&self, req: Option<&Request>, res: Option<&Response>) -> Result> - where - R: Debug, - { - // deduce cache policy from req and res - let (Some(req), Some(res)) = (req, res) else { - return Err(RpxyError::Cache("Invalid null request and/or response")); + debug!("Stale cache entry and file object: {cache_key}"); + let _evicted_entry = self.evict_cache_entry(&cache_key); + self.evict_cache_file(&cached_object.target).await; + return None; }; - let new_policy = CachePolicy::new(req, res); - if new_policy.is_storable() { - debug!("Response is cacheable: {:?}\n{:?}", req, res.headers()); - Ok(Some(new_policy)) - } else { - Ok(None) - } + // Finally retrieve the file object + let mgr = self.cache_file_manager.read().await; + let res_body = match mgr.read(&cached_object.target).await { + Ok(res_body) => res_body, + Err(e) => { + warn!("Failed to read from file cache: {e}"); + let _evicted_entry = self.evict_cache_entry(&cache_key); + self.evict_cache_file(&cached_object.target).await; + return None; + } + }; + + debug!("Cache hit: {cache_key}"); + Some(Response::from_parts(res_parts, res_body)) } pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: &CachePolicy) -> Result<()> { let my_cache = self.inner.clone(); + let mgr = self.cache_file_manager.clone(); let uri = uri.clone(); let bytes_clone = body_bytes.clone(); let policy_clone = policy.clone(); - let mgr_clone = self.cache_file_manager.clone(); + let max_each_size = self.max_each_size; self.runtime_handle.spawn(async move { - let moka_key = derive_moka_key_from_uri(&uri); + if bytes_clone.len() > max_each_size { + warn!("Too large to cache"); + return Err(RpxyError::Cache("Too large to cache")); + } + let cache_key = derive_cache_key_from_uri(&uri); let cache_filename = derive_filename_from_uri(&uri); - warn!("{:?} bytes to be written", bytes_clone.len()); - if let Err(e) = my_cache - .try_get_with(moka_key, async { - let mut mgr = mgr_clone.write().await; - mgr.write(&cache_filename, &bytes_clone, &policy_clone).await - }) - .await - { - error!("Failed to put the body into the file object or cache entry: {e}"); - }; + debug!("Cache file of {:?} bytes to be written", bytes_clone.len()); - debug!("Current cache entries: {:?}", my_cache); + let mut mgr = mgr.write().await; + let Ok(cache_object) = mgr.create(&cache_filename, &bytes_clone, &policy_clone).await else { + error!("Failed to put the body into the file object or cache entry"); + return Err(RpxyError::Cache("Failed to put the body into the file object or cache entry")); + }; + let push_opt = { + let Ok(mut lock) = my_cache.lock() else { + error!("Failed to acquire mutex lock for writing cache entry"); + return Err(RpxyError::Cache("Failed to acquire mutex lock for writing cache entry")); + }; + lock.push(cache_key.clone(), cache_object) + }; + if let Some((k, v)) = push_opt { + if k != cache_key { + info!("Over the cache capacity. Evict least recent used entry"); + if let Err(e) = mgr.remove(&v.target).await { + warn!("Eviction failed during file object removal over the capacity: {:?}", e); + }; + } + } + + debug!("Cached a new file: {} - {}", cache_key, cache_filename); + Ok(()) }); Ok(()) } } + +fn derive_filename_from_uri(uri: &hyper::Uri) -> String { + let mut hasher = Sha256::new(); + hasher.update(uri.to_string()); + let digest = hasher.finalize(); + general_purpose::URL_SAFE_NO_PAD.encode(digest) +} + +fn derive_cache_key_from_uri(uri: &hyper::Uri) -> String { + uri.to_string() +} + +pub fn get_policy_if_cacheable(req: Option<&Request>, res: Option<&Response>) -> Result> +where + R: Debug, +{ + // deduce cache policy from req and res + let (Some(req), Some(res)) = (req, res) else { + return Err(RpxyError::Cache("Invalid null request and/or response")); + }; + + let new_policy = CachePolicy::new(req, res); + if new_policy.is_storable() { + debug!("Response is cacheable: {:?}\n{:?}", req, res.headers()); + Ok(Some(new_policy)) + } else { + Ok(None) + } +} diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index 28fc263..ed480bf 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -1,4 +1,4 @@ -use super::cache::RpxyCache; +use super::cache::{get_policy_if_cacheable, RpxyCache}; use crate::{error::RpxyError, globals::Globals, log::*, CryptoSource}; use async_trait::async_trait; use bytes::Buf; @@ -55,7 +55,7 @@ where if self.cache.is_some() { if let Some(cached_response) = self.cache.as_ref().unwrap().get(&req).await { // if found, return it as response. - debug!("Cache hit - Return from cache"); + info!("Cache hit - Return from cache"); return Ok(cached_response); }; @@ -76,13 +76,9 @@ where } // check cacheability and store it if cacheable - let Ok(Some(cache_policy)) = self - .cache - .as_ref() - .unwrap() - .is_cacheable(synth_req.as_ref(), res.as_ref().ok()) else { - return res; - }; + let Ok(Some(cache_policy)) = get_policy_if_cacheable(synth_req.as_ref(), res.as_ref().ok()) else { + return res; + }; let (parts, body) = res.unwrap().into_parts(); // TODO: Inefficient? let Ok(mut bytes) = hyper::body::aggregate(body).await else { From a0d9c54b8f03411ef6563559b19c88568791dbcf Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 16 Aug 2023 23:16:27 +0900 Subject: [PATCH 17/30] fix: fix feature option in rpxy-bin --- rpxy-bin/Cargo.toml | 3 +-- rpxy-bin/src/config/toml.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 96771c1..70e5fac 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -12,10 +12,9 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["http3-quinn", "cache"] +default = ["http3-quinn"] http3-quinn = ["rpxy-lib/http3-quinn"] http3-s2n = ["rpxy-lib/http3-s2n"] -cache = [] [dependencies] rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index 984553a..f655347 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -32,7 +32,6 @@ pub struct Http3Option { pub max_idle_timeout: Option, } -#[cfg(feature = "cache")] #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct CacheOption { pub cache_dir: Option, From 8ecc83fe78242db59fd5f21297c76b89555a9a6e Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 16:29:45 +0900 Subject: [PATCH 18/30] deps: change alloc to mimalloc --- rpxy-bin/Cargo.toml | 11 ++++------- rpxy-bin/src/main.rs | 6 +----- rpxy-lib/Cargo.toml | 6 +++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 70e5fac..69d21c8 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -21,11 +21,11 @@ rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ "sticky-cookie", ] } -anyhow = "1.0.73" +anyhow = "1.0.75" rustc-hash = "1.1.0" serde = { version = "1.0.183", default-features = false, features = ["derive"] } derive_builder = "0.12.0" -tokio = { version = "1.31.0", default-features = false, features = [ +tokio = { version = "1.32.0", default-features = false, features = [ "net", "rt-multi-thread", "time", @@ -34,9 +34,10 @@ tokio = { version = "1.31.0", default-features = false, features = [ ] } async-trait = "0.1.73" rustls-pemfile = "1.0.3" +mimalloc = { version = "*", default-features = false } # config -clap = { version = "4.3.21", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.3.22", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.7.6", default-features = false, features = ["parse"] } hot_reload = "0.1.4" @@ -45,8 +46,4 @@ tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -[target.'cfg(not(target_env = "msvc"))'.dependencies] -tikv-jemallocator = "0.5.4" - - [dev-dependencies] diff --git a/rpxy-bin/src/main.rs b/rpxy-bin/src/main.rs index 474eff1..f04a6f1 100644 --- a/rpxy-bin/src/main.rs +++ b/rpxy-bin/src/main.rs @@ -1,9 +1,5 @@ -#[cfg(not(target_env = "msvc"))] -use tikv_jemallocator::Jemalloc; - -#[cfg(not(target_env = "msvc"))] #[global_allocator] -static GLOBAL: Jemalloc = Jemalloc; +static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; mod cert_file_reader; mod config; diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 7d48b44..59b76f6 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -23,7 +23,7 @@ rustc-hash = "1.1.0" bytes = "1.4.0" derive_builder = "0.12.0" futures = { version = "0.3.28", features = ["alloc", "async-await"] } -tokio = { version = "1.31.0", default-features = false, features = [ +tokio = { version = "1.32.0", default-features = false, features = [ "net", "rt-multi-thread", "time", @@ -35,8 +35,8 @@ async-trait = "0.1.73" hot_reload = "0.1.4" # reloading certs # Error handling -anyhow = "1.0.73" -thiserror = "1.0.45" +anyhow = "1.0.75" +thiserror = "1.0.47" # http and tls hyper = { version = "0.14.27", default-features = false, features = [ From 32b173966c422a4f660cefa29ac012330e4d7e92 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 18:15:36 +0900 Subject: [PATCH 19/30] feat: separated feature cache --- .github/workflows/docker_build_push.yml | 2 +- rpxy-bin/Cargo.toml | 3 ++- rpxy-bin/src/config/toml.rs | 23 ++++++++--------- rpxy-bin/src/constants.rs | 7 +----- rpxy-lib/Cargo.toml | 7 +++--- rpxy-lib/src/constants.rs | 9 +++++++ rpxy-lib/src/globals.rs | 20 ++++++++++----- rpxy-lib/src/handler/cache.rs | 4 +-- rpxy-lib/src/handler/forwarder.rs | 33 ++++++++++++++++++++----- rpxy-lib/src/handler/mod.rs | 1 + rpxy-lib/src/lib.rs | 3 +++ 11 files changed, 76 insertions(+), 36 deletions(-) diff --git a/.github/workflows/docker_build_push.yml b/.github/workflows/docker_build_push.yml index e2d801c..2f4d565 100644 --- a/.github/workflows/docker_build_push.yml +++ b/.github/workflows/docker_build_push.yml @@ -42,7 +42,7 @@ jobs: - target: "s2n" dockerfile: ./docker/Dockerfile build-args: | - "CARGO_FEATURES=--no-default-features --features http3-s2n" + "CARGO_FEATURES=--no-default-features --features http3-s2n cache" "ADDITIONAL_DEPS=pkg-config libssl-dev cmake libclang1 gcc g++" platforms: linux/amd64,linux/arm64 tags-suffix: "-s2n" diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 69d21c8..326a0fc 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -12,9 +12,10 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["http3-quinn"] +default = ["http3-quinn", "cache"] http3-quinn = ["rpxy-lib/http3-quinn"] http3-s2n = ["rpxy-lib/http3-s2n"] +cache = ["rpxy-lib/cache"] [dependencies] rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index f655347..2d03895 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -6,7 +6,7 @@ use crate::{ use rpxy_lib::{reexports::Uri, AppConfig, ProxyConfig, ReverseProxyConfig, TlsConfig, UpstreamUri}; use rustc_hash::FxHashMap as HashMap; use serde::Deserialize; -use std::{fs, net::SocketAddr, path::PathBuf}; +use std::{fs, net::SocketAddr}; #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct ConfigToml { @@ -32,6 +32,7 @@ pub struct Http3Option { pub max_idle_timeout: Option, } +#[cfg(feature = "cache")] #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] pub struct CacheOption { pub cache_dir: Option, @@ -43,6 +44,7 @@ pub struct CacheOption { pub struct Experimental { #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] pub h3: Option, + #[cfg(feature = "cache")] pub cache: Option, pub ignore_sni_consistency: Option, } @@ -169,20 +171,19 @@ impl TryInto for &ConfigToml { proxy_config.sni_consistency = !ignore; } + #[cfg(feature = "cache")] if let Some(cache_option) = &exp.cache { proxy_config.cache_enabled = true; proxy_config.cache_dir = match &cache_option.cache_dir { - Some(cache_dir) => Some(PathBuf::from(cache_dir)), - None => Some(PathBuf::from(CACHE_DIR)), - }; - proxy_config.cache_max_entry = match &cache_option.max_cache_entry { - Some(num) => Some(*num), - None => Some(MAX_CACHE_ENTRY), - }; - proxy_config.cache_max_each_size = match &cache_option.max_cache_each_size { - Some(num) => Some(*num), - None => Some(MAX_CACHE_EACH_SIZE), + Some(cache_dir) => Some(std::path::PathBuf::from(cache_dir)), + None => Some(std::path::PathBuf::from(CACHE_DIR)), }; + if let Some(num) = cache_option.max_cache_entry { + proxy_config.cache_max_entry = num; + } + if let Some(num) = cache_option.max_cache_each_size { + proxy_config.cache_max_each_size = num; + } } } diff --git a/rpxy-bin/src/constants.rs b/rpxy-bin/src/constants.rs index 54fa7dd..53c8bbc 100644 --- a/rpxy-bin/src/constants.rs +++ b/rpxy-bin/src/constants.rs @@ -2,11 +2,6 @@ pub const LISTEN_ADDRESSES_V4: &[&str] = &["0.0.0.0"]; pub const LISTEN_ADDRESSES_V6: &[&str] = &["[::]"]; pub const CONFIG_WATCH_DELAY_SECS: u32 = 20; +#[cfg(feature = "cache")] // Cache directory pub const CACHE_DIR: &str = "./cache"; -// # of entries in cache -pub const MAX_CACHE_ENTRY: usize = 1_000; -// max size for each file in bytes -pub const MAX_CACHE_EACH_SIZE: usize = 65_535; - -// TODO: max cache size in total diff --git a/rpxy-lib/Cargo.toml b/rpxy-lib/Cargo.toml index 59b76f6..8dc7c8f 100644 --- a/rpxy-lib/Cargo.toml +++ b/rpxy-lib/Cargo.toml @@ -12,10 +12,11 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["http3-quinn", "sticky-cookie"] +default = ["http3-quinn", "sticky-cookie", "cache"] http3-quinn = ["quinn", "h3", "h3-quinn", "socket2"] http3-s2n = ["h3", "s2n-quic", "s2n-quic-rustls", "s2n-quic-h3"] sticky-cookie = ["base64", "sha2", "chrono"] +cache = ["http-cache-semantics", "lru"] [dependencies] rand = "0.8.5" @@ -74,8 +75,8 @@ s2n-quic-h3 = { path = "../submodules/s2n-quic/quic/s2n-quic-h3/", optional = tr s2n-quic-rustls = { path = "../submodules/s2n-quic/quic/s2n-quic-rustls/", optional = true } # cache -http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/" } -lru = { version = "0.11.0" } +http-cache-semantics = { path = "../submodules/rusty-http-cache-semantics/", optional = true } +lru = { version = "0.11.0", optional = true } # cookie handling for sticky cookie chrono = { version = "0.4.26", default-features = false, features = [ diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index b7b0bff..510836a 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -31,3 +31,12 @@ pub mod H3 { #[cfg(feature = "sticky-cookie")] /// For load-balancing with sticky cookie pub const STICKY_COOKIE_NAME: &str = "rpxy_srv_id"; + +#[cfg(feature = "cache")] +// # of entries in cache +pub const MAX_CACHE_ENTRY: usize = 1_000; +#[cfg(feature = "cache")] +// max size for each file in bytes +pub const MAX_CACHE_EACH_SIZE: usize = 65_535; + +// TODO: max cache size in total diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 8a782b6..6f2efb0 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -9,11 +9,11 @@ use crate::{ utils::{BytesName, PathNameBytesExp}, }; use rustc_hash::FxHashMap as HashMap; +use std::net::SocketAddr; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; -use std::{net::SocketAddr, path::PathBuf}; use tokio::time::Duration; /// Global object containing proxy configurations and shared object like counters. @@ -53,10 +53,14 @@ pub struct ProxyConfig { // experimentals pub sni_consistency: bool, // Handler + #[cfg(feature = "cache")] pub cache_enabled: bool, - pub cache_dir: Option, - pub cache_max_entry: Option, - pub cache_max_each_size: Option, + #[cfg(feature = "cache")] + pub cache_dir: Option, + #[cfg(feature = "cache")] + pub cache_max_entry: usize, + #[cfg(feature = "cache")] + pub cache_max_each_size: usize, // All need to make packet acceptor #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] @@ -93,10 +97,14 @@ impl Default for ProxyConfig { sni_consistency: true, + #[cfg(feature = "cache")] cache_enabled: false, + #[cfg(feature = "cache")] cache_dir: None, - cache_max_entry: None, - cache_max_each_size: None, + #[cfg(feature = "cache")] + cache_max_entry: MAX_CACHE_ENTRY, + #[cfg(feature = "cache")] + cache_max_each_size: MAX_CACHE_EACH_SIZE, #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] http3: false, diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index c676c45..ce8aac4 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -121,14 +121,14 @@ impl RpxyCache { let path = globals.proxy_config.cache_dir.as_ref().unwrap(); let cache_file_manager = Arc::new(RwLock::new(CacheFileManager::new(path, &globals.runtime_handle).await)); let inner = Arc::new(Mutex::new(LruCache::new( - std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry.unwrap()).unwrap(), + std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry).unwrap(), ))); Some(Self { cache_file_manager, inner, runtime_handle: globals.runtime_handle.clone(), - max_each_size: globals.proxy_config.cache_max_each_size.unwrap(), + max_each_size: globals.proxy_config.cache_max_each_size, }) } diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index ed480bf..ce49433 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -1,8 +1,11 @@ +#[cfg(feature = "cache")] use super::cache::{get_policy_if_cacheable, RpxyCache}; -use crate::{error::RpxyError, globals::Globals, log::*, CryptoSource}; +#[cfg(feature = "cache")] +use crate::log::*; +use crate::{error::RpxyError, globals::Globals, CryptoSource}; use async_trait::async_trait; +#[cfg(feature = "cache")] use bytes::Buf; -use derive_builder::Builder; use hyper::{ body::{Body, HttpBody}, client::{connect::Connect, HttpConnector}, @@ -11,6 +14,8 @@ use hyper::{ }; use hyper_rustls::HttpsConnector; +#[cfg(feature = "cache")] +/// Build synthetic request to cache fn build_synth_req_for_cache(req: &Request) -> Request<()> { let mut builder = Request::builder() .method(req.method()) @@ -30,12 +35,12 @@ pub trait ForwardRequest { async fn request(&self, req: Request) -> Result, Self::Error>; } -#[derive(Builder, Clone)] /// Forwarder struct responsible to cache handling pub struct Forwarder where C: Connect + Clone + Sync + Send + 'static, { + #[cfg(feature = "cache")] cache: Option, inner: Client, inner_h2: Client, // `h2c` or http/2-only client is defined separately @@ -50,6 +55,8 @@ where C: Connect + Clone + Sync + Send + 'static, { type Error = RpxyError; + + #[cfg(feature = "cache")] async fn request(&self, req: Request) -> Result, Self::Error> { let mut synth_req = None; if self.cache.is_some() { @@ -99,10 +106,19 @@ where // res Ok(Response::from_parts(parts, Body::from(aggregated))) } + + #[cfg(not(feature = "cache"))] + async fn request(&self, req: Request) -> Result, Self::Error> { + match req.version() { + Version::HTTP_2 => self.inner_h2.request(req).await.map_err(RpxyError::Hyper), // handles `h2c` requests + _ => self.inner.request(req).await.map_err(RpxyError::Hyper), + } + } } impl Forwarder, Body> { - pub async fn new(globals: &std::sync::Arc>) -> Self { + /// Build forwarder + pub async fn new(_globals: &std::sync::Arc>) -> Self { // let connector = TrustDnsResolver::default().into_rustls_webpki_https_connector(); let connector = hyper_rustls::HttpsConnectorBuilder::new() .with_webpki_roots() @@ -119,7 +135,12 @@ impl Forwarder, Body> { let inner = Client::builder().build::<_, Body>(connector); let inner_h2 = Client::builder().http2_only(true).build::<_, Body>(connector_h2); - let cache = RpxyCache::new(globals).await; - Self { inner, inner_h2, cache } + #[cfg(feature = "cache")] + { + let cache = RpxyCache::new(_globals).await; + Self { inner, inner_h2, cache } + } + #[cfg(not(feature = "cache"))] + Self { inner, inner_h2 } } } diff --git a/rpxy-lib/src/handler/mod.rs b/rpxy-lib/src/handler/mod.rs index f2d3e43..84e0226 100644 --- a/rpxy-lib/src/handler/mod.rs +++ b/rpxy-lib/src/handler/mod.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "cache")] mod cache; mod forwarder; mod handler_main; diff --git a/rpxy-lib/src/lib.rs b/rpxy-lib/src/lib.rs index 0596625..fd242c5 100644 --- a/rpxy-lib/src/lib.rs +++ b/rpxy-lib/src/lib.rs @@ -60,11 +60,14 @@ where if !proxy_config.sni_consistency { info!("Ignore consistency between TLS SNI and Host header (or Request line). Note it violates RFC."); } + #[cfg(feature = "cache")] if proxy_config.cache_enabled { info!( "Cache is enabled: cache dir = {:?}", proxy_config.cache_dir.as_ref().unwrap() ); + } else { + info!("Cache is disabled") } // build global From 8498bbb69f7ec1b039d44c6f1101084487d17240 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 18:27:28 +0900 Subject: [PATCH 20/30] Update docker_build_push.yml --- .github/workflows/docker_build_push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_build_push.yml b/.github/workflows/docker_build_push.yml index 2f4d565..f7cea2b 100644 --- a/.github/workflows/docker_build_push.yml +++ b/.github/workflows/docker_build_push.yml @@ -42,7 +42,7 @@ jobs: - target: "s2n" dockerfile: ./docker/Dockerfile build-args: | - "CARGO_FEATURES=--no-default-features --features http3-s2n cache" + "CARGO_FEATURES=--no-default-features --features=http3-s2n,cache" "ADDITIONAL_DEPS=pkg-config libssl-dev cmake libclang1 gcc g++" platforms: linux/amd64,linux/arm64 tags-suffix: "-s2n" From 5f6758ff9e1eecd5abe70785f1546a28f6353aaf Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 18:59:28 +0900 Subject: [PATCH 21/30] feat: on memory cache if less than 4k or specified cache size. --- config-example.toml | 7 +-- rpxy-bin/src/config/toml.rs | 4 ++ rpxy-lib/src/constants.rs | 3 ++ rpxy-lib/src/globals.rs | 4 ++ rpxy-lib/src/handler/cache.rs | 93 +++++++++++++++++++++++++---------- 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/config-example.toml b/config-example.toml index 694ff7c..424417a 100644 --- a/config-example.toml +++ b/config-example.toml @@ -110,6 +110,7 @@ max_idle_timeout = 10 # secs. 0 represents an infinite timeout. # If this specified, file cache feature is enabled [experimental.cache] -cache_dir = './cache' # optional. default is "./cache" relative to the current working directory -max_cache_entry = 1000 # optional. default is 1k -max_cache_each_size = 65535 # optional. default is 64k +cache_dir = './cache' # optional. default is "./cache" relative to the current working directory +max_cache_entry = 1000 # optional. default is 1k +max_cache_each_size = 65535 # optional. default is 64k +max_cache_each_size_onmemory = 4096 # optional. default is 4k diff --git a/rpxy-bin/src/config/toml.rs b/rpxy-bin/src/config/toml.rs index 2d03895..e678012 100644 --- a/rpxy-bin/src/config/toml.rs +++ b/rpxy-bin/src/config/toml.rs @@ -38,6 +38,7 @@ pub struct CacheOption { pub cache_dir: Option, pub max_cache_entry: Option, pub max_cache_each_size: Option, + pub max_cache_each_size_on_memory: Option, } #[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)] @@ -184,6 +185,9 @@ impl TryInto for &ConfigToml { if let Some(num) = cache_option.max_cache_each_size { proxy_config.cache_max_each_size = num; } + if let Some(num) = cache_option.max_cache_each_size_on_memory { + proxy_config.cache_max_each_size_on_memory = num; + } } } diff --git a/rpxy-lib/src/constants.rs b/rpxy-lib/src/constants.rs index 510836a..ebec1fc 100644 --- a/rpxy-lib/src/constants.rs +++ b/rpxy-lib/src/constants.rs @@ -38,5 +38,8 @@ pub const MAX_CACHE_ENTRY: usize = 1_000; #[cfg(feature = "cache")] // max size for each file in bytes pub const MAX_CACHE_EACH_SIZE: usize = 65_535; +#[cfg(feature = "cache")] +// on memory cache if less than or equel to +pub const MAX_CACHE_EACH_SIZE_ON_MEMORY: usize = 4_096; // TODO: max cache size in total diff --git a/rpxy-lib/src/globals.rs b/rpxy-lib/src/globals.rs index 6f2efb0..d1c0130 100644 --- a/rpxy-lib/src/globals.rs +++ b/rpxy-lib/src/globals.rs @@ -61,6 +61,8 @@ pub struct ProxyConfig { pub cache_max_entry: usize, #[cfg(feature = "cache")] pub cache_max_each_size: usize, + #[cfg(feature = "cache")] + pub cache_max_each_size_on_memory: usize, // All need to make packet acceptor #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] @@ -105,6 +107,8 @@ impl Default for ProxyConfig { cache_max_entry: MAX_CACHE_ENTRY, #[cfg(feature = "cache")] cache_max_each_size: MAX_CACHE_EACH_SIZE, + #[cfg(feature = "cache")] + cache_max_each_size_on_memory: MAX_CACHE_EACH_SIZE_ON_MEMORY, #[cfg(any(feature = "http3-quinn", feature = "http3-s2n"))] http3: false, diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index ce8aac4..22ed51b 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -20,10 +20,16 @@ use tokio::{ sync::RwLock, }; +#[derive(Clone, Debug)] +pub enum CacheFileOrOnMemory { + File(PathBuf), + OnMemory(Vec), +} + #[derive(Clone, Debug)] struct CacheObject { pub policy: CachePolicy, - pub target: PathBuf, + pub target: CacheFileOrOnMemory, } #[derive(Debug)] @@ -64,7 +70,7 @@ impl CacheFileManager { self.cnt += 1; Ok(CacheObject { policy: policy.clone(), - target: cache_filepath, + target: CacheFileOrOnMemory::File(cache_filepath), }) } @@ -109,6 +115,8 @@ pub struct RpxyCache { runtime_handle: tokio::runtime::Handle, /// Maximum size of each cache file object max_each_size: usize, + /// Maximum size of cache object on memory + max_each_size_on_memory: usize, } impl RpxyCache { @@ -124,11 +132,21 @@ impl RpxyCache { std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry).unwrap(), ))); + let max_each_size = globals.proxy_config.cache_max_each_size; + let mut max_each_size_on_memory = globals.proxy_config.cache_max_each_size_on_memory; + if max_each_size < max_each_size_on_memory { + warn!( + "Maximum size of on memory cache per entry must be smaller than or equal to the maximum of each file cache" + ); + max_each_size_on_memory = max_each_size; + } + Some(Self { cache_file_manager, inner, runtime_handle: globals.runtime_handle.clone(), - max_each_size: globals.proxy_config.cache_max_each_size, + max_each_size, + max_each_size_on_memory, }) } @@ -174,24 +192,35 @@ impl RpxyCache { // So, we have to evict stale cache entries and cache file objects if found. debug!("Stale cache entry and file object: {cache_key}"); let _evicted_entry = self.evict_cache_entry(&cache_key); - self.evict_cache_file(&cached_object.target).await; + // For cache file + if let CacheFileOrOnMemory::File(path) = cached_object.target { + self.evict_cache_file(&path).await; + } return None; }; - // Finally retrieve the file object - let mgr = self.cache_file_manager.read().await; - let res_body = match mgr.read(&cached_object.target).await { - Ok(res_body) => res_body, - Err(e) => { - warn!("Failed to read from file cache: {e}"); - let _evicted_entry = self.evict_cache_entry(&cache_key); - self.evict_cache_file(&cached_object.target).await; - return None; - } - }; + // Finally retrieve the file/on-memory object + match cached_object.target { + CacheFileOrOnMemory::File(path) => { + let mgr = self.cache_file_manager.read().await; + let res_body = match mgr.read(&path).await { + Ok(res_body) => res_body, + Err(e) => { + warn!("Failed to read from file cache: {e}"); + let _evicted_entry = self.evict_cache_entry(&cache_key); + self.evict_cache_file(&path).await; + return None; + } + }; - debug!("Cache hit: {cache_key}"); - Some(Response::from_parts(res_parts, res_body)) + debug!("Cache hit from file: {cache_key}"); + Some(Response::from_parts(res_parts, res_body)) + } + CacheFileOrOnMemory::OnMemory(object) => { + debug!("Cache hit from on memory: {cache_key}"); + Some(Response::from_parts(res_parts, Body::from(object))) + } + } } pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: &CachePolicy) -> Result<()> { @@ -201,6 +230,7 @@ impl RpxyCache { let bytes_clone = body_bytes.clone(); let policy_clone = policy.clone(); let max_each_size = self.max_each_size; + let max_each_size_on_memory = self.max_each_size_on_memory; self.runtime_handle.spawn(async move { if bytes_clone.len() > max_each_size { @@ -212,10 +242,20 @@ impl RpxyCache { debug!("Cache file of {:?} bytes to be written", bytes_clone.len()); - let mut mgr = mgr.write().await; - let Ok(cache_object) = mgr.create(&cache_filename, &bytes_clone, &policy_clone).await else { - error!("Failed to put the body into the file object or cache entry"); - return Err(RpxyError::Cache("Failed to put the body into the file object or cache entry")); + let cache_object = if bytes_clone.len() > max_each_size_on_memory { + let mut mgr = mgr.write().await; + let Ok(cache_object) = mgr.create(&cache_filename, &bytes_clone, &policy_clone).await else { + error!("Failed to put the body into the file object or cache entry"); + return Err(RpxyError::Cache("Failed to put the body into the file object or cache entry")); + }; + debug!("Cached a new file: {} - {}", cache_key, cache_filename); + cache_object + } else { + debug!("Cached a new object on memory: {}", cache_key); + CacheObject { + policy: policy_clone, + target: CacheFileOrOnMemory::OnMemory(bytes_clone.to_vec()), + } }; let push_opt = { let Ok(mut lock) = my_cache.lock() else { @@ -227,13 +267,14 @@ impl RpxyCache { if let Some((k, v)) = push_opt { if k != cache_key { info!("Over the cache capacity. Evict least recent used entry"); - if let Err(e) = mgr.remove(&v.target).await { - warn!("Eviction failed during file object removal over the capacity: {:?}", e); - }; + if let CacheFileOrOnMemory::File(path) = v.target { + let mut mgr = mgr.write().await; + if let Err(e) = mgr.remove(&path).await { + warn!("Eviction failed during file object removal over the capacity: {:?}", e); + }; + } } } - - debug!("Cached a new file: {} - {}", cache_key, cache_filename); Ok(()) }); From b0156080a9f6a6e38bb74a89ab7812422d55a938 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 19:00:36 +0900 Subject: [PATCH 22/30] config --- config-example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-example.toml b/config-example.toml index 424417a..47a5947 100644 --- a/config-example.toml +++ b/config-example.toml @@ -113,4 +113,4 @@ max_idle_timeout = 10 # secs. 0 represents an infinite timeout. cache_dir = './cache' # optional. default is "./cache" relative to the current working directory max_cache_entry = 1000 # optional. default is 1k max_cache_each_size = 65535 # optional. default is 64k -max_cache_each_size_onmemory = 4096 # optional. default is 4k +max_cache_each_size_onmemory = 4096 # optional. default is 4k if 0, it is always file cache. From cf4108c034629aa79448c327d6ffc62ebe89d212 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 19:08:33 +0900 Subject: [PATCH 23/30] fix: fix typo --- TODO.md | 2 +- config-example.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 78a364e..1e25ee1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ # TODO List - [Done in 0.6.0] But we need more sophistication on `Forwarder` struct. ~~Fix strategy for `h2c` requests on forwarded requests upstream. This needs to update forwarder definition. Also, maybe forwarder would have a cache corresponding to the following task.~~ -- [Initial implementation in v0.6.0] ~~**Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))**~~ Using `lru` crate might be inefficient in terms of the speed. Also, this cache feature should be a separated `feature` (But I think okay to be included in `default`). +- [Initial implementation in v0.6.0] ~~**Cache option for the response with `Cache-Control: public` header directive ([#55](https://github.com/junkurihara/rust-rpxy/issues/55))**~~ Using `lru` crate might be inefficient in terms of the speed. - Consider more sophisticated architecture for cache - Persistent cache (if possible). - etc etc diff --git a/config-example.toml b/config-example.toml index 47a5947..ec79f3d 100644 --- a/config-example.toml +++ b/config-example.toml @@ -110,7 +110,7 @@ max_idle_timeout = 10 # secs. 0 represents an infinite timeout. # If this specified, file cache feature is enabled [experimental.cache] -cache_dir = './cache' # optional. default is "./cache" relative to the current working directory -max_cache_entry = 1000 # optional. default is 1k -max_cache_each_size = 65535 # optional. default is 64k -max_cache_each_size_onmemory = 4096 # optional. default is 4k if 0, it is always file cache. +cache_dir = './cache' # optional. default is "./cache" relative to the current working directory +max_cache_entry = 1000 # optional. default is 1k +max_cache_each_size = 65535 # optional. default is 64k +max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is always file cache. From e7ac459958287cb6580a440ccea8e7f24dbdd84d Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 18 Aug 2023 19:14:15 +0900 Subject: [PATCH 24/30] docs: readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index a4c71ed..9956935 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,19 @@ tls = { https_redirection = true, tls_cert_path = './server.crt', tls_cert_key_p However, currently we have a limitation on HTTP/3 support for applications that enables client authentication. If an application is set with client authentication, HTTP/3 doesn't work for the application. +### Hybrid Caching Feature Using File and On-Memory + +If `[experimental.cache]` is specified, you can leverage the local caching feature using temporary files and on-memory objects. Note that `max_cache_each_size` must be larger or equal to `max_cache_each_size_on_memory`. + +```toml +# If this specified, file cache feature is enabled +[experimental.cache] +cache_dir = './cache' # optional. default is "./cache" relative to the current working directory +max_cache_entry = 1000 # optional. default is 1k +max_cache_each_size = 65535 # optional. default is 64k +max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is always file cache. +``` + ## TIPS ### Using Private Key Issued by Let's Encrypt From 97f50f2310d94305fde86dcd55ac963cd1c09534 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Mon, 21 Aug 2023 20:01:53 +0900 Subject: [PATCH 25/30] deps --- rpxy-bin/Cargo.toml | 4 ++-- submodules/quinn | 2 +- submodules/s2n-quic | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index 326a0fc..af3e2f1 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -24,7 +24,7 @@ rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ anyhow = "1.0.75" rustc-hash = "1.1.0" -serde = { version = "1.0.183", default-features = false, features = ["derive"] } +serde = { version = "1.0.185", default-features = false, features = ["derive"] } derive_builder = "0.12.0" tokio = { version = "1.32.0", default-features = false, features = [ "net", @@ -38,7 +38,7 @@ rustls-pemfile = "1.0.3" mimalloc = { version = "*", default-features = false } # config -clap = { version = "4.3.22", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.3.23", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.7.6", default-features = false, features = ["parse"] } hot_reload = "0.1.4" diff --git a/submodules/quinn b/submodules/quinn index 4f25f50..5cca306 160000 --- a/submodules/quinn +++ b/submodules/quinn @@ -1 +1 @@ -Subproject commit 4f25f501ef4d009af9d3bef44d322c09c327b2df +Subproject commit 5cca3063f6f7747dcd9ec6e080ee48dcb5cfc4a7 diff --git a/submodules/s2n-quic b/submodules/s2n-quic index c7e3f51..e6402b7 160000 --- a/submodules/s2n-quic +++ b/submodules/s2n-quic @@ -1 +1 @@ -Subproject commit c7e3f517f3267f2e19b605cb53e6bf265e70e5af +Subproject commit e6402b7f8649bc9d90b69aedc83c387b0372bc94 From a7aac1a0d443ca43b37caf86e037029ce09ec9c0 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 22 Aug 2023 18:45:14 +0900 Subject: [PATCH 26/30] refactor: cache file manager wrapper to hide the rwlock operations --- rpxy-lib/src/handler/cache.rs | 106 ++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index 22ed51b..ee952e9 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -21,41 +21,53 @@ use tokio::{ }; #[derive(Clone, Debug)] +/// Cache target in hybrid manner of on-memory and file system pub enum CacheFileOrOnMemory { + /// Pointer to the temporary cache file File(PathBuf), + /// Cached body itself OnMemory(Vec), } #[derive(Clone, Debug)] +/// Cache object definition struct CacheObject { + /// Cache policy to determine if the stored cache can be used as a response to a new incoming request pub policy: CachePolicy, + /// Cache target: on-memory object or temporary file pub target: CacheFileOrOnMemory, } #[derive(Debug)] -struct CacheFileManager { +/// Manager inner for cache on file system +struct CacheFileManagerInner { + /// Directory of temporary files cache_dir: PathBuf, + /// Counter of current cached files cnt: usize, + /// Async runtime runtime_handle: tokio::runtime::Handle, } -impl CacheFileManager { - async fn new(path: &PathBuf, runtime_handle: &tokio::runtime::Handle) -> Self { - // Create cache file dir - // Clean up the file dir before init - // TODO: Persistent cache is really difficult. maybe SQLite is needed. +impl CacheFileManagerInner { + /// Build new cache file manager. + /// This first creates cache file dir if not exists, and cleans up the file inside the directory. + /// TODO: Persistent cache is really difficult. `sqlite` or something like that is needed. + async fn new(path: impl AsRef, runtime_handle: &tokio::runtime::Handle) -> Self { + let path_buf = path.as_ref().to_path_buf(); if let Err(e) = fs::remove_dir_all(path).await { warn!("Failed to clean up the cache dir: {e}"); }; - fs::create_dir_all(path).await.unwrap(); + fs::create_dir_all(&path_buf).await.unwrap(); Self { - cache_dir: path.clone(), + cache_dir: path_buf.clone(), cnt: 0, runtime_handle: runtime_handle.clone(), } } - async fn create(&mut self, cache_filename: &str, body_bytes: &Bytes, policy: &CachePolicy) -> Result { + /// Create a new temporary file cache + async fn create(&mut self, cache_filename: &str, body_bytes: &Bytes) -> Result { let cache_filepath = self.cache_dir.join(cache_filename); let Ok(mut file) = File::create(&cache_filepath).await else { return Err(RpxyError::Cache("Failed to create file")); @@ -68,12 +80,10 @@ impl CacheFileManager { }; } self.cnt += 1; - Ok(CacheObject { - policy: policy.clone(), - target: CacheFileOrOnMemory::File(cache_filepath), - }) + Ok(CacheFileOrOnMemory::File(cache_filepath)) } + /// Retrieve a stored temporary file cache async fn read(&self, path: impl AsRef) -> Result { let Ok(mut file) = File::open(&path).await else { warn!("Cache file object cannot be opened"); @@ -96,6 +106,7 @@ impl CacheFileManager { Ok(res_body) } + /// Remove file async fn remove(&mut self, path: impl AsRef) -> Result<()> { fs::remove_file(path.as_ref()).await?; self.cnt -= 1; @@ -105,10 +116,43 @@ impl CacheFileManager { } } +#[derive(Debug, Clone)] +/// Cache file manager outer that is responsible to handle `RwLock` +struct CacheFileManager { + inner: Arc>, +} + +impl CacheFileManager { + /// Build manager + async fn new(path: impl AsRef, runtime_handle: &tokio::runtime::Handle) -> Self { + Self { + inner: Arc::new(RwLock::new(CacheFileManagerInner::new(path, runtime_handle).await)), + } + } + /// Evict a temporary file cache + async fn evict(&self, path: impl AsRef) { + // Acquire the write lock + let mut inner = self.inner.write().await; + if let Err(e) = inner.remove(path).await { + warn!("Eviction failed during file object removal: {:?}", e); + }; + } + /// Read a temporary file cache + async fn read(&self, path: impl AsRef) -> Result { + let mgr = self.inner.read().await; + mgr.read(&path).await + } + /// Create a temporary file cache + async fn create(&mut self, cache_filename: &str, body_bytes: &Bytes) -> Result { + let mut mgr = self.inner.write().await; + mgr.create(cache_filename, body_bytes).await + } +} + #[derive(Clone, Debug)] pub struct RpxyCache { /// Managing cache file objects through RwLock's lock mechanism for file lock - cache_file_manager: Arc>, + cache_file_manager: CacheFileManager, /// Lru cache storing http message caching policy inner: Arc>>, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう /// Async runtime @@ -127,7 +171,7 @@ impl RpxyCache { } let path = globals.proxy_config.cache_dir.as_ref().unwrap(); - let cache_file_manager = Arc::new(RwLock::new(CacheFileManager::new(path, &globals.runtime_handle).await)); + let cache_file_manager = CacheFileManager::new(path, &globals.runtime_handle).await; let inner = Arc::new(Mutex::new(LruCache::new( std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry).unwrap(), ))); @@ -158,14 +202,6 @@ impl RpxyCache { lock.pop_entry(cache_key) } - async fn evict_cache_file(&self, filepath: impl AsRef) { - // Acquire the write lock - let mut mgr = self.cache_file_manager.write().await; - if let Err(e) = mgr.remove(filepath).await { - warn!("Eviction failed during file object removal: {:?}", e); - }; - } - /// Get cached response pub async fn get(&self, req: &Request) -> Option> { debug!("Current cache entries: {:?}", self.inner); @@ -190,11 +226,11 @@ impl RpxyCache { // This might be okay to keep as is since it would be updated later. // However, there is no guarantee that newly got objects will be still cacheable. // So, we have to evict stale cache entries and cache file objects if found. - debug!("Stale cache entry and file object: {cache_key}"); + debug!("Stale cache entry: {cache_key}"); let _evicted_entry = self.evict_cache_entry(&cache_key); // For cache file if let CacheFileOrOnMemory::File(path) = cached_object.target { - self.evict_cache_file(&path).await; + self.cache_file_manager.evict(&path).await; } return None; }; @@ -202,13 +238,12 @@ impl RpxyCache { // Finally retrieve the file/on-memory object match cached_object.target { CacheFileOrOnMemory::File(path) => { - let mgr = self.cache_file_manager.read().await; - let res_body = match mgr.read(&path).await { + let res_body = match self.cache_file_manager.read(&path).await { Ok(res_body) => res_body, Err(e) => { warn!("Failed to read from file cache: {e}"); let _evicted_entry = self.evict_cache_entry(&cache_key); - self.evict_cache_file(&path).await; + self.cache_file_manager.evict(&path).await; return None; } }; @@ -225,7 +260,7 @@ impl RpxyCache { pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: &CachePolicy) -> Result<()> { let my_cache = self.inner.clone(); - let mgr = self.cache_file_manager.clone(); + let mut mgr = self.cache_file_manager.clone(); let uri = uri.clone(); let bytes_clone = body_bytes.clone(); let policy_clone = policy.clone(); @@ -243,13 +278,15 @@ impl RpxyCache { debug!("Cache file of {:?} bytes to be written", bytes_clone.len()); let cache_object = if bytes_clone.len() > max_each_size_on_memory { - let mut mgr = mgr.write().await; - let Ok(cache_object) = mgr.create(&cache_filename, &bytes_clone, &policy_clone).await else { + let Ok(target) = mgr.create(&cache_filename, &bytes_clone).await else { error!("Failed to put the body into the file object or cache entry"); return Err(RpxyError::Cache("Failed to put the body into the file object or cache entry")); }; debug!("Cached a new file: {} - {}", cache_key, cache_filename); - cache_object + CacheObject { + policy: policy_clone, + target, + } } else { debug!("Cached a new object on memory: {}", cache_key); CacheObject { @@ -268,10 +305,7 @@ impl RpxyCache { if k != cache_key { info!("Over the cache capacity. Evict least recent used entry"); if let CacheFileOrOnMemory::File(path) = v.target { - let mut mgr = mgr.write().await; - if let Err(e) = mgr.remove(&path).await { - warn!("Eviction failed during file object removal over the capacity: {:?}", e); - }; + mgr.evict(&path).await; } } } From 7cfcd60243096c506781495f96eaf874e59c4c83 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 22 Aug 2023 21:15:34 +0900 Subject: [PATCH 27/30] refactor: cache manager wrapper to hide mutex lock --- rpxy-lib/src/handler/cache.rs | 127 ++++++++++++++++++++---------- rpxy-lib/src/handler/forwarder.rs | 1 - 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/rpxy-lib/src/handler/cache.rs b/rpxy-lib/src/handler/cache.rs index ee952e9..44cdc11 100644 --- a/rpxy-lib/src/handler/cache.rs +++ b/rpxy-lib/src/handler/cache.rs @@ -11,7 +11,10 @@ use sha2::{Digest, Sha256}; use std::{ fmt::Debug, path::{Path, PathBuf}, - sync::{Arc, Mutex}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, time::SystemTime, }; use tokio::{ @@ -147,6 +150,64 @@ impl CacheFileManager { let mut mgr = self.inner.write().await; mgr.create(cache_filename, body_bytes).await } + async fn count(&self) -> usize { + let mgr = self.inner.read().await; + mgr.cnt + } +} + +#[derive(Debug, Clone)] +/// Lru cache manager that is responsible to handle `Mutex` as an outer of `LruCache` +struct LruCacheManager { + inner: Arc>>, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう + cnt: Arc, +} + +impl LruCacheManager { + /// Build LruCache + fn new(cache_max_entry: usize) -> Self { + Self { + inner: Arc::new(Mutex::new(LruCache::new( + std::num::NonZeroUsize::new(cache_max_entry).unwrap(), + ))), + cnt: Arc::new(AtomicUsize::default()), + } + } + /// Count entries + fn count(&self) -> usize { + self.cnt.load(Ordering::Relaxed) + } + /// Evict an entry + fn evict(&self, cache_key: &str) -> Option<(String, CacheObject)> { + let Ok(mut lock) = self.inner.lock() else { + error!("Mutex can't be locked to evict a cache entry"); + return None; + }; + let res = lock.pop_entry(cache_key); + self.cnt.store(lock.len(), Ordering::Relaxed); + res + } + /// Get an entry + fn get(&self, cache_key: &str) -> Result> { + let Ok(mut lock) = self.inner.lock() else { + error!("Mutex can't be locked for checking cache entry"); + return Err(RpxyError::Cache("Mutex can't be locked for checking cache entry")); + }; + let Some(cached_object) = lock.get(cache_key) else { + return Ok(None); + }; + Ok(Some(cached_object.clone())) + } + /// Push an entry + fn push(&self, cache_key: &str, cache_object: CacheObject) -> Result> { + let Ok(mut lock) = self.inner.lock() else { + error!("Failed to acquire mutex lock for writing cache entry"); + return Err(RpxyError::Cache("Failed to acquire mutex lock for writing cache entry")); + }; + let res = Ok(lock.push(cache_key.to_string(), cache_object)); + self.cnt.store(lock.len(), Ordering::Relaxed); + res + } } #[derive(Clone, Debug)] @@ -154,7 +215,7 @@ pub struct RpxyCache { /// Managing cache file objects through RwLock's lock mechanism for file lock cache_file_manager: CacheFileManager, /// Lru cache storing http message caching policy - inner: Arc>>, // TODO: keyはstring urlでいいのか疑問。全requestに対してcheckすることになりそう + inner: LruCacheManager, /// Async runtime runtime_handle: tokio::runtime::Handle, /// Maximum size of each cache file object @@ -172,9 +233,7 @@ impl RpxyCache { let path = globals.proxy_config.cache_dir.as_ref().unwrap(); let cache_file_manager = CacheFileManager::new(path, &globals.runtime_handle).await; - let inner = Arc::new(Mutex::new(LruCache::new( - std::num::NonZeroUsize::new(globals.proxy_config.cache_max_entry).unwrap(), - ))); + let inner = LruCacheManager::new(globals.proxy_config.cache_max_entry); let max_each_size = globals.proxy_config.cache_max_each_size; let mut max_each_size_on_memory = globals.proxy_config.cache_max_each_size_on_memory; @@ -194,29 +253,25 @@ impl RpxyCache { }) } - fn evict_cache_entry(&self, cache_key: &str) -> Option<(String, CacheObject)> { - let Ok(mut lock) = self.inner.lock() else { - error!("Mutex can't be locked to evict a cache entry"); - return None; - }; - lock.pop_entry(cache_key) + /// Count cache entries + pub async fn count(&self) -> (usize, usize, usize) { + let total = self.inner.count(); + let file = self.cache_file_manager.count().await; + let on_memory = total - file; + (total, on_memory, file) } /// Get cached response pub async fn get(&self, req: &Request) -> Option> { - debug!("Current cache entries: {:?}", self.inner); + debug!( + "Current cache status: (total, on-memory, file) = {:?}", + self.count().await + ); let cache_key = req.uri().to_string(); // First check cache chance - let cached_object = { - let Ok(mut lock) = self.inner.lock() else { - error!("Mutex can't be locked for checking cache entry"); - return None; - }; - let Some(cached_object) = lock.get(&cache_key) else { - return None; - }; - cached_object.clone() + let Ok(Some(cached_object)) = self.inner.get(&cache_key) else { + return None; }; // Secondly check the cache freshness as an HTTP message @@ -227,9 +282,9 @@ impl RpxyCache { // However, there is no guarantee that newly got objects will be still cacheable. // So, we have to evict stale cache entries and cache file objects if found. debug!("Stale cache entry: {cache_key}"); - let _evicted_entry = self.evict_cache_entry(&cache_key); + let _evicted_entry = self.inner.evict(&cache_key); // For cache file - if let CacheFileOrOnMemory::File(path) = cached_object.target { + if let CacheFileOrOnMemory::File(path) = &cached_object.target { self.cache_file_manager.evict(&path).await; } return None; @@ -242,7 +297,7 @@ impl RpxyCache { Ok(res_body) => res_body, Err(e) => { warn!("Failed to read from file cache: {e}"); - let _evicted_entry = self.evict_cache_entry(&cache_key); + let _evicted_entry = self.inner.evict(&cache_key); self.cache_file_manager.evict(&path).await; return None; } @@ -258,6 +313,7 @@ impl RpxyCache { } } + /// Put response into the cache pub async fn put(&self, uri: &hyper::Uri, body_bytes: &Bytes, policy: &CachePolicy) -> Result<()> { let my_cache = self.inner.clone(); let mut mgr = self.cache_file_manager.clone(); @@ -273,16 +329,13 @@ impl RpxyCache { return Err(RpxyError::Cache("Too large to cache")); } let cache_key = derive_cache_key_from_uri(&uri); - let cache_filename = derive_filename_from_uri(&uri); - debug!("Cache file of {:?} bytes to be written", bytes_clone.len()); + debug!("Object of size {:?} bytes to be cached", bytes_clone.len()); let cache_object = if bytes_clone.len() > max_each_size_on_memory { - let Ok(target) = mgr.create(&cache_filename, &bytes_clone).await else { - error!("Failed to put the body into the file object or cache entry"); - return Err(RpxyError::Cache("Failed to put the body into the file object or cache entry")); - }; - debug!("Cached a new file: {} - {}", cache_key, cache_filename); + let cache_filename = derive_filename_from_uri(&uri); + let target = mgr.create(&cache_filename, &bytes_clone).await?; + debug!("Cached a new cache file: {} - {}", cache_key, cache_filename); CacheObject { policy: policy_clone, target, @@ -294,14 +347,8 @@ impl RpxyCache { target: CacheFileOrOnMemory::OnMemory(bytes_clone.to_vec()), } }; - let push_opt = { - let Ok(mut lock) = my_cache.lock() else { - error!("Failed to acquire mutex lock for writing cache entry"); - return Err(RpxyError::Cache("Failed to acquire mutex lock for writing cache entry")); - }; - lock.push(cache_key.clone(), cache_object) - }; - if let Some((k, v)) = push_opt { + + if let Some((k, v)) = my_cache.push(&cache_key, cache_object)? { if k != cache_key { info!("Over the cache capacity. Evict least recent used entry"); if let CacheFileOrOnMemory::File(path) = v.target { @@ -338,7 +385,7 @@ where let new_policy = CachePolicy::new(req, res); if new_policy.is_storable() { - debug!("Response is cacheable: {:?}\n{:?}", req, res.headers()); + // debug!("Response is cacheable: {:?}\n{:?}", req, res.headers()); Ok(Some(new_policy)) } else { Ok(None) diff --git a/rpxy-lib/src/handler/forwarder.rs b/rpxy-lib/src/handler/forwarder.rs index ce49433..43cf098 100644 --- a/rpxy-lib/src/handler/forwarder.rs +++ b/rpxy-lib/src/handler/forwarder.rs @@ -87,7 +87,6 @@ where return res; }; let (parts, body) = res.unwrap().into_parts(); - // TODO: Inefficient? let Ok(mut bytes) = hyper::body::aggregate(body).await else { return Err(RpxyError::Cache("Failed to write byte buffer")); }; From 0453978d5f4686d3616e7d1e0240bbab19560bd1 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 22 Aug 2023 21:27:12 +0900 Subject: [PATCH 28/30] docs: readme --- CHANGELOG.md | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 492db57..0938efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Feat: Enabled `h2c` (HTTP/2 cleartext) requests to upstream app servers (in the previous versions, only HTTP/1.1 is allowed for cleartext requests) - Feat: Initial implementation of caching feature using file + on memory cache. (Caveats: No persistance of the cache. Once config is updated, the cache is totally eliminated.) -- Refactor: logs of minor improvements +- Refactor: lots of minor improvements ### Bugfix diff --git a/README.md b/README.md index 9956935..951c35a 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,8 @@ max_cache_each_size = 65535 # optional. default is 64k max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is always file cache. ``` +Note that once `rpxy` restarts or the config is updated, the cache is totally eliminated not only from the on-memory table but also from the file system. + ## TIPS ### Using Private Key Issued by Let's Encrypt From 119637f24cd287ed7cb7398c85b3adc37824b3b6 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 23 Aug 2023 17:04:56 +0900 Subject: [PATCH 29/30] fix: docker entrypoint.sh --- docker/entrypoint.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 180ab93..63e997b 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -58,8 +58,9 @@ EOF ####################################### function setup_ubuntu () { + id ${USER} > /dev/null # Check the existence of the user, if not exist, create it. - if [ ! $(id ${USER}) ]; then + if [ $? -eq 1 ]; then echo "rpxy: Create user ${USER} with ${USER_ID}:${GROUP_ID}" groupadd -g ${GROUP_ID} ${USER} useradd -u ${USER_ID} -g ${GROUP_ID} ${USER} @@ -81,8 +82,9 @@ function setup_ubuntu () { ####################################### function setup_alpine () { + id ${USER} > /dev/null # Check the existence of the user, if not exist, create it. - if [ ! $(id ${USER}) ]; then + if [ $? -eq 1 ]; then echo "rpxy: Create user ${USER} with ${USER_ID}:${GROUP_ID}" addgroup -g ${GROUP_ID} ${USER} adduser -H -D -u ${USER_ID} -G ${USER} ${USER} From 9bbd8fc0dba22a0bab88866a713f6087d4e1a98d Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 24 Aug 2023 19:50:37 +0900 Subject: [PATCH 30/30] docs+deps: preparing v0.6.0 --- CHANGELOG.md | 4 +++- README.md | 6 +++--- rpxy-bin/Cargo.toml | 4 ++-- submodules/quinn | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0938efa..f094cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # CHANGELOG -## 0.6.0 (unreleased) +## 0.7.0 (unreleased) + +## 0.6.0 ### Improvement diff --git a/README.md b/README.md index 951c35a..5561511 100644 --- a/README.md +++ b/README.md @@ -281,9 +281,9 @@ tls = { https_redirection = true, tls_cert_path = './server.crt', tls_cert_key_p However, currently we have a limitation on HTTP/3 support for applications that enables client authentication. If an application is set with client authentication, HTTP/3 doesn't work for the application. -### Hybrid Caching Feature Using File and On-Memory +### Hybrid Caching Feature with Temporary File and On-Memory Cache -If `[experimental.cache]` is specified, you can leverage the local caching feature using temporary files and on-memory objects. Note that `max_cache_each_size` must be larger or equal to `max_cache_each_size_on_memory`. +If `[experimental.cache]` is specified in `config.toml`, you can leverage the local caching feature using temporary files and on-memory objects. An example configuration is as follows. ```toml # If this specified, file cache feature is enabled @@ -294,7 +294,7 @@ max_cache_each_size = 65535 # optional. default is 64k max_cache_each_size_on_memory = 4096 # optional. default is 4k if 0, it is always file cache. ``` -Note that once `rpxy` restarts or the config is updated, the cache is totally eliminated not only from the on-memory table but also from the file system. +A *storable* (in the context of an HTTP message) response is stored if its size is less than or equal to `max_cache_each_size` in bytes. If it is also less than or equal to `max_cache_each_size_on_memory`, it is stored as an on-memory object. Otherwise, it is stored as a temporary file. Note that `max_cache_each_size` must be larger or equal to `max_cache_each_size_on_memory`. Also note that once `rpxy` restarts or the config is updated, the cache is totally eliminated not only from the on-memory table but also from the file system. ## TIPS diff --git a/rpxy-bin/Cargo.toml b/rpxy-bin/Cargo.toml index af3e2f1..99ee25f 100644 --- a/rpxy-bin/Cargo.toml +++ b/rpxy-bin/Cargo.toml @@ -24,7 +24,7 @@ rpxy-lib = { path = "../rpxy-lib/", default-features = false, features = [ anyhow = "1.0.75" rustc-hash = "1.1.0" -serde = { version = "1.0.185", default-features = false, features = ["derive"] } +serde = { version = "1.0.186", default-features = false, features = ["derive"] } derive_builder = "0.12.0" tokio = { version = "1.32.0", default-features = false, features = [ "net", @@ -38,7 +38,7 @@ rustls-pemfile = "1.0.3" mimalloc = { version = "*", default-features = false } # config -clap = { version = "4.3.23", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.3.24", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.7.6", default-features = false, features = ["parse"] } hot_reload = "0.1.4" diff --git a/submodules/quinn b/submodules/quinn index 5cca306..7f26029 160000 --- a/submodules/quinn +++ b/submodules/quinn @@ -1 +1 @@ -Subproject commit 5cca3063f6f7747dcd9ec6e080ee48dcb5cfc4a7 +Subproject commit 7f260292848a93d615eb43e6e88114a97e64daf1