From ad6bb437fedd785ffcd24ecf00db47b106928280 Mon Sep 17 00:00:00 2001 From: termack Date: Sun, 9 Jun 2024 14:54:39 -0300 Subject: [PATCH] add possibility to have multiple addresses on jequi_proxy, add content-type detection on jequi_serve_static and change path config --- .gitignore | 1 + Cargo.lock | 61 +++++++++ Makefile | 6 +- TODO | 2 + api/src/lib.rs | 2 +- conf.yaml | 24 ++-- jequi/src/config.rs | 115 ++++++++++------ jequi/src/http1/read.rs | 4 +- jequi/src/http1/test.rs | 2 +- jequi/src/http2/conn.rs | 3 +- jequi/src/http2/frame.rs | 6 +- jequi/src/lib.rs | 13 +- jequi/src/request.rs | 28 +++- jequi/src/response.rs | 4 +- jequi/test/test.conf | 6 +- plugins/jequi_go/go/handle/handler.go | 11 +- plugins/jequi_go/src/lib.rs | 9 +- plugins/jequi_proxy/Cargo.toml | 1 + plugins/jequi_proxy/src/lib.rs | 48 +++++-- plugins/jequi_serve_static/Cargo.toml | 4 +- .../jequi_serve_static/src/content_type.rs | 29 ++++ plugins/jequi_serve_static/src/lib.rs | 127 +++++++++--------- plugins/jequi_serve_static/test/aa.js | 1 + test/host/path/index.html | 2 + test/host/path/style.css | 1 + test/host/uri/index.html | 1 - test/style1.css | 1 + 27 files changed, 355 insertions(+), 157 deletions(-) create mode 100644 plugins/jequi_serve_static/src/content_type.rs create mode 100644 plugins/jequi_serve_static/test/aa.js create mode 100644 test/host/path/index.html create mode 100644 test/host/path/style.css delete mode 100644 test/host/uri/index.html diff --git a/.gitignore b/.gitignore index e1bd1c1..bb169c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ *jequi.pid expanded.rs +jequi_go.* diff --git a/Cargo.lock b/Cargo.lock index a7c6345..33565be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,17 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dyn-clone" version = "1.0.13" @@ -275,6 +286,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.0" @@ -492,6 +514,7 @@ dependencies = [ "hyper-tls", "jequi", "openssl", + "rand", "serde", "serde_yaml", "tokio", @@ -502,6 +525,8 @@ dependencies = [ name = "jequi_serve_static" version = "0.1.0" dependencies = [ + "derivative", + "http", "jequi", "serde", "serde_yaml", @@ -741,6 +766,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.75" @@ -759,6 +790,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.3.5" diff --git a/Makefile b/Makefile index 1e4a4a1..c8984c0 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,12 @@ clear: go_setup: if ! [ -f /etc/jequi/libjequi.so ]; then \ - sudo cp target/debug/libjequi.so /etc/jequi/libjequi.so; \ + sudo cp $(PWD)/target/debug/libjequi.so /etc/jequi/libjequi.so; \ fi \ && cd ./plugins/jequi_go/go/jequi \ - && go generate \ && go mod edit -replace github.com/handle=$(HANDLER_PATH) \ - && go mod tidy + && go mod tidy \ + && go generate go: clear go_setup cd ./plugins/jequi_go/go/jequi && LIB_DIR=$(LIB_DIR) go build -o $(LIB_DIR)/$(LIB_NAME).so -buildmode=c-shared diff --git a/TODO b/TODO index 00251d2..5b40802 100644 --- a/TODO +++ b/TODO @@ -2,6 +2,8 @@ IMPORTANT: Make a better implementation for reading body and http2 and implement Add Websocket HTTP2 and some other things in http1 (like keepalive, chunked, gzip) Check test coverage +Better way to load go code +Organize plugins that are becoming too big More tests (read body async, http2, some other things) Reload isn't working correctly when remove or add a new plugin (example: comment static_files_path and uncomment proxy_address) Add proxy and jequi_go proxy tests diff --git a/api/src/lib.rs b/api/src/lib.rs index 9d18975..3bc4e49 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -60,7 +60,7 @@ pub unsafe extern "C" fn get_request_body(req: *mut Request) -> *const c_char { #[no_mangle] pub unsafe extern "C" fn get_request_uri(req: *mut Request) -> *const c_char { let req: &mut Request = unsafe { get_object_from_pointer(req) }; - CString::new(req.uri.as_str()).unwrap().into_raw() + CString::new(req.uri.raw()).unwrap().into_raw() } #[no_mangle] diff --git a/conf.yaml b/conf.yaml index 30dbbbf..a5c3388 100644 --- a/conf.yaml +++ b/conf.yaml @@ -1,22 +1,22 @@ tls_active: true -proxy_address: "www.google.com" ssl_certificate: jequi/test/leaf-cert.pem ssl_key: jequi/test/leaf-cert.key -go_library_path: "jequi_go.so" +static_files_path: "test/" +go_library_path: "./jequi_go.so" host: jequi.com: ssl_certificate: jequi/test/leaf-cert.pem ssl_key: jequi/test/leaf-cert.key http2: true chunk_size: 4000 - uri: - /api: - go_library_path: "jequi_go.so" - /app: - static_files_path: "test/host/uri/" - go_library_path: "jequi_go.so" - proxy_address: "www.google.com" + path: + /api/: + go_library_path: "./jequi_go.so" + /app/: + static_files_path: "test/host/path/" + go_library_path: "./jequi_go.so" + proxy_address: ["www.google.com","https://github.com"] # static_files_path: "test/" -uri: - /api/v2: - go_library_path: "jequi_go.so" +path: + /api/v2/: + go_library_path: "./jequi_go.so" diff --git a/jequi/src/config.rs b/jequi/src/config.rs index 47295f8..d27643d 100644 --- a/jequi/src/config.rs +++ b/jequi/src/config.rs @@ -1,4 +1,10 @@ -use std::{any::Any, collections::HashMap, sync::Arc}; +use core::panic; +use std::{ + any::Any, + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; use serde::Deserialize; use serde_yaml::from_reader; @@ -48,7 +54,7 @@ fn merge_config_and_load_plugins( // merge_yaml(&mut config_parser, config_to_merge); let config_parser = config_to_merge; if let Value::Mapping(ref mut config_parser) = config_parser { - config_parser.insert(key_to_add.into(), value_to_add.into()); + config_parser.insert(format!("config_{}", key_to_add).into(), value_to_add.into()); } load_plugins(config_parser) } @@ -68,88 +74,89 @@ impl ConfigMap { host.clone(), load_plugins, ); - let mut uri_config: Option>> = None; - for (uri, mut uri_config_parser) in host_config_parser.uri.into_iter().flatten() { + let mut path_config: Option>> = None; + for (path, mut path_config_parser) in host_config_parser.path.into_iter().flatten() { let mut config_parser = config_parser.clone(); let plugin_list = merge_config_and_load_plugins( &mut config_parser, - &mut uri_config_parser, - "uri", - uri.clone(), + &mut path_config_parser, + "path", + path.to_string_lossy().to_string(), load_plugins, ); - uri_config.get_or_insert_default().insert(uri, plugin_list); + path_config + .get_or_insert_default() + .insert(path, plugin_list); } main_conf.host.get_or_insert_default().insert( host, HostConfig { - uri: uri_config, + path: path_config, config: plugin_list, }, ); } - for (uri, mut uri_config_parser) in main_conf_parser.uri.into_iter().flatten() { + for (path, mut path_config_parser) in main_conf_parser.path.into_iter().flatten() { let mut config_parser = config_parser.clone(); let plugin_list = merge_config_and_load_plugins( &mut config_parser, - &mut uri_config_parser, - "uri", - uri.clone(), + &mut path_config_parser, + "path", + path.to_string_lossy().to_string(), load_plugins, ); main_conf - .uri + .path .get_or_insert_default() - .insert(uri, plugin_list); + .insert(path, plugin_list); } main_conf.config = load_plugins(&config_parser); main_conf } - pub fn get_config_for_request(&self, host: Option<&str>, uri: Option<&str>) -> &Vec { + pub fn get_config_for_request(&self, host: Option<&str>, path: Option<&str>) -> &Vec { let mut config = &self.config; - let mut uri_map = &self.uri; + let mut path_map = &self.path; if let Some(host_map) = &self.host && let Some(host) = host && let Some(host_config) = host_map.get(host.split(':').next().unwrap()) { config = &host_config.config; - uri_map = &host_config.uri; + path_map = &host_config.path; } - let uri = match uri { - Some(uri) => uri, + let path = match path { + Some(path) => path, None => return config, }; - let uri_map = match uri_map { - Some(uri_map) => { - if uri_map.is_empty() { + let path_map = match path_map { + Some(path_map) => { + if path_map.is_empty() { return config; } - uri_map + path_map } None => return config, }; - let mut uri: &str = uri; - if let Some(i) = uri.find('?') { - uri = &uri[..i]; - } + let mut path = PathBuf::from(path); - if let Some(config) = uri_map.get(uri) { + if let Some(config) = path_map.get(&path) { return config; } - while let Some(i) = uri.rfind('/') { - uri = &uri[..i]; - if let Some(config) = uri_map.get(uri) { + loop { + if let Some(config) = path_map.get(&path) { + return config; + } + + if !path.pop() { return config; } } - config } } @@ -165,7 +172,7 @@ impl ConfigMapParser { #[cfg(test)] mod tests { - use std::{sync::Arc, vec}; + use std::{path::Path, sync::Arc}; use crate::{load_plugin, Config, ConfigMap, ConfigMapParser, JequiConfig, Plugin}; @@ -205,11 +212,27 @@ mod tests { let host_jequi_com = config_map.host.as_ref().unwrap().get("jequi.com").unwrap(); assert_eq!(get_config(&host_jequi_com.config).ip, "1.1.2.1"); assert_eq!( - get_config(host_jequi_com.uri.as_ref().unwrap().get("/app").unwrap()).ip, + get_config( + host_jequi_com + .path + .as_ref() + .unwrap() + .get(Path::new("/app")) + .unwrap() + ) + .ip, "1.1.2.2" ); assert_eq!( - get_config(host_jequi_com.uri.as_ref().unwrap().get("/api").unwrap()).ip, + get_config( + host_jequi_com + .path + .as_ref() + .unwrap() + .get(Path::new("/api")) + .unwrap() + ) + .ip, "1.1.2.3" ); assert_eq!( @@ -226,11 +249,27 @@ mod tests { "1.1.3.1" ); assert_eq!( - get_config(config_map.uri.as_ref().unwrap().get("/app").unwrap()).ip, + get_config( + config_map + .path + .as_ref() + .unwrap() + .get(Path::new("/app")) + .unwrap() + ) + .ip, "1.2.1.1" ); assert_eq!( - get_config(config_map.uri.as_ref().unwrap().get("/test").unwrap()).ip, + get_config( + config_map + .path + .as_ref() + .unwrap() + .get(Path::new("/test")) + .unwrap() + ) + .ip, "1.2.1.2" ); diff --git a/jequi/src/http1/read.rs b/jequi/src/http1/read.rs index 1180ddb..4e4bc29 100644 --- a/jequi/src/http1/read.rs +++ b/jequi/src/http1/read.rs @@ -13,7 +13,7 @@ use tokio::{ pin, }; -use crate::{body::RequestBody, RawStream, Request}; +use crate::{body::RequestBody, RawStream, Request, Uri}; use super::Http1Conn; @@ -37,7 +37,7 @@ impl Http1Conn { self.read_until_handle_eof(b'\n', &mut version).await?; self.request.method = String::from_utf8_lossy(&method[..method.len() - 1]).to_string(); - self.request.uri = String::from_utf8_lossy(uri.trim_ascii()).to_string(); + self.request.uri = Uri::from(String::from_utf8_lossy(uri.trim_ascii()).to_string()); self.version = String::from_utf8_lossy(version.trim_ascii()).to_string(); Ok(()) } diff --git a/jequi/src/http1/test.rs b/jequi/src/http1/test.rs index 50c4d14..ce5ee92 100644 --- a/jequi/src/http1/test.rs +++ b/jequi/src/http1/test.rs @@ -58,7 +58,7 @@ async fn parse_first_line_test() { assert_eq!( expected_results[i], - new_result(req.request.method, req.request.uri, req.version,), + new_result(req.request.method, req.request.uri.0, req.version,), "Testing parse for line: {}", String::from_utf8_lossy(r) ) diff --git a/jequi/src/http2/conn.rs b/jequi/src/http2/conn.rs index ec3e9e7..62a2316 100644 --- a/jequi/src/http2/conn.rs +++ b/jequi/src/http2/conn.rs @@ -67,7 +67,8 @@ impl Http2Conn { compressed_headers, ); - let config = config_map.get_config_for_request(request.host.as_deref(), Some(&request.uri)); + let config = + config_map.get_config_for_request(request.host.as_deref(), Some(request.uri.path())); let conf = get_plugin!(config, jequi).unwrap(); self.conn diff --git a/jequi/src/http2/frame.rs b/jequi/src/http2/frame.rs index b641b25..831b2f0 100644 --- a/jequi/src/http2/frame.rs +++ b/jequi/src/http2/frame.rs @@ -11,7 +11,7 @@ use tokio::{ sync::mpsc::Sender, }; -use crate::{ConfigMap, Request, Response}; +use crate::{ConfigMap, Request, Response, Uri}; use super::{Stream, END_STREAM_FLAG, PADDED_FLAG, PRIORITY_FLAG}; @@ -193,7 +193,9 @@ impl> Http2Frame

{ b":method" => { request.method = String::from_utf8_lossy(v.as_ref()).to_string() } - b":path" => request.uri = String::from_utf8_lossy(v.as_ref()).to_string(), + b":path" => { + request.uri = Uri::from(String::from_utf8_lossy(v.as_ref()).to_string()) + } b":authority" => { request.host = Some(String::from_utf8_lossy(v.as_ref()).to_string()) } diff --git a/jequi/src/lib.rs b/jequi/src/lib.rs index 3e23fc1..53869fb 100644 --- a/jequi/src/lib.rs +++ b/jequi/src/lib.rs @@ -17,6 +17,7 @@ use std::{ any::Any, collections::HashMap, fmt::{self, Debug}, + path::PathBuf, sync::Arc, }; @@ -58,20 +59,20 @@ pub type ConfigList = Vec; #[derive(Debug)] pub struct HostConfig { - pub uri: Option>, + pub path: Option>, pub config: ConfigList, } #[derive(Default, Debug)] pub struct ConfigMap { pub host: Option>, - pub uri: Option>, + pub path: Option>, pub config: ConfigList, } #[derive(Deserialize)] pub struct HostConfigParser { - pub uri: Option>, + pub path: Option>, #[serde(flatten)] pub config: Value, } @@ -79,7 +80,7 @@ pub struct HostConfigParser { #[derive(Deserialize)] pub struct ConfigMapParser { pub host: Option>, - pub uri: Option>, + pub path: Option>, #[serde(flatten)] pub config: Value, } @@ -117,9 +118,11 @@ pub enum RawStream { Normal(T), } +pub struct Uri(String); + pub struct Request { pub method: String, - pub uri: String, + pub uri: Uri, pub headers: HeaderMap, pub host: Option, pub body: Arc, diff --git a/jequi/src/request.rs b/jequi/src/request.rs index 5825eb4..224fae0 100644 --- a/jequi/src/request.rs +++ b/jequi/src/request.rs @@ -6,13 +6,35 @@ use http::{HeaderMap, HeaderValue}; use crate::body::GetBody; use crate::{body::RequestBody, Request}; -use crate::{ConfigMap, Response}; +use crate::{ConfigMap, Response, Uri}; + +impl From for Uri { + fn from(item: String) -> Self { + Self(item) + } +} + +impl Uri { + pub fn raw(&self) -> &str { + self.0.as_str() + } + + pub fn path(&self) -> &str { + self.0.splitn(2, '?').next().unwrap() + } + + pub fn query_string(&self) -> Option<&str> { + let mut it = self.0.splitn(2, '?'); + it.next(); + it.next() + } +} impl Request { pub fn new() -> Request { Request { method: String::new(), - uri: String::new(), + uri: Uri::from(String::new()), headers: HeaderMap::new(), host: None, body: Arc::new(RequestBody::default()), @@ -45,7 +67,7 @@ impl Request { &Utc::now().format("%a, %e %b %Y %T GMT").to_string(), ); - let config = config_map.get_config_for_request(self.host.as_deref(), Some(&self.uri)); + let config = config_map.get_config_for_request(self.host.as_deref(), Some(self.uri.path())); for handle_plugin in config.iter().map(|x| &x.request_handler.0).flat_map(|x| x) { if let Some(fut) = handle_plugin(self, response) { diff --git a/jequi/src/response.rs b/jequi/src/response.rs index 81c0760..ce93b52 100644 --- a/jequi/src/response.rs +++ b/jequi/src/response.rs @@ -47,7 +47,7 @@ mod tests { use http::HeaderMap; use tokio::io::{AsyncReadExt, BufStream}; - use crate::{body::RequestBody, http1::Http1Conn, RawStream, Request, Response}; + use crate::{body::RequestBody, http1::Http1Conn, RawStream, Request, Response, Uri}; fn new_response( headers: HeaderMap, @@ -61,7 +61,7 @@ mod tests { version, request: Request { method: String::new(), - uri: String::new(), + uri: Uri::from(String::new()), headers: HeaderMap::new(), host: None, body: Arc::new(RequestBody::default()), diff --git a/jequi/test/test.conf b/jequi/test/test.conf index b45005c..babf159 100644 --- a/jequi/test/test.conf +++ b/jequi/test/test.conf @@ -3,15 +3,15 @@ ip: 1.1.1.1 host: jequi.com: ip: 1.1.2.1 - uri: + path: /app: ip: 1.1.2.2 /api: ip: 1.1.2.3 www.jequi.com: ip: 1.1.3.1 -uri: +path: /app: ip: 1.2.1.1 /test: - ip: 1.2.1.2 \ No newline at end of file + ip: 1.2.1.2 diff --git a/plugins/jequi_go/go/handle/handler.go b/plugins/jequi_go/go/handle/handler.go index 2e5be27..0cbecaf 100644 --- a/plugins/jequi_go/go/handle/handler.go +++ b/plugins/jequi_go/go/handle/handler.go @@ -2,15 +2,16 @@ package handle import ( "fmt" - // "strings" "github.com/jequi_go" ) func HandleRequest(req jequi_go.Request, resp jequi_go.Response) { - resp.SetHeader("hello", "world") - // resp.WriteBody("Hello World!\n") - // resp.SetStatus(404) + // fmt.Println(req.GetUri(), strings.HasSuffix(req.GetUri(), ".js")) + + // resp.SetHeader("hello", "world") + // // resp.WriteBody("Hello World!\n") + // // resp.SetStatus(404) fmt.Printf("Method: %q, Uri: %q, User-Agent: %q, Body: %q\n", req.GetMethod(), req.GetUri(), @@ -33,5 +34,5 @@ func HandleProxyRequest(req jequi_go.Request, resp jequi_go.Response) *string { // req.SetUri(newUri) // // return &address - return nil + return nil } diff --git a/plugins/jequi_go/src/lib.rs b/plugins/jequi_go/src/lib.rs index 84aed03..89803cf 100644 --- a/plugins/jequi_go/src/lib.rs +++ b/plugins/jequi_go/src/lib.rs @@ -121,7 +121,7 @@ mod tests { use std::{io::Cursor, process::Command}; - use jequi::{http1::Http1Conn, JequiConfig, RawStream}; + use jequi::{http1::Http1Conn, JequiConfig, RawStream, Uri}; use serde_yaml::{Mapping, Value}; use crate::Config; @@ -155,12 +155,15 @@ mod tests { let conf = Config::load(&Value::Mapping(yaml_config), &mut Vec::new()).unwrap(); - http.request.uri = "/file".to_string(); + http.request.uri = Uri::from("/file".to_string()); conf.handle_request(&mut http.request, &mut http.response); assert_eq!(http.response.status, 200); assert_eq!(&http.response.body_buffer[..], b"hello"); - assert_eq!(http.response.get_header("test").unwrap(), &http.request.uri); + assert_eq!( + http.response.get_header("test").unwrap(), + &http.request.uri.raw() + ); } } diff --git a/plugins/jequi_proxy/Cargo.toml b/plugins/jequi_proxy/Cargo.toml index 51c9e6f..f591f6d 100644 --- a/plugins/jequi_proxy/Cargo.toml +++ b/plugins/jequi_proxy/Cargo.toml @@ -10,6 +10,7 @@ jequi = { path = "../../jequi" } serde = { version = "1.0.183", features = ["derive"] } tokio = { version = "1", features = ["full"] } serde_yaml = "0.9.25" +rand = "0.8.5" futures = "0.3.29" http = "0.2.9" hyper = { version = "0.14.27", features = ["full"] } diff --git a/plugins/jequi_proxy/src/lib.rs b/plugins/jequi_proxy/src/lib.rs index 3e282d7..79b6f47 100644 --- a/plugins/jequi_proxy/src/lib.rs +++ b/plugins/jequi_proxy/src/lib.rs @@ -5,8 +5,9 @@ use futures::future::{BoxFuture, FutureExt}; use hyper::body::{self}; use hyper::{Body, Client}; use hyper_tls::HttpsConnector; -use jequi::{JequiConfig, Plugin, Request, RequestHandler, Response}; -use serde::Deserialize; +use jequi::{JequiConfig, Plugin, Request, RequestHandler, Response, Uri}; +use rand::seq::SliceRandom; +use serde::{de, Deserialize}; use serde_yaml::Value; use std::any::Any; use std::ffi::CStr; @@ -27,7 +28,7 @@ pub unsafe extern "C" fn set_request_uri(req: *mut Request, value: *const c_char if !uri.starts_with('/') { uri.insert(0, '/'); } - req.uri = uri; + req.uri = Uri::from(uri); } pub fn load_plugin(config_yaml: &Value, configs: &mut Vec>) -> Option { @@ -63,9 +64,16 @@ impl fmt::Debug for RequestProxyHandler { } } +#[derive(Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum ProxyAddress { + Address(String), + Addresses(Vec), +} + #[derive(Deserialize, Default, Debug)] pub struct Config { - pub proxy_address: Option, + pub proxy_address: Option, #[serde(skip)] proxy_handlers: Option>, } @@ -101,22 +109,36 @@ impl Config { } } - let mut proxy_address = &proxy_address; + let mut proxy_address = proxy_address.as_ref(); if proxy_address.is_none() { - proxy_address = &self.proxy_address; + proxy_address = self.proxy_address.as_ref().map(|a| match a { + ProxyAddress::Address(address) => address, + ProxyAddress::Addresses(addresses) => { + addresses.choose(&mut rand::thread_rng()).unwrap() + } + }) } + let (address, scheme) = { + let proxy_address = proxy_address.unwrap(); + + let mut it = proxy_address.splitn(2, "://"); + + let first = it.next().unwrap(); + match it.next() { + Some(address) => (address, first), + None => (first, "https"), + } + }; + let url = http::Uri::builder() - .scheme("https") - .authority(proxy_address.as_ref().unwrap().deref()) - .path_and_query(req.uri.deref()) + .scheme(scheme) + .authority(address) + .path_and_query(req.uri.path()) .build() .unwrap(); let mut request_builder = http::Request::builder().method(req.method.deref()).uri(url); - req.headers.insert( - "Host", - proxy_address.as_ref().unwrap().deref().parse().unwrap(), - ); + req.headers.insert("Host", address.parse().unwrap()); *request_builder.headers_mut().unwrap() = req.headers.clone(); let bodyy = req.get_body().await.clone(); let body = match bodyy.as_deref() { diff --git a/plugins/jequi_serve_static/Cargo.toml b/plugins/jequi_serve_static/Cargo.toml index 23b311b..df3bc1a 100644 --- a/plugins/jequi_serve_static/Cargo.toml +++ b/plugins/jequi_serve_static/Cargo.toml @@ -9,4 +9,6 @@ edition = "2021" jequi = { path = "../../jequi" } serde = {version = "1.0.183",features = ["derive"] } tokio = { version = "1", features = ["full"] } -serde_yaml = "0.9.25" \ No newline at end of file +serde_yaml = "0.9.25" +http = "0.2.9" +derivative = "2.2.0" diff --git a/plugins/jequi_serve_static/src/content_type.rs b/plugins/jequi_serve_static/src/content_type.rs new file mode 100644 index 0000000..b7f606f --- /dev/null +++ b/plugins/jequi_serve_static/src/content_type.rs @@ -0,0 +1,29 @@ +use std::path::PathBuf; + +pub fn get_content_type_by_path(path: &PathBuf) -> Option<&str> { + let content_type = match path.extension()?.to_str().unwrap() { + "js" => "text/javascript", + "css" => "text/css", + "csv" => "text/csv", + "gif" => "image/gif", + "html" => "text/html", + "jpeg" => "image/jpeg", + "jpg" => "image/jpeg", + "json" => "application/json", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "mpeg" => "video/mpeg", + "txt" => "text/plain", + "ttf" => "font/ttf", + "weba" => "audio/webm", + "webm" => "video/webm", + "webp" => "image/webp", + "woff" => "font/woff", + "woff2" => "font/woff2", + "xhtml" => "application/xhtml+xml", + "xml" => "application/xml", + "zip" => "application/zip", + _ => return None, + }; + Some(content_type) +} diff --git a/plugins/jequi_serve_static/src/lib.rs b/plugins/jequi_serve_static/src/lib.rs index fddfea8..fa7772b 100644 --- a/plugins/jequi_serve_static/src/lib.rs +++ b/plugins/jequi_serve_static/src/lib.rs @@ -1,3 +1,5 @@ +mod content_type; +use derivative::Derivative; use jequi::{JequiConfig, Plugin, Request, RequestHandler, Response}; use serde::{de, Deserialize}; use serde_yaml::Value; @@ -76,33 +78,30 @@ impl Default for PathKind { } } -#[derive(Deserialize, Clone, Default, Debug, PartialEq)] +#[derive(Deserialize, Clone, Debug, PartialEq, Derivative)] +#[derivative(Default)] +#[serde(default)] pub struct Config { pub static_files_path: Option, - uri: Option, + #[derivative(Default(value = "true"))] + pub infer_content_type: bool, + config_path: Option, } impl Config { - pub const fn new() -> Self { - Config { - static_files_path: None, - uri: None, - } - } - fn handle_request(&self, req: &Request, resp: &mut Response) { let final_path = &mut PathBuf::new(); match self.static_files_path.as_ref().unwrap() { PathKind::File(file_path) => final_path.push(file_path), - PathKind::Dir(path) => { - let mut uri = req.uri.as_str(); - if let Some(uri_config) = self.uri.as_deref() { - uri = uri.strip_prefix(uri_config).unwrap_or(uri); + PathKind::Dir(dir_path) => { + let mut path = req.uri.path(); + if let Some(path_config) = self.config_path.as_deref() { + path = path.strip_prefix(path_config).unwrap_or(path); } - uri = uri.trim_start_matches('/'); + path = path.trim_start_matches('/'); let mut file_path = PathBuf::new(); - for p in Path::new(uri) { + for p in Path::new(path) { if p == ".." { file_path.pop(); } else { @@ -114,12 +113,12 @@ impl Config { file_path.push("index.html") } - final_path.push(path); + final_path.push(dir_path); final_path.push(file_path); } }; - match std::fs::read(final_path) { + match std::fs::read(&final_path) { Ok(content) => resp.write_body(&content).unwrap(), Err(e) if e.kind() == ErrorKind::PermissionDenied => { resp.status = 403; @@ -132,6 +131,12 @@ impl Config { }; resp.status = 200; + + if self.infer_content_type { + if let Some(content_type) = content_type::get_content_type_by_path(final_path) { + resp.set_header("Content-Type", content_type); + } + } } } @@ -167,35 +172,59 @@ mod tests { path::Path, }; - use jequi::{http1::Http1Conn, RawStream}; + use http::HeaderValue; + use jequi::{http1::Http1Conn, RawStream, Uri}; + use tokio::io::{AsyncRead, AsyncWrite}; use crate::{Config, PathKind}; + fn test_handle_request( + conf: &Config, + http: &mut Http1Conn, + uri: &str, + expected_status: usize, + expected_resp: &[u8], + ) { + http.request.uri = Uri::from(uri.to_string()); + http.response.body_buffer.truncate(0); + + conf.handle_request(&http.request, &mut http.response); + + assert_eq!( + http.response.status, expected_status, + "status error for {}", + uri + ); + assert_eq!( + &http.response.body_buffer[..], + expected_resp, + "response error for {}", + uri + ); + } + #[tokio::test] async fn handle_static_files_test() { let mut http = Http1Conn::new(RawStream::Normal(Cursor::new(vec![]))); - // Normal test - http.request.uri = "/file".to_string(); - let mut conf = Config { static_files_path: Some(PathKind::Dir(TEST_PATH.into())), ..Default::default() }; - conf.handle_request(&http.request, &mut http.response); + // Normal test + test_handle_request(&conf, &mut http, "/file", 200, b"hello"); - assert_eq!(http.response.status, 200); - assert_eq!(&http.response.body_buffer[..], b"hello"); + // Content type test + test_handle_request(&conf, &mut http, "/aa.js", 200, b"console.log(\"a\")\n"); - // lfi test - http.request.uri = "/file/./../../file".to_string(); - http.response.body_buffer.truncate(0); + assert_eq!( + http.response.get_header("Content-Type"), + Some(&HeaderValue::from_static("text/javascript")) + ); - conf.handle_request(&http.request, &mut http.response); - - assert_eq!(http.response.status, 200); - assert_eq!(&http.response.body_buffer[..], b"hello"); + // lfi test + test_handle_request(&conf, &mut http, "/file/./../../file", 200, b"hello"); // Forbidden test let path = format!("{}noperm", TEST_PATH); @@ -207,44 +236,20 @@ mod tests { fs::set_permissions(path, fs::Permissions::from_mode(0o000)).unwrap(); - http.request.uri = "/noperm".to_string(); - http.response.body_buffer.truncate(0); - - conf.handle_request(&http.request, &mut http.response); - - assert_eq!(http.response.status, 403); - assert_eq!(&http.response.body_buffer[..], b""); + test_handle_request(&conf, &mut http, "/noperm", 403, b""); // Notfound test - http.request.uri = "/notfound".to_string(); - http.response.body_buffer.truncate(0); - - conf.handle_request(&http.request, &mut http.response); - - assert_eq!(http.response.status, 404); - assert_eq!(&http.response.body_buffer[..], b""); + test_handle_request(&conf, &mut http, "/notfound", 404, b""); // Uri config test - conf.uri = Some("/uri".to_string()); - - http.request.uri = "/uri/file".to_string(); - http.response.body_buffer.truncate(0); - - conf.handle_request(&http.request, &mut http.response); + conf.config_path = Some("/uri".to_string()); - assert_eq!(http.response.status, 200); - assert_eq!(&http.response.body_buffer[..], b"hello"); + test_handle_request(&conf, &mut http, "/uri/file", 200, b"hello"); // File as path test conf.static_files_path = Some(PathKind::File(format!("{}file", TEST_PATH).into())); - conf.uri = None; - - http.request.uri = "/blablabla".to_string(); - http.response.body_buffer.truncate(0); - - conf.handle_request(&http.request, &mut http.response); + conf.config_path = None; - assert_eq!(http.response.status, 200); - assert_eq!(&http.response.body_buffer[..], b"hello"); + test_handle_request(&conf, &mut http, "/blablabla", 200, b"hello"); } } diff --git a/plugins/jequi_serve_static/test/aa.js b/plugins/jequi_serve_static/test/aa.js new file mode 100644 index 0000000..17d8c79 --- /dev/null +++ b/plugins/jequi_serve_static/test/aa.js @@ -0,0 +1 @@ +console.log("a") diff --git a/test/host/path/index.html b/test/host/path/index.html new file mode 100644 index 0000000..4d7ea7c --- /dev/null +++ b/test/host/path/index.html @@ -0,0 +1,2 @@ + +jequi.com/app :D diff --git a/test/host/path/style.css b/test/host/path/style.css new file mode 100644 index 0000000..00dedf6 --- /dev/null +++ b/test/host/path/style.css @@ -0,0 +1 @@ +abcde diff --git a/test/host/uri/index.html b/test/host/uri/index.html deleted file mode 100644 index c0ed363..0000000 --- a/test/host/uri/index.html +++ /dev/null @@ -1 +0,0 @@ -jequi.com/app :D diff --git a/test/style1.css b/test/style1.css index e69de29..00dedf6 100644 --- a/test/style1.css +++ b/test/style1.css @@ -0,0 +1 @@ +abcde