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}