poem_proxy/lib.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_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//! .ws_insecure() // Enables proxy-ing web sockets, sets the proxy to use ws instead of wss
20//! .enable_nesting() // Sets the proxy to support nested routes
21//! .finish(); // Finishes constructing the configuration
22//!
23//! let app = Route::new().nest( "/", proxy.data( pconfig ) ); // Set the endpoint and pass in the configuration
24//!
25//! Server::new(TcpListener::bind("127.0.0.1:3000")).run(app); // Start the server
26//! ```
27//!
28//! # Configuration
29//!
30//! Configuration of this endpoint is done through the
31//! [ProxyConfig](ProxyConfig) builder-struct. There are lots of configuration options
32//! available, so click that link to learn more about all of them! Below is a brief
33//! overview:
34//!
35//! ```
36//! use poem_proxy::ProxyConfig;
37//!
38//! // Configure proxy endpoint, pass in the target server address and port number
39//! let proxy_config = ProxyConfig::new( "localhost:5173" ) // 5173 is for Sveltekit
40//!
41//! // One of the following lines is required to proxy web requests (post, get, etc)
42//! .web_insecure() // http from proxy to server
43//! .web_secure() // https from proxy to server
44//!
45//! // One of the following lines is required to proxy websockets
46//! .ws_insecure() // ws from proxy to server
47//! .ws_secure() // wss from proxy to server
48//!
49//! // The following option is required to support nesting
50//! .enable_nesting()
51//!
52//! // This returns a concrete ProxyConfig struct to be passed into the endpoint data
53//! .finish();
54//! ```
55//!
56//! # Endpoint
57//!
58//! This [Endpoint](poem::Endpoint) is a very basic but capable proxy. It works by simply
59//! accepting web/socket requests and sending its own request to the target. Then, it
60//! sends everything it receives from the target to the connected client.
61//!
62//! This can be used with poem's built-in routing. You can apply specific request types,
63//! or even use [at](poem::Route::at) and [nest](poem::Route::at).
64//!
65//! The [Quickstart](#quickstart) section shows a working example, so this section doesn't.
66
67use futures_util::{ SinkExt, StreamExt };
68use poem::{
69 Request, Result, Response, Error, handler, Body, FromRequest, IntoResponse,
70 http::{ StatusCode, Method, HeaderMap },
71 web::{ Data, websocket::{ WebSocket } }
72};
73use tokio_tungstenite::connect_async;
74use tokio::sync::RwLock;
75use std::sync::Arc;
76
77/// A configuration object that allows for fine-grained control over a proxy endpoint.
78#[derive(Clone, Debug)]
79pub struct ProxyConfig {
80
81 /// This is the url where requests and websocket connections are to be
82 /// forwarded to. Port numbers are supported here, though they may be
83 /// broken off into their own parameter in the future.
84 proxy_target: String,
85
86 /// Whether to use https (true) or http for requests to the proxied server. If not
87 /// set, the proxy will not forward web requests.
88 web_secure: Option<bool>,
89
90 /// Whether to use wss (true) or ws for websocket requests to the proxied server. If
91 /// not set, the proxy will not forward web sockets.
92 ws_secure: Option<bool>,
93
94 /// Whether or not nesting should be supported when forwarding requests
95 /// to the server.
96 support_nesting: bool,
97}
98
99impl Default for ProxyConfig {
100
101 /// Returns the default value for the [ProxyConfig], which corresponds
102 /// to the following:
103 /// > `proxy_target: "http://localhost:3000"`
104 ///
105 /// > `web_secure: None`
106 ///
107 /// > `ws_secure: None`
108 ///
109 /// > `support_nesting: false`
110 fn default() -> Self {
111 Self {
112 proxy_target: "http://localhost:3000".into(),
113 web_secure: None, ws_secure: None, support_nesting: false
114 }
115 }
116}
117
118/// # Implementation of Builder Functions
119///
120/// The ProxyConfig struct follows the builder pattern to enable explicit
121/// and succinct configuration of the proxy endpoint.
122impl ProxyConfig {
123
124 /// Function that creates a new ProxyConfig for a given target
125 /// and sets all other parameters to their default values. See
126 /// [the default implementation](ProxyConfig::default) for more
127 /// information.
128 pub fn new<'a>( target: impl Into<String> ) -> ProxyConfig {
129 ProxyConfig {
130 proxy_target: target.into(),
131 ..ProxyConfig::default()
132 }
133 }
134
135 /// This function sets the endpoint to forward websockets over
136 /// https instead of http. (This is WSS - WebSocket Secure)
137 pub fn ws_secure<'a>( &'a mut self ) -> &'a mut ProxyConfig {
138 self.ws_secure = Some( true );
139 self
140 }
141
142 /// This function sets the endpoint to forward websockets over
143 /// http instead of https. This means any information being sent
144 /// through the websocket has the potential to be
145 /// [intercepted by malicious actors](https://brightsec.com/blog/websocket-security-top-vulnerabilities/#unencrypted-tcp-channel).
146 pub fn ws_insecure<'a>( &'a mut self ) -> &'a mut ProxyConfig {
147 self.ws_secure = Some( false );
148 self
149 }
150
151 /// This function sets the endpoint to forward requests to the
152 /// target over the https protocol. This is a secure and encrypted
153 /// communication channel that should be utilized when possible.
154 pub fn web_secure<'a>( &'a mut self ) -> &'a mut ProxyConfig {
155 self.web_secure = Some( true );
156 self
157 }
158
159 /// This function sets the endpoint to forward requests to the
160 /// target over the http protocol. This is an insecure and unencrypted
161 /// communication channel that should be used very carefully.
162 pub fn web_insecure<'a>( &'a mut self ) -> &'a mut ProxyConfig {
163 self.web_secure = Some( false );
164 self
165 }
166
167 /// This function sets the waypoint to support nesting.
168 ///
169 /// For example,
170 /// if `endpoint.target` is `https://google.com` and the proxy is reached
171 /// at `https://proxy_address/favicon.png`, the proxy server will forward
172 /// the request to `https://google.com/favicon.png`.
173 pub fn enable_nesting<'a>( &'a mut self ) -> &'a mut ProxyConfig {
174 self.support_nesting = true;
175 self
176 }
177
178 /// This function sets the waypoint to ignore nesting.
179 ///
180 /// For example,
181 /// if `endpoint.target` is `https://google.com` and the proxy is reached
182 /// at `https://proxy_address/favicon.png`, the proxy server will forward
183 /// the request to `https://google.com`.
184 pub fn disable_nesting<'a>( &'a mut self ) -> &'a mut ProxyConfig {
185 self.support_nesting = false;
186 self
187 }
188
189 /// Finishes off the building proccess by returning a new ProxyConfig object
190 /// (not reference) that contains all the settings that were previously
191 /// specified.
192 pub fn finish<'a>( &'a mut self ) -> ProxyConfig {
193 self.clone()
194 }
195
196}
197
198/// # Convenience Functions
199///
200/// These functions make it possible to get information from the ProxyConfig struct.
201impl ProxyConfig {
202
203 /// Returns the target url of the request, including the proper protocol information
204 /// and the correct pathing if nesting is enabled
205 ///
206 /// An example output would be
207 ///
208 /// > `"https://proxy.domain.com"`
209 pub fn get_web_request_uri( &self, subpath: Option<String> ) -> Result<String, ()> {
210 let Some( secure ) = self.web_secure else {
211 return Err(());
212 };
213
214 let base = if secure {
215 format!( "https://{}", self.proxy_target )
216 } else {
217 format!( "http://{}", self.proxy_target )
218 };
219
220 let sub = if self.support_nesting && subpath.is_some() {
221 subpath.unwrap()
222 } else {
223 "".into()
224 };
225
226 println!( "base: {} | sub: {}", base, sub );
227
228 Ok( base+&sub )
229 }
230
231 /// Returns the target url of the websocket, including the proper protocol information.
232 ///
233 /// An example output would be
234 ///
235 /// > `"wss://websocket.domain.com"`
236 pub fn get_web_socket_uri( &self ) -> Result<String, ()> {
237 let Some( secure ) = self.ws_secure else {
238 return Err(());
239 };
240
241 Ok(
242 if secure {
243 format!( "wss://{}", self.proxy_target )
244 } else {
245 format!( "ws://{}", self.proxy_target )
246 }
247 )
248 }
249
250}
251
252/// The websocket-enabled proxy handler
253#[handler]
254pub async fn proxy(
255 req: &Request,
256 headers: &HeaderMap,
257 config: Data<&ProxyConfig>,
258 method: Method,
259 body: Body,
260 ) -> Result<Response> {
261
262 // If we need a websocket connection,
263 if let Ok( ws ) = WebSocket::from_request_without_body( req ).await {
264
265 // Get the websocket URI if websockets are supported, otherwise return an error
266 let Ok( uri ) = config.get_web_socket_uri() else {
267 return Err( Error::from_string( "Proxy endpoint not configured to support websockets!", StatusCode::NOT_IMPLEMENTED ) )
268 };
269
270 // Generate websocket request:
271 let mut w_request = http::Request::builder().uri( &uri );
272 for (key, value) in headers.iter() {
273 w_request = w_request.header( key, value );
274 }
275
276 // Start the websocket connection
277 return Ok(
278 ws.on_upgrade(move |socket| async move {
279 let ( mut clientsink, mut clientstream ) = socket.split();
280
281 // Start connection to server
282 let ( mut serversocket, _ ) = connect_async( w_request.body(()).unwrap() ).await.unwrap();
283 let ( mut serversink, mut serverstream ) = serversocket.split();
284
285 // Tie both threads so if one exits the other does too
286 let client_live = Arc::new( RwLock::new( true ) );
287 let server_live = client_live.clone();
288
289 // Relay client messages to the server we are proxying
290 tokio::spawn( async move {
291 while let Some( Ok( msg ) ) = clientstream.next().await {
292
293 // When a message is received, forward it to the server
294 // Break the loop if there are errors
295 match serversink.send( msg.into() ).await {
296 Err( _ ) => break,
297 _ => {},
298 };
299
300 // Stop the connection if it is no longer live
301 // let j = *connection_live.read().await;
302 if !*client_live.read().await { break };
303 };
304
305 // Stop the other thread that is paired with this one
306 *client_live.write().await = false;
307 });
308
309 // Relay server messages to the client
310 tokio::spawn( async move {
311 while let Some( Ok( msg ) ) = serverstream.next().await {
312
313 // When a server message is received, forward it to the
314 // client, and break the loop if there are errors
315 match clientsink.send( msg.into() ).await {
316 Err( _ ) => break,
317 _ => {},
318 };
319
320 // Stop the connection if it is no longer live
321 if !*server_live.read().await { break };
322 };
323
324 // Stop the other thread that is paired with this one
325 *server_live.write().await = false;
326 });
327 }).into_response()
328 );
329 }
330
331 // Not using websocket (http/https):
332 else {
333
334 // Update the uri to point to the proxied server
335 // let request_uri = target.to_owned() + &req.uri().to_string();
336
337 // Get the websocket URI if websockets are supported, otherwise return an error
338 let Ok( uri ) = config.get_web_request_uri( Some( req.uri().to_string() ) ) else {
339 return Err( Error::from_string( "Proxy endpoint not configured to support web requests!", StatusCode::NOT_IMPLEMENTED ) )
340 };
341
342 // Now generate a request for the proxied server, based on information
343 // that we have from the current request
344 let client = reqwest::Client::new();
345 let res = match method {
346 Method::GET => {
347 client.get( uri )
348 .headers( req.headers().clone() )
349 .body( body.into_bytes().await.unwrap() )
350 .send()
351 .await
352 },
353 Method::POST => {
354 client.post( uri )
355 .headers( req.headers().clone() )
356 .body( body.into_bytes().await.unwrap() )
357 .send()
358 .await
359 },
360 _ => {
361 return Err( Error::from_string( "Unsupported Method! The proxy endpoint currently only supports GET and POST requests!", StatusCode::METHOD_NOT_ALLOWED ) )
362 }
363 };
364
365 // Check on the response and forward everything from the server to our client,
366 // including headers and the body of the response, among other things.
367 match res {
368 Ok( result ) => {
369 let mut res = Response::default();
370 res.extensions().clone_from( &result.extensions() );
371 result.headers().iter().for_each(|(key, val)| {
372 res.headers_mut().insert( key, val.to_owned() );
373 });
374 res.set_status( result.status() );
375 res.set_version( result.version() );
376 res.set_body( result.bytes().await.unwrap() );
377 Ok( res )
378 },
379
380 // The request to the back-end server failed. Why?
381 Err( error ) => {
382 Err( Error::from_string( error.to_string(), error.status().unwrap_or( StatusCode::BAD_GATEWAY ) ) )
383 }
384 }
385 }
386}