diff --git a/Cargo.lock b/Cargo.lock
index aa87e5f..9f7ba70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -852,6 +852,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "minimal-lexical"
version = "0.2.1"
@@ -1179,9 +1189,11 @@ dependencies = [
"headers",
"http",
"http-body-util",
+ "httpdate",
"hyper",
"hyper-util",
"mime",
+ "mime_guess",
"multer",
"nix",
"parking_lot",
@@ -1306,6 +1318,19 @@ dependencies = [
"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]]
name = "quick-xml"
version = "0.30.0"
@@ -1450,8 +1475,11 @@ dependencies = [
"http",
"hyper-util",
"lazy_static",
+ "mime",
+ "percent-encoding",
"poem",
"poem-openapi",
+ "queryst",
"rand",
"regex",
"reqwest",
@@ -2126,6 +2154,15 @@ dependencies = [
"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]]
name = "unicode-bidi"
version = "0.3.14"
diff --git a/Cargo.toml b/Cargo.toml
index 38196f9..9c7a19c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,11 @@ futures-util = "0.3.30"
http = "1.0.0"
hyper-util = { version = "0.1.2", features = ["full"] }
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"] }
+queryst = "3.0.0"
rand = "0.8.5"
regex = "1.10.2"
reqwest = { git = "https://github.com/seanmonstar/reqwest.git", branch = "hyper-v1", version = "0.11.23" }
diff --git a/regions/index.html b/regions/index.html
new file mode 100644
index 0000000..60a6c53
--- /dev/null
+++ b/regions/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Hello, World!
+
+
+ Hello, there!
+ Here's the roadsign benchmarking test data!
+
+
diff --git a/regions/index.toml b/regions/index.toml
index dbe1fe7..22e2901 100644
--- a/regions/index.toml
+++ b/regions/index.toml
@@ -5,5 +5,5 @@ id = "root"
hosts = ["localhost"]
paths = ["/"]
[[locations.destinations]]
-id = "echo"
-uri = "https://postman-echo.com/get"
+id = "static"
+uri = "files://regions/index.html"
diff --git a/regions/kokodayo.txt b/regions/kokodayo.txt
new file mode 100644
index 0000000..035c7d9
--- /dev/null
+++ b/regions/kokodayo.txt
@@ -0,0 +1 @@
+Ko Ko Da Yo~
diff --git a/src/main.rs b/src/main.rs
index f7a69e1..2906943 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,7 @@ mod config;
mod proxies;
mod sideload;
-use poem::{listener::TcpListener, Endpoint, EndpointExt, Route, Server};
+use poem::{listener::TcpListener, EndpointExt, Route, Server};
use poem_openapi::OpenApiService;
use tracing::{error, info, Level};
diff --git a/src/proxies/browser.rs b/src/proxies/browser.rs
new file mode 100644
index 0000000..56f0aff
--- /dev/null
+++ b/src/proxies/browser.rs
@@ -0,0 +1,52 @@
+use std::fmt::Write;
+
+pub struct DirectoryTemplate<'a> {
+ pub path: &'a str,
+ pub files: Vec,
+}
+
+impl<'a> DirectoryTemplate<'a> {
+ pub fn render(&self) -> String {
+ let mut s = format!(
+ r#"
+
+
+ Index of {}
+
+
+ Index of /{}
+ "#,
+ self.path, self.path
+ );
+
+ for file in &self.files {
+ if file.is_dir {
+ let _ = write!(
+ s,
+ r#"- {}/
"#,
+ file.url, file.filename
+ );
+ } else {
+ let _ = write!(
+ s,
+ r#"- {}
"#,
+ file.url, file.filename
+ );
+ }
+ }
+
+ s.push_str(
+ r#"
+
+ "#,
+ );
+
+ s
+ }
+}
+
+pub struct FileRef {
+ pub url: String,
+ pub filename: String,
+ pub is_dir: bool,
+}
diff --git a/src/proxies/config.rs b/src/proxies/config.rs
index 2bfa414..552f54a 100644
--- a/src/proxies/config.rs
+++ b/src/proxies/config.rs
@@ -1,6 +1,10 @@
use std::collections::HashMap;
+use queryst::parse;
use serde::{Deserialize, Serialize};
+use serde_json::json;
+
+use super::responder::StaticResponderConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Region {
@@ -15,6 +19,7 @@ pub struct Location {
pub paths: Vec,
pub headers: Option>,
pub queries: Option>,
+ pub methods: Option>,
pub destinations: Vec,
}
@@ -46,13 +51,33 @@ impl Destination {
}
pub fn get_queries(&self) -> &str {
- self.uri.as_str().splitn(2, "?").collect::>()[1]
+ self.uri
+ .as_str()
+ .splitn(2, '?')
+ .collect::>()
+ .get(1)
+ .unwrap_or(&"")
}
pub fn get_host(&self) -> &str {
- (self.uri.as_str().splitn(2, "://").collect::>()[1])
- .splitn(2, "?")
- .collect::>()[0]
+ (self
+ .uri
+ .as_str()
+ .splitn(2, "://")
+ .collect::>()
+ .get(1)
+ .unwrap_or(&""))
+ .splitn(2, '?')
+ .collect::>()[0]
+ }
+
+ pub fn get_websocket_uri(&self) -> Result {
+ let parts = self.uri.as_str().splitn(2, "://").collect::>();
+ 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 {
@@ -63,10 +88,32 @@ impl Destination {
}
}
- pub fn get_websocket_uri(&self) -> Result {
- let url = self.uri.as_str().splitn(2, "://").collect::>()[1];
+ pub fn get_static_config(&self) -> Result {
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(()),
}
}
diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs
index 3ea38bb..aeb2178 100644
--- a/src/proxies/mod.rs
+++ b/src/proxies/mod.rs
@@ -1,3 +1,4 @@
+use http::Method;
use poem::http::{HeaderMap, Uri};
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -5,8 +6,10 @@ use wildmatch::WildMatch;
use self::config::{Location, Region};
+pub mod browser;
pub mod config;
pub mod loader;
+pub mod responder;
pub mod route;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -19,7 +22,7 @@ impl Instance {
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| {
region.locations.iter().find(|location| {
let mut hosts = location.hosts.iter();
@@ -37,6 +40,12 @@ impl Instance {
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() {
match !val.keys().all(|item| {
headers.get(item).unwrap()
diff --git a/src/proxies/responder.rs b/src/proxies/responder.rs
new file mode 100644
index 0000000..f9fbeac
--- /dev/null
+++ b/src/proxies/responder.rs
@@ -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 {
+ 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,
+ pub fallback: Option,
+}
+
+pub async fn respond_static(
+ cfg: StaticResponderConfig,
+ method: Method,
+ req: &Request,
+) -> Result {
+ 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::().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))
+ }
+ }
+}
diff --git a/src/proxies/route.rs b/src/proxies/route.rs
index 9432fc0..69c0be6 100644
--- a/src/proxies/route.rs
+++ b/src/proxies/route.rs
@@ -1,4 +1,3 @@
-use futures_util::{SinkExt, StreamExt};
use poem::{
handler,
http::{HeaderMap, StatusCode, Uri},
@@ -8,16 +7,10 @@ use poem::{
use rand::seq::SliceRandom;
use reqwest::Method;
-use lazy_static::lazy_static;
-use std::sync::Arc;
-use tokio::sync::RwLock;
-use tokio_tungstenite::connect_async;
-
-use crate::proxies::config::{Destination, DestinationType};
-
-lazy_static! {
- pub static ref CLIENT: reqwest::Client = reqwest::Client::new();
-}
+use crate::proxies::{
+ config::{Destination, DestinationType},
+ responder,
+};
#[handler]
pub async fn handle(
@@ -28,7 +21,7 @@ pub async fn handle(
method: Method,
body: Body,
) -> Result {
- let location = match app.filter(uri, headers) {
+ let location = match app.filter(uri, method.clone(), headers) {
Some(val) => val,
None => {
return Err(Error::from_string(
@@ -50,13 +43,13 @@ pub async fn handle(
headers: &HeaderMap,
method: Method,
body: Body,
- ) -> Result {
+ ) -> Result {
// Handle websocket
if let Ok(ws) = WebSocket::from_request_without_body(req).await {
// Get uri
let Ok(uri) = end.get_websocket_uri() else {
return Err(Error::from_string(
- "Proxy endpoint not configured to support websockets!",
+ "This destination was not support websockets.",
StatusCode::NOT_IMPLEMENTED,
));
};
@@ -68,47 +61,7 @@ pub async fn handle(
}
// Start the websocket connection
- return Ok(ws
- .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());
+ return Ok(responder::repond_websocket(ws_req, ws).await);
}
// Handle normal web request
@@ -116,38 +69,27 @@ pub async fn handle(
DestinationType::Hypertext => {
let Ok(uri) = end.get_hypertext_uri() else {
return Err(Error::from_string(
- "Proxy endpoint not configured to support web requests!",
+ "This destination was not support web requests.",
StatusCode::NOT_IMPLEMENTED,
));
};
- 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),
- )),
- }
+ responder::respond_hypertext(uri, ori, method, body, headers).await
}
- _ => Err(Error::from_status(StatusCode::NOT_IMPLEMENTED)),
+ 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,
+ ));
+ };
+
+ responder::respond_static(cfg, method, req).await
+ }
+ _ => Err(Error::from_string(
+ "Unsupported destination protocol.",
+ StatusCode::NOT_IMPLEMENTED,
+ )),
}
}