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}