Skip to content

feat: Add http proxy support #2330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/src/proxy/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// Example: Using Tonic with HTTP Proxy Support
///
/// This example demonstrates how to use the proxy functionality in Tonic.
/// The proxy support includes both explicit proxy configuration and automatic
/// detection from environment variables.

use tonic::transport::Endpoint;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 1: Explicit proxy configuration
let endpoint_with_proxy = Endpoint::from_static("https://httpbin.org/get")
.proxy_uri("http://username:[email protected]:8080".parse()?);

// Example 2: Environment-based proxy detection
let endpoint_with_env_proxy = Endpoint::from_static("https://api.github.com")
.proxy_from_env(true);

// Example 3: Both explicit proxy and environment detection
let endpoint_combined = Endpoint::from_static("http://example.com")
.proxy_uri("http://explicit-proxy.com:3128".parse()?)
.proxy_from_env(true);

// Example 4: Creating a lazy channel (doesn't actually connect)
let _channel = endpoint_with_proxy.connect_lazy();

Ok(())
}
349 changes: 349 additions & 0 deletions tests/integration_tests/tests/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
use integration_tests::pb::{test_server, Input, Output};
use std::{
io::{BufRead, BufReader, Write},
net::{SocketAddr, TcpListener as StdTcpListener},
sync::{Arc, Mutex},
thread,
time::Duration,
};
use tokio::net::TcpListener;
use tonic::{transport::Server, Request, Response, Status};

/// Test environment variable guard that automatically restores original values
#[allow(dead_code)]
struct EnvGuard {
vars: Vec<(String, Option<String>)>,
}

#[allow(dead_code)]
impl EnvGuard {
fn new(var_names: &[&str]) -> Self {
let vars = var_names
.iter()
.map(|name| (name.to_string(), std::env::var(name).ok()))
.collect();
Self { vars }
}

fn set(&self, name: &str, value: &str) {
std::env::set_var(name, value);
}

fn remove(&self, name: &str) {
std::env::remove_var(name);
}
}

impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, original_value) in &self.vars {
match original_value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
}

/// Global mutex to ensure environment variable tests run serially
static ENV_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

struct MockProxy {
port: u16,
connections: Arc<Mutex<Vec<String>>>,
}

impl MockProxy {
fn new() -> Self {
let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let connections = Arc::new(Mutex::new(Vec::new()));
let connections_clone = connections.clone();

// Spawn proxy server in background thread
thread::spawn(move || {
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let connections = connections_clone.clone();
thread::spawn(move || {
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();

if reader.read_line(&mut request_line).is_ok() {
// Log the connection
connections.lock().unwrap().push(request_line.clone());

if request_line.starts_with("CONNECT") {
let _ = stream
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n");
} else {
let _ =
stream.write_all(b"HTTP/1.1 200 OK\r\n\r\nProxy response");
}
}
});
}
Err(_) => break,
}
}
});

// Give the proxy server a moment to start
thread::sleep(Duration::from_millis(100));

Self { port, connections }
}

fn get_proxy_url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}

fn get_connection_logs(&self) -> Vec<String> {
self.connections.lock().unwrap().clone()
}
}

async fn run_test_server() -> SocketAddr {
struct TestService;

#[tonic::async_trait]
impl test_server::Test for TestService {
async fn unary_call(&self, _req: Request<Input>) -> Result<Response<Output>, Status> {
Ok(Response::new(Output {}))
}
}

let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();

let service = TestService;
tokio::spawn(async move {
Server::builder()
.add_service(test_server::TestServer::new(service))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.unwrap();
});

// Give the server a moment to start
tokio::time::sleep(Duration::from_millis(100)).await;
addr
}

#[tokio::test]
async fn test_explicit_http_proxy() {
let proxy = MockProxy::new();
let proxy_url = proxy.get_proxy_url();

let server_addr = run_test_server().await;

let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{server_addr}"))
.unwrap()
.proxy_uri(proxy_url.parse().unwrap());

let channel_result = endpoint.connect().await;

println!("Connection result: {:?}", channel_result.is_ok());
let logs = proxy.get_connection_logs();
println!("Proxy logs: {:?}", logs);

Check warning on line 149 in tests/integration_tests/tests/proxy.rs

View workflow job for this annotation

GitHub Actions / clippy

variables can be used directly in the `format!` string

// Check that the proxy received a connection attempt
// The key test is whether the proxy was contacted, not whether connection failed
if !logs.is_empty() {
println!("Explicit proxy test passed - proxy was contacted");
// Verify that the proxy received an HTTP request
let first_request = &logs[0];
assert!(
first_request.starts_with("GET")
|| first_request.starts_with("POST")
|| first_request.starts_with("CONNECT"),
"Proxy should have received an HTTP request, got: {}",
first_request.trim()
);
} else {
println!("Explicit proxy test failed - proxy was not contacted");
println!("This suggests the proxy configuration is not working properly");
panic!("Proxy should have been contacted but wasn't");
}
}

