blob: 9ba08f9d5d0acb1d3eeb0f16bd0cbf20c4366e72 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::Policy;
use base64::Engine;
use maka_sandbox::Destination;
use rustls_platform_verifier::BuilderVerifierExt;
use std::{io, sync::Arc, time::Duration};
use tokio::{
io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader},
net::TcpStream,
};
use tokio_rustls::{TlsConnector, rustls};
pub(crate) trait Stream: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> Stream for T {}
pub(crate) type Tunnel = Box<dyn Stream>;
/// Routing is captured by the caller; neither environment variables nor a
/// failed proxy connection can select a different route.
pub(crate) async fn connect(policy: &Policy, destination: &Destination) -> io::Result<Tunnel> {
tokio::time::timeout(Duration::from_secs(15), connect_inner(policy, destination))
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "proxy connection timed out"))?
}
async fn connect_inner(policy: &Policy, destination: &Destination) -> io::Result<Tunnel> {
let target = (destination.host(), destination.port());
// CONNECT carries no URL scheme. Like HTTPS transport, it uses HTTPS_PROXY
// (then ALL_PROXY), including tunnels to nonstandard destination ports.
let Some(proxy) = policy.proxy_for("https", destination.host()) else {
return Ok(Box::new(TcpStream::connect(target).await?));
};
let host = proxy
.host_str()
.ok_or_else(|| io::Error::other("proxy host missing"))?
.trim_matches(['[', ']']);
let port = proxy
.port_or_known_default()
.ok_or_else(|| io::Error::other("proxy port missing"))?;
let user = decode(proxy.username())?;
let password = decode(proxy.password().unwrap_or_default())?;
if matches!(proxy.scheme(), "socks5" | "socks5h") {
let target = if proxy.scheme() == "socks5" {
tokio_socks::TargetAddr::Ip(
tokio::net::lookup_host(target)
.await?
.next()
.ok_or_else(|| io::Error::other("SOCKS destination resolution failed"))?,
)
} else {
tokio_socks::IntoTargetAddr::into_target_addr(target).map_err(io::Error::other)?
};
let stream = if user.is_empty() {
tokio_socks::tcp::Socks5Stream::connect((host, port), target).await
} else {
tokio_socks::tcp::Socks5Stream::connect_with_password(
(host, port),
target,
&user,
&password,
)
.await
}
.map_err(|_| io::Error::other("upstream SOCKS connection failed"))?;
return Ok(Box::new(stream));
}
let tcp = TcpStream::connect((host, port)).await?;
let mut stream: Tunnel = match proxy.scheme() {
"http" => Box::new(tcp),
"https" => {
let config = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()
.map_err(io::Error::other)?
.with_platform_verifier()
.map_err(io::Error::other)?
.with_no_client_auth();
let name = rustls::pki_types::ServerName::try_from(host.to_owned())
.map_err(io::Error::other)?;
Box::new(
TlsConnector::from(Arc::new(config))
.connect(name, tcp)
.await?,
)
}
_ => return Err(io::Error::other("unsupported upstream proxy")),
};
let authority = authority(destination);
let mut request = format!("CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\n");
if !user.is_empty() {
let credentials =
base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}"));
request.push_str(&format!("Proxy-Authorization: Basic {credentials}\r\n"));
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
// Preserve read-ahead after the header boundary without a syscall per byte.
let mut stream = BufReader::new(stream);
let mut head = Vec::new();
{
let mut bounded = (&mut stream).take(16 * 1024);
loop {
if bounded.read_until(b'\n', &mut head).await? == 0 {
return Err(io::Error::other(
"incomplete or oversized upstream proxy headers",
));
}
if head.ends_with(b"\r\n\r\n") {
break;
}
}
}
let status = head.split(|b| *b == b'\n').next().unwrap_or_default();
if !(status.starts_with(b"HTTP/1.1 200 ")
|| status.starts_with(b"HTTP/1.0 200 ")
|| status == b"HTTP/1.1 200\r"
|| status == b"HTTP/1.0 200\r")
{
return Err(io::Error::other("upstream proxy refused tunnel"));
}
Ok(Box::new(stream))
}
fn decode(value: &str) -> io::Result<String> {
percent_encoding::percent_decode_str(value)
.decode_utf8()
.map(|v| v.into_owned())
.map_err(io::Error::other)
}
pub(crate) fn authority(destination: &Destination) -> String {
if destination.host().contains(':') {
format!("[{}]:{}", destination.host(), destination.port())
} else {
format!("{}:{}", destination.host(), destination.port())
}
}