blob: 3d798b590219550536c92f88ce5287bb2d131979 [file] [log] [blame]
// 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),
}
}