carllerche / tower-web

A fast, boilerplate free, web framework for Rust
MIT License
981 stars 51 forks source link

Writing an HTTP proxy #140

Closed ebkalderon closed 5 years ago

ebkalderon commented 6 years ago

I am exploring tower-web by writing a simple HTTP proxy. In this application, I would like to perform some header validation on each request, forward it to a pre-defined endpoint, and then forward the received response back to the original client. I am using a hyper::Client in each HTTP method handler to connect to the endpoint, but I am finding that I am duplicating the header validation code across each handler, which seems rather brittle to me. Is there a clean way to wrap a given type T where T implements IntoResource such that I can enforce this header validation for all incoming requests on all handlers in one place?

carllerche commented 6 years ago

This is a very similar request to #82. My current thought is to punt this until Tower Web is merged with Warp. I believe that warp's filter API would be the best way to add HTTP functionality to an entire resource.

Thoughts?

ebkalderon commented 6 years ago

I've actually managed to mostly get the effect I wanted by writing some middleware which validates the headers of each incoming request, using the existing CorsMiddleware as a reference. But I'm also curious to hear how Warp's filter API might fare as an alternative?

ebkalderon commented 6 years ago

Also, quick question also in regards to easy proxying with tower-web: I saw the suggestion in #105 which implements Extract for http::Request<()>. Is there an easy way to implement Extract for Request such that it can use extract_body() to also retrieve the body? It would be nice to facilitate easy redirection by receiving a complete request, manipulating its headers and URI, and relaying it directly to its destination. Perhaps it might look something like this:

#[derive(Debug)]
struct SimpleProxy {
    target: Uri,
}

impl_web! {
    impl SimpleProxy {
        #[get("/*path")]
        fn proxy_get(&self, mut request: Request<Body>) -> impl Future<Item = Response<Body>> {
            let new_uri = format!("{}{}", self.target, request.uri().path());
            *request.uri_mut() = new_uri.parse().unwrap();
            *request.headers_mut().insert(HOST, self.target.host().unwrap());
            hyper::Client::new().request(request)
        }
    }
}
carllerche commented 6 years ago

I don't think that there will be a way to get Request<Body>, but there will be a way to get the raw body. I need to work on that still though.

ebkalderon commented 6 years ago

I see what you mean. Currently, I'm working around it by creating the following newtype:

#[derive(Debug)]
pub struct Request(pub http::Request<()>);

impl<B: BufStream> Extract<B> for Request {
    type Future = Immediate<Self>;

    fn extract(ctx: &Context) -> Self::Future {
        let request = http::Request::builder()
            .method(ctx.request().method())
            .version(ctx.request().version())
            .uri(ctx.request().uri())
            .body(())
            .map_err(|e| ExtractError::invalid_argument(&e))
            .map(|mut request| {
                request
                    .headers_mut()
                    .extend(ctx.request().headers().clone());
                Request(request)
            });

        Immediate::result(request)
    }
}

Then I use it like so in my application:

#[derive(Debug)]
struct SimpleProxy {
    target: Uri,
}

impl_web! {
    impl SimpleProxy {
        #[get("/*path")]
        fn proxy_get(&self, mut request: Request, body: Vec<u8>) -> impl Future<Item = Response<Body>> {
            let Request(mut request) = request;
            let new_uri = format!("{}{}", self.target, request.uri().path());
            *request.uri_mut() = new_uri.parse().unwrap();
            *request.headers_mut().insert(HOST, self.target.host().unwrap());
            let request = request.map(body);
            hyper::Client::new().request(request)
        }
    }
}

Not sure if there is an easier way to accommodate this in tower-web, though.

carllerche commented 6 years ago

tower-web can provide an implementation of Extract for Request<()>.

ebkalderon commented 6 years ago

Awesome! That would be most helpful.

carllerche commented 6 years ago

@ebkalderon Do you want to try providing a PR for it? The steps are roughly:

  1. Create extract/http.rs
  2. impl<B: BufStream> Extract<B> for http::Request<()> { ... }

For Future, use Immediate<http::Request<()>

Then, the implementation is Immediate::ok(ctx.request().clone()) I think.

A test or two would be nice 👍

ebkalderon commented 5 years ago

@carllerche I deeply apologize for the lack of response; this slipped through the cracks somehow. I can prepare a pull request for this now, if you're still interested!

ebkalderon commented 5 years ago

Okay, I've just created #208, and it is ready for review.

By the way, ctx.request().clone() is unfortunately not possible because http::Request doesn't implement nor derive Clone. This was the reason why my Extract implementation shown above in this thread manually duplicates the http::Request with a builder (omitting the body and extensions, which cannot be cloned either).