use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::time::Duration;
fn main() {
if let Err(e) = relay() {
eprintln!("stdio_transport: {e}");
std::process::exit(1);
}
}
fn relay() -> io::Result<()> {
let mut request = Vec::new();
io::stdin().read_to_end(&mut request)?;
let (head, body) = split_head(&request)?;
let mut lines = head.lines();
let start = lines.next().unwrap_or_default();
let url = start
.split_whitespace()
.nth(1)
.ok_or_else(|| io::Error::other("request line carries no target"))?;
let (authority, path) = split_url(url)?;
let secs = |name: &str| {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.map(Duration::from_secs)
};
let mut socket = match secs("BZ_TRANSPORT_CONNECT_TIMEOUT") {
Some(budget) => {
let addr = std::net::ToSocketAddrs::to_socket_addrs(&authority)?
.next()
.ok_or_else(|| io::Error::other("host resolved to no address"))?;
TcpStream::connect_timeout(&addr, budget)?
}
None => TcpStream::connect(&authority)?,
};
socket.set_read_timeout(secs("BZ_TRANSPORT_IDLE_TIMEOUT"))?;
let mut out = format!("{} {path} HTTP/1.1\r\nHost: {authority}\r\n", verb(start)).into_bytes();
for line in lines.filter(|l| !l.is_empty()) {
out.extend_from_slice(line.as_bytes());
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
out.extend_from_slice(b"Connection: close\r\n\r\n");
out.extend_from_slice(body);
socket.write_all(&out)?;
socket.flush()?;
let mut stdout = io::stdout();
let mut buf = [0u8; 8192];
loop {
match socket.read(&mut buf)? {
0 => return stdout.flush(),
n => {
stdout.write_all(&buf[..n])?;
stdout.flush()?;
}
}
}
}
fn verb(start: &str) -> &str {
start.split_whitespace().next().unwrap_or("GET")
}
fn split_head(msg: &[u8]) -> io::Result<(String, &[u8])> {
let lf = msg.windows(2).position(|w| w == b"\n\n").map(|i| i + 2);
let crlf = msg.windows(4).position(|w| w == b"\r\n\r\n").map(|i| i + 4);
let end = lf
.into_iter()
.chain(crlf)
.min()
.ok_or_else(|| io::Error::other("request head has no blank line"))?;
Ok((
String::from_utf8_lossy(&msg[..end]).into_owned(),
&msg[end..],
))
}
fn split_url(url: &str) -> io::Result<(String, String)> {
let rest = url
.strip_prefix("http://")
.ok_or_else(|| io::Error::other(format!("only http:// targets are relayed: {url}")))?;
let (authority, path) = match rest.find('/') {
Some(i) => (&rest[..i], &rest[i..]),
None => (rest, "/"),
};
let authority = match authority.contains(':') {
true => authority.to_owned(),
false => format!("{authority}:80"),
};
Ok((authority, path.to_owned()))
}