
https://github.com/zed-industries/zed/issues/30972 brought up another case where our context is not enough to track the actual source of the issue: we get a general top-level error without inner error. The reason for this was `.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?; ` on the top level. The PR finally reworks the way we use anyhow to reduce such issues (or at least make it simpler to bubble them up later in a fix). On top of that, uses a few more anyhow methods for better readability. * `.ok_or_else(|| anyhow!("..."))`, `map_err` and other similar error conversion/option reporting cases are replaced with `context` and `with_context` calls * in addition to that, various `anyhow!("failed to do ...")` are stripped with `.context("Doing ...")` messages instead to remove the parasitic `failed to` text * `anyhow::ensure!` is used instead of `if ... { return Err(...); }` calls * `anyhow::bail!` is used instead of `return Err(anyhow!(...));` Release Notes: - N/A
176 lines
5.7 KiB
Rust
176 lines
5.7 KiB
Rust
//! socks proxy
|
|
use anyhow::{Context as _, Result};
|
|
use http_client::Url;
|
|
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
|
|
|
/// Identification to a Socks V4 Proxy
|
|
struct Socks4Identification<'a> {
|
|
user_id: &'a str,
|
|
}
|
|
|
|
/// Authorization to a Socks V5 Proxy
|
|
struct Socks5Authorization<'a> {
|
|
username: &'a str,
|
|
password: &'a str,
|
|
}
|
|
|
|
/// Socks Proxy Protocol Version
|
|
///
|
|
/// V4 allows idenfication using a user_id
|
|
/// V5 allows authorization using a username and password
|
|
enum SocksVersion<'a> {
|
|
V4(Option<Socks4Identification<'a>>),
|
|
V5(Option<Socks5Authorization<'a>>),
|
|
}
|
|
|
|
pub(crate) async fn connect_socks_proxy_stream(
|
|
proxy: &Url,
|
|
rpc_host: (&str, u16),
|
|
) -> Result<Box<dyn AsyncReadWrite>> {
|
|
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
|
|
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
|
// SOCKS proxies are often used in contexts where security and privacy are critical,
|
|
// so any fallback could expose users to significant risks.
|
|
anyhow::bail!("Parsing proxy url failed");
|
|
};
|
|
|
|
// Connect to proxy and wrap protocol later
|
|
let stream = tokio::net::TcpStream::connect(socks_proxy)
|
|
.await
|
|
.context("Failed to connect to socks proxy")?;
|
|
|
|
let socks: Box<dyn AsyncReadWrite> = match version {
|
|
SocksVersion::V4(None) => {
|
|
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
|
|
.await
|
|
.context("error connecting to socks")?;
|
|
Box::new(socks)
|
|
}
|
|
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
|
|
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
|
|
.await
|
|
.context("error connecting to socks")?;
|
|
Box::new(socks)
|
|
}
|
|
SocksVersion::V5(None) => {
|
|
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
|
|
.await
|
|
.context("error connecting to socks")?;
|
|
Box::new(socks)
|
|
}
|
|
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
|
|
let socks = Socks5Stream::connect_with_password_and_socket(
|
|
stream, rpc_host, username, password,
|
|
)
|
|
.await
|
|
.context("error connecting to socks")?;
|
|
Box::new(socks)
|
|
}
|
|
};
|
|
|
|
Ok(socks)
|
|
}
|
|
|
|
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
|
|
let scheme = proxy.scheme();
|
|
let socks_version = if scheme.starts_with("socks4") {
|
|
let identification = match proxy.username() {
|
|
"" => None,
|
|
username => Some(Socks4Identification { user_id: username }),
|
|
};
|
|
SocksVersion::V4(identification)
|
|
} else if scheme.starts_with("socks") {
|
|
let authorization = proxy.password().map(|password| Socks5Authorization {
|
|
username: proxy.username(),
|
|
password,
|
|
});
|
|
SocksVersion::V5(authorization)
|
|
} else {
|
|
return None;
|
|
};
|
|
|
|
let host = proxy.host()?.to_string();
|
|
let port = proxy.port_or_known_default()?;
|
|
|
|
Some(((host, port), socks_version))
|
|
}
|
|
|
|
pub(crate) trait AsyncReadWrite:
|
|
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
|
|
{
|
|
}
|
|
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
|
|
for T
|
|
{
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use url::Url;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_socks4() {
|
|
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
|
|
|
|
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
|
assert_eq!(host, "proxy.example.com");
|
|
assert_eq!(port, 1080);
|
|
assert!(matches!(version, SocksVersion::V4(None)))
|
|
}
|
|
|
|
#[test]
|
|
fn parse_socks4_with_identification() {
|
|
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
|
|
|
|
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
|
assert_eq!(host, "proxy.example.com");
|
|
assert_eq!(port, 1080);
|
|
assert!(matches!(
|
|
version,
|
|
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn parse_socks5() {
|
|
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
|
|
|
|
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
|
assert_eq!(host, "proxy.example.com");
|
|
assert_eq!(port, 1080);
|
|
assert!(matches!(version, SocksVersion::V5(None)))
|
|
}
|
|
|
|
#[test]
|
|
fn parse_socks5_with_authorization() {
|
|
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
|
|
|
|
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
|
assert_eq!(host, "proxy.example.com");
|
|
assert_eq!(port, 1080);
|
|
assert!(matches!(
|
|
version,
|
|
SocksVersion::V5(Some(Socks5Authorization {
|
|
username: "username",
|
|
password: "password"
|
|
}))
|
|
))
|
|
}
|
|
|
|
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
|
/// SOCKS proxies are often used in contexts where security and privacy are critical,
|
|
/// so any fallback could expose users to significant risks.
|
|
#[tokio::test]
|
|
async fn fails_on_bad_proxy() {
|
|
// Should fail connecting because http is not a valid Socks proxy scheme
|
|
let proxy = Url::parse("http://localhost:2313").unwrap();
|
|
|
|
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
|
|
match result {
|
|
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
|
|
Ok(_) => panic!("Connecting on bad proxy should fail"),
|
|
};
|
|
}
|
|
}
|