// Copyright (c) 2016 The Rouille developers | |
// Licensed under the Apache License, Version 2.0 | |
// <LICENSE-APACHE or | |
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT | |
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, | |
// at your option. All files in the project carrying such | |
// notice may not be copied, modified, or distributed except | |
// according to those terms. | |
//! Dispatch a request to another HTTP server. | |
//! | |
//! This module provides functionnalities to dispatch a request to another server. This can be | |
//! used to make rouille behave as a reverse proxy. | |
//! | |
//! This function call will return immediately after the remote server has finished sending its | |
//! headers. The socket to the remote will be stored in the `ResponseBody` of the response. | |
//! | |
//! # Proxy() vs full_proxy() | |
//! | |
//! The difference between `proxy()` and `full_proxy()` is that if the target server fails to | |
//! return a proper error, the `proxy()` function will return an error (in the form of a | |
//! `ProxyError`) while the `full_proxy()` will return a `Response` with a status code indicating | |
//! an error. | |
//! | |
//! The `full_proxy()` function will only return an error if the body was already extracted from | |
//! the request before it was called. Since this indicates a logic error in the code, it is a good | |
//! idea to `unwrap()` the `Result` returned by `full_proxy()`. | |
//! | |
//! # Example | |
//! | |
//! You can for example dispatch to a different server depending on the host requested by the | |
//! client. | |
//! | |
//! ``` | |
//! use rouille::{Request, Response}; | |
//! use rouille::proxy; | |
//! | |
//! fn handle_request(request: &Request) -> Response { | |
//! let config = match request.header("Host") { | |
//! Some(h) if h == "domain1.com" => { | |
//! proxy::ProxyConfig { | |
//! addr: "domain1.handler.localnetwork", | |
//! replace_host: None, | |
//! } | |
//! }, | |
//! | |
//! Some(h) if h == "domain2.com" => { | |
//! proxy::ProxyConfig { | |
//! addr: "domain2.handler.localnetwork", | |
//! replace_host: None, | |
//! } | |
//! }, | |
//! | |
//! _ => return Response::empty_404() | |
//! }; | |
//! | |
//! proxy::full_proxy(request, config).unwrap() | |
//! } | |
//! ``` | |
use std::borrow::Cow; | |
use std::error; | |
use std::fmt; | |
use std::io; | |
use std::io::Error as IoError; | |
use std::io::BufRead; | |
use std::io::Read; | |
use std::io::Write; | |
use std::net::TcpStream; | |
use std::net::ToSocketAddrs; | |
use std::time::Duration; | |
use Request; | |
use Response; | |
use ResponseBody; | |
/// Error that can happen when dispatching the request to another server. | |
#[derive(Debug)] | |
pub enum ProxyError { | |
/// Can't pass through the body of the request because it was already extracted. | |
BodyAlreadyExtracted, | |
/// Could not read the body from the request, or could not connect to the remote server, or | |
/// the connection to the remote server closed unexpectedly. | |
IoError(IoError), | |
/// The destination server didn't produce compliant HTTP. | |
HttpParseError, | |
} | |
impl From<IoError> for ProxyError { | |
fn from(err: IoError) -> ProxyError { | |
ProxyError::IoError(err) | |
} | |
} | |
impl error::Error for ProxyError { | |
#[inline] | |
fn description(&self) -> &str { | |
match *self { | |
ProxyError::BodyAlreadyExtracted => { | |
"the body of the request was already extracted" | |
}, | |
ProxyError::IoError(_) => { | |
"could not read the body from the request, or could not connect to the remote \ | |
server, or the connection to the remote server closed unexpectedly" | |
}, | |
ProxyError::HttpParseError => { | |
"the destination server didn't produce compliant HTTP" | |
}, | |
} | |
} | |
#[inline] | |
fn cause(&self) -> Option<&error::Error> { | |
match *self { | |
ProxyError::IoError(ref e) => Some(e), | |
_ => None | |
} | |
} | |
} | |
impl fmt::Display for ProxyError { | |
#[inline] | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { | |
write!(fmt, "{}", error::Error::description(self)) | |
} | |
} | |
/// Configuration for the reverse proxy. | |
#[derive(Debug, Clone)] | |
pub struct ProxyConfig<A> { | |
/// The address to connect to. For example `example.com:80`. | |
pub addr: A, | |
/// If `Some`, the `Host` header will be replaced with this value. | |
pub replace_host: Option<Cow<'static, str>>, | |
} | |
/// Sends the request to another HTTP server using the configuration. | |
/// | |
/// If the function fails to get a response from the target, an error is returned. If you want | |
/// to instead return a response with a status code such as 502 (`Bad Gateway`) or 504 | |
/// (`Gateway Time-out`), see `full_proxy`. | |
/// | |
/// > **Note**: Implementation is very hacky for the moment. | |
/// | |
/// > **Note**: SSL is not supported. | |
// TODO: ^ | |
pub fn proxy<A>(request: &Request, config: ProxyConfig<A>) -> Result<Response, ProxyError> | |
where A: ToSocketAddrs | |
{ | |
let mut socket = try!(TcpStream::connect(config.addr)); | |
try!(socket.set_read_timeout(Some(Duration::from_secs(60)))); | |
try!(socket.set_write_timeout(Some(Duration::from_secs(60)))); | |
let mut data = match request.data() { | |
Some(d) => d, | |
None => return Err(ProxyError::BodyAlreadyExtracted), | |
}; | |
try!(socket.write_all(format!("{} {} HTTP/1.1\r\n", request.method(), request.raw_url()).as_bytes())); | |
for (header, value) in request.headers() { | |
let value = if header == "Host" { | |
if let Some(ref replace) = config.replace_host { | |
&**replace | |
} else { | |
value | |
} | |
} else { | |
value | |
}; | |
if header == "Connection" { | |
continue; | |
} | |
try!(socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())); | |
} | |
try!(socket.write_all(b"Connection: close\r\n\r\n")); | |
try!(io::copy(&mut data, &mut socket)); | |
let mut socket = io::BufReader::new(socket); | |
let mut headers = Vec::new(); | |
let status_code; | |
{ | |
let mut lines = socket.by_ref().lines(); | |
{ | |
let line = try!(match lines.next() { | |
Some(l) => l, | |
None => return Err(ProxyError::HttpParseError), | |
}); | |
let mut splits = line.splitn(3, ' '); | |
let _ = splits.next(); | |
let status_str = match splits.next() { | |
Some(l) => l, | |
None => return Err(ProxyError::HttpParseError), | |
}; | |
status_code = match status_str.parse() { | |
Ok(s) => s, | |
Err(_) => return Err(ProxyError::HttpParseError), | |
}; | |
} | |
for header in lines { | |
let header = try!(header); | |
if header.is_empty() { break; } | |
let mut splits = header.splitn(2, ':'); | |
let header = match splits.next() { | |
Some(v) => v, | |
None => return Err(ProxyError::HttpParseError), | |
}; | |
let val = match splits.next() { | |
Some(v) => v, | |
None => return Err(ProxyError::HttpParseError), | |
}; | |
let val = &val[1..]; | |
headers.push((header.to_owned().into(), val.to_owned().into())); | |
} | |
} | |
Ok(Response { | |
status_code, | |
headers, | |
data: ResponseBody::from_reader(socket), | |
upgrade: None, | |
}) | |
} | |
/// Error that can happen when calling `full_proxy`. | |
#[derive(Debug)] | |
pub enum FullProxyError { | |
/// Can't pass through the body of the request because it was already extracted. | |
BodyAlreadyExtracted, | |
} | |
impl error::Error for FullProxyError { | |
#[inline] | |
fn description(&self) -> &str { | |
match *self { | |
FullProxyError::BodyAlreadyExtracted => { | |
"the body of the request was already extracted" | |
}, | |
} | |
} | |
} | |
impl fmt::Display for FullProxyError { | |
#[inline] | |
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { | |
write!(fmt, "{}", error::Error::description(self)) | |
} | |
} | |
/// Sends the request to another HTTP server using the configuration. | |
/// | |
/// Contrary to `proxy`, if the server fails to return a proper response then a response is | |
/// generated with the status code 502 or 504. | |
/// | |
/// The only possible remaining error is if the body of the request was already extracted. Since | |
/// this would be a logic error, it is acceptable to unwrap it. | |
pub fn full_proxy<A>(request: &Request, config: ProxyConfig<A>) -> Result<Response, FullProxyError> | |
where A: ToSocketAddrs | |
{ | |
match proxy(request, config) { | |
Ok(r) => Ok(r), | |
Err(ProxyError::IoError(_)) => Ok(Response::text("Gateway Time-out").with_status_code(504)), | |
Err(ProxyError::HttpParseError) => Ok(Response::text("Bad Gateway").with_status_code(502)), | |
Err(ProxyError::BodyAlreadyExtracted) => Err(FullProxyError::BodyAlreadyExtracted), | |
} | |
} |