Compare commits

...

3 Commits

Author SHA1 Message Date
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
5 changed files with 156 additions and 66 deletions

14
Cargo.lock generated
View File

@@ -61,6 +61,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "bitflags"
version = "2.10.0"
@@ -164,8 +170,10 @@ dependencies = [
name = "header_proxy"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"env_logger",
"httparse",
"lazy_static",
"log",
"rand",
@@ -178,6 +186,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"

View File

@@ -10,3 +10,5 @@ clap = { version = "4.5.53", features = ["env", "derive"] }
tokio = { version = "1.48.0", features = ["full"] }
rand = "0.9.2"
lazy_static = "1.5.0"
httparse = "1.10.1"
anyhow = "1.0.100"

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"]

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,33 +1,25 @@
use clap::Parser;
use rand::distr::{Alphanumeric, SampleString};
use rustls_pki_types::ServerName;
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::net::TcpListener;
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
/// Simple program that redirect requests to a given server in request
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// 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,
/// The name of the header that contain target host and port
#[arg(short, long, default_value = "x-target-host")]
#[arg(short, long, env, default_value = "x-target-host")]
target_host_port_header: String,
/// Name of optional header that contains path to add to the request
#[arg(short, long, default_value = "x-path-prefix")]
path_prefix_heder: String,
#[arg(short, long, env)]
path_prefix_header: Option<String>,
}
lazy_static::lazy_static! {
@@ -56,61 +48,87 @@ async fn main() -> Result<(), Box<dyn Error>> {
tokio::spawn(async move {
let conn_id = rand_str(5);
log::debug!(
"Handle new connection from {}",
log::info!(
"[{conn_id}] Handle new connection from {}",
client_socket.peer_addr().unwrap()
);
let stream = TcpStream::connect(TODO)
.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 buf_client = [0u8; 1024];
let mut buf_client = [0u8; 10000];
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 {
tokio::select! {
count = client_read.read(&mut buf_client) => {
let count = match count{ Ok(count) => count, Err(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 {
log::warn!("[{conn_id}] infinite loop (client), closing connection");
drop(upstream);
return;
break;
}
// We need to modify some headers (if not done already) to adapt the request to the server
let buff = if !modified_headers {
// Check for URL prefix
if let Some(prefix) = &args.prefix
&& !String::from_utf8_lossy(&buf_client[..count]).split_once('\n').map(|l|l.0).unwrap_or("").contains(&format!(" {prefix}")) {
client_write.write_all(ERR_NOT_PROXIFIABLE).await.expect("Failed to respond to client");
client_write.flush().await.expect("Failed to flush response to client!");
return;
}
modified_headers = true;
manipulate_headers(&buf_client[..count], &args.upstream_dns)
if let Err(e)=upstream.write_all(&buf_client[..count]).await {
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"));
}
count = upstream.read(&mut buf_server) => {
@@ -118,47 +136,83 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(count) => count,
Err(e) => {
log::error!("[{conn_id}] Failed to read from upstream! {e}");
return;
break;
}
};
if count == 0 {
log::warn!("[{conn_id}] infinite loop (upstream), closing connection");
drop(upstream);
return;
break;
}
log::info!("[{conn_id}] Got a new upstream read {count} - {base_file_name}");
client_write.write_all(&buf_server[..count]).await.expect("Failed to write to client");
log::debug!("[{conn_id}] Got a new upstream read {count}");
if let Err(e) = client_write.write_all(&buf_server[..count]).await {
log::error!("Failed to write to upstream! {e}");
break;
};
}
}
}
log::info!("[{conn_id}] Connection finished.");
});
}
}
fn manipulate_headers(buff: &[u8], host: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(buff.len());
struct ProcessHeadersResult {
buff: Vec<u8>,
target_host: String,
}
let mut i = 0;
while i < buff.len() {
if buff[i] != b'\n' || i + 1 == buff.len() || !buff[i + 1..].starts_with(b"Host:") {
out.push(buff[i]);
i += 1;
continue;
fn process_headers(buff: &[u8]) -> anyhow::Result<Option<ProcessHeadersResult>> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut req = httparse::Request::new(&mut headers);
let parsing_res = req.parse(buff)?;
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;
out.push(b'\n');
// Check if path needs to be prefixed
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());
while buff[i] != b'\r' && buff[i] != b'\n' {
i += 1;
// Perform prefix injection
let mut buff = buff.to_vec();
if let Some(prefix) = prefix_path {
let pos = buff.iter().position(|c| c == &b' ');
if let Some(pos) = pos {
for (num, c) in prefix.as_bytes().iter().enumerate() {
buff.insert(pos + 1 + num, *c);
}
}
}
out
Ok(Some(ProcessHeadersResult {
target_host: target_host.to_string(),
buff,
}))
}
#[cfg(test)]