use std::time::Duration;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
use crate::error::McError;
use super::ProxyAuth;
pub(crate) async fn connect(
proxy_addr: &str,
target_host: &str,
target_port: u16,
auth: Option<&ProxyAuth>,
tout: Duration,
) -> Result<TcpStream, McError> {
let mut stream = timeout(tout, TcpStream::connect(proxy_addr))
.await
.map_err(|_| McError::timeout())?
.map_err(|e| McError::connection(e.to_string()))?;
stream.set_nodelay(true).map_err(McError::Io)?;
let target = format!("{target_host}:{target_port}");
let mut req = format!(
"CONNECT {target} HTTP/1.1\r\nHost: {target}\r\n"
);
if let Some(a) = auth {
let credentials = format!("{}:{}", a.username, a.password);
let encoded = B64.encode(credentials.as_bytes());
req.push_str(&format!("Proxy-Authorization: Basic {encoded}\r\n"));
}
req.push_str("\r\n");
timeout(tout, stream.write_all(req.as_bytes()))
.await
.map_err(|_| McError::timeout())?
.map_err(McError::Io)?;
let response = read_response(&mut stream, tout).await?;
validate_connect_response(&response)?;
Ok(stream)
}
async fn read_response(stream: &mut TcpStream, tout: Duration) -> Result<String, McError> {
let mut buf = Vec::with_capacity(256);
let mut byte = [0u8; 1];
loop {
match timeout(tout, stream.read_exact(&mut byte)).await {
Err(_) => return Err(McError::timeout()),
Ok(Err(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
let partial = String::from_utf8_lossy(&buf).into_owned();
return Err(McError::proxy_error(format!(
"proxy closed connection after {} bytes (partial response: {:?})",
partial.len(),
partial.lines().next().unwrap_or("(empty)")
)));
}
Ok(Err(e)) => return Err(McError::Io(e)),
Ok(Ok(_)) => {}
}
buf.push(byte[0]);
if buf.ends_with(b"\r\n\r\n") {
break;
}
if buf.len() > 4096 {
return Err(McError::proxy_error("HTTP CONNECT response exceeds 4096 bytes"));
}
}
String::from_utf8(buf)
.map_err(|_| McError::proxy_error("HTTP CONNECT response is not valid UTF-8"))
}
pub(crate) fn validate_connect_response(response: &str) -> Result<(), McError> {
let status_line = response.lines().next().unwrap_or("");
let mut parts = status_line.splitn(3, ' ');
parts.next();
let code: u16 = parts
.next()
.and_then(|s| s.parse().ok())
.ok_or_else(|| McError::proxy_error(
format!("could not parse HTTP CONNECT response: {status_line}")
))?;
if (200..300).contains(&code) {
Ok(())
} else {
let reason = parts.next().unwrap_or("").trim();
Err(McError::proxy_error(format!(
"HTTP CONNECT failed with {code} {reason}"
)))
}
}
#[doc(hidden)]
pub fn validate_response_test(response: &str) -> Result<(), crate::McError> {
validate_connect_response(response)
}