reverse_proxy_service/
lib.rs

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