atm0s_media_server_console_front/
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 atm0s_media_server_console_front::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 atm0s_media_server_console_front::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.
109#[allow(unused)]
110impl ProxyConfig {
111    /// Function that creates a new ProxyConfig for a given target
112    /// and sets all other parameters to their default values. See
113    /// [the default implementation](ProxyConfig::default) for more
114    /// information.
115    pub fn new(target: impl Into<String>) -> ProxyConfig {
116        ProxyConfig {
117            proxy_target: target.into(),
118            ..ProxyConfig::default()
119        }
120    }
121
122    /// This function sets the endpoint to forward requests to the
123    /// target over the https protocol. This is a secure and encrypted
124    /// communication channel that should be utilized when possible.
125    pub fn web_secure(&mut self) -> &mut ProxyConfig {
126        self.web_secure = Some(true);
127        self
128    }
129
130    /// This function sets the endpoint to forward requests to the
131    /// target over the http protocol. This is an insecure and unencrypted
132    /// communication channel that should be used very carefully.
133    pub fn web_insecure(&mut self) -> &mut ProxyConfig {
134        self.web_secure = Some(false);
135        self
136    }
137
138    /// This function sets the waypoint to support nesting.
139    ///
140    /// For example,
141    /// if `endpoint.target` is `https://google.com` and the proxy is reached
142    /// at `https://proxy_address/favicon.png`, the proxy server will forward
143    /// the request to `https://google.com/favicon.png`.
144    pub fn enable_nesting(&mut self) -> &mut ProxyConfig {
145        self.support_nesting = true;
146        self
147    }
148
149    /// This function sets the waypoint to ignore nesting.
150    ///
151    /// For example,
152    /// if `endpoint.target` is `https://google.com` and the proxy is reached
153    /// at `https://proxy_address/favicon.png`, the proxy server will forward
154    /// the request to `https://google.com`.
155    pub fn disable_nesting(&mut self) -> &mut ProxyConfig {
156        self.support_nesting = false;
157        self
158    }
159
160    /// Finishes off the building process by returning a new ProxyConfig object
161    /// (not reference) that contains all the settings that were previously
162    /// specified.
163    pub fn finish(&mut self) -> ProxyConfig {
164        self.clone()
165    }
166}
167
168/// # Convenience Functions
169///
170/// These functions make it possible to get information from the ProxyConfig struct.
171impl ProxyConfig {
172    /// Returns the target url of the request, including the proper protocol information
173    /// and the correct pathing if nesting is enabled
174    ///
175    /// An example output would be
176    ///
177    /// > `"https://proxy.domain.com"`
178    pub fn get_web_request_uri(&self, subpath: Option<String>) -> Option<String> {
179        let secure = self.web_secure?;
180
181        let base = if secure {
182            format!("https://{}", self.proxy_target)
183        } else {
184            format!("http://{}", self.proxy_target)
185        };
186
187        let sub = self.support_nesting.then_some(subpath).flatten().unwrap_or_default();
188
189        log::trace!("base: {} | sub: {}", base, sub);
190
191        Some(base + &sub)
192    }
193}
194
195/// The websocket-enabled proxy handler
196#[handler]
197pub async fn proxy(req: &Request, config: Data<&ProxyConfig>, method: Method, body: Body) -> Result<Response> {
198    // Update the uri to point to the proxied server
199    // let request_uri = target.to_owned() + &req.uri().to_string();
200
201    // Get the websocket URI if websockets are supported, otherwise return an error
202    let Some(uri) = config.get_web_request_uri(Some(req.uri().to_string())) else {
203        return Err(Error::from_string("Proxy endpoint not configured to support web requests!", StatusCode::NOT_IMPLEMENTED));
204    };
205
206    // Now generate a request for the proxied server, based on information
207    // that we have from the current request
208    let client = reqwest::Client::new();
209    let res = match method {
210        Method::GET => client.get(uri).headers(req.headers().clone()).body(body.into_bytes().await.unwrap()).send().await,
211        Method::POST => client.post(uri).headers(req.headers().clone()).body(body.into_bytes().await.unwrap()).send().await,
212        _ => {
213            return Err(Error::from_string(
214                "Unsupported Method! The proxy endpoint currently only supports GET and POST requests!",
215                StatusCode::METHOD_NOT_ALLOWED,
216            ))
217        }
218    };
219
220    // Check on the response and forward everything from the server to our client,
221    // including headers and the body of the response, among other things.
222    match res {
223        Ok(result) => {
224            let mut res = Response::default();
225            res.extensions().clone_from(&result.extensions());
226            result.headers().iter().for_each(|(key, val)| {
227                res.headers_mut().insert(key, val.to_owned());
228            });
229            res.set_status(result.status());
230            res.set_version(result.version());
231            res.set_body(result.bytes().await.unwrap());
232            Ok(res)
233        }
234
235        // The request to the back-end server failed. Why?
236        Err(error) => Err(Error::from_string(error.to_string(), error.status().unwrap_or(StatusCode::BAD_GATEWAY))),
237    }
238}