use std::{
io,
net::SocketAddr,
sync::atomic::{AtomicU64, Ordering},
};
use compio::{
BufResult,
io::{AsyncRead, AsyncWriteExt},
net::{TcpListener, TcpStream},
runtime::Runtime,
};
use compio_pool::{LocalPool, ManageConnection, Pool};
static ORIGIN_CONN_ID: AtomicU64 = AtomicU64::new(0);
struct Framed {
stream: TcpStream,
buf: Vec<u8>,
healthy: bool,
}
impl Framed {
fn new(stream: TcpStream) -> Self {
Self {
stream,
buf: Vec::with_capacity(8 * 1024),
healthy: true,
}
}
async fn read_more(&mut self) -> io::Result<bool> {
if self.buf.len() == self.buf.capacity() {
self.buf.reserve(8 * 1024);
}
let taken = std::mem::take(&mut self.buf);
let BufResult(res, b) = self.stream.read(taken).await;
self.buf = b;
match res {
Ok(0) => Ok(false),
Ok(_) => Ok(true),
Err(e) => Err(e),
}
}
async fn read_message(&mut self) -> io::Result<Option<Vec<u8>>> {
let head_end = loop {
if let Some(pos) = find(&self.buf, b"\r\n\r\n") {
break pos + 4;
}
if !self.read_more().await? {
return Ok(None);
}
};
let total = head_end + content_length(&self.buf[..head_end]);
while self.buf.len() < total {
if !self.read_more().await? {
return Ok(None); }
}
Ok(Some(self.buf.drain(..total).collect()))
}
async fn write_all_bytes(&mut self, bytes: Vec<u8>) -> io::Result<()> {
let BufResult(res, _) = self.stream.write_all(bytes).await;
res
}
}
struct Origin {
addr: SocketAddr,
}
impl ManageConnection for Origin {
type Connection = Framed;
type Error = io::Error;
async fn connect(&self) -> io::Result<Framed> {
let stream = TcpStream::connect(self.addr).await?;
stream.set_nodelay(true)?;
Ok(Framed::new(stream))
}
async fn is_valid(&self, conn: &mut Framed) -> io::Result<()> {
if conn.healthy {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "upstream marked dead"))
}
}
fn has_broken(&self, conn: &mut Framed) -> bool {
!conn.healthy
}
}
fn main() -> io::Result<()> {
let listen: SocketAddr = std::env::args()
.nth(1)
.unwrap_or_else(|| "127.0.0.1:8080".into())
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad listen addr: {e}")))?;
let capacity: u32 = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(16);
let runtime = Runtime::builder().build().expect("build compio runtime");
runtime.block_on(async move {
let origin_addr = match std::env::var("ORIGIN") {
Ok(s) => s.parse().map_err(|e| {
io::Error::new(io::ErrorKind::InvalidInput, format!("bad ORIGIN: {e}"))
})?,
Err(_) => {
let any: SocketAddr = "127.0.0.1:0".parse().unwrap();
let origin = TcpListener::bind(any).await?;
let addr = origin.local_addr()?;
compio::runtime::spawn(origin_loop(origin)).detach();
addr
}
};
let pool = Pool::builder()
.max_size(capacity)
.build(Origin { addr: origin_addr });
let local = pool.local();
let proxy = TcpListener::bind(listen).await?;
println!(
"http_proxy on {listen} -> origin {origin_addr}: 1 runtime, up to {capacity} pooled \
upstreams"
);
println!("try: for i in $(seq 5); do curl -si http://{listen}/ | grep -i x-upstream; done");
loop {
let Ok((stream, _peer)) = proxy.accept().await else {
continue;
};
let _ = stream.set_nodelay(true);
compio::runtime::spawn(handle_client(local.clone(), stream)).detach();
}
})
}
async fn handle_client(local: LocalPool<Origin>, client_stream: TcpStream) {
let mut client = Framed::new(client_stream);
loop {
let request = match client.read_message().await {
Ok(Some(req)) => req,
_ => return, };
let mut up = match local.get().await {
Ok(up) => up,
Err(_) => {
let _ = client.write_all_bytes(bad_gateway("no upstream available")).await;
return;
}
};
let response = if up.write_all_bytes(request).await.is_ok() {
up.read_message().await
} else {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "upstream write failed"))
};
match response {
Ok(Some(resp)) => {
drop(up); if client.write_all_bytes(resp).await.is_err() {
return;
}
}
_ => {
up.healthy = false; let _ = client.write_all_bytes(bad_gateway("upstream error")).await;
return;
}
}
}
}
async fn origin_loop(listener: TcpListener) {
loop {
let Ok((stream, _peer)) = listener.accept().await else {
continue;
};
let _ = stream.set_nodelay(true);
let id = ORIGIN_CONN_ID.fetch_add(1, Ordering::Relaxed);
compio::runtime::spawn(origin_conn(stream, id)).detach();
}
}
async fn origin_conn(stream: TcpStream, id: u64) {
let mut conn = Framed::new(stream);
let mut served: u64 = 0;
loop {
match conn.read_message().await {
Ok(Some(_req)) => {
served += 1;
let body = format!("served by upstream connection #{id}, request #{served}\n");
let resp = format!(
"HTTP/1.1 200 OK\r\n\
content-type: text/plain\r\n\
content-length: {}\r\n\
x-upstream-conn: {id}\r\n\
x-upstream-reqs: {served}\r\n\
connection: keep-alive\r\n\
\r\n\
{body}",
body.len()
);
if conn.write_all_bytes(resp.into_bytes()).await.is_err() {
return;
}
}
_ => return,
}
}
}
fn bad_gateway(why: &str) -> Vec<u8> {
let body = format!("502 Bad Gateway: {why}\n");
format!(
"HTTP/1.1 502 Bad Gateway\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
)
.into_bytes()
}
fn content_length(head: &[u8]) -> usize {
let text = String::from_utf8_lossy(head).to_ascii_lowercase();
for line in text.split("\r\n") {
if let Some(value) = line.strip_prefix("content-length:") {
return value.trim().parse().unwrap_or(0);
}
}
0
}
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || hay.len() < needle.len() {
return None;
}
hay.windows(needle.len()).position(|w| w == needle)
}