Compare commits

...

44 Commits

Author SHA1 Message Date
d4a922198c Update Rust crate tokio to 1.50.0
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-04 00:19:50 +00:00
265620f883 Merge pull request 'Update Rust crate anyhow to 1.0.102' (#13) from renovate/anyhow-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-22 00:15:05 +00:00
6a75ad6382 Update Rust crate anyhow to 1.0.102
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-21 00:15:23 +00:00
d73f2774df Merge pull request 'Update Rust crate clap to 4.5.60' (#12) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-21 00:15:20 +00:00
5880d71172 Update Rust crate clap to 4.5.60
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-20 00:14:53 +00:00
055a4ecb12 Merge pull request 'Update Rust crate clap to 4.5.59' (#11) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-18 00:27:13 +00:00
d667b5f308 Update Rust crate clap to 4.5.59
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-17 00:27:21 +00:00
57f6cb9ab7 Merge pull request 'Update Rust crate env_logger to 0.11.9' (#10) from renovate/env_logger-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-15 00:27:30 +00:00
ee6809956c Update Rust crate env_logger to 0.11.9
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-14 00:27:09 +00:00
e95280e684 Merge pull request 'Update Rust crate clap to 4.5.58' (#9) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-14 00:27:05 +00:00
f21439060e Update Rust crate clap to 4.5.58
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-13 00:27:15 +00:00
836909a844 Merge pull request 'Update Rust crate rand to 0.10.0' (#8) from renovate/rand-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-10 00:26:37 +00:00
ec10a54f18 Update Rust crate rand to 0.10.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-09 00:26:43 +00:00
5c9952e007 Merge pull request 'Update Rust crate anyhow to 1.0.101' (#7) from renovate/anyhow-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-07 00:26:56 +00:00
fdd38507a0 Update Rust crate anyhow to 1.0.101
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-06 00:26:31 +00:00
244949c111 Merge pull request 'Update Rust crate clap to 4.5.57' (#6) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-05 00:26:39 +00:00
7c85a03d6f Update Rust crate clap to 4.5.57
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-02-04 00:26:17 +00:00
e18c2aa981 Merge pull request 'Update Rust crate clap to 4.5.56' (#5) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-31 00:25:52 +00:00
5ecb9ec2a4 Update Rust crate clap to 4.5.56
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-01-30 00:26:33 +00:00
e8e29b29ad Merge pull request 'Update Rust crate clap to 4.5.55' (#4) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-29 00:26:24 +00:00
0e907f4a7b Update Rust crate clap to 4.5.55
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-01-28 00:26:23 +00:00
eb334170d8 Merge pull request 'Update Rust crate tokio to 1.49.0' (#3) from renovate/tokio-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-07 00:21:55 +00:00
94e9ee8ab3 Update Rust crate tokio to 1.49.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-01-06 00:22:05 +00:00
dd76599744 Merge pull request 'Update Rust crate clap to 4.5.54' (#2) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-06 00:22:02 +00:00
e1f4dde529 Update Rust crate clap to 4.5.54
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-01-05 00:22:18 +00:00
2104f519eb Merge pull request 'Update Rust crate log to 0.4.29' (#1) from renovate/log-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-04 00:15:23 +00:00
aba1121d18 Update Rust crate log to 0.4.29
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-12-03 00:14:40 +00:00
21fab535d4 Update
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 12:20:10 +01:00
8b613a1b6f update
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 12:02:30 +01:00
a26ff16bfb Minor fix 2025-11-26 11:58:53 +01:00
d8ea9db3c2 Add release files
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 11:52:58 +01:00
72c167dcd5 Initial release
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-26 11:11:32 +01:00
952dd052bd WIP adaptations
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-26 10:39:29 +01:00
3fd42b86c3 WIP adaptations 2025-11-26 10:19:05 +01:00
bb5430b175 Merge pull request 'Update Rust crate clap to 4.5.53' (#58) from renovate/clap-4.x into master 2025-11-21 00:17:27 +00:00
286239021a Update Rust crate clap to 4.5.53 2025-11-20 00:13:34 +00:00
68a3243409 Merge pull request 'Update Rust crate clap to 4.5.52' (#57) from renovate/clap-4.x into master 2025-11-19 00:12:46 +00:00
ee4cd4eeaf Update Rust crate clap to 4.5.52 2025-11-18 00:14:49 +00:00
5719735c17 Merge pull request 'Update Rust crate webpki-roots to 1.0.4' (#56) from renovate/webpki-roots-1.x into master 2025-11-03 00:13:57 +00:00
f8b65369dc Update Rust crate webpki-roots to 1.0.4 2025-11-02 00:13:39 +00:00
4d7b6f0450 Merge pull request 'Update Rust crate clap to 4.5.51' (#55) from renovate/clap-4.x into master 2025-10-31 00:40:56 +00:00
e9828fb55d Update Rust crate clap to 4.5.51 2025-10-30 00:41:31 +00:00
e579b83990 Merge pull request 'Update Rust crate rustls-pki-types to 1.13.0' (#54) from renovate/rustls-pki-types-1.x into master 2025-10-29 00:40:48 +00:00
98ae7d7942 Update Rust crate rustls-pki-types to 1.13.0 2025-10-28 00:51:24 +00:00
6 changed files with 577 additions and 444 deletions

712
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
[package] [package]
name = "proxy_saver" name = "header_proxy"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
log = "0.4.28" log = "0.4.29"
env_logger = "0.11.8" env_logger = "0.11.9"
clap = { version = "4.5.50", features = ["env", "derive"] } clap = { version = "4.5.60", features = ["env", "derive"] }
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.50.0", features = ["full"] }
tokio-rustls = { version = "0.26.4", features = ["ring", "tls12"], default-features = false } rand = "0.10.0"
rustls-pki-types = "1.12.0" lazy_static = "1.5.0"
webpki-roots = "1.0.3" httparse = "1.10.1"
rand = "0.9.2" anyhow = "1.0.102"

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y libcurl4 \
&& rm -rf /var/lib/apt/lists/*
COPY header_proxy /usr/local/bin/header_proxy
ENTRYPOINT ["/usr/local/bin/header_proxy"]

View File

@@ -1,2 +1,2 @@
# Proxy saver # HTTP header proxy
Proxify HTTP request to HTTPS upstream, saving requests and responses. Proxify HTTP request to another HTTP upstream, optionally manipulating path.

10
build_docker_image.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
cargo build --release
TEMP_DIR=$(mktemp -d)
cp target/release/header_proxy "$TEMP_DIR"
docker build -f Dockerfile "$TEMP_DIR" -t pierre42100/header_proxy
rm -r $TEMP_DIR

View File

@@ -1,51 +1,38 @@
use clap::Parser; use clap::Parser;
use rand::distr::{Alphanumeric, SampleString}; use rand::distr::{Alphanumeric, SampleString};
use rustls_pki_types::ServerName;
use std::error::Error; use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
/// Simple program that proxify requests and save responses /// Simple program that redirect requests to a given server in request
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
/// The address the server will listen to /// The address the server will listen to
#[arg(short, long, default_value = "0.0.0.0:8000")] #[arg(short, long, env, default_value = "0.0.0.0:8000")]
listen_address: String, listen_address: String,
/// Upstream address this server will connect to /// The name of the header that contain target host and port
#[arg(short, long, default_value = "communiquons.org")] #[arg(short, long, env, default_value = "x-target-host")]
upstream_dns: String, target_host_port_header: String,
/// Upstream address this server will connect to /// Name of optional header that contains path to add to the request.
#[arg(short('I'), long, default_value = "10.0.1.10:443")] ///
upstream_ip: String, /// If this value is defined, all clients packets are inspected in research for path to
/// manipulate
/// The path on the server this server will save requests and responses #[arg(short, long, env)]
#[arg(short, long, default_value = "storage")] path_prefix_header: Option<String>,
storage_path: String,
/// Only forward requests that match a given prefix
#[arg(short, long)]
prefix: Option<String>,
} }
/// Get the current time since epoch lazy_static::lazy_static! {
pub fn time() -> u64 { static ref ARGS: Args = {
SystemTime::now() Args::parse()
.duration_since(UNIX_EPOCH) };
.unwrap()
.as_secs()
} }
const ERR_NOT_PROXIFIALBE: &[u8; 44] = b"HTTP/1.1 400 Forbidden\r\n\r\nNot proxifiable.\r\n";
pub fn rand_str(len: usize) -> String { pub fn rand_str(len: usize) -> String {
Alphanumeric.sample_string(&mut rand::rng(), len) Alphanumeric.sample_string(&mut rand::rng(), len)
} }
@@ -54,105 +41,105 @@ pub fn rand_str(len: usize) -> String {
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let args = Args::parse(); log::info!("Will start to listen on {}", ARGS.listen_address);
let listener = TcpListener::bind(&ARGS.listen_address).await?;
log::info!("Will start to listen on {}", args.listen_address);
let listener = TcpListener::bind(&args.listen_address).await?;
std::fs::create_dir_all(Path::new(args.storage_path.as_str())).unwrap();
loop { loop {
// Asynchronously wait for an inbound socket. // Asynchronously wait for an inbound socket.
let (mut client_socket, _) = listener.accept().await?; let (mut client_socket, _) = listener.accept().await?;
tokio::spawn(async move { tokio::spawn(async move {
let conn_id = rand_str(10); let conn_id = rand_str(5);
log::info!( log::info!(
"[{conn_id}] Start new connection from {}", "[{conn_id}] Handle new connection from {}",
client_socket.peer_addr().unwrap() client_socket.peer_addr().unwrap()
); );
let args = Args::parse();
let base_file_name = format!(
"{}-{}-{}",
client_socket.peer_addr().unwrap().ip(),
time(),
conn_id
);
let mut req_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(Path::new(&args.storage_path).join(format!("{base_file_name}-req")))
.expect("Failed to create req file");
let mut res_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(Path::new(&args.storage_path).join(format!("{base_file_name}-res")))
.expect("Failed to create req file");
let mut root_cert_store = RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = ClientConfig::builder()
.with_root_certificates(root_cert_store)
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let dnsname = ServerName::try_from(args.upstream_dns.to_string()).unwrap();
let stream = TcpStream::connect(args.upstream_ip)
.await
.expect("Failed to connect to upstream");
let mut upstream = connector
.connect(dnsname, stream)
.await
.expect("Failed to establish TLS connection");
let (mut client_read, mut client_write) = client_socket.split(); let (mut client_read, mut client_write) = client_socket.split();
let mut buf_client = [0u8; 10000];
let mut buf_client = [0u8; 1024];
let mut buf_server = [0u8; 1024]; let mut buf_server = [0u8; 1024];
let mut modified_headers = false; // Perform first read operation manually to manipulate path and determine target server
let mut total = 0;
let headers_processed = loop {
let count = match client_read.read(&mut buf_client[total..]).await {
Ok(count) => count,
Err(e) => {
log::error!(
"[{conn_id}] Failed to read initial data from client, closing connection! {e}"
);
return;
}
};
if count == 0 {
log::error!("[{conn_id}] read from client count is null, cannot continue!");
let _ = client_write.write_all(ERR_NOT_PROXIFIALBE).await;
return;
}
total += count;
match process_headers(&buf_client[..total]) {
Ok(None) => {
log::debug!("[{conn_id}] Insufficient amount of data, need to continue");
continue;
}
Ok(Some(res)) => break res,
Err(e) => {
log::error!("[{conn_id}] failed to parse initial request headers! {e}");
let _ = client_write.write_all(ERR_NOT_PROXIFIALBE).await;
return;
}
}
};
// Connect to upstream
let mut upstream = match TcpStream::connect(headers_processed.target_host).await {
Ok(upstream) => upstream,
Err(e) => {
log::error!("[{conn_id}] Could not connect to upstream! {e}");
let _ = client_write.write_all(ERR_NOT_PROXIFIALBE).await;
return;
}
};
// Transfer modified headers to upstream
if let Err(e) = upstream.write_all(&headers_processed.buff).await {
log::error!("[{conn_id}] Could not forward initial bytes to upstream! {e}");
let _ = client_write.write_all(ERR_NOT_PROXIFIALBE).await;
return;
}
// Enter in loop to forward remaining data
loop { loop {
tokio::select! { tokio::select! {
count = client_read.read(&mut buf_client) => { count = client_read.read(&mut buf_client) => {
let count = match count{ Ok(count) => count, Err(e) => { let count = match count{ Ok(count) => count, Err(e) => {
log::error!("[{conn_id}] Failed to read data from client, closing connection! {e}"); log::error!("[{conn_id}] Failed to read data from client, closing connection! {e}");
return; break;
}}; }};
log::info!("[{conn_id}] Got a new client read {count} - {base_file_name}"); log::debug!("[{conn_id}] Got a new client read {count}");
if count == 0 { if count == 0 {
log::warn!("[{conn_id}] infinite loop (client), closing connection"); log::warn!("[{conn_id}] infinite loop (client), closing connection");
drop(upstream); drop(upstream);
return; break;
} }
// We need to modify some headers (if not done already) to adapt the request to the server // In case of connection reuse, we need to reanalyze data
let buff = if !modified_headers { if ARGS.path_prefix_header.is_some() &&
let Ok(Some(res))= process_headers(&buf_client[..count])
// Check for URL prefix && let Err(e) = upstream.write_all(&res.buff).await {
if let Some(prefix) = &args.prefix log::error!("[{conn_id}] Failed to write to upstream! {e}");
&& !String::from_utf8_lossy(&buf_client[..count]).split_once('\n').map(|l|l.0).unwrap_or("").contains(&format!(" {prefix}")) { break;
client_write.write_all(b"HTTP/1.1 401 Forbidden\r\n\r\nNot proxifiable.\r\n").await.expect("Failed to respond to client");
client_write.flush().await.expect("Failed to flush response to client!");
return;
} }
modified_headers = true; if let Err(e)=upstream.write_all(&buf_client[..count]).await {
manipulate_headers(&buf_client[..count], &args.upstream_dns) log::error!("[{conn_id}] Failed to write to upstream! {e}");
break;
} }
else {
buf_client[..count].to_vec()
};
upstream.write_all(&buff).await.unwrap_or_else(|_| panic!("[{conn_id}] Failed to write to upstream"));
req_file.write_all(&buff).unwrap_or_else(|_| panic!("[{conn_id}] Failed to write to req"));
} }
count = upstream.read(&mut buf_server) => { count = upstream.read(&mut buf_server) => {
@@ -160,48 +147,88 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(count) => count, Ok(count) => count,
Err(e) => { Err(e) => {
log::error!("[{conn_id}] Failed to read from upstream! {e}"); log::error!("[{conn_id}] Failed to read from upstream! {e}");
return; break;
} }
}; };
if count == 0 { if count == 0 {
log::warn!("[{conn_id}] infinite loop (upstream), closing connection"); log::warn!("[{conn_id}] infinite loop (upstream), closing connection");
drop(upstream); drop(upstream);
return; break;
} }
log::info!("[{conn_id}] Got a new upstream read {count} - {base_file_name}"); log::debug!("[{conn_id}] Got a new upstream read {count}");
client_write.write_all(&buf_server[..count]).await.expect("Failed to write to client"); if let Err(e) = client_write.write_all(&buf_server[..count]).await {
res_file.write_all(&buf_server[..count]).expect("Failed to write to res"); log::error!("Failed to write to upstream! {e}");
break;
};
} }
} }
} }
log::info!("[{conn_id}] Connection finished.");
}); });
} }
} }
fn manipulate_headers(buff: &[u8], host: &str) -> Vec<u8> { struct ProcessHeadersResult {
let mut out = Vec::with_capacity(buff.len()); buff: Vec<u8>,
target_host: String,
}
let mut i = 0; fn process_headers(buff: &[u8]) -> anyhow::Result<Option<ProcessHeadersResult>> {
while i < buff.len() { let mut headers = [httparse::EMPTY_HEADER; 64];
if buff[i] != b'\n' || i + 1 == buff.len() || !buff[i + 1..].starts_with(b"Host:") { let mut req = httparse::Request::new(&mut headers);
out.push(buff[i]); let parsing_res = req.parse(buff)?;
i += 1;
continue; let target_host = headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case(&ARGS.target_host_port_header))
.map(|h| String::from_utf8_lossy(h.value));
log::debug!("Request headers: {:?}", headers);
let Some(target_host) = target_host else {
if parsing_res.is_partial() {
return Ok(None);
} else {
anyhow::bail!(
"Request is complete without header {}",
ARGS.target_host_port_header
)
} }
};
i += 1; // Check if path needs to be prefixed
out.push(b'\n'); let prefix_path = if let Some(hname) = &ARGS.path_prefix_header {
headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case(hname))
.map(|h| String::from_utf8_lossy(h.value).replace(['\n', '\r', '\t', ' '], ""))
} else {
None
};
out.append(&mut format!("Host: {host}").as_bytes().to_vec()); // Perform prefix injection
let mut buff = buff.to_vec();
while buff[i] != b'\r' && buff[i] != b'\n' { if let Some(prefix) = prefix_path {
i += 1; let pos = buff.iter().position(|c| c == &b' ');
log::debug!("Add path prefix to request {prefix}");
if let Some(pos) = pos {
for (num, c) in prefix.as_bytes().iter().enumerate() {
buff.insert(pos + 1 + num, *c);
}
} else {
log::warn!("Unable to inject prefix!");
} }
} }
out log::trace!("Final request: {}", String::from_utf8_lossy(&buff));
Ok(Some(ProcessHeadersResult {
target_host: target_host.to_string(),
buff,
}))
} }
#[cfg(test)] #[cfg(test)]