diff --git a/Cargo.toml b/Cargo.toml index aa343467..819d6a65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,11 @@ name = "hello" path = "examples/hello.rs" required-features = ["runtime"] +[[example]] +name = "http_proxy" +path = "examples/http_proxy.rs" +required-features = ["runtime"] + [[example]] name = "multi_server" path = "examples/multi_server.rs" diff --git a/examples/README.md b/examples/README.md index 6635d04a..33a7b7be 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,6 +13,8 @@ parses it with serde and outputs the result. * [`hello`](hello.rs) - A simple server that returns "Hello World!" using a closure wrapped to provide a [`Service`](../src/service/service.rs). +* [`http_proxy`](http_proxy.rs) - A simple HTTP(S) proxy that handle and upgrade CONNECT request and then proxy data between client and remote server. + * [`multi_server`](multi_server.rs) - A server that listens to two different ports, a different [`Service`](../src/service/service.rs) by port, spawning two [`futures`](../src/rt.rs). * [`params`](params.rs) - A webserver that accept a form, with a name and a number, checks the parameters are presents and validates the input. diff --git a/examples/http_proxy.rs b/examples/http_proxy.rs new file mode 100644 index 00000000..1695d1a1 --- /dev/null +++ b/examples/http_proxy.rs @@ -0,0 +1,118 @@ +#![deny(warnings)] + +use std::convert::Infallible; +use std::net::SocketAddr; + +use futures_util::future::try_join; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::upgrade::Upgraded; +use hyper::{Body, Client, Method, Request, Response, Server}; + +use tokio::net::TcpStream; + +type HttpClient = Client; + +// To try this example: +// 1. cargo run --example http_proxy +// 2. config http_proxy in command line +// $ export http_proxy=http://127.0.0.1:8100 +// $ export https_proxy=http://127.0.0.1:8100 +// 3. send requests +// $ curl -i https://www.some_domain.com/ +#[tokio::main] +async fn main() { + let addr = SocketAddr::from(([127, 0, 0, 1], 8100)); + let client = HttpClient::new(); + + let make_service = make_service_fn(move |_| { + let client = client.clone(); + async move { Ok::<_, Infallible>(service_fn(move |req| proxy(client.clone(), req))) } + }); + + let server = Server::bind(&addr).serve(make_service); + + println!("Listening on http://{}", addr); + + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } +} + +async fn proxy(client: HttpClient, req: Request) -> Result, hyper::Error> { + println!("req: {:?}", req); + + if Method::CONNECT == req.method() { + // Recieved an HTTP request like: + // ``` + // CONNECT www.domain.com:443 HTTP/1.1 + // Host: www.domain.com:443 + // Proxy-Connection: Keep-Alive + // ``` + // + // When HTTP method is CONNECT we should return an empty body + // then we can eventually upgrade the connection and talk a new protocol. + // + // Note: only after client recieved an empty body with STATUS_OK can the + // connection be upgraded, so we can't return a response inside + // `on_upgrade` future. + if let Some(addr) = host_addr(req.uri()) { + tokio::task::spawn(async move { + match req.into_body().on_upgrade().await { + Ok(upgraded) => { + if let Err(e) = tunnel(upgraded, addr).await { + eprintln!("server io error: {}", e); + }; + } + Err(e) => eprintln!("upgrade error: {}", e), + } + }); + + Ok(Response::new(Body::empty())) + } else { + eprintln!("CONNECT host is not socket addr: {:?}", req.uri()); + let mut resp = Response::new(Body::from("CONNECT must be to a socket address")); + *resp.status_mut() = http::StatusCode::BAD_REQUEST; + + Ok(resp) + } + } else { + client.request(req).await + } +} + +fn host_addr(uri: &http::Uri) -> Option { + uri.authority().and_then(|auth| auth.as_str().parse().ok()) +} + +// Create a TCP connection to host:port, build a tunnel between the connection and +// the upgraded connection +async fn tunnel(upgraded: Upgraded, addr: SocketAddr) -> std::io::Result<()> { + // Connect to remote server + let mut server = TcpStream::connect(addr).await?; + + // Proxying data + let amounts = { + let (mut server_rd, mut server_wr) = server.split(); + let (mut client_rd, mut client_wr) = tokio::io::split(upgraded); + + let client_to_server = tokio::io::copy(&mut client_rd, &mut server_wr); + let server_to_client = tokio::io::copy(&mut server_rd, &mut client_wr); + + try_join(client_to_server, server_to_client).await + }; + + // Print message when done + match amounts { + Ok((from_client, from_server)) => { + println!( + "client wrote {} bytes and received {} bytes", + from_client, from_server + ); + } + Err(e) => { + println!("tunnel error: {}", e); + } + }; + Ok(()) +}