├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── lib.rs └── tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | dist: trusty 3 | rust: 4 | - stable 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-reverse-proxy" 3 | version = "0.1.0" 4 | authors = ["Felipe Noronha "] 5 | repository = "https://github.com/felipenoris/actix-reverse-proxy.git" 6 | description = "A simple configurable Reverse Proxy for the Actix framework." 7 | license = "MIT" 8 | 9 | [lib] 10 | name = "actix_reverse_proxy" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | actix-web = "0.7" 15 | futures = "0.1" 16 | lazy_static = "1.1" 17 | 18 | [profile.release] 19 | lto = true 20 | opt-level = 3 21 | codegen-units = 1 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Felipe Noronha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # actix-reverse-proxy 3 | 4 | [![License][license-img]](LICENSE) 5 | [![travis][travis-img]][travis-url] 6 | 7 | [license-img]: http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat 8 | [travis-img]: https://img.shields.io/travis/felipenoris/actix-reverse-proxy/master.svg?label=Linux 9 | [travis-url]: https://travis-ci.org/felipenoris/actix-reverse-proxy 10 | [![Project Status: Abandoned – Initial development has started, but there has not yet been a stable, usable release; the project has been abandoned and the author(s) do not intend on continuing development.](https://www.repostatus.org/badges/latest/abandoned.svg)](https://www.repostatus.org/#abandoned) 11 | 12 | This is a simple configurable Reverse Proxy for Actix framework. 13 | 14 | Based on https://github.com/DoumanAsh/actix-reverse-proxy. 15 | 16 | This project has been abandoned in favor of [hyper-reverse-proxy](https://github.com/felipenoris/hyper-reverse-proxy) (see discussion [here](https://github.com/DoumanAsh/actix-reverse-proxy/issues/2)). 17 | 18 | ## Working Example 19 | 20 | Create a new project. 21 | 22 | ```shell 23 | cargo new myproxy 24 | ``` 25 | 26 | Add the following to your `Cargo.toml`. 27 | 28 | ```toml 29 | [dependencies] 30 | actix-web = "0.7" 31 | futures = "0.1" 32 | actix-reverse-proxy = { git = "https://github.com/felipenoris/actix-reverse-proxy" } 33 | ``` 34 | 35 | Edit `main.rs` with the following. In this example, calls to `http://0.0.0.0:13900/dummy/anything?hey=you` 36 | will be proxied to `http://127.0.0.1:13901/dummy/anything?hey=you`. 37 | 38 | ```rust 39 | extern crate actix_web; 40 | extern crate futures; 41 | extern crate actix_reverse_proxy; 42 | 43 | use actix_web::{server, App, HttpRequest}; 44 | use futures::Future; 45 | use actix_reverse_proxy::ReverseProxy; 46 | 47 | use std::time::Duration; 48 | 49 | const REVERSE_PROXY_BIND_ADDRESS: &'static str = "0.0.0.0:13900"; 50 | 51 | fn dummy(req: HttpRequest) -> impl Future { 52 | ReverseProxy::new("http://127.0.0.1:13901") 53 | .timeout(Duration::from_secs(1)) 54 | .forward(req) 55 | } 56 | 57 | fn main() { 58 | server::new(|| App::new() 59 | .resource("/dummy/{tail:.*}", |r| r.with_async(dummy)) 60 | ) 61 | .bind(REVERSE_PROXY_BIND_ADDRESS) 62 | .unwrap() 63 | .run(); 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | // Based on: 3 | // https://github.com/DoumanAsh/actix-reverse-proxy 4 | // https://golang.org/src/net/http/httputil/reverseproxy.go 5 | 6 | extern crate actix_web; 7 | extern crate futures; 8 | 9 | #[macro_use] 10 | extern crate lazy_static; 11 | 12 | use actix_web::{HttpRequest, HttpResponse, HttpMessage, client}; 13 | use actix_web::http::header::{HeaderName, HeaderMap}; 14 | use futures::{Stream, Future}; 15 | 16 | use std::time::Duration; 17 | use std::net::SocketAddr; 18 | 19 | #[cfg(test)] 20 | mod tests; 21 | 22 | lazy_static! { 23 | static ref HEADER_X_FORWARDED_FOR: HeaderName = HeaderName::from_lowercase(b"x-forwarded-for").unwrap(); 24 | 25 | static ref HOP_BY_HOP_HEADERS: Vec = vec![ 26 | HeaderName::from_lowercase(b"connection").unwrap(), 27 | HeaderName::from_lowercase(b"proxy-connection").unwrap(), 28 | HeaderName::from_lowercase(b"keep-alive").unwrap(), 29 | HeaderName::from_lowercase(b"proxy-authenticate").unwrap(), 30 | HeaderName::from_lowercase(b"proxy-authorization").unwrap(), 31 | HeaderName::from_lowercase(b"te").unwrap(), 32 | HeaderName::from_lowercase(b"trailer").unwrap(), 33 | HeaderName::from_lowercase(b"transfer-encoding").unwrap(), 34 | HeaderName::from_lowercase(b"upgrade").unwrap(), 35 | ]; 36 | 37 | static ref HEADER_TE: HeaderName = HeaderName::from_lowercase(b"te").unwrap(); 38 | 39 | static ref HEADER_CONNECTION: HeaderName = HeaderName::from_lowercase(b"connection").unwrap(); 40 | } 41 | 42 | static DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); 43 | 44 | pub struct ReverseProxy<'a> { 45 | forward_url: &'a str, 46 | timeout: Duration, 47 | } 48 | 49 | fn add_client_ip(fwd_header_value: &mut String, client_addr: SocketAddr) { 50 | if !fwd_header_value.is_empty() { 51 | fwd_header_value.push_str(", "); 52 | } 53 | 54 | let client_ip_str = &format!("{}", client_addr.ip()); 55 | fwd_header_value.push_str(client_ip_str); 56 | } 57 | 58 | fn remove_connection_headers(headers: &mut HeaderMap) { 59 | let mut headers_to_delete: Vec = Vec::new(); 60 | let header_connection = &(*HEADER_CONNECTION); 61 | 62 | if headers.contains_key(header_connection) { 63 | if let Ok(connection_header_value) = headers[header_connection].to_str() { 64 | for h in connection_header_value.split(',').map(|s| s.trim()) { 65 | headers_to_delete.push(String::from(h)); 66 | } 67 | } 68 | } 69 | 70 | for h in headers_to_delete { 71 | headers.remove(h); 72 | } 73 | } 74 | 75 | fn remove_request_hop_by_hop_headers(headers: &mut HeaderMap) { 76 | for h in HOP_BY_HOP_HEADERS.iter() { 77 | if headers.contains_key(h) && (headers[h] == "" || ( h == *HEADER_TE && headers[h] == "trailers") ) { 78 | continue; 79 | } 80 | headers.remove(h); 81 | } 82 | } 83 | 84 | impl<'a> ReverseProxy<'a> { 85 | 86 | pub fn new(forward_url: &'a str) -> ReverseProxy<'a> { 87 | ReverseProxy{ forward_url, timeout: DEFAULT_TIMEOUT } 88 | } 89 | 90 | pub fn timeout(mut self, duration: Duration) -> ReverseProxy<'a> { 91 | self.timeout = duration; 92 | self 93 | } 94 | 95 | fn x_forwarded_for_value(&self, req: &HttpRequest) -> String { 96 | let mut result = String::new(); 97 | 98 | for (key, value) in req.headers() { 99 | if key == *HEADER_X_FORWARDED_FOR { 100 | result.push_str(value.to_str().unwrap()); 101 | break; 102 | } 103 | } 104 | 105 | // adds client IP address 106 | // to x-forwarded-for header 107 | // if it's available 108 | if let Some(peer_addr) = req.peer_addr() { 109 | add_client_ip(&mut result, peer_addr); 110 | } 111 | 112 | result 113 | } 114 | 115 | fn forward_uri(&self, req: &HttpRequest) -> String { 116 | let forward_url: &str = self.forward_url; 117 | 118 | let forward_uri = match req.uri().query() { 119 | Some(query) => format!("{}{}?{}", forward_url, req.uri().path(), query), 120 | None => format!("{}{}", forward_url, req.uri().path()), 121 | }; 122 | 123 | forward_uri 124 | } 125 | 126 | pub fn forward(&self, req: HttpRequest) -> impl Future { 127 | 128 | let mut forward_req = client::ClientRequest::build_from(&req); 129 | forward_req.uri(self.forward_uri(&req).as_str()); 130 | forward_req.set_header( &(*HEADER_X_FORWARDED_FOR), self.x_forwarded_for_value(&req)); 131 | 132 | let forward_body = req.payload().from_err(); 133 | let mut forward_req = forward_req 134 | .no_default_headers() 135 | .set_header_if_none(actix_web::http::header::USER_AGENT, "") 136 | .body(actix_web::Body::Streaming(Box::new(forward_body))) 137 | .expect("To create valid forward request"); 138 | 139 | remove_connection_headers(forward_req.headers_mut()); 140 | remove_request_hop_by_hop_headers(forward_req.headers_mut()); 141 | 142 | println!("#### REVERSE PROXY REQUEST HEADERS"); 143 | for (key, value) in forward_req.headers() { 144 | println!("[{:?}] = {:?}", key, value); 145 | } 146 | 147 | forward_req.send() 148 | .timeout(self.timeout) 149 | .map_err(|error| { 150 | println!("Error: {}", error); 151 | error.into() 152 | }) 153 | .map(|resp| { 154 | let mut back_rsp = HttpResponse::build(resp.status()); 155 | 156 | // copy headers 157 | for (key, value) in resp.headers() { 158 | if !HOP_BY_HOP_HEADERS.contains(key) { 159 | back_rsp.header(key.clone(), value.clone()); 160 | } 161 | } 162 | 163 | let back_body = resp.payload().from_err(); 164 | let mut back_rsp = back_rsp 165 | .body(actix_web::Body::Streaming(Box::new(back_body))); 166 | 167 | remove_connection_headers(back_rsp.headers_mut()); 168 | 169 | println!("#### REVERSE PROXY RESPONSE HEADERS"); 170 | for (key, value) in back_rsp.headers() { 171 | println!("[{:?}] = {:?}", key, value); 172 | } 173 | 174 | back_rsp 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | 2 | use add_client_ip; 3 | use actix_web::http::header::HeaderName; 4 | use std::net::SocketAddr; 5 | use std::str::FromStr; 6 | 7 | #[test] 8 | fn compare_headers() { 9 | let h1 = HeaderName::from_bytes(b"X-Forwarded-For").unwrap(); 10 | let h2 = HeaderName::from_lowercase(b"x-forwarded-for").unwrap(); 11 | assert_eq!(h1, h2); 12 | } 13 | 14 | #[test] 15 | fn test_add_client_ip() { 16 | let mut header_value = String::from("192.168.25.12"); 17 | let client_address = SocketAddr::from_str("127.0.0.1:8000").unwrap(); 18 | add_client_ip(&mut header_value, client_address); 19 | assert_eq!(&header_value, "192.168.25.12, 127.0.0.1"); 20 | } 21 | --------------------------------------------------------------------------------