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}