From 8a89fcb2c26beefd0e04d565fa0988c197af8f76 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 24 Jun 2022 19:33:46 -0400 Subject: [PATCH] wip --- Cargo.toml | 5 +- config-example.toml | 14 +-- src/backend.rs | 2 +- src/config.rs | 6 +- src/main.rs | 6 +- src/proxy/proxy_handler.rs | 203 +++++++++++++++++++++++++------------ src/proxy/proxy_main.rs | 14 +-- 7 files changed, 153 insertions(+), 97 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68f94c8..d000dbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ publish = false [features] default = ["tls"] tls = ["tokio-rustls", "rustls-pemfile"] -forward-hyper-trust-dns = ["hyper-trust-dns"] [dependencies] anyhow = "1.0.57" @@ -46,9 +45,9 @@ hyper-trust-dns = { version = "0.4.2", default-features = false, features = [ "dnssec-ring", "dns-over-https-rustls", "rustls-webpki", -], optional = true } -hyper-tls = "0.5.0" +] } rustls = "0.20.6" +# phf = { version = "0.10", features = ["macros"] } [dev-dependencies] diff --git a/config-example.toml b/config-example.toml index 38cb49e..7291c48 100644 --- a/config-example.toml +++ b/config-example.toml @@ -13,13 +13,13 @@ https_port = 8443 # Backend settings # ################################### -[[backend]] +[[application]] app_name = 'localhost' # this should be option, if null then same as hostname hostname = 'localhost' -redirect_to_https = true +https_redirection = true reverse_proxy = [ - { path = '*', destination = 'https://192.168.10.0:3000' }, - { path = '/path/to', destination = 'https://192.168.10.1:4000/path/to' }, + { path = '*', destination = '192.168.10.0:3000', tls = true }, + { path = '/path/to', destination = '192.168.10.1:4000', tls = true }, ] ## List of destinations to send data to. ## At this point, round-robin is used for load-balancing if multiple URLs are specified. @@ -29,10 +29,10 @@ tls_cert_path = 'localhost1.pem' tls_cert_key_path = 'localhost1.pem' -[[backend]] +[[application]] app_name = 'locahost_application' hostname = 'localhost.localdomain' -redirect_to_https = true -reverse_proxy = [{ path = '/', destination = 'https://www.google.com/' }] +https_redirection = true +reverse_proxy = [{ path = '/', destination = 'www.google.com', tls = true }] tls_cert_path = 'localhost2.pem' tls_cert_key_path = 'localhost2.pem' diff --git a/src/backend.rs b/src/backend.rs index 76e5aa6..2d13479 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -12,7 +12,7 @@ pub struct Backend { pub app_name: String, pub hostname: String, pub reverse_proxy: ReverseProxy, - pub redirect_to_https: Option, + pub https_redirection: Option, pub tls_cert_path: Option, pub tls_cert_key_path: Option, pub server_config: Mutex>, diff --git a/src/config.rs b/src/config.rs index 715cc6e..7844918 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,7 +24,7 @@ pub fn parse_opts(globals: &mut Globals, backends: &mut HashMap let mut map_example: HashMap = HashMap::new(); map_example.insert( "/maps".to_string(), - "https://bing.com/".parse::().unwrap(), + Uri::builder().authority("www.bing.com").build().unwrap(), ); backends.insert( "localhost".to_string(), @@ -32,10 +32,10 @@ pub fn parse_opts(globals: &mut Globals, backends: &mut HashMap app_name: "Localhost to Google except for maps".to_string(), hostname: "localhost".to_string(), reverse_proxy: ReverseProxy { - default_destination_uri: "https://google.com/".parse::().unwrap(), + default_destination_uri: Uri::builder().authority("www.google.com").build().unwrap(), destination_uris: map_example, }, - redirect_to_https: Some(true), // TODO: ここはtlsが存在する時はSomeにすべき。Noneはtlsがないときのみのはず + https_redirection: Some(true), // TODO: ここはtlsが存在する時はSomeにすべき。Noneはtlsがないときのみのはず tls_cert_path: Some(PathBuf::from(r"localhost1.pem")), tls_cert_key_path: Some(PathBuf::from(r"localhost1.pem")), diff --git a/src/main.rs b/src/main.rs index b0878c0..0907fa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,6 @@ use crate::{ }; use futures::future::select_all; use hyper::Client; -#[cfg(feature = "forward-hyper-trust-dns")] use hyper_trust_dns::TrustDnsResolver; use std::{collections::HashMap, io::Write, sync::Arc}; use tokio::time::Duration; @@ -40,7 +39,7 @@ fn main() { let mut runtime_builder = tokio::runtime::Builder::new_multi_thread(); runtime_builder.enable_all(); - runtime_builder.thread_name("rust-rpxy"); + runtime_builder.thread_name("rpxy"); let runtime = runtime_builder.build().unwrap(); runtime.block_on(async { @@ -69,10 +68,7 @@ fn main() { // entrypoint creates and spawns tasks of proxy services async fn entrypoint(globals: Arc, backends: Arc>) -> Result<()> { - #[cfg(feature = "forward-hyper-trust-dns")] let connector = TrustDnsResolver::default().into_rustls_webpki_https_connector(); - #[cfg(not(feature = "forward-hyper-trust-dns"))] - let connector = hyper_tls::HttpsConnector::new(); let forwarder = Arc::new(Client::builder().build::<_, hyper::Body>(connector)); let addresses = globals.listen_sockets.clone(); diff --git a/src/proxy/proxy_handler.rs b/src/proxy/proxy_handler.rs index 147dab1..caac334 100644 --- a/src/proxy/proxy_handler.rs +++ b/src/proxy/proxy_handler.rs @@ -1,69 +1,113 @@ -use crate::{backend::Backend, error::*, globals::Globals, log::*}; -use futures::{ - select, - task::{Context, Poll}, - Future, FutureExt, -}; +use super::Proxy; +use crate::{error::*, log::*}; use hyper::{ client::connect::Connect, - http, - server::conn::Http, - service::{service_fn, Service}, - Body, Client, HeaderMap, Method, Request, Response, StatusCode, Uri, -}; -use std::{collections::HashMap, net::SocketAddr, pin::Pin, sync::Arc}; -use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, - net::TcpListener, - runtime::Handle, - time::Duration, + header::{HeaderMap, HeaderName, HeaderValue}, + Body, Request, Response, StatusCode, Uri, }; +use std::net::SocketAddr; -// TODO: ここでbackendの名前単位でリクエストを分岐させる -pub async fn handle_request( - req: Request, +// pub static HEADERS: phf::Map<&'static str, HeaderName> = phf_map! { +// "CONNECTION" => HeaderName::from_static("connection"), +// "ws" => "wss", +// }; + +impl Proxy +where + T: Connect + Clone + Sync + Send + 'static, +{ + // TODO: ここでbackendの名前単位でリクエストを分岐させる + pub async fn handle_request( + self, + req: Request, + client_ip: SocketAddr, // アクセス制御用 + ) -> Result> { + debug!("Handling request: {:?}", req); + // Here we start to handle with hostname + // Find backend application for given hostname + let (hostname, _port) = if let Ok(v) = parse_host_port(&req, self.tls_enabled) { + v + } else { + return http_error(StatusCode::SERVICE_UNAVAILABLE); + }; + let backend = if let Some(be) = self.backends.get(hostname.as_str()) { + be + } else { + return http_error(StatusCode::SERVICE_UNAVAILABLE); + }; + + // Redirect to https if tls_enabled is false and redirect_to_https is true + let path_and_query = req.uri().path_and_query().unwrap().as_str().to_owned(); + if !self.tls_enabled && backend.https_redirection.unwrap_or(false) { + debug!("Redirect to secure connection: {}", hostname); + return secure_redirection(&hostname, self.globals.https_port, &path_and_query); + } + + // Find reverse proxy for given path + let path = req.uri().path(); + let destination_host_uri = if let Some(uri) = backend.reverse_proxy.destination_uris.get(path) { + uri.to_owned() + } else { + backend.reverse_proxy.default_destination_uri.clone() + }; + + // TODO: Upgrade + // TODO: X-Forwarded-For + // TODO: Transfer Encoding + + // Build request from destination information + let req_forwarded = if let Ok(req) = + generate_request_forwarded(client_ip, req, destination_host_uri, path_and_query) + { + req + } else { + error!("Failed to generate destination uri for reverse proxy"); + return http_error(StatusCode::SERVICE_UNAVAILABLE); + }; + debug!("Request to be forwarded: {:?}", req_forwarded); + + // // Forward request to + // let res_backend = match self.forwarder.request(req_forwarded).await { + // Ok(res) => res, + // Err(e) => { + // error!("Failed to get response from backend: {}", e); + // return http_error(StatusCode::BAD_REQUEST); + // } + // }; + // debug!("Response from backend: {:?}", res_backend.status()); + // Ok(res_backend) + + http_error(StatusCode::NOT_FOUND) + } +} + +// Motivated by https://github.com/felipenoris/hyper-reverse-proxy +fn generate_request_forwarded( client_ip: SocketAddr, - tls_enabled: bool, - globals: Arc, - backends: Arc>, -) -> Result> { - debug!("req: {:?}", req); - // Here we start to handle with hostname - // Find backend application for given hostname - let (hostname, _port) = parse_hostname_port(&req, tls_enabled)?; - let path = req.uri().path(); - let path_and_query = req.uri().path_and_query().unwrap().as_str(); - println!("{:?}", path_and_query); - let backend = if let Some(be) = backends.get(hostname.as_str()) { - be - } else { - return http_error(StatusCode::SERVICE_UNAVAILABLE); - }; + mut req: Request, + destination_host_uri: Uri, + path_and_query: String, +) -> Result> { + debug!("Generate request to be forwarded"); - // Redirect to https if tls_enabled is false and redirect_to_https is true - if !tls_enabled && backend.redirect_to_https.unwrap_or(false) { - debug!("Redirect to https: {}", hostname); - return https_redirection(hostname, globals.https_port, path_and_query); + // update "host" key in request header + if req.headers().contains_key("host") { + // HTTP/1.1 + req.headers_mut().insert( + "host", + HeaderValue::from_str(destination_host_uri.host().unwrap()) + .map_err(|_| anyhow!("Failed to insert destination host into forwarded request"))?, + ); } - // Find reverse proxy for given path - let destination_uri = if let Some(uri) = backend.reverse_proxy.destination_uris.get(path) { - uri.to_owned() - } else { - backend.reverse_proxy.default_destination_uri.clone() - }; + // update uri in request + *req.uri_mut() = Uri::builder() + .scheme(destination_host_uri.scheme().unwrap().as_str()) + .authority(destination_host_uri.authority().unwrap().as_str()) + .path_and_query(&path_and_query) + .build()?; - debug!("destination_uri: {}", destination_uri); - // if req.version() == hyper::Version::HTTP_11 { - // Ok(Response::new(Body::from("Hello World"))) - // } else { - // Note: it's usually better to return a Response - // with an appropriate StatusCode instead of an Err. - // Err("not HTTP/1.1, abort connection") - // http_error(StatusCode::NOT_FOUND) - https_redirection("www.google.com".to_string(), Some(443_u16), "/") - // } - // }); + Ok(req) } fn http_error(status_code: StatusCode) -> Result> { @@ -74,19 +118,19 @@ fn http_error(status_code: StatusCode) -> Result> { Ok(response) } -fn https_redirection( - hostname: String, - https_port: Option, +fn secure_redirection( + hostname: &str, + tls_port: Option, path_and_query: &str, ) -> Result> { - let dest_uri: String = if let Some(https_port) = https_port { - if https_port == 443 { + let dest_uri: String = if let Some(tls_port) = tls_port { + if tls_port == 443 { format!("https://{}{}", hostname, path_and_query) } else { - format!("https://{}:{}{}", hostname, https_port, path_and_query) + format!("https://{}:{}{}", hostname, tls_port, path_and_query) } } else { - return http_error(StatusCode::SERVICE_UNAVAILABLE); + bail!("Internal error! TLS port is not set internally."); }; let response = Response::builder() .status(StatusCode::MOVED_PERMANENTLY) @@ -96,7 +140,7 @@ fn https_redirection( Ok(response) } -fn parse_hostname_port(req: &Request, tls_enabled: bool) -> Result<(String, u16)> { +fn parse_host_port(req: &Request, tls_enabled: bool) -> Result<(String, u16)> { let hostname_port_headers = req.headers().get("host"); let hostname_uri = req.uri().host(); let port_uri = req.uri().port_u16(); @@ -107,7 +151,6 @@ fn parse_hostname_port(req: &Request, tls_enabled: bool) -> Result<(String let (hostname, port) = match (hostname_uri, hostname_port_headers) { (Some(x), _) => { - let hostname = hostname_uri.unwrap(); let port = if let Some(p) = port_uri { p } else if tls_enabled { @@ -115,7 +158,7 @@ fn parse_hostname_port(req: &Request, tls_enabled: bool) -> Result<(String } else { 80 }; - (hostname.to_string(), port) + (x.to_string(), port) } (None, Some(x)) => { let hp_as_uri = x.to_str().unwrap().parse::().unwrap(); @@ -138,3 +181,29 @@ fn parse_hostname_port(req: &Request, tls_enabled: bool) -> Result<(String Ok((hostname, port)) } + +// fn get_upgrade_type(headers: &HeaderMap) -> Option { +// #[allow(clippy::blocks_in_if_conditions)] +// if headers +// .get(&*CONNECTION_HEADER) +// .map(|value| { +// value +// .to_str() +// .unwrap() +// .split(',') +// .any(|e| e.trim() == *UPGRADE_HEADER) +// }) +// .unwrap_or(false) +// { +// if let Some(upgrade_value) = headers.get(&*UPGRADE_HEADER) { +// debug!( +// "Found upgrade header with value: {}", +// upgrade_value.to_str().unwrap().to_owned() +// ); + +// return Some(upgrade_value.to_str().unwrap().to_owned()); +// } +// } + +// None +// } diff --git a/src/proxy/proxy_main.rs b/src/proxy/proxy_main.rs index b28883a..c18175f 100644 --- a/src/proxy/proxy_main.rs +++ b/src/proxy/proxy_main.rs @@ -1,7 +1,7 @@ -use super::proxy_handler::handle_request; +// use super::proxy_handler::handle_request; use crate::{backend::Backend, error::*, globals::Globals, log::*}; use hyper::{ - client::connect::Connect, server::conn::Http, service::service_fn, Body, Client, Method, Request, + client::connect::Connect, server::conn::Http, service::service_fn, Body, Client, Request, }; use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use tokio::{ @@ -64,15 +64,7 @@ where // server.serve_connection(stream, self), server.serve_connection( stream, - service_fn(move |req: Request| { - handle_request( - req, - peer_addr, - self.tls_enabled, - self.globals.clone(), - self.backends.clone(), - ) - }), + service_fn(move |req: Request| self.clone().handle_request(req, peer_addr)), ), ) .await