Skip to content

Commit b130b13

Browse files
authored
Merge pull request #27 from msalinas92/feature/https-support
Feature/https support
2 parents d19d4d3 + 0fb84db commit b130b13

File tree

6 files changed

+173
-9
lines changed

6 files changed

+173
-9
lines changed

Cargo.lock

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ tower = "0.5.2"
6161
tower-http = { version = "0.4", features = ["cors"] }
6262
rust-embed = "8.2.0"
6363
mime_guess = "2.0"
64+
hyper-tls = "0.5"
65+
url = "2"
6466

6567
[dev-dependencies]
6668
tokio = { version = "1", features = ["macros", "rt-multi-thread", "test-util"] }

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ ARG BINARY
44

55
COPY --chmod=755 ${BINARY} /cachebolt
66

7-
ENTRYPOINT ["/cachebolt"]
7+
ENTRYPOINT ["/cachebolt"]

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ latency_failover:
5353
# 🚫 List of request headers to ignore when computing cache keys (case-insensitive)
5454
ignored_headers:
5555
- postman-token
56-
- if-none-match
56+
- if-none-match

src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub struct LatencyFailover {
5858
pub default_max_latency_ms: u64,
5959

6060
/// Specific path-based rules, applied in order.
61+
#[serde(default)] // <--- Esto lo hace opcional en YAML y por defecto = []
6162
pub path_rules: Vec<MaxLatencyRule>,
6263
}
6364

src/proxy.rs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use axum::response::IntoResponse;
1515
use bytes::Bytes;
1616
use hyper::client::HttpConnector;
17+
use hyper_tls::HttpsConnector;
18+
type HttpsClient = Client<HttpsConnector<HttpConnector>>;
1719
use hyper::{Body, Client, Request, Response};
1820
use once_cell::sync::Lazy;
1921
use sha2::{Digest, Sha256};
@@ -47,10 +49,12 @@ pub static SEMAPHORE: Lazy<Arc<Semaphore>> =
4749
Lazy::new(|| Arc::new(Semaphore::new(*MAX_CONCURRENT_REQUESTS)));
4850

4951
/// Shared HTTP client for all outbound requests
50-
static HTTP_CLIENT: Lazy<Client<HttpConnector>> = Lazy::new(Client::new);
52+
static HTTP_CLIENT: Lazy<HttpsClient> = Lazy::new(|| {
53+
let https = HttpsConnector::new();
54+
Client::builder().build::<_, Body>(https)
55+
});
5156

5257
/// Background task that persistently writes cache entries to the configured backend
53-
5458
static CACHE_WRITER: Lazy<mpsc::Sender<(String, Bytes, Vec<(String, String)>)>> = Lazy::new(|| {
5559
let (tx, mut rx) = mpsc::channel::<(String, Bytes, Vec<(String, String)>)>(100);
5660
tokio::spawn(async move {
@@ -82,6 +86,11 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
8286
let uri = req.uri().to_string();
8387
tracing::debug!("🔗 Received request for URI: {}", uri);
8488

89+
tracing::debug!("🔎 Incoming request headers:");
90+
for (k, v) in req.headers().iter() {
91+
tracing::debug!(" {}: {:?}", k, v);
92+
}
93+
8594
// Increment total request counter for each URI
8695
counter!("cachebolt_proxy_requests_total", "uri" => uri.clone()).increment(1);
8796

@@ -169,9 +178,11 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
169178
}
170179

171180
// Split response into parts
172-
let (parts, body) = resp.into_parts();
181+
let (mut parts, body) = resp.into_parts();
173182
let body_bytes = hyper::body::to_bytes(body).await.unwrap_or_default();
174183

184+
parts.headers.remove("content-length");
185+
175186
let headers_vec = parts
176187
.headers
177188
.iter()
@@ -210,7 +221,10 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
210221
);
211222
}
212223
} else {
213-
tracing::info!("⏩ Cache bypass activated for '{}' due to client header", uri);
224+
tracing::info!(
225+
"⏩ Cache bypass activated for '{}' due to client header",
226+
uri
227+
);
214228
}
215229

216230
Response::from_parts(parts, Body::from(body_bytes))
@@ -304,18 +318,58 @@ pub fn hash_uri(uri: &str) -> String {
304318
}
305319

306320
/// Sends an outbound GET request to the downstream backend
321+
/// Sends an outbound GET request to the downstream backend, forwarding all headers except 'accept-encoding'.
322+
/// This prevents curl: (52) Empty reply from server errors caused by unsupported encodings.
323+
///
324+
/// # Arguments
325+
/// - `uri`: The path to append to the downstream base URL.
326+
/// - `original_req`: The incoming Axum request, from which headers are forwarded.
327+
///
328+
/// # Returns
329+
/// - `Ok(Response)` with the downstream response if successful.
330+
/// - `Err(())` if the downstream call fails or the request could not be built.
307331
pub async fn forward_request(uri: &str, original_req: Request<Body>) -> Result<Response<Body>, ()> {
332+
// Get the config and build the downstream full URL
308333
let cfg = CONFIG.get().unwrap();
309334
let full_url = format!("{}{}", cfg.downstream_base_url, uri);
310335

336+
// Debug: Log the scheme, host, and path of the downstream URL
337+
if let Ok(parsed_url) = url::Url::parse(&full_url) {
338+
tracing::info!(
339+
"🌐 Downstream request: scheme='{}' host='{}' path='{}'",
340+
parsed_url.scheme(),
341+
parsed_url.host_str().unwrap_or(""),
342+
parsed_url.path()
343+
);
344+
}
345+
346+
// Parse downstream_base_url to extract the host (domain)
347+
let downstream_host = url::Url::parse(&cfg.downstream_base_url)
348+
.ok()
349+
.and_then(|u| u.host_str().map(|s| s.to_string()))
350+
.unwrap_or_else(|| "".to_string());
351+
352+
// Build the request, starting with the URL and GET method
311353
let mut builder = Request::builder().uri(full_url.clone()).method("GET");
312354

313-
// Clone headers from the original request
355+
// Copy all headers from the incoming request,
356+
// except for 'accept-encoding' and 'host'
357+
// (We want to control the Host header for SNI/proxying, and avoid content-encoding issues.)
314358
for (key, value) in original_req.headers().iter() {
359+
if key.as_str().eq_ignore_ascii_case("accept-encoding")
360+
|| key.as_str().eq_ignore_ascii_case("host")
361+
{
362+
continue;
363+
}
315364
builder = builder.header(key, value);
316365
}
317366

318-
// Build the request
367+
// Inject the Host header, if it was successfully extracted from the downstream_base_url
368+
if !downstream_host.is_empty() {
369+
builder = builder.header("Host", downstream_host);
370+
}
371+
372+
// Build the final request object with empty body
319373
let req = match builder.body(Body::empty()) {
320374
Ok(req) => req,
321375
Err(e) => {
@@ -324,7 +378,7 @@ pub async fn forward_request(uri: &str, original_req: Request<Body>) -> Result<R
324378
}
325379
};
326380

327-
// Make the request without timeout
381+
// Send the HTTP request to the downstream service
328382
match HTTP_CLIENT.request(req).await {
329383
Ok(resp) => Ok(resp),
330384
Err(e) => {

0 commit comments

Comments
 (0)