axum_proxy/
lib.rs

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