From 41aecc3e7ff1a14f4519bb22b71a82f4ec2ef286 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 13 Jan 2017 07:50:51 -0800 Subject: [PATCH] Expose the remote address via `remote()` in `Request`. This commit also includes the following changes: * `FromRequest` for `SocketAddr` implemented: extracts remote address. * All built-in `FromRequest` implementations are documented. * Request preprocessing overrides remote IP with value from X-Real-IP header. * `MockRequest` allows setting the remote address with `remote()`. Resolves #38. --- lib/src/request/from_request.rs | 71 +++++++++++++++++++++++++++ lib/src/request/request.rs | 59 ++++++++++++++++++++-- lib/src/rocket.rs | 40 +++++++++++---- lib/src/testing.rs | 40 +++++++++++++++ lib/tests/remote-rewrite.rs | 87 +++++++++++++++++++++++++++++++++ 5 files changed, 283 insertions(+), 14 deletions(-) create mode 100644 lib/tests/remote-rewrite.rs diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 3253bc19..16ac5cba 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -1,4 +1,5 @@ use std::fmt::Debug; +use std::net::SocketAddr; use outcome::{self, IntoOutcome}; use request::Request; @@ -65,6 +66,65 @@ impl IntoOutcome for Result { /// matching request. Note that users can request an `Option` to catch /// `Forward`s. /// +/// # Provided Implementations +/// +/// Rocket implements `FromRequest` for several built-in types. Their behavior +/// is documented here. +/// +/// * **URI** +/// +/// Extracts the [URI](/rocket/http/uri/struct.URI.html) from the incoming +/// request. +/// +/// _This implementation always returns successfully._ +/// +/// * **Method** +/// +/// Extracts the [Method](/rocket/http/enum.Method.html) from the incoming +/// request. +/// +/// _This implementation always returns successfully._ +/// +/// * **&Cookies** +/// +/// Returns a borrow to the [Cookies](/rocket/http/type.Cookies.html) in the +/// incoming request. Note that `Cookies` implements internal mutability, so +/// a handle to `&Cookies` allows you to get _and_ set cookies in the +/// request. +/// +/// _This implementation always returns successfully._ +/// +/// * **ContentType** +/// +/// Extracts the [ContentType](/rocket/http/struct.ContentType.html) from +/// the incoming request. If the request didn't specify a Content-Type, a +/// Content-Type of `*/*` (`Any`) is returned. +/// +/// _This implementation always returns successfully._ +/// +/// * **SocketAddr** +/// +/// Extracts the remote address of the incoming request as a `SocketAddr`. +/// If the remote address is not known, the request is forwarded. +/// +/// _This implementation always returns successfully._ +/// +/// * **Option<T>** _where_ **T: FromRequest** +/// +/// The type `T` is derived from the incoming request using `T`'s +/// `FromRequest` implementation. If the derivation is a `Success`, the +/// dervived value is returned in `Some`. Otherwise, a `None` is returned. +/// +/// _This implementation always returns successfully._ +/// +/// * **Result<T, T::Error>** _where_ **T: FromRequest** +/// +/// The type `T` is derived from the incoming request using `T`'s +/// `FromRequest` implementation. If derivation is a `Success`, the value is +/// returned in `Ok`. If the derivation is a `Failure`, the error value is +/// returned in `Err`. If the derivation is a `Forward`, the request is +/// forwarded. +/// /// # Example /// /// Imagine you're running an authenticated API service that requires that some @@ -161,6 +221,17 @@ impl<'a, 'r> FromRequest<'a, 'r> for ContentType { } } +impl<'a, 'r> FromRequest<'a, 'r> for SocketAddr { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + match request.remote() { + Some(addr) => Success(addr), + None => Forward(()) + } + } +} + impl<'a, 'r, T: FromRequest<'a, 'r>> FromRequest<'a, 'r> for Result { type Error = (); diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index f6868d11..b5982930 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::net::SocketAddr; use std::fmt; use term_painter::Color::*; @@ -24,6 +25,7 @@ pub struct Request<'r> { method: Method, uri: URI<'r>, headers: HeaderMap<'r>, + remote: Option, params: RefCell>, cookies: Cookies, } @@ -46,6 +48,7 @@ impl<'r> Request<'r> { method: method, uri: uri.into(), headers: HeaderMap::new(), + remote: None, params: RefCell::new(Vec::new()), cookies: Cookies::new(&[]), } @@ -123,6 +126,49 @@ impl<'r> Request<'r> { self.params = RefCell::new(Vec::new()); } + /// Returns the address of the remote connection that initiated this + /// request if the address is known. If the address is not known, `None` is + /// returned. + /// + /// # Example + /// + /// ```rust + /// use rocket::Request; + /// use rocket::http::Method; + /// + /// let request = Request::new(Method::Get, "/uri"); + /// assert!(request.remote().is_none()); + /// ``` + #[inline(always)] + pub fn remote(&self) -> Option { + self.remote + } + + /// Sets the remote address of `self` to `address`. + /// + /// # Example + /// + /// Set the remote address to be 127.0.0.1:8000: + /// + /// ```rust + /// use rocket::Request; + /// use rocket::http::Method; + /// use std::net::{SocketAddr, IpAddr, Ipv4Addr}; + /// + /// let mut request = Request::new(Method::Get, "/uri"); + /// + /// let (ip, port) = (IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000); + /// let localhost = SocketAddr::new(ip, port); + /// request.set_remote(localhost); + /// + /// assert_eq!(request.remote(), Some(localhost)); + /// ``` + #[doc(hidden)] + #[inline(always)] + pub fn set_remote(&mut self, address: SocketAddr) { + self.remote = Some(address); + } + /// Returns a `HeaderMap` of all of the headers in `self`. /// /// # Example @@ -185,8 +231,8 @@ impl<'r> Request<'r> { /// Returns a borrow to the cookies in `self`. /// - /// Note that `Cookie` implements internal mutability, so this method allows - /// you to get _and_ set cookies in `self`. + /// Note that `Cookies` implements internal mutability, so this method + /// allows you to get _and_ set cookies in `self`. /// /// # Example /// @@ -274,6 +320,7 @@ impl<'r> Request<'r> { /// Set `self`'s parameters given that the route used to reach this request /// was `route`. This should only be used internally by `Rocket` as improper /// use may result in out of bounds indexing. + /// TODO: Figure out the mount path from here. #[doc(hidden)] #[inline(always)] pub fn set_params(&self, route: &Route) { @@ -348,8 +395,9 @@ impl<'r> Request<'r> { #[doc(hidden)] pub fn from_hyp(h_method: hyper::Method, h_headers: hyper::header::Headers, - h_uri: hyper::RequestUri) - -> Result, String> { + h_uri: hyper::RequestUri, + h_addr: SocketAddr, + ) -> Result, String> { // Get a copy of the URI for later use. let uri = match h_uri { hyper::RequestUri::AbsolutePath(s) => s, @@ -376,6 +424,9 @@ impl<'r> Request<'r> { request.add_header(header); } + // Set the remote address. + request.set_remote(h_addr); + Ok(request) } } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index cf871fd9..17d22c81 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::str::from_utf8_unchecked; use std::cmp::min; +use std::net::SocketAddr; use std::io::{self, Write}; use term_painter::Color::*; @@ -41,11 +42,11 @@ impl hyper::Handler for Rocket { hyp_req: hyper::Request<'h, 'k>, res: hyper::FreshResponse<'h>) { // Get all of the information from Hyper. - let (_, h_method, h_headers, h_uri, _, h_body) = hyp_req.deconstruct(); + let (h_addr, h_method, h_headers, h_uri, _, h_body) = hyp_req.deconstruct(); // Convert the Hyper request into a Rocket request. - let mut request = match Request::from_hyp(h_method, h_headers, h_uri) { - Ok(request) => request, + let mut req = match Request::from_hyp(h_method, h_headers, h_uri, h_addr) { + Ok(req) => req, Err(e) => { error!("Bad incoming request: {}", e); let dummy = Request::new(Method::Get, URI::new("")); @@ -59,13 +60,13 @@ impl hyper::Handler for Rocket { Ok(data) => data, Err(reason) => { error_!("Bad data in request: {}", reason); - let r = self.handle_error(Status::InternalServerError, &request); + let r = self.handle_error(Status::InternalServerError, &req); return self.issue_response(r, res); } }; // Dispatch the request to get a response, then write that response out. - let response = self.dispatch(&mut request, data); + let response = self.dispatch(&mut req, data); self.issue_response(response, res) } } @@ -132,15 +133,33 @@ impl Rocket { } } - /// Preprocess the request for Rocket-specific things. At this time, we're - /// only checking for _method in forms. Keep this in-sync with derive_form - /// when preprocessing form fields. + /// Preprocess the request for Rocket things. Currently, this means: + /// + /// * Rewriting the method in the request if _method form field exists. + /// * Rewriting the remote IP if the 'X-Real-IP' header is set. + /// + /// Keep this in-sync with derive_form when preprocessing form fields. fn preprocess_request(&self, req: &mut Request, data: &Data) { + // Rewrite the remote IP address. The request must already have an + // address associated with it to do this since we need to know the port. + if let Some(current) = req.remote() { + let ip = req.headers() + .get_one("X-Real-IP") + .and_then(|ip_str| ip_str.parse().map_err(|_| { + warn_!("The 'X-Real-IP' header is malformed: {}", ip_str) + }).ok()); + + if let Some(ip) = ip { + req.set_remote(SocketAddr::new(ip, current.port())); + } + } + // Check if this is a form and if the form contains the special _method // field which we use to reinterpret the request's method. let data_len = data.peek().len(); let (min_len, max_len) = ("_method=get".len(), "_method=delete".len()); - if req.method() == Method::Post && req.content_type().is_form() && data_len >= min_len { + let is_form = req.content_type().is_form(); + if is_form && req.method() == Method::Post && data_len >= min_len { let form = unsafe { from_utf8_unchecked(&data.peek()[..min(data_len, max_len)]) }; @@ -157,6 +176,8 @@ impl Rocket { #[doc(hidden)] #[inline(always)] pub fn dispatch<'r>(&self, request: &'r mut Request, data: Data) -> Response<'r> { + info!("{}:", request); + // Do a bit of preprocessing before routing. self.preprocess_request(request, &data); @@ -207,7 +228,6 @@ impl Rocket { pub fn route<'r>(&self, request: &'r Request, mut data: Data) -> handler::Outcome<'r> { // Go through the list of matching routes until we fail or succeed. - info!("{}:", request); let matches = self.router.route(request); for route in matches { // Retrieve and set the requests parameters. diff --git a/lib/src/testing.rs b/lib/src/testing.rs index 1fb55d66..134f6ed5 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -108,6 +108,8 @@ use ::{Rocket, Request, Response, Data}; use http::{Method, Header, Cookie}; +use std::net::SocketAddr; + /// A type for mocking requests for testing Rocket applications. pub struct MockRequest { request: Request<'static>, @@ -143,6 +145,44 @@ impl MockRequest { self } + /// Set the remote address of this request. + /// + /// # Examples + /// + /// Set the remote address to "8.8.8.8:80": + /// + /// ```rust + /// use rocket::http::Method::*; + /// use rocket::testing::MockRequest; + /// + /// let address = "8.8.8.8:80".parse().unwrap(); + /// let req = MockRequest::new(Get, "/").remote(address); + /// ``` + #[inline] + pub fn remote(mut self, address: SocketAddr) -> Self { + self.request.set_remote(address); + self + } + + /// Adds a header to this request. Does not consume `self`. + /// + /// # Examples + /// + /// Add the Content-Type header: + /// + /// ```rust + /// use rocket::http::Method::*; + /// use rocket::testing::MockRequest; + /// use rocket::http::ContentType; + /// + /// let mut req = MockRequest::new(Get, "/"); + /// req.add_header(ContentType::JSON); + /// ``` + #[inline] + pub fn add_header<'h, H: Into>>(&mut self, header: H) { + self.request.add_header(header.into()); + } + /// Add a cookie to this request. /// /// # Examples diff --git a/lib/tests/remote-rewrite.rs b/lib/tests/remote-rewrite.rs new file mode 100644 index 00000000..56c99402 --- /dev/null +++ b/lib/tests/remote-rewrite.rs @@ -0,0 +1,87 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use std::net::SocketAddr; + +#[get("/")] +fn get_ip(remote: SocketAddr) -> String { + remote.to_string() +} + +#[cfg(feature = "testing")] +mod remote_rewrite_tests { + use super::*; + use rocket::testing::MockRequest; + use rocket::http::Method::*; + use rocket::http::{Header, Status}; + + use std::net::SocketAddr; + + const KNOWN_IP: &'static str = "127.0.0.1:8000"; + + fn check_ip(header: Option>, ip: Option) { + let address: SocketAddr = KNOWN_IP.parse().unwrap(); + let port = address.port(); + + let rocket = rocket::ignite().mount("/", routes![get_ip]); + let mut req = MockRequest::new(Get, "/").remote(address); + if let Some(header) = header { + req.add_header(header); + } + + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.status(), Status::Ok); + let body_str = response.body().and_then(|b| b.into_string()); + match ip { + Some(ip) => assert_eq!(body_str, Some(format!("{}:{}", ip, port))), + None => assert_eq!(body_str, Some(KNOWN_IP.into())) + } + } + + #[test] + fn x_real_ip_rewrites() { + let ip = "8.8.8.8"; + check_ip(Some(Header::new("X-Real-IP", ip)), Some(ip.to_string())); + + let ip = "129.120.111.200"; + check_ip(Some(Header::new("X-Real-IP", ip)), Some(ip.to_string())); + } + + #[test] + fn x_real_ip_rewrites_ipv6() { + let ip = "2001:db8:0:1:1:1:1:1"; + check_ip(Some(Header::new("X-Real-IP", ip)), Some(format!("[{}]", ip))); + + let ip = "2001:db8::2:1"; + check_ip(Some(Header::new("X-Real-IP", ip)), Some(format!("[{}]", ip))); + } + + #[test] + fn uncased_header_rewrites() { + let ip = "8.8.8.8"; + check_ip(Some(Header::new("x-REAL-ip", ip)), Some(ip.to_string())); + + let ip = "1.2.3.4"; + check_ip(Some(Header::new("x-real-ip", ip)), Some(ip.to_string())); + } + + #[test] + fn no_header_no_rewrite() { + check_ip(Some(Header::new("real-ip", "?")), None); + check_ip(None, None); + } + + #[test] + fn bad_header_doesnt_rewrite() { + let ip = "092348092348"; + check_ip(Some(Header::new("X-Real-IP", ip)), None); + + let ip = "1200:100000:0120129"; + check_ip(Some(Header::new("X-Real-IP", ip)), None); + + let ip = "192.168.1.900"; + check_ip(Some(Header::new("X-Real-IP", ip)), None); + } +}