#[tokio::test]
async fn test_proxy_from_environment() {
// Acquire lock to ensure environment tests don't interfere with each other
let _env_lock = ENV_TEST_MUTEX.lock().unwrap();

Check warning on line 174 in tests/integration_tests/tests/proxy.rs

View workflow job for this annotation

GitHub Actions / clippy

this `MutexGuard` is held across an await point

let _env_guard = EnvGuard::new(&[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
]);

// Clear any existing proxy environment variables
for var in &[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
] {
std::env::remove_var(var);
}

let proxy = MockProxy::new();
let proxy_url = proxy.get_proxy_url();

std::env::set_var("http_proxy", &proxy_url);

let server_addr = run_test_server().await;

let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{server_addr}"))
.unwrap()
.proxy_from_env(true);

// Attempt to connect (may succeed or fail, but proxy should be contacted)
let _channel_result = endpoint.connect().await;

// Check that the proxy received a connection
let logs = proxy.get_connection_logs();

assert!(
!logs.is_empty(),
"Proxy should have received at least one connection from environment config"
);

// Verify that the proxy received a CONNECT request (for HTTPS) or other HTTP request
let first_request = &logs[0];
assert!(
first_request.starts_with("CONNECT")
|| first_request.starts_with("GET")
|| first_request.starts_with("POST"),
"Proxy should have received an HTTP request, got: {}",
first_request.trim()
);
}

#[tokio::test]
async fn test_no_proxy_bypass() {
// Acquire lock to ensure environment tests don't interfere with each other
let _env_lock = ENV_TEST_MUTEX.lock().unwrap();

Check warning on line 233 in tests/integration_tests/tests/proxy.rs

View workflow job for this annotation

GitHub Actions / clippy

this `MutexGuard` is held across an await point

let _env_guard = EnvGuard::new(&[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
]);

// Clear any existing proxy environment variables
for var in &[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
] {
std::env::remove_var(var);
}

let proxy = MockProxy::new();
let proxy_url = proxy.get_proxy_url();

std::env::set_var("http_proxy", &proxy_url);
std::env::set_var("no_proxy", "127.0.0.1,localhost");

let server_addr = run_test_server().await;

let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{server_addr}"))
.unwrap()
.proxy_from_env(true);

// This should attempt a direct connection since 127.0.0.1 is in no_proxy
let _channel_result = endpoint.connect().await;

// The connection might succeed or fail, but the proxy should NOT be contacted
let _logs = proxy.get_connection_logs();

// Since we're connecting to 127.0.0.1 and it's in no_proxy, the proxy should not be used
// Note: This is a bit tricky to test perfectly since even failed direct connections
// won't show up in proxy logs, which is what we want
}

#[tokio::test]
async fn test_proxy_precedence() {
// Acquire lock to ensure environment tests don't interfere with each other
let _env_lock = ENV_TEST_MUTEX.lock().unwrap();

Check warning on line 282 in tests/integration_tests/tests/proxy.rs

View workflow job for this annotation

GitHub Actions / clippy

this `MutexGuard` is held across an await point

let _env_guard = EnvGuard::new(&[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
]);

// Clear any existing proxy environment variables
for var in &[
"http_proxy",
"HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"no_proxy",
"NO_PROXY",
] {
std::env::remove_var(var);
}

let proxy = MockProxy::new();
let env_proxy_url = proxy.get_proxy_url();

let explicit_proxy = MockProxy::new();
let explicit_proxy_url = explicit_proxy.get_proxy_url();

std::env::set_var("http_proxy", &env_proxy_url);

let server_addr = run_test_server().await;

let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{server_addr}"))
.unwrap()
.proxy_uri(explicit_proxy_url.parse().unwrap())
.proxy_from_env(true);

// Attempt to connect (may succeed or fail, but explicit proxy should be contacted)
let _channel_result = endpoint.connect().await;

// Check that the explicit proxy received the connection, not the env proxy
let explicit_logs = explicit_proxy.get_connection_logs();
let _env_logs = proxy.get_connection_logs();

assert!(
!explicit_logs.is_empty(),
"Explicit proxy should have received connection"
);
// Note: env proxy might still get connections due to timing, but explicit should be used
}

#[tokio::test]
async fn test_proxy_configuration_methods() {
// Test that proxy configuration methods can be chained and don't panic
let server_addr = run_test_server().await;

// Test method chaining
let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{}", server_addr))

Check warning on line 340 in tests/integration_tests/tests/proxy.rs

View workflow job for this annotation

GitHub Actions / clippy

variables can be used directly in the `format!` string
.unwrap()
.proxy_uri("http://proxy.example.com:8080".parse().unwrap())
.proxy_from_env(true)
.timeout(Duration::from_secs(5));

assert_eq!(endpoint.uri().to_string(), format!("http://{server_addr}/"));

let _channel = endpoint.connect_lazy();
}
Loading
Loading