✨ Static files
This commit is contained in:
		
							
								
								
									
										37
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										37
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -852,6 +852,16 @@ version = "0.3.17" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "mime_guess" | ||||||
|  | version = "2.0.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" | ||||||
|  | dependencies = [ | ||||||
|  |  "mime", | ||||||
|  |  "unicase", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "minimal-lexical" | name = "minimal-lexical" | ||||||
| version = "0.2.1" | version = "0.2.1" | ||||||
| @@ -1179,9 +1189,11 @@ dependencies = [ | |||||||
|  "headers", |  "headers", | ||||||
|  "http", |  "http", | ||||||
|  "http-body-util", |  "http-body-util", | ||||||
|  |  "httpdate", | ||||||
|  "hyper", |  "hyper", | ||||||
|  "hyper-util", |  "hyper-util", | ||||||
|  "mime", |  "mime", | ||||||
|  |  "mime_guess", | ||||||
|  "multer", |  "multer", | ||||||
|  "nix", |  "nix", | ||||||
|  "parking_lot", |  "parking_lot", | ||||||
| @@ -1306,6 +1318,19 @@ dependencies = [ | |||||||
|  "unicode-ident", |  "unicode-ident", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "queryst" | ||||||
|  | version = "3.0.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c1cbeb75ac695daf201ca2d66d9c684f873b135f28af4f2c79952478cab3b9d9" | ||||||
|  | dependencies = [ | ||||||
|  |  "lazy_static", | ||||||
|  |  "percent-encoding", | ||||||
|  |  "regex", | ||||||
|  |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "quick-xml" | name = "quick-xml" | ||||||
| version = "0.30.0" | version = "0.30.0" | ||||||
| @@ -1450,8 +1475,11 @@ dependencies = [ | |||||||
|  "http", |  "http", | ||||||
|  "hyper-util", |  "hyper-util", | ||||||
|  "lazy_static", |  "lazy_static", | ||||||
|  |  "mime", | ||||||
|  |  "percent-encoding", | ||||||
|  "poem", |  "poem", | ||||||
|  "poem-openapi", |  "poem-openapi", | ||||||
|  |  "queryst", | ||||||
|  "rand", |  "rand", | ||||||
|  "regex", |  "regex", | ||||||
|  "reqwest", |  "reqwest", | ||||||
| @@ -2126,6 +2154,15 @@ dependencies = [ | |||||||
|  "version_check", |  "version_check", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "unicase" | ||||||
|  | version = "2.7.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" | ||||||
|  | dependencies = [ | ||||||
|  |  "version_check", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "unicode-bidi" | name = "unicode-bidi" | ||||||
| version = "0.3.14" | version = "0.3.14" | ||||||
|   | |||||||
| @@ -11,8 +11,11 @@ futures-util = "0.3.30" | |||||||
| http = "1.0.0" | http = "1.0.0" | ||||||
| hyper-util = { version = "0.1.2", features = ["full"] } | hyper-util = { version = "0.1.2", features = ["full"] } | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| poem = { version = "2.0.0", features = ["tokio-metrics", "websocket"] } | mime = "0.3.17" | ||||||
|  | percent-encoding = "2.3.1" | ||||||
|  | poem = { version = "2.0.0", features = ["tokio-metrics", "websocket", "static-files"] } | ||||||
| poem-openapi = { version = "4.0.0", features = ["swagger-ui"] } | poem-openapi = { version = "4.0.0", features = ["swagger-ui"] } | ||||||
|  | queryst = "3.0.0" | ||||||
| rand = "0.8.5" | rand = "0.8.5" | ||||||
| regex = "1.10.2" | regex = "1.10.2" | ||||||
| reqwest = { git = "https://github.com/seanmonstar/reqwest.git", branch = "hyper-v1", version = "0.11.23" } | reqwest = { git = "https://github.com/seanmonstar/reqwest.git", branch = "hyper-v1", version = "0.11.23" } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								regions/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								regions/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="utf-8" /> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||||
|  |     <title>Hello, World!</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <p>Hello, there!</p> | ||||||
|  |     <p>Here's the roadsign benchmarking test data!</p> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
| @@ -5,5 +5,5 @@ id = "root" | |||||||
| hosts = ["localhost"] | hosts = ["localhost"] | ||||||
| paths = ["/"] | paths = ["/"] | ||||||
| [[locations.destinations]] | [[locations.destinations]] | ||||||
| id = "echo" | id = "static" | ||||||
| uri = "https://postman-echo.com/get" | uri = "files://regions/index.html" | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								regions/kokodayo.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								regions/kokodayo.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | Ko Ko Da Yo~ | ||||||
| @@ -2,7 +2,7 @@ mod config; | |||||||
| mod proxies; | mod proxies; | ||||||
| mod sideload; | mod sideload; | ||||||
|  |  | ||||||
| use poem::{listener::TcpListener, Endpoint, EndpointExt, Route, Server}; | use poem::{listener::TcpListener, EndpointExt, Route, Server}; | ||||||
| use poem_openapi::OpenApiService; | use poem_openapi::OpenApiService; | ||||||
| use tracing::{error, info, Level}; | use tracing::{error, info, Level}; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								src/proxies/browser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/proxies/browser.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | use std::fmt::Write; | ||||||
|  |  | ||||||
|  | pub struct DirectoryTemplate<'a> { | ||||||
|  |     pub path: &'a str, | ||||||
|  |     pub files: Vec<FileRef>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> DirectoryTemplate<'a> { | ||||||
|  |     pub fn render(&self) -> String { | ||||||
|  |         let mut s = format!( | ||||||
|  |             r#" | ||||||
|  |         <html> | ||||||
|  |             <head> | ||||||
|  |             <title>Index of {}</title> | ||||||
|  |         </head> | ||||||
|  |         <body> | ||||||
|  |         <h1>Index of /{}</h1> | ||||||
|  |         <ul>"#, | ||||||
|  |             self.path, self.path | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         for file in &self.files { | ||||||
|  |             if file.is_dir { | ||||||
|  |                 let _ = write!( | ||||||
|  |                     s, | ||||||
|  |                     r#"<li><a href="{}">{}/</a></li>"#, | ||||||
|  |                     file.url, file.filename | ||||||
|  |                 ); | ||||||
|  |             } else { | ||||||
|  |                 let _ = write!( | ||||||
|  |                     s, | ||||||
|  |                     r#"<li><a href="{}">{}</a></li>"#, | ||||||
|  |                     file.url, file.filename | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         s.push_str( | ||||||
|  |             r#"</ul> | ||||||
|  |         </body> | ||||||
|  |         </html>"#, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         s | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct FileRef { | ||||||
|  |     pub url: String, | ||||||
|  |     pub filename: String, | ||||||
|  |     pub is_dir: bool, | ||||||
|  | } | ||||||
| @@ -1,6 +1,10 @@ | |||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use queryst::parse; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  | use serde_json::json; | ||||||
|  |  | ||||||
|  | use super::responder::StaticResponderConfig; | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| pub struct Region { | pub struct Region { | ||||||
| @@ -15,6 +19,7 @@ pub struct Location { | |||||||
|     pub paths: Vec<String>, |     pub paths: Vec<String>, | ||||||
|     pub headers: Option<HashMap<String, String>>, |     pub headers: Option<HashMap<String, String>>, | ||||||
|     pub queries: Option<Vec<String>>, |     pub queries: Option<Vec<String>>, | ||||||
|  |     pub methods: Option<Vec<String>>, | ||||||
|     pub destinations: Vec<Destination>, |     pub destinations: Vec<Destination>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,15 +51,35 @@ impl Destination { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn get_queries(&self) -> &str { |     pub fn get_queries(&self) -> &str { | ||||||
|         self.uri.as_str().splitn(2, "?").collect::<Vec<_>>()[1] |         self.uri | ||||||
|  |             .as_str() | ||||||
|  |             .splitn(2, '?') | ||||||
|  |             .collect::<Vec<_>>() | ||||||
|  |             .get(1) | ||||||
|  |             .unwrap_or(&"") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn get_host(&self) -> &str { |     pub fn get_host(&self) -> &str { | ||||||
|         (self.uri.as_str().splitn(2, "://").collect::<Vec<_>>()[1]) |         (self | ||||||
|             .splitn(2, "?") |             .uri | ||||||
|  |             .as_str() | ||||||
|  |             .splitn(2, "://") | ||||||
|  |             .collect::<Vec<_>>() | ||||||
|  |             .get(1) | ||||||
|  |             .unwrap_or(&"")) | ||||||
|  |         .splitn(2, '?') | ||||||
|         .collect::<Vec<_>>()[0] |         .collect::<Vec<_>>()[0] | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn get_websocket_uri(&self) -> Result<String, ()> { | ||||||
|  |         let parts = self.uri.as_str().splitn(2, "://").collect::<Vec<_>>(); | ||||||
|  |         let url = parts.get(1).unwrap_or(&""); | ||||||
|  |         match self.get_protocol() { | ||||||
|  |             "http" | "https" => Ok(url.replace("http", "ws")), | ||||||
|  |             _ => Err(()), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn get_hypertext_uri(&self) -> Result<String, ()> { |     pub fn get_hypertext_uri(&self) -> Result<String, ()> { | ||||||
|         match self.get_protocol() { |         match self.get_protocol() { | ||||||
|             "http" => Ok("http://".to_string() + self.get_host()), |             "http" => Ok("http://".to_string() + self.get_host()), | ||||||
| @@ -63,10 +88,32 @@ impl Destination { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn get_websocket_uri(&self) -> Result<String, ()> { |     pub fn get_static_config(&self) -> Result<StaticResponderConfig, ()> { | ||||||
|         let url = self.uri.as_str().splitn(2, "://").collect::<Vec<_>>()[1]; |  | ||||||
|         match self.get_protocol() { |         match self.get_protocol() { | ||||||
|             "http" | "https" => Ok(url.replace("http", "ws")), |             "file" | "files" => { | ||||||
|  |                 let queries = parse(self.get_queries()).unwrap_or(json!({})); | ||||||
|  |                 Ok(StaticResponderConfig { | ||||||
|  |                     uri: self.get_host().to_string(), | ||||||
|  |                     utf8: queries | ||||||
|  |                         .get("utf8") | ||||||
|  |                         .and_then(|val| val.as_bool()) | ||||||
|  |                         .unwrap_or(false), | ||||||
|  |                     with_slash: queries | ||||||
|  |                         .get("slash") | ||||||
|  |                         .and_then(|val| val.as_bool()) | ||||||
|  |                         .unwrap_or(false), | ||||||
|  |                     browse: queries | ||||||
|  |                         .get("browse") | ||||||
|  |                         .and_then(|val| val.as_bool()) | ||||||
|  |                         .unwrap_or(false), | ||||||
|  |                     index: queries | ||||||
|  |                         .get("index") | ||||||
|  |                         .and_then(|val| val.as_str().map(str::to_string)), | ||||||
|  |                     fallback: queries | ||||||
|  |                         .get("fallback") | ||||||
|  |                         .and_then(|val| val.as_str().map(str::to_string)), | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|             _ => Err(()), |             _ => Err(()), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | use http::Method; | ||||||
| use poem::http::{HeaderMap, Uri}; | use poem::http::{HeaderMap, Uri}; | ||||||
| use regex::Regex; | use regex::Regex; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| @@ -5,8 +6,10 @@ use wildmatch::WildMatch; | |||||||
|  |  | ||||||
| use self::config::{Location, Region}; | use self::config::{Location, Region}; | ||||||
|  |  | ||||||
|  | pub mod browser; | ||||||
| pub mod config; | pub mod config; | ||||||
| pub mod loader; | pub mod loader; | ||||||
|  | pub mod responder; | ||||||
| pub mod route; | pub mod route; | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||
| @@ -19,7 +22,7 @@ impl Instance { | |||||||
|         Instance { regions: vec![] } |         Instance { regions: vec![] } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn filter(&self, uri: &Uri, headers: &HeaderMap) -> Option<&Location> { |     pub fn filter(&self, uri: &Uri, method: Method, headers: &HeaderMap) -> Option<&Location> { | ||||||
|         self.regions.iter().find_map(|region| { |         self.regions.iter().find_map(|region| { | ||||||
|             region.locations.iter().find(|location| { |             region.locations.iter().find(|location| { | ||||||
|                 let mut hosts = location.hosts.iter(); |                 let mut hosts = location.hosts.iter(); | ||||||
| @@ -37,6 +40,12 @@ impl Instance { | |||||||
|                     return false; |                     return false; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 if let Some(val) = location.methods.clone() { | ||||||
|  |                     if !val.iter().any(|item| *item == method.to_string()) { | ||||||
|  |                         return false; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 if let Some(val) = location.headers.clone() { |                 if let Some(val) = location.headers.clone() { | ||||||
|                     match !val.keys().all(|item| { |                     match !val.keys().all(|item| { | ||||||
|                         headers.get(item).unwrap() |                         headers.get(item).unwrap() | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								src/proxies/responder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/proxies/responder.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | |||||||
|  | use futures_util::{SinkExt, StreamExt}; | ||||||
|  | use http::{header, request::Builder, HeaderMap, Method, StatusCode, Uri}; | ||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use poem::{ | ||||||
|  |     web::{websocket::WebSocket, StaticFileRequest}, | ||||||
|  |     Body, Error, FromRequest, IntoResponse, Request, Response, | ||||||
|  | }; | ||||||
|  | use std::{ | ||||||
|  |     ffi::OsStr, | ||||||
|  |     path::{Path, PathBuf}, | ||||||
|  |     sync::Arc, | ||||||
|  | }; | ||||||
|  | use tokio::sync::RwLock; | ||||||
|  | use tokio_tungstenite::connect_async; | ||||||
|  |  | ||||||
|  | use super::browser::{DirectoryTemplate, FileRef}; | ||||||
|  |  | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref CLIENT: reqwest::Client = reqwest::Client::new(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn repond_websocket(req: Builder, ws: WebSocket) -> Response { | ||||||
|  |     ws.on_upgrade(move |socket| async move { | ||||||
|  |         let (mut clientsink, mut clientstream) = socket.split(); | ||||||
|  |  | ||||||
|  |         // Start connection to server | ||||||
|  |         let (serversocket, _) = connect_async(req.body(()).unwrap()).await.unwrap(); | ||||||
|  |         let (mut serversink, mut serverstream) = serversocket.split(); | ||||||
|  |  | ||||||
|  |         let client_live = Arc::new(RwLock::new(true)); | ||||||
|  |         let server_live = client_live.clone(); | ||||||
|  |  | ||||||
|  |         tokio::spawn(async move { | ||||||
|  |             while let Some(Ok(msg)) = clientstream.next().await { | ||||||
|  |                 if (serversink.send(msg.into()).await).is_err() { | ||||||
|  |                     break; | ||||||
|  |                 }; | ||||||
|  |                 if !*client_live.read().await { | ||||||
|  |                     break; | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             *client_live.write().await = false; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // Relay server messages to the client | ||||||
|  |         tokio::spawn(async move { | ||||||
|  |             while let Some(Ok(msg)) = serverstream.next().await { | ||||||
|  |                 if (clientsink.send(msg.into()).await).is_err() { | ||||||
|  |                     break; | ||||||
|  |                 }; | ||||||
|  |                 if !*server_live.read().await { | ||||||
|  |                     break; | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             *server_live.write().await = false; | ||||||
|  |         }); | ||||||
|  |     }) | ||||||
|  |     .into_response() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn respond_hypertext( | ||||||
|  |     uri: String, | ||||||
|  |     ori: &Uri, | ||||||
|  |     method: Method, | ||||||
|  |     body: Body, | ||||||
|  |     headers: &HeaderMap, | ||||||
|  | ) -> Result<Response, Error> { | ||||||
|  |     let res = CLIENT | ||||||
|  |         .request(method, uri + ori.path() + ori.query().unwrap_or("")) | ||||||
|  |         .headers(headers.clone()) | ||||||
|  |         .body(body.into_bytes().await.unwrap()) | ||||||
|  |         .send() | ||||||
|  |         .await; | ||||||
|  |  | ||||||
|  |     match res { | ||||||
|  |         Ok(result) => { | ||||||
|  |             let mut res = Response::default(); | ||||||
|  |             res.extensions().clone_from(&result.extensions()); | ||||||
|  |             result.headers().iter().for_each(|(key, val)| { | ||||||
|  |                 res.headers_mut().insert(key, val.to_owned()); | ||||||
|  |             }); | ||||||
|  |             res.set_status(result.status()); | ||||||
|  |             res.set_version(result.version()); | ||||||
|  |             res.set_body(result.bytes().await.unwrap()); | ||||||
|  |             Ok(res) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Err(error) => Err(Error::from_string( | ||||||
|  |             error.to_string(), | ||||||
|  |             error.status().unwrap_or(StatusCode::BAD_GATEWAY), | ||||||
|  |         )), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct StaticResponderConfig { | ||||||
|  |     pub uri: String, | ||||||
|  |     pub utf8: bool, | ||||||
|  |     pub with_slash: bool, | ||||||
|  |     pub browse: bool, | ||||||
|  |     pub index: Option<String>, | ||||||
|  |     pub fallback: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn respond_static( | ||||||
|  |     cfg: StaticResponderConfig, | ||||||
|  |     method: Method, | ||||||
|  |     req: &Request, | ||||||
|  | ) -> Result<Response, Error> { | ||||||
|  |     if method != Method::GET { | ||||||
|  |         return Err(Error::from_string( | ||||||
|  |             "This destination only support GET request.", | ||||||
|  |             StatusCode::METHOD_NOT_ALLOWED, | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let path = req | ||||||
|  |         .uri() | ||||||
|  |         .path() | ||||||
|  |         .trim_start_matches('/') | ||||||
|  |         .trim_end_matches('/'); | ||||||
|  |  | ||||||
|  |     let path = percent_encoding::percent_decode_str(path) | ||||||
|  |         .decode_utf8() | ||||||
|  |         .map_err(|_| Error::from_status(StatusCode::NOT_FOUND))?; | ||||||
|  |  | ||||||
|  |     let base_path = cfg.uri.parse::<PathBuf>().unwrap(); | ||||||
|  |     let mut file_path = base_path.clone(); | ||||||
|  |     for p in Path::new(&*path) { | ||||||
|  |         if p == OsStr::new(".") { | ||||||
|  |             continue; | ||||||
|  |         } else if p == OsStr::new("..") { | ||||||
|  |             file_path.pop(); | ||||||
|  |         } else { | ||||||
|  |             file_path.push(p); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if !file_path.starts_with(cfg.uri) { | ||||||
|  |         return Err(Error::from_status(StatusCode::FORBIDDEN)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if !file_path.exists() { | ||||||
|  |         if let Some(file) = cfg.fallback { | ||||||
|  |             let fallback_path = base_path.join(file); | ||||||
|  |             if fallback_path.is_file() { | ||||||
|  |                 return Ok(StaticFileRequest::from_request_without_body(req) | ||||||
|  |                     .await? | ||||||
|  |                     .create_response(&fallback_path, cfg.utf8)? | ||||||
|  |                     .into_response()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return Err(Error::from_status(StatusCode::NOT_FOUND)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if file_path.is_file() { | ||||||
|  |         Ok(StaticFileRequest::from_request_without_body(req) | ||||||
|  |             .await? | ||||||
|  |             .create_response(&file_path, cfg.utf8)? | ||||||
|  |             .into_response()) | ||||||
|  |     } else { | ||||||
|  |         if cfg.with_slash | ||||||
|  |             && !req.original_uri().path().ends_with('/') | ||||||
|  |             && (cfg.index.is_some() || cfg.browse) | ||||||
|  |         { | ||||||
|  |             let redirect_to = format!("{}/", req.original_uri().path()); | ||||||
|  |             return Ok(Response::builder() | ||||||
|  |                 .status(StatusCode::FOUND) | ||||||
|  |                 .header(header::LOCATION, redirect_to) | ||||||
|  |                 .finish()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(index_file) = &cfg.index { | ||||||
|  |             let index_path = file_path.join(index_file); | ||||||
|  |             if index_path.is_file() { | ||||||
|  |                 return Ok(StaticFileRequest::from_request_without_body(req) | ||||||
|  |                     .await? | ||||||
|  |                     .create_response(&index_path, cfg.utf8)? | ||||||
|  |                     .into_response()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if cfg.browse { | ||||||
|  |             let read_dir = file_path | ||||||
|  |                 .read_dir() | ||||||
|  |                 .map_err(|_| Error::from_status(StatusCode::FORBIDDEN))?; | ||||||
|  |             let mut template = DirectoryTemplate { | ||||||
|  |                 path: &path, | ||||||
|  |                 files: Vec::new(), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             for res in read_dir { | ||||||
|  |                 let entry = res.map_err(|_| Error::from_status(StatusCode::FORBIDDEN))?; | ||||||
|  |  | ||||||
|  |                 if let Some(filename) = entry.file_name().to_str() { | ||||||
|  |                     let mut base_url = req.original_uri().path().to_string(); | ||||||
|  |                     if !base_url.ends_with('/') { | ||||||
|  |                         base_url.push('/'); | ||||||
|  |                     } | ||||||
|  |                     let filename_url = percent_encoding::percent_encode( | ||||||
|  |                         filename.as_bytes(), | ||||||
|  |                         percent_encoding::NON_ALPHANUMERIC, | ||||||
|  |                     ); | ||||||
|  |                     template.files.push(FileRef { | ||||||
|  |                         url: format!("{base_url}{filename_url}"), | ||||||
|  |                         filename: filename.to_string(), | ||||||
|  |                         is_dir: entry.path().is_dir(), | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             let html = template.render(); | ||||||
|  |             Ok(Response::builder() | ||||||
|  |                 .header(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.as_ref()) | ||||||
|  |                 .body(Body::from_string(html))) | ||||||
|  |         } else { | ||||||
|  |             Err(Error::from_status(StatusCode::NOT_FOUND)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,4 +1,3 @@ | |||||||
| use futures_util::{SinkExt, StreamExt}; |  | ||||||
| use poem::{ | use poem::{ | ||||||
|     handler, |     handler, | ||||||
|     http::{HeaderMap, StatusCode, Uri}, |     http::{HeaderMap, StatusCode, Uri}, | ||||||
| @@ -8,16 +7,10 @@ use poem::{ | |||||||
| use rand::seq::SliceRandom; | use rand::seq::SliceRandom; | ||||||
| use reqwest::Method; | use reqwest::Method; | ||||||
|  |  | ||||||
| use lazy_static::lazy_static; | use crate::proxies::{ | ||||||
| use std::sync::Arc; |     config::{Destination, DestinationType}, | ||||||
| use tokio::sync::RwLock; |     responder, | ||||||
| use tokio_tungstenite::connect_async; | }; | ||||||
|  |  | ||||||
| use crate::proxies::config::{Destination, DestinationType}; |  | ||||||
|  |  | ||||||
| lazy_static! { |  | ||||||
|     pub static ref CLIENT: reqwest::Client = reqwest::Client::new(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[handler] | #[handler] | ||||||
| pub async fn handle( | pub async fn handle( | ||||||
| @@ -28,7 +21,7 @@ pub async fn handle( | |||||||
|     method: Method, |     method: Method, | ||||||
|     body: Body, |     body: Body, | ||||||
| ) -> Result<impl IntoResponse, Error> { | ) -> Result<impl IntoResponse, Error> { | ||||||
|     let location = match app.filter(uri, headers) { |     let location = match app.filter(uri, method.clone(), headers) { | ||||||
|         Some(val) => val, |         Some(val) => val, | ||||||
|         None => { |         None => { | ||||||
|             return Err(Error::from_string( |             return Err(Error::from_string( | ||||||
| @@ -50,13 +43,13 @@ pub async fn handle( | |||||||
|         headers: &HeaderMap, |         headers: &HeaderMap, | ||||||
|         method: Method, |         method: Method, | ||||||
|         body: Body, |         body: Body, | ||||||
|     ) -> Result<impl IntoResponse, Error> { |     ) -> Result<Response, Error> { | ||||||
|         // Handle websocket |         // Handle websocket | ||||||
|         if let Ok(ws) = WebSocket::from_request_without_body(req).await { |         if let Ok(ws) = WebSocket::from_request_without_body(req).await { | ||||||
|             // Get uri |             // Get uri | ||||||
|             let Ok(uri) = end.get_websocket_uri() else { |             let Ok(uri) = end.get_websocket_uri() else { | ||||||
|                 return Err(Error::from_string( |                 return Err(Error::from_string( | ||||||
|                     "Proxy endpoint not configured to support websockets!", |                     "This destination was not support websockets.", | ||||||
|                     StatusCode::NOT_IMPLEMENTED, |                     StatusCode::NOT_IMPLEMENTED, | ||||||
|                 )); |                 )); | ||||||
|             }; |             }; | ||||||
| @@ -68,47 +61,7 @@ pub async fn handle( | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Start the websocket connection |             // Start the websocket connection | ||||||
|             return Ok(ws |             return Ok(responder::repond_websocket(ws_req, ws).await); | ||||||
|                 .on_upgrade(move |socket| async move { |  | ||||||
|                     let (mut clientsink, mut clientstream) = socket.split(); |  | ||||||
|  |  | ||||||
|                     // Start connection to server |  | ||||||
|                     let (serversocket, _) = connect_async(ws_req.body(()).unwrap()).await.unwrap(); |  | ||||||
|                     let (mut serversink, mut serverstream) = serversocket.split(); |  | ||||||
|  |  | ||||||
|                     let client_live = Arc::new(RwLock::new(true)); |  | ||||||
|                     let server_live = client_live.clone(); |  | ||||||
|  |  | ||||||
|                     tokio::spawn(async move { |  | ||||||
|                         while let Some(Ok(msg)) = clientstream.next().await { |  | ||||||
|                             match serversink.send(msg.into()).await { |  | ||||||
|                                 Err(_) => break, |  | ||||||
|                                 _ => {} |  | ||||||
|                             }; |  | ||||||
|                             if !*client_live.read().await { |  | ||||||
|                                 break; |  | ||||||
|                             }; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         *client_live.write().await = false; |  | ||||||
|                     }); |  | ||||||
|  |  | ||||||
|                     // Relay server messages to the client |  | ||||||
|                     tokio::spawn(async move { |  | ||||||
|                         while let Some(Ok(msg)) = serverstream.next().await { |  | ||||||
|                             match clientsink.send(msg.into()).await { |  | ||||||
|                                 Err(_) => break, |  | ||||||
|                                 _ => {} |  | ||||||
|                             }; |  | ||||||
|                             if !*server_live.read().await { |  | ||||||
|                                 break; |  | ||||||
|                             }; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         *server_live.write().await = false; |  | ||||||
|                     }); |  | ||||||
|                 }) |  | ||||||
|                 .into_response()); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Handle normal web request |         // Handle normal web request | ||||||
| @@ -116,40 +69,29 @@ pub async fn handle( | |||||||
|             DestinationType::Hypertext => { |             DestinationType::Hypertext => { | ||||||
|                 let Ok(uri) = end.get_hypertext_uri() else { |                 let Ok(uri) = end.get_hypertext_uri() else { | ||||||
|                     return Err(Error::from_string( |                     return Err(Error::from_string( | ||||||
|                         "Proxy endpoint not configured to support web requests!", |                         "This destination was not support web requests.", | ||||||
|                         StatusCode::NOT_IMPLEMENTED, |                         StatusCode::NOT_IMPLEMENTED, | ||||||
|                     )); |                     )); | ||||||
|                 }; |                 }; | ||||||
|  |  | ||||||
|                 let res = CLIENT |                 responder::respond_hypertext(uri, ori, method, body, headers).await | ||||||
|                     .request(method, uri + ori.path() + ori.query().unwrap_or("")) |  | ||||||
|                     .headers(headers.clone()) |  | ||||||
|                     .body(body.into_bytes().await.unwrap()) |  | ||||||
|                     .send() |  | ||||||
|                     .await; |  | ||||||
|  |  | ||||||
|                 match res { |  | ||||||
|                     Ok(result) => { |  | ||||||
|                         let mut res = Response::default(); |  | ||||||
|                         res.extensions().clone_from(&result.extensions()); |  | ||||||
|                         result.headers().iter().for_each(|(key, val)| { |  | ||||||
|                             res.headers_mut().insert(key, val.to_owned()); |  | ||||||
|                         }); |  | ||||||
|                         res.set_status(result.status()); |  | ||||||
|                         res.set_version(result.version()); |  | ||||||
|                         res.set_body(result.bytes().await.unwrap()); |  | ||||||
|                         Ok(res) |  | ||||||
|             } |             } | ||||||
|  |             DestinationType::StaticFiles => { | ||||||
|  |                 let Ok(cfg) = end.get_static_config() else { | ||||||
|  |                     return Err(Error::from_string( | ||||||
|  |                         "This destination was not support static files.", | ||||||
|  |                         StatusCode::NOT_IMPLEMENTED, | ||||||
|  |                     )); | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|                     Err(error) => Err(Error::from_string( |                 responder::respond_static(cfg, method, req).await | ||||||
|                         error.to_string(), |             } | ||||||
|                         error.status().unwrap_or(StatusCode::BAD_GATEWAY), |             _ => Err(Error::from_string( | ||||||
|  |                 "Unsupported destination protocol.", | ||||||
|  |                 StatusCode::NOT_IMPLEMENTED, | ||||||
|             )), |             )), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|             _ => Err(Error::from_status(StatusCode::NOT_IMPLEMENTED)), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     forward(destination, req, uri, headers, method, body).await |     forward(destination, req, uri, headers, method, body).await | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user