Skip to main content

tower_proxy/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! `tower-proxy` is tower [`Service`s](tower_service::Service) that performs "reverse
4//! proxy" with various rewriting rules.
5//!
6//! Internally these services use [`hyper_util::client::legacy::Client`] to send an incoming request to another
7//! server. The [`connector`](hyper_util::client::legacy::connect::Connect) for a client can be
8#![cfg_attr(
9    not(any(feature = "https", feature = "nativetls", feature = "__rustls")),
10    doc = "[`HttpConnector`](hyper_util::client::legacy::connect::HttpConnector),"
11)]
12#![cfg_attr(
13    all(
14        any(feature = "https", feature = "nativetls"),
15        not(feature = "__rustls")
16    ),
17    doc = "[`HttpConnector`](hyper_util::client::legacy::connect::HttpConnector), [`HttpsConnector`](hyper_tls::HttpsConnector),"
18)]
19#![cfg_attr(
20    all(
21        not(any(feature = "https", feature = "nativetls")),
22        feature = "__rustls"
23    ),
24    doc = "[`HttpConnector`](hyper_util::client::legacy::connect::HttpConnector), [`client::RustlsConnector`],"
25)]
26#![cfg_attr(
27    all(any(feature = "https", feature = "nativetls"), feature = "__rustls"),
28    doc = "[`HttpConnector`](hyper_util::client::legacy::connect::HttpConnector), [`HttpsConnector`](hyper_tls::HttpsConnector), [`client::RustlsConnector`],"
29)]
30//! or any ones whichever you want.
31//!
32//! # Examples
33//!
34//! There are two types of services, [`OneshotService`] and [`ReusedService`]. The
35//! [`OneshotService`] *owns* the `Client`, while the [`ReusedService`] *shares* the `Client`
36//! via [`Arc`](std::sync::Arc).
37//!
38//!
39//! ## General usage
40//!
41//! ```
42//! # async fn run_test() {
43//! use tower_proxy::ReusedServiceBuilder;
44//! use tower_proxy::{ReplaceAll, ReplaceN};
45//!
46//! use hyper::body::Bytes;
47//! use http_body_util::Full;
48//! use http::Request;
49//! use tower_service::Service as _;
50//!
51//! let svc_builder = tower_proxy::builder_http("example.com:1234").unwrap();
52//!
53//! let req1 = Request::builder()
54//!     .method("GET")
55//!     .uri("https://myserver.com/foo/bar/foo")
56//!     .body(Full::new(Bytes::new()))
57//!     .unwrap();
58//!
59//! // Clones Arc<Client>
60//! let mut svc1 = svc_builder.build(ReplaceAll("foo", "baz"));
61//! // http://example.com:1234/baz/bar/baz
62//! let _res = svc1.call(req1).await.unwrap();
63//!
64//! let req2 = Request::builder()
65//!     .method("POST")
66//!     .uri("https://myserver.com/foo/bar/foo")
67//!     .header("Content-Type", "application/x-www-form-urlencoded")
68//!     .body(Full::new(Bytes::from("key=value")))
69//!     .unwrap();
70//!
71//! let mut svc2 = svc_builder.build(ReplaceN("foo", "baz", 1));
72//! // http://example.com:1234/baz/bar/foo
73//! let _res = svc2.call(req2).await.unwrap();
74//! # }
75//! ```
76//!
77//! In this example, the `svc1` and `svc2` shares the same `Client`, holding the `Arc<Client>`s
78//! inside them.
79//!
80//! For more information of rewriting rules (`ReplaceAll`, `ReplaceN` *etc.*), see the
81//! documentations of [`rewrite`].
82//!
83//!
84//! ## With axum
85//!
86//! ```
87//! # #[cfg(feature = "axum")] {
88//! use tower_proxy::ReusedServiceBuilder;
89//! use tower_proxy::{TrimPrefix, AppendSuffix, Static};
90//!
91//! use axum::Router;
92//!
93//! #[tokio::main]
94//! async fn main() {
95//!     let host1 = tower_proxy::builder_http("example.com").unwrap();
96//!     let host2 = tower_proxy::builder_http("example.net:1234").unwrap();
97//!
98//!     let app = Router::new()
99//!         .route_service("/healthcheck", host1.build(Static("/")))
100//!         .route_service("/users/{*path}", host1.build(TrimPrefix("/users")))
101//!         .route_service("/posts", host2.build(AppendSuffix("/")));
102//!
103//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
104//!        .await
105//!        .unwrap();
106//!
107//!    axum::serve(listener, app).await.unwrap();
108//! }
109//! # }
110//! ```
111//!
112//!
113//! # Return Types
114//!
115//! The return type ([`std::future::Future::Output`]) of [`ReusedService`] and
116//! [`OneshotService`] is `Result<Result<Response<Incoming>, ProxyError>, Infallible>`.
117#![cfg_attr(
118    feature = "axum",
119    doc = "This is because axum's [`Router`](axum::Router) accepts only such `Service`s."
120)]
121//!
122#![cfg_attr(
123    feature = "axum",
124    doc = "The [`ProxyError`] type implements [`IntoResponse`](axum::response::IntoResponse) if you enable the \
125    `axum` feature. \
126    It returns an empty body, with the status code `INTERNAL_SERVER_ERROR`. The description of this \
127    error will be logged out with [`tracing::event!`] at the [`tracing::Level::ERROR`] level in the \
128    [`IntoResponse::into_response`](axum::response::IntoResponse::into_response) method. \
129"
130)]
131//!
132//! # Features
133//!
134//! By default only `http1` is enabled.
135//!
136//! - `http1`: uses `hyper/http1`
137//! - `http2`: uses `hyper/http2`
138//! - `https`: alias to `nativetls`
139//! - `nativetls`: uses the `hyper-tls` crate
140//! - `rustls`: alias to `rustls-webpki-roots`
141//! - `rustls-webpki-roots`: uses the `hyper-rustls` crate, with the feature `webpki-roots`
142//! - `rustls-native-roots`: uses the `hyper-rustls` crate, with the feature `rustls-native-certs`
143//! - `rustls-http2`: `http2` plus `rustls`, and `rustls/http2` is enabled
144
145#![cfg_attr(
146    feature = "axum",
147    doc = " - `axum`: implements [`IntoResponse`](axum::response::IntoResponse) for [`ProxyError`]"
148)]
149//! You must turn on either `http1`or `http2`. You cannot use the services if, for example, only
150//! the `https` feature is on.
151//!
152//! Through this document, we use `rustls` to mean *any* of `rustls*` features unless otherwise
153//! specified.
154
155mod error;
156pub use error::ProxyError;
157
158#[cfg(any(feature = "http1", feature = "http2"))]
159#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2"))))]
160pub mod client;
161
162pub mod rewrite;
163pub use rewrite::*;
164
165mod future;
166pub use future::RevProxyFuture;
167
168#[cfg(any(feature = "http1", feature = "http2"))]
169mod oneshot;
170#[cfg(any(feature = "http1", feature = "http2"))]
171#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2"))))]
172pub use oneshot::OneshotService;
173
174#[cfg(any(feature = "http1", feature = "http2"))]
175mod reused;
176#[cfg(any(feature = "http1", feature = "http2"))]
177#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2"))))]
178pub use reused::Builder as ReusedServiceBuilder;
179#[cfg(any(feature = "http1", feature = "http2"))]
180#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2"))))]
181pub use reused::ReusedService;
182#[cfg(all(
183    any(feature = "http1", feature = "http2"),
184    any(feature = "https", feature = "nativetls")
185))]
186#[cfg_attr(
187    docsrs,
188    doc(cfg(all(
189        any(feature = "http1", feature = "http2"),
190        any(feature = "https", feature = "nativetls")
191    )))
192)]
193pub use reused::builder_https;
194#[cfg(all(any(feature = "http1", feature = "http2"), feature = "nativetls"))]
195#[cfg_attr(
196    docsrs,
197    doc(cfg(all(any(feature = "http1", feature = "http2"), feature = "nativetls")))
198)]
199pub use reused::builder_nativetls;
200#[cfg(all(any(feature = "http1", feature = "http2"), feature = "__rustls"))]
201#[cfg_attr(
202    docsrs,
203    doc(cfg(all(any(feature = "http1", feature = "http2"), feature = "rustls")))
204)]
205pub use reused::builder_rustls;
206#[cfg(any(feature = "http1", feature = "http2"))]
207#[cfg_attr(docsrs, doc(cfg(any(feature = "http1", feature = "http2"))))]
208pub use reused::{builder, builder_http};
209
210#[cfg(not(feature = "http1"))]
211compile_error!("http1 is a mandatory feature");
212
213#[cfg(all(
214    any(feature = "rustls-ring", feature = "rustls-aws-lc"),
215    not(any(feature = "rustls-webpki-roots", feature = "rustls-native-roots"))
216))]
217compile_error!(
218    "When enabling rustls-ring and/or rustls-aws-lc, you must enable rustls-webpki-roots and/or rustls-native-roots"
219);
220
221#[cfg(test)]
222mod test_helper {
223    use std::convert::Infallible;
224
225    use http::{Request, Response, StatusCode, Version};
226    use http_body_util::BodyExt as _;
227    use hyper::body::Incoming;
228    use mockito::{Matcher, ServerGuard};
229    use pretty_assertions::assert_eq;
230    use tower_service::Service;
231
232    use super::{ProxyError, RevProxyFuture};
233
234    async fn call<S, B>(
235        service: &mut S,
236        (method, suffix, content_type, body): (&str, &str, Option<&str>, B),
237        expected: (StatusCode, &str),
238    ) where
239        S: Service<
240                Request<String>,
241                Response = Result<Response<Incoming>, ProxyError>,
242                Error = Infallible,
243                Future = RevProxyFuture,
244            >,
245        B: Into<String>,
246    {
247        let mut builder = Request::builder()
248            .method(method)
249            .uri(format!("https://example.com{}", suffix));
250
251        if let Some(content_type) = content_type {
252            builder = builder.header("Content-Type", content_type);
253        }
254
255        let request = builder.body(body.into()).unwrap();
256
257        let result = service.call(request).await.unwrap();
258        assert!(result.is_ok());
259
260        let response = result.unwrap();
261        assert_eq!(response.status(), expected.0);
262
263        let body = response.into_body().collect().await;
264        assert!(body.is_ok());
265
266        assert_eq!(body.unwrap().to_bytes(), expected.1);
267    }
268
269    pub async fn match_path<S>(server: &mut ServerGuard, svc: &mut S)
270    where
271        S: Service<
272                Request<String>,
273                Response = Result<Response<Incoming>, ProxyError>,
274                Error = Infallible,
275                Future = RevProxyFuture,
276            >,
277    {
278        let _mk = server
279            .mock("GET", "/goo/bar/goo/baz/goo")
280            .with_body("ok")
281            .create_async()
282            .await;
283
284        call(
285            svc,
286            ("GET", "/foo/bar/foo/baz/foo", None, ""),
287            (StatusCode::OK, "ok"),
288        )
289        .await;
290
291        call(
292            svc,
293            ("GET", "/foo/bar/foo/baz", None, ""),
294            (StatusCode::NOT_IMPLEMENTED, ""),
295        )
296        .await;
297    }
298
299    pub async fn downgrade_version<S>(server: &mut ServerGuard, svc: &mut S)
300    where
301        S: Service<
302                Request<String>,
303                Response = Result<Response<Incoming>, ProxyError>,
304                Error = Infallible,
305                Future = RevProxyFuture,
306            >,
307    {
308        let _mk = server
309            .mock("GET", "/goo")
310            .with_body("ok")
311            .create_async()
312            .await;
313
314        let request = Request::builder()
315            .method("GET")
316            .uri("https://example.com/foo")
317            .version(Version::HTTP_2)
318            .body(String::new())
319            .unwrap();
320
321        let response = svc.call(request).await.unwrap().unwrap();
322        assert_eq!(response.status(), StatusCode::OK);
323
324        let body = response.into_body().collect().await.unwrap();
325        assert_eq!(body.to_bytes(), "ok");
326    }
327
328    pub async fn match_query<S>(server: &mut ServerGuard, svc: &mut S)
329    where
330        S: Service<
331                Request<String>,
332                Response = Result<Response<Incoming>, ProxyError>,
333                Error = Infallible,
334                Future = RevProxyFuture,
335            >,
336    {
337        let _mk = server
338            .mock("GET", "/goo")
339            .match_query(Matcher::UrlEncoded("greeting".into(), "good day".into()))
340            .with_body("ok")
341            .create_async()
342            .await;
343
344        call(
345            svc,
346            ("GET", "/foo?greeting=good%20day", None, ""),
347            (StatusCode::OK, "ok"),
348        )
349        .await;
350
351        call(
352            svc,
353            ("GET", "/foo", None, ""),
354            (StatusCode::NOT_IMPLEMENTED, ""),
355        )
356        .await;
357    }
358
359    pub async fn match_post<S>(server: &mut ServerGuard, svc: &mut S)
360    where
361        S: Service<
362                Request<String>,
363                Response = Result<Response<Incoming>, ProxyError>,
364                Error = Infallible,
365                Future = RevProxyFuture,
366            >,
367    {
368        let _mk = server
369            .mock("POST", "/goo")
370            .match_body("test")
371            .with_body("ok")
372            .create_async()
373            .await;
374
375        call(svc, ("POST", "/foo", None, "test"), (StatusCode::OK, "ok")).await;
376
377        call(
378            svc,
379            ("PUT", "/foo", None, "test"),
380            (StatusCode::NOT_IMPLEMENTED, ""),
381        )
382        .await;
383
384        call(
385            svc,
386            ("POST", "/foo", None, "tests"),
387            (StatusCode::NOT_IMPLEMENTED, ""),
388        )
389        .await;
390    }
391
392    pub async fn match_header<S>(server: &mut ServerGuard, svc: &mut S)
393    where
394        S: Service<
395                Request<String>,
396                Response = Result<Response<Incoming>, ProxyError>,
397                Error = Infallible,
398                Future = RevProxyFuture,
399            >,
400    {
401        let _mk = server
402            .mock("POST", "/goo")
403            .match_header("content-type", "application/json")
404            .match_body(r#"{"key":"value"}"#)
405            .with_body("ok")
406            .create_async()
407            .await;
408
409        call(
410            svc,
411            (
412                "POST",
413                "/foo",
414                Some("application/json"),
415                r#"{"key":"value"}"#,
416            ),
417            (StatusCode::OK, "ok"),
418        )
419        .await;
420
421        call(
422            svc,
423            ("POST", "/foo", None, r#"{"key":"value"}"#),
424            (StatusCode::NOT_IMPLEMENTED, ""),
425        )
426        .await;
427
428        call(
429            svc,
430            (
431                "POST",
432                "/foo",
433                Some("application/json"),
434                r#"{"key":"values"}"#,
435            ),
436            (StatusCode::NOT_IMPLEMENTED, ""),
437        )
438        .await;
439    }
440}