poem_http_common/dev_proxy.rs
1//! Poem-proxy is a simple and easy-to-use proxy [Endpoint](poem::Endpoint) compatible with the
2//! [Poem Web Framework](poem). It supports the forwarding of http get and post requests
3//! as well as websockets right out of the box!
4//!
5//! # Table of Contents
6//!
7//! - [Quickstart](#quickstart)
8//! - [Proxy Configuration](#proxy-configuration)
9//! - [Endpoint](#endpoint)
10//!
11//! # Quickstart
12//!
13//! ```
14//! use poem::{get, handler, listener::TcpListener, web::Path, IntoResponse, Route, Server, EndpointExt};
15//! use poem_http_common::dev_proxy::{proxy, ProxyConfig};
16//!
17//! let pconfig = ProxyConfig::new( "localhost:5173" )
18//! .web_insecure() // Enables proxy-ing web requests, sets the proxy to use http instead of https
19//! .enable_nesting() // Sets the proxy to support nested routes
20//! .finish(); // Finishes constructing the configuration
21//!
22//! let app = Route::new().nest( "/", proxy.data( pconfig ) ); // Set the endpoint and pass in the configuration
23//!
24//! Server::new(TcpListener::bind("127.0.0.1:3000")).run(app); // Start the server
25//! ```
26//!
27//! # Configuration
28//!
29//! Configuration of this endpoint is done through the
30//! [ProxyConfig](ProxyConfig) builder-struct. There are lots of configuration options
31//! available, so click that link to learn more about all of them! Below is a brief
32//! overview:
33//!
34//! ```
35//! use poem_http_common::dev_proxy::ProxyConfig;
36//!
37//! // Configure proxy endpoint, pass in the target server address and port number
38//! let proxy_config = ProxyConfig::new( "localhost:5173" ) // 5173 is for Sveltekit
39//!
40//! // One of the following lines is required to proxy web requests (post, get, etc)
41//! .web_insecure() // http from proxy to server
42//! .web_secure() // https from proxy to server
43//!
44//! // The following option is required to support nesting
45//! .enable_nesting()
46//!
47//! // This returns a concrete ProxyConfig struct to be passed into the endpoint data
48//! .finish();
49//! ```
50//!
51//! # Endpoint
52//!
53//! This [Endpoint](poem::Endpoint) is a very basic but capable proxy. It works by simply
54//! accepting web/socket requests and sending its own request to the target. Then, it
55//! sends everything it receives from the target to the connected client.
56//!
57//! This can be used with poem's built-in routing. You can apply specific request types,
58//! or even use [at](poem::Route::at) and [nest](poem::Route::at).
59//!
60//! The [Quickstart](#quickstart) section shows a working example, so this section doesn't.
61
62use poem::{
63 handler,
64 http::{Method, StatusCode},
65 web::Data,
66 Body, Error, Request, Response, Result,
67};
68
69/// A configuration object that allows for fine-grained control over a proxy endpoint.
70#[derive(Clone, Debug)]
71pub struct ProxyConfig {
72 /// This is the url where requests and websocket connections are to be
73 /// forwarded to. Port numbers are supported here, though they may be
74 /// broken off into their own parameter in the future.
75 proxy_target: String,
76
77 /// Whether to use https (true) or http for requests to the proxied server. If not
78 /// set, the proxy will not forward web requests.
79 web_secure: Option<bool>,
80
81 /// Whether or not nesting should be supported when forwarding requests
82 /// to the server.
83 support_nesting: bool,
84}
85
86impl Default for ProxyConfig {
87 /// Returns the default value for the [ProxyConfig], which corresponds
88 /// to the following:
89 /// > `proxy_target: "http://localhost:3000"`
90 ///
91 /// > `web_secure: None`
92 ///
93 /// > `ws_secure: None`
94 ///
95 /// > `support_nesting: false`
96 fn default() -> Self {
97 Self {
98 proxy_target: "http://localhost:3000".into(),
99 web_secure: None,
100 support_nesting: false,
101 }
102 }
103}
104
105/// # Implementation of Builder Functions
106///
107/// The ProxyConfig struct follows the builder pattern to enable explicit
108/// and succinct configuration of the proxy endpoint.
109impl ProxyConfig {
110 /// Function that creates a new ProxyConfig for a given target
111 /// and sets all other parameters to their default values. See
112 /// [the default implementation](ProxyConfig::default) for more
113 /// information.
114 pub fn new<'a>(target: impl Into<String>) -> ProxyConfig {
115 ProxyConfig {
116 proxy_target: target.into(),
117 ..ProxyConfig::default()
118 }
119 }
120
121 /// This function sets the endpoint to forward requests to the
122 /// target over the https protocol. This is a secure and encrypted
123 /// communication channel that should be utilized when possible.
124 pub fn web_secure<'a>(&'a mut self) -> &'a mut ProxyConfig {
125 self.web_secure = Some(true);
126 self
127 }
128
129 /// This function sets the endpoint to forward requests to the
130 /// target over the http protocol. This is an insecure and unencrypted
131 /// communication channel that should be used very carefully.
132 pub fn web_insecure<'a>(&'a mut self) -> &'a mut ProxyConfig {
133 self.web_secure = Some(false);
134 self
135 }
136
137 /// This function sets the waypoint to support nesting.
138 ///
139 /// For example,
140 /// if `endpoint.target` is `https://google.com` and the proxy is reached
141 /// at `https://proxy_address/favicon.png`, the proxy server will forward
142 /// the request to `https://google.com/favicon.png`.
143 pub fn enable_nesting<'a>(&'a mut self) -> &'a mut ProxyConfig {
144 self.support_nesting = true;
145 self
146 }
147
148 /// This function sets the waypoint to ignore nesting.
149 ///
150 /// For example,
151 /// if `endpoint.target` is `https://google.com` and the proxy is reached
152 /// at `https://proxy_address/favicon.png`, the proxy server will forward
153 /// the request to `https://google.com`.
154 pub fn disable_nesting<'a>(&'a mut self) -> &'a mut ProxyConfig {
155 self.support_nesting = false;
156 self
157 }
158
159 /// Finishes off the building process by returning a new ProxyConfig object
160 /// (not reference) that contains all the settings that were previously
161 /// specified.
162 pub fn finish<'a>(&'a mut self) -> ProxyConfig {
163 self.clone()
164 }
165}
166
167/// # Convenience Functions
168///
169/// These functions make it possible to get information from the ProxyConfig struct.
170impl ProxyConfig {
171 /// Returns the target url of the request, including the proper protocol information
172 /// and the correct pathing if nesting is enabled
173 ///
174 /// An example output would be
175 ///
176 /// > `"https://proxy.domain.com"`
177 pub fn get_web_request_uri(&self, subpath: Option<String>) -> Result<String, ()> {
178 let Some(secure) = self.web_secure else {
179 return Err(());
180 };
181
182 let base = if secure {
183 format!("https://{}", self.proxy_target)
184 } else {
185 format!("http://{}", self.proxy_target)
186 };
187
188 let sub = if self.support_nesting && subpath.is_some() {
189 subpath.unwrap()
190 } else {
191 "".into()
192 };
193
194 println!("base: {} | sub: {}", base, sub);
195
196 Ok(base + &sub)
197 }
198}
199
200/// The websocket-enabled proxy handler
201#[handler]
202pub async fn proxy(req: &Request, config: Data<&ProxyConfig>, method: Method, body: Body) -> Result<Response> {
203 // Update the uri to point to the proxied server
204 // let request_uri = target.to_owned() + &req.uri().to_string();
205
206 // Get the websocket URI if websockets are supported, otherwise return an error
207 let Ok(uri) = config.get_web_request_uri(Some(req.uri().to_string())) else {
208 return Err(Error::from_string(
209 "Proxy endpoint not configured to support web requests!",
210 StatusCode::NOT_IMPLEMENTED,
211 ));
212 };
213
214 // Now generate a request for the proxied server, based on information
215 // that we have from the current request
216 let client = reqwest::Client::new();
217 let res = match method {
218 Method::GET => {
219 client
220 .get(uri)
221 .headers(req.headers().clone())
222 .body(body.into_bytes().await.unwrap())
223 .send()
224 .await
225 }
226 Method::POST => {
227 client
228 .post(uri)
229 .headers(req.headers().clone())
230 .body(body.into_bytes().await.unwrap())
231 .send()
232 .await
233 }
234 _ => {
235 return Err(Error::from_string(
236 "Unsupported Method! The proxy endpoint currently only supports GET and POST requests!",
237 StatusCode::METHOD_NOT_ALLOWED,
238 ))
239 }
240 };
241
242 // Check on the response and forward everything from the server to our client,
243 // including headers and the body of the response, among other things.
244 match res {
245 Ok(result) => {
246 let mut res = Response::default();
247 res.extensions().clone_from(&result.extensions());
248 result.headers().iter().for_each(|(key, val)| {
249 res.headers_mut().insert(key, val.to_owned());
250 });
251 res.set_status(result.status());
252 res.set_version(result.version());
253 res.set_body(result.bytes().await.unwrap());
254 Ok(res)
255 }
256
257 // The request to the back-end server failed. Why?
258 Err(error) => Err(Error::from_string(
259 error.to_string(),
260 error.status().unwrap_or(StatusCode::BAD_GATEWAY),
261 )),
262 }
263}