diff --git a/Cargo.lock b/Cargo.lock index af16a3d..fbbb963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,7 @@ dependencies = [ "bsnext_client", "bsnext_dto", "bsnext_fs", + "bsnext_guards", "bsnext_input", "bsnext_resp", "bsnext_utils", @@ -549,6 +550,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "bsnext_guards" +version = "0.1.0" +dependencies = [ + "axum", + "http", + "serde", + "tracing", + "urlpattern", +] + [[package]] name = "bsnext_html" version = "0.2.3" @@ -566,6 +578,7 @@ name = "bsnext_input" version = "0.2.3" dependencies = [ "anyhow", + "bsnext_guards", "bsnext_resp", "bsnext_tracing", "clap", @@ -577,6 +590,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "shell-words", "thiserror", "toml", ] @@ -623,6 +637,7 @@ version = "0.2.3" dependencies = [ "anyhow", "axum", + "bsnext_guards", "bytes", "http", "http-body-util", @@ -631,7 +646,6 @@ dependencies = [ "tokio", "tower 0.4.13", "tracing", - "urlpattern", ] [[package]] @@ -2676,6 +2690,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/crates/bsnext_core/Cargo.toml b/crates/bsnext_core/Cargo.toml index 7dc1076..39b0474 100644 --- a/crates/bsnext_core/Cargo.toml +++ b/crates/bsnext_core/Cargo.toml @@ -14,6 +14,7 @@ bsnext_fs = { path = "../bsnext_fs" } bsnext_resp = { path = "../bsnext_resp" } bsnext_client = { path = "../bsnext_client" } bsnext_dto = { path = "../bsnext_dto" } +bsnext_guards = { path = "../bsnext_guards" } axum-server = { version = "0.6.0", features = ["tls-rustls"] } axum-extra = { version = "0.9.3", features = ["typed-header"] } diff --git a/crates/bsnext_core/examples/abc.rs b/crates/bsnext_core/examples/abc.rs index 44051eb..2b21311 100644 --- a/crates/bsnext_core/examples/abc.rs +++ b/crates/bsnext_core/examples/abc.rs @@ -1,4 +1,5 @@ use actix::Actor; +use bsnext_core::runtime_ctx::RuntimeCtx; use bsnext_core::server::actor::ServerActor; use bsnext_core::server::handler_listen::Listen; use bsnext_core::servers_supervisor::get_servers_handler::{GetServersMessage, IncomingEvents}; @@ -28,6 +29,7 @@ async fn main() { let a = s .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.recipient(), }) diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs index e40191f..50dd2c7 100644 --- a/crates/bsnext_core/src/handler_stack.rs +++ b/crates/bsnext_core/src/handler_stack.rs @@ -2,6 +2,7 @@ use crate::handlers::proxy::{proxy_handler, ProxyConfig}; use crate::not_found::not_found_service::not_found_loader; use crate::optional_layers::optional_layers; use crate::raw_loader::serve_raw_one; +use crate::runtime_ctx::RuntimeCtx; use crate::serve_dir::try_many_services_dir; use axum::handler::Handler; use axum::middleware::{from_fn, from_fn_with_state}; @@ -9,15 +10,17 @@ use axum::routing::{any, any_service, get_service, MethodRouter}; use axum::{Extension, Router}; use bsnext_input::route::{DirRoute, FallbackRoute, Opts, ProxyRoute, RawRoute, Route, RouteKind}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; use tower_http::services::{ServeDir, ServeFile}; #[derive(Debug, PartialEq)] pub enum HandlerStack { None, // todo: make this a separate thing - Raw { - raw: RawRoute, - opts: Opts, + Raw(RawRouteOpts), + RawAndDirs { + raw: RawRouteOpts, + dirs: Vec, }, Dirs(Vec), Proxy { @@ -38,8 +41,14 @@ pub struct DirRouteOpts { fallback_route: Option, } +#[derive(Debug, PartialEq)] +pub struct RawRouteOpts { + raw_route: RawRoute, + opts: Opts, +} + impl DirRouteOpts { - pub fn as_serve_dir(&self) -> ServeDir { + pub fn as_serve_dir(&self, cwd: &Path) -> ServeDir { match &self.dir_route.base { Some(base_dir) => { tracing::trace!( @@ -50,8 +59,19 @@ impl DirRouteOpts { ServeDir::new(base_dir.join(&self.dir_route.dir)) } None => { - tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); - ServeDir::new(&self.dir_route.dir) + let pb = PathBuf::from(&self.dir_route.dir); + if pb.is_absolute() { + tracing::trace!("no root given, using `{}` directly", self.dir_route.dir); + ServeDir::new(&self.dir_route.dir) + } else { + let joined = cwd.join(pb); + tracing::trace!( + "prepending the current directory to relative path {} {}", + cwd.display(), + joined.display() + ); + ServeDir::new(joined) + } } } .append_index_html_on_directories(true) @@ -102,7 +122,7 @@ impl RouteMap { } } - pub fn into_router(self) -> Router { + pub fn into_router(self, ctx: &RuntimeCtx) -> Router { let mut router = Router::new(); tracing::trace!("processing `{}` different routes", self.mapping.len()); @@ -114,8 +134,8 @@ impl RouteMap { route_list.len() ); - let stack = routes_to_stack(&route_list); - let path_router = stack_to_router(&path, stack); + let stack = routes_to_stack(route_list); + let path_router = stack_to_router(&path, stack, ctx); tracing::trace!("will merge router at path: `{path}`"); router = router.merge(path_router); @@ -128,10 +148,10 @@ impl RouteMap { pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack { match state { HandlerStack::None => match route.kind { - RouteKind::Raw(raw_route) => HandlerStack::Raw { - raw: raw_route, + RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { + raw_route, opts: route.opts, - }, + }), RouteKind::Proxy(new_proxy_route) => HandlerStack::Proxy { proxy: new_proxy_route, opts: route.opts, @@ -140,15 +160,22 @@ pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack { HandlerStack::Dirs(vec![DirRouteOpts::new(dir, route.opts, route.fallback)]) } }, - HandlerStack::Raw { raw, opts } => match route.kind { + HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => match route.kind { // if a second 'raw' is seen, just use it, discarding the previous - RouteKind::Raw(raw_route) => HandlerStack::Raw { - raw: raw_route, + RouteKind::Raw(raw_route) => HandlerStack::Raw(RawRouteOpts { + raw_route, opts: route.opts, + }), + RouteKind::Dir(dir) => HandlerStack::RawAndDirs { + dirs: vec![DirRouteOpts::new(dir, route.opts, None)], + raw: RawRouteOpts { raw_route, opts }, }, // 'raw' handlers never get updated - _ => HandlerStack::Raw { raw, opts }, + _ => HandlerStack::Raw(RawRouteOpts { raw_route, opts }), }, + HandlerStack::RawAndDirs { .. } => { + todo!("support RawAndDirs") + } HandlerStack::Dirs(mut dirs) => match route.kind { RouteKind::Dir(next_dir) => { dirs.push(DirRouteOpts::new(next_dir, route.opts, route.fallback)); @@ -204,22 +231,29 @@ pub fn fallback_to_layered_method_router(route: FallbackRoute) -> MethodRouter { } } -pub fn routes_to_stack(routes: &[Route]) -> HandlerStack { - routes.iter().fold(HandlerStack::None, |s, route| { - append_stack(s, route.clone()) - }) +pub fn routes_to_stack(routes: Vec) -> HandlerStack { + routes.into_iter().fold(HandlerStack::None, append_stack) } -pub fn stack_to_router(path: &str, stack: HandlerStack) -> Router { +pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router { match stack { HandlerStack::None => unreachable!(), - HandlerStack::Raw { raw, opts } => { - let svc = any_service(serve_raw_one.with_state(raw)); + HandlerStack::Raw(RawRouteOpts { raw_route, opts }) => { + let svc = any_service(serve_raw_one.with_state(raw_route)); let out = optional_layers(svc, &opts); Router::new().route_service(path, out) } + HandlerStack::RawAndDirs { + dirs, + raw: RawRouteOpts { raw_route, opts }, + } => { + let svc = any_service(serve_raw_one.with_state(raw_route)); + let raw_out = optional_layers(svc, &opts); + let service = serve_dir_layer(&dirs, Router::new(), ctx); + Router::new().route(path, raw_out).fallback_service(service) + } HandlerStack::Dirs(dirs) => { - let service = serve_dir_layer(&dirs, Router::new()); + let service = serve_dir_layer(&dirs, Router::new(), ctx); Router::new() .nest_service(path, service) .layer(from_fn(not_found_loader)) @@ -236,26 +270,30 @@ pub fn stack_to_router(path: &str, stack: HandlerStack) -> Router { Router::new().nest_service(path, optional_layers(as_service, &opts)) } HandlerStack::DirsProxy { dirs, proxy, opts } => { - let proxy_router = stack_to_router(path, HandlerStack::Proxy { proxy, opts }); - let r1 = serve_dir_layer(&dirs, Router::new().fallback_service(proxy_router)); + let proxy_router = stack_to_router(path, HandlerStack::Proxy { proxy, opts }, ctx); + let r1 = serve_dir_layer(&dirs, Router::new().fallback_service(proxy_router), ctx); Router::new().nest_service(path, r1) } } } -fn serve_dir_layer(dir_list_with_opts: &[DirRouteOpts], initial: Router) -> Router { +fn serve_dir_layer( + dir_list_with_opts: &[DirRouteOpts], + initial: Router, + ctx: &RuntimeCtx, +) -> Router { let serve_dir_items = dir_list_with_opts .iter() .map(|dir_route| match &dir_route.fallback_route { None => { - let serve_dir_service = dir_route.as_serve_dir(); + let serve_dir_service = dir_route.as_serve_dir(ctx.cwd()); let service = get_service(serve_dir_service); optional_layers(service, &dir_route.opts) } Some(fallback) => { let stack = fallback_to_layered_method_router(fallback.clone()); let serve_dir_service = dir_route - .as_serve_dir() + .as_serve_dir(ctx.cwd()) .fallback(stack) .call_fallback_on_method_not_allowed(true); let service = any_service(serve_dir_service); @@ -272,6 +310,7 @@ mod test { use super::*; use crate::server::router::common::to_resp_parts_and_body; use axum::body::Body; + use std::env::current_dir; use bsnext_input::Input; use http::Request; @@ -287,9 +326,10 @@ mod test { .servers .iter() .find(|x| x.identity.is_named("raw")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) } @@ -302,9 +342,10 @@ mod test { .servers .iter() .find(|x| x.identity.is_named("2dirs+proxy")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) @@ -317,9 +358,10 @@ mod test { .servers .iter() .find(|s| s.identity.is_named("raw+opts")) + .map(ToOwned::to_owned) .unwrap(); - let actual = routes_to_stack(&first.routes); + let actual = routes_to_stack(first.routes); assert_debug_snapshot!(actual); Ok(()) @@ -338,7 +380,7 @@ mod test { .unwrap(); let route_map = RouteMap::new_from_routes(&first.routes); - let router = route_map.into_router(); + let router = route_map.into_router(&RuntimeCtx::default()); let request = Request::get("/styles.css").body(Body::empty())?; // Define the request @@ -349,6 +391,39 @@ mod test { assert_eq!(body, "body { background: red }"); } + Ok(()) + } + #[tokio::test] + async fn test_raw_with_dir_fallback() -> anyhow::Result<()> { + let yaml = include_str!("../../../examples/basic/handler_stack.yml"); + let input = serde_yaml::from_str::(&yaml)?; + + { + let first = input + .servers + .iter() + .find(|x| x.identity.is_named("raw+dir")) + .unwrap(); + + let route_map = RouteMap::new_from_routes(&first.routes); + let router = route_map.into_router(&RuntimeCtx::default()); + let raw_request = Request::get("/").body(Body::empty())?; + let response = router.oneshot(raw_request).await?; + let (_parts, body) = to_resp_parts_and_body(response).await; + assert_eq!(body, "hello world!"); + + let cwd = current_dir().unwrap(); + let cwd = cwd.ancestors().nth(2).unwrap(); + let ctx = RuntimeCtx::new(cwd); + let route_map = RouteMap::new_from_routes(&first.routes); + let router = route_map.into_router(&ctx); + let dir_request = Request::get("/script.js").body(Body::empty())?; + let response = router.oneshot(dir_request).await?; + let (_parts, body) = to_resp_parts_and_body(response).await; + let expected = include_str!("../../../examples/basic/public/script.js"); + assert_eq!(body, expected); + } + Ok(()) } } diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs index 4eaf9fd..b83a93e 100644 --- a/crates/bsnext_core/src/handlers/proxy.rs +++ b/crates/bsnext_core/src/handlers/proxy.rs @@ -5,7 +5,7 @@ use axum::handler::Handler; use axum::response::{IntoResponse, Response}; use axum::routing::any; use axum::Extension; -use bsnext_resp::injector_guard::InjectorGuard; +use bsnext_guards::route_guard::RouteGuard; use bsnext_resp::InjectHandling; use http::{HeaderValue, StatusCode, Uri}; use hyper_tls::HttpsConnector; diff --git a/crates/bsnext_core/src/lib.rs b/crates/bsnext_core/src/lib.rs index e842677..751d4de 100644 --- a/crates/bsnext_core/src/lib.rs +++ b/crates/bsnext_core/src/lib.rs @@ -10,5 +10,6 @@ pub mod optional_layers; pub mod panic_handler; pub mod proxy_loader; pub mod raw_loader; +pub mod runtime_ctx; pub mod serve_dir; pub mod ws; diff --git a/crates/bsnext_core/src/proxy_loader.rs b/crates/bsnext_core/src/proxy_loader.rs index 55420dc..ab5eb82 100644 --- a/crates/bsnext_core/src/proxy_loader.rs +++ b/crates/bsnext_core/src/proxy_loader.rs @@ -2,6 +2,7 @@ mod test { use crate::handler_stack::RouteMap; + use crate::runtime_ctx::RuntimeCtx; use crate::server::router::common::{test_proxy, to_resp_parts_and_body}; use axum::body::Body; use axum::extract::Request; @@ -37,7 +38,7 @@ mod test { { let routes = serde_yaml::from_str::>(&routes_input)?; let router = RouteMap::new_from_routes(&routes) - .into_router() + .into_router(&RuntimeCtx::default()) .layer(Extension(client)); let expected_body = "target!"; diff --git a/crates/bsnext_core/src/raw_loader.rs b/crates/bsnext_core/src/raw_loader.rs index 8ba0be0..d81aa45 100644 --- a/crates/bsnext_core/src/raw_loader.rs +++ b/crates/bsnext_core/src/raw_loader.rs @@ -52,6 +52,7 @@ async fn raw_resp_for(uri: Uri, route: &RawRoute) -> impl IntoResponse { mod raw_test { use super::*; use crate::handler_stack::RouteMap; + use crate::runtime_ctx::RuntimeCtx; use crate::server::router::common::to_resp_parts_and_body; use bsnext_input::route::Route; use tower::ServiceExt; @@ -75,7 +76,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/route1").body(Body::empty())?; // Make a one-shot request on the router @@ -86,7 +87,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/raw1").body(Body::empty())?; // Make a one-shot request on the router @@ -97,7 +98,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/json").body(Body::empty())?; // Make a one-shot request on the router @@ -108,7 +109,7 @@ mod raw_test { { let routes: Vec = serde_yaml::from_str(routes_input)?; - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); // Define the request let request = Request::get("/sse").body(Body::empty())?; // Make a one-shot request on the router diff --git a/crates/bsnext_core/src/runtime_ctx.rs b/crates/bsnext_core/src/runtime_ctx.rs new file mode 100644 index 0000000..7aa6a14 --- /dev/null +++ b/crates/bsnext_core/src/runtime_ctx.rs @@ -0,0 +1,24 @@ +use std::env::current_dir; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct RuntimeCtx { + cwd: PathBuf, +} + +impl Default for RuntimeCtx { + fn default() -> Self { + Self { + cwd: current_dir().expect("failed to get current directory"), + } + } +} + +impl RuntimeCtx { + pub fn new>(path: P) -> Self { + Self { cwd: path.into() } + } + pub fn cwd(&self) -> &PathBuf { + &self.cwd + } +} diff --git a/crates/bsnext_core/src/serve_dir.rs b/crates/bsnext_core/src/serve_dir.rs index 6f7f4a4..492885a 100644 --- a/crates/bsnext_core/src/serve_dir.rs +++ b/crates/bsnext_core/src/serve_dir.rs @@ -61,6 +61,7 @@ mod test { use crate::handler_stack::RouteMap; use crate::server::router::common::to_resp_parts_and_body; + use crate::runtime_ctx::RuntimeCtx; use bsnext_input::route::Route; use std::env::current_dir; @@ -84,7 +85,7 @@ mod test { let routes = serde_yaml::from_str::>(&routes_input)?; { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); let expected_body = include_str!("../../../examples/basic/public/index.html"); // Define the request @@ -96,7 +97,7 @@ mod test { } { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&RuntimeCtx::default()); let expected_body = include_str!("../../../examples/kitchen-sink/input.html"); // Define the request diff --git a/crates/bsnext_core/src/server/handler_listen.rs b/crates/bsnext_core/src/server/handler_listen.rs index bdf2487..dc6d2b7 100644 --- a/crates/bsnext_core/src/server/handler_listen.rs +++ b/crates/bsnext_core/src/server/handler_listen.rs @@ -1,4 +1,5 @@ use crate::handler_stack::RouteMap; +use crate::runtime_ctx::RuntimeCtx; use crate::server::actor::ServerActor; use crate::server::router::make_router; use crate::server::state::ServerState; @@ -17,6 +18,7 @@ use tokio::sync::{oneshot, RwLock}; pub struct Listen { pub parent: Recipient, pub evt_receiver: Recipient, + pub runtime_ctx: RuntimeCtx, } impl actix::Handler for ServerActor { @@ -30,12 +32,14 @@ impl actix::Handler for ServerActor { let h1 = handle.clone(); let h2 = handle.clone(); - let router = RouteMap::new_from_routes(&self.config.combined_routes()).into_router(); + let router = + RouteMap::new_from_routes(&self.config.combined_routes()).into_router(&msg.runtime_ctx); let app_state = Arc::new(ServerState { // parent: , routes: Arc::new(RwLock::new(self.config.combined_routes())), raw_router: Arc::new(RwLock::new(router)), + runtime_ctx: msg.runtime_ctx, client_config: Arc::new(RwLock::new(self.config.clients.clone())), id: self.config.identity.as_id(), parent: Some(msg.parent.clone()), diff --git a/crates/bsnext_core/src/server/handler_patch.rs b/crates/bsnext_core/src/server/handler_patch.rs index fd823f7..6f1b80f 100644 --- a/crates/bsnext_core/src/server/handler_patch.rs +++ b/crates/bsnext_core/src/server/handler_patch.rs @@ -41,12 +41,17 @@ impl actix::Handler for ServerActor { .clients .changeset_for(&msg.server_config.clients); + let Some(app_state) = &self.app_state else { + unreachable!("app_state should be set"); + }; + // todo(alpha): use the actix dedicated methods for async state mutation? Box::pin({ let c = span.clone(); + let ctx = app_state.runtime_ctx.clone(); async move { - let router = RouteMap::new_from_routes(&routes).into_router(); + let router = RouteMap::new_from_routes(&routes).into_router(&ctx); let mut mut_raw_router = app_state_clone.raw_router.write().await; *mut_raw_router = router; drop(mut_raw_router); diff --git a/crates/bsnext_core/src/server/router/common.rs b/crates/bsnext_core/src/server/router/common.rs index c2d0639..71dc803 100644 --- a/crates/bsnext_core/src/server/router/common.rs +++ b/crates/bsnext_core/src/server/router/common.rs @@ -3,6 +3,7 @@ use crate::server::state::ServerState; use std::net::SocketAddr; use crate::handler_stack::RouteMap; +use crate::runtime_ctx::RuntimeCtx; use axum::body::Body; use axum::extract::Request; use axum::response::Response; @@ -22,8 +23,10 @@ use tower::ServiceExt; pub fn into_state(val: ServerConfig) -> ServerState { let (sender, _) = tokio::sync::broadcast::channel::(10); - let router = RouteMap::new_from_routes(&val.combined_routes()).into_router(); + let runtime_ctx = RuntimeCtx::default(); + let router = RouteMap::new_from_routes(&val.combined_routes()).into_router(&runtime_ctx); ServerState { + runtime_ctx: RuntimeCtx::default(), routes: Arc::new(RwLock::new(val.combined_routes())), raw_router: Arc::new(RwLock::new(router)), client_config: Arc::new(RwLock::new(val.clients.clone())), diff --git a/crates/bsnext_core/src/server/state.rs b/crates/bsnext_core/src/server/state.rs index e75b858..dc64f30 100644 --- a/crates/bsnext_core/src/server/state.rs +++ b/crates/bsnext_core/src/server/state.rs @@ -1,3 +1,4 @@ +use crate::runtime_ctx::RuntimeCtx; use crate::servers_supervisor::get_servers_handler::{GetServersMessage, IncomingEvents}; use actix::Recipient; use axum::Router; @@ -11,6 +12,7 @@ use tokio::sync::{broadcast, RwLock}; #[derive(Clone)] pub struct ServerState { pub routes: Arc>>, + pub runtime_ctx: RuntimeCtx, pub raw_router: Arc>, pub client_config: Arc>, pub id: u64, diff --git a/crates/bsnext_core/src/servers_supervisor/actor.rs b/crates/bsnext_core/src/servers_supervisor/actor.rs index f724121..44c5a54 100644 --- a/crates/bsnext_core/src/servers_supervisor/actor.rs +++ b/crates/bsnext_core/src/servers_supervisor/actor.rs @@ -8,6 +8,7 @@ use bsnext_input::Input; use std::collections::HashSet; use std::net::SocketAddr; +use crate::runtime_ctx::RuntimeCtx; use crate::server::handler_listen::Listen; use crate::server::handler_patch::Patch; use bsnext_dto::internal::{ @@ -117,6 +118,8 @@ impl ServersSupervisor { let c = server_config.clone(); actor_addr .send(Listen { + // todo: tie this to the input somehow? + runtime_ctx: RuntimeCtx::default(), parent: self_addr.clone().recipient(), evt_receiver: self_addr.clone().recipient(), }) diff --git a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap index 69f7894..de7684c 100644 --- a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap +++ b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_01.snap @@ -2,21 +2,23 @@ source: crates/bsnext_core/src/handler_stack.rs expression: actual --- -Raw { - raw: Raw { - raw: "body { background: red }", +Raw( + RawRouteOpts { + raw_route: Raw { + raw: "body { background: red }", + }, + opts: Opts { + cors: None, + delay: None, + watch: Bool( + true, + ), + inject: Bool( + true, + ), + headers: None, + cache: Prevent, + compression: None, + }, }, - opts: Opts { - cors: None, - delay: None, - watch: Bool( - true, - ), - inject: Bool( - true, - ), - headers: None, - cache: Prevent, - compression: None, - }, -} +) diff --git a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap index 53109cd..c359413 100644 --- a/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap +++ b/crates/bsnext_core/src/snapshots/bsnext_core__handler_stack__test__handler_stack_03.snap @@ -2,25 +2,27 @@ source: crates/bsnext_core/src/handler_stack.rs expression: actual --- -Raw { - raw: Raw { - raw: "console.log(\"hello world!\")", - }, - opts: Opts { - cors: Some( - Cors( +Raw( + RawRouteOpts { + raw_route: Raw { + raw: "console.log(\"hello world!\")", + }, + opts: Opts { + cors: Some( + Cors( + true, + ), + ), + delay: None, + watch: Bool( + true, + ), + inject: Bool( true, ), - ), - delay: None, - watch: Bool( - true, - ), - inject: Bool( - true, - ), - headers: None, - cache: Prevent, - compression: None, + headers: None, + cache: Prevent, + compression: None, + }, }, -} +) diff --git a/crates/bsnext_core/tests/server.rs b/crates/bsnext_core/tests/server.rs index 56ca312..832cd72 100644 --- a/crates/bsnext_core/tests/server.rs +++ b/crates/bsnext_core/tests/server.rs @@ -1,4 +1,5 @@ use actix::Actor; +use bsnext_core::runtime_ctx::RuntimeCtx; use bsnext_core::server::actor::ServerActor; use bsnext_core::server::handler_listen::Listen; use bsnext_core::servers_supervisor::file_changed_handler::FilesChanged; @@ -32,6 +33,7 @@ async fn system_test_01() { let listen_result = s .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.recipient(), }) @@ -70,6 +72,7 @@ async fn system_test_02() { let list_result = server_actor .send(Listen { + runtime_ctx: RuntimeCtx::default(), parent: parent.clone().recipient(), evt_receiver: parent.clone().recipient(), }) diff --git a/crates/bsnext_guards/Cargo.toml b/crates/bsnext_guards/Cargo.toml new file mode 100644 index 0000000..76d403b --- /dev/null +++ b/crates/bsnext_guards/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bsnext_guards" +version = "0.1.0" +edition = "2021" + +[dependencies] +urlpattern = { version = "0.3.0" } +serde = { workspace = true } +tracing = { workspace = true } +axum = { workspace = true } +http = { workspace = true } \ No newline at end of file diff --git a/crates/bsnext_guards/src/lib.rs b/crates/bsnext_guards/src/lib.rs new file mode 100644 index 0000000..c0ca76e --- /dev/null +++ b/crates/bsnext_guards/src/lib.rs @@ -0,0 +1,24 @@ +use crate::path_matcher::PathMatcher; +use http::Uri; + +pub mod path_matcher; +pub mod route_guard; + +#[derive(Debug, Default, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum MatcherList { + #[default] + None, + Item(PathMatcher), + Items(Vec), +} + +impl MatcherList { + pub fn test_uri(&self, uri: &Uri) -> bool { + match self { + MatcherList::None => true, + MatcherList::Item(matcher) => matcher.test_uri(uri), + MatcherList::Items(matchers) => matchers.iter().any(|m| m.test_uri(uri)), + } + } +} diff --git a/crates/bsnext_guards/src/path_matcher.rs b/crates/bsnext_guards/src/path_matcher.rs new file mode 100644 index 0000000..87a9a6a --- /dev/null +++ b/crates/bsnext_guards/src/path_matcher.rs @@ -0,0 +1,167 @@ +use http::Uri; +use std::str::FromStr; +use urlpattern::UrlPatternInit; +use urlpattern::UrlPatternMatchInput; +use urlpattern::{UrlPattern, UrlPatternOptions}; + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum PathMatcher { + Str(String), + Def(PathMatcherDef), +} + +impl PathMatcher { + pub fn str(str: impl Into) -> Self { + Self::Str(str.into()) + } + pub fn pathname(str: impl Into) -> Self { + Self::Def(PathMatcherDef { + pathname: Some(str.into()), + search: None, + }) + } + pub fn query(str: impl Into) -> Self { + Self::Def(PathMatcherDef { + pathname: None, + search: Some(str.into()), + }) + } +} + +#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] +pub struct PathMatcherDef { + pub(crate) pathname: Option, + pub(crate) search: Option, +} + +impl PathMatcher { + pub fn test_uri(&self, uri: &Uri) -> bool { + let Some(path_and_query) = uri.path_and_query() else { + tracing::error!("how is this possible?"); + return false; + }; + + let path = path_and_query.path(); + let seary = path_and_query.query(); + + let incoming = UrlPatternInit { + pathname: Some(path.into()), + search: seary.map(ToOwned::to_owned), + ..Default::default() + }; + + // convert the config into UrlPatternInit + // example: /style.css + let matching_options: UrlPatternInit = match self { + PathMatcher::Str(str) => { + if let Ok(uri) = &Uri::from_str(str) { + if let Some(pq) = uri.path_and_query() { + let path = pq.path(); + let query = pq.query(); + UrlPatternInit { + pathname: Some(path.into()), + search: query.map(ToOwned::to_owned), + ..Default::default() + } + } else { + tracing::error!("could not parse the matching string you gave {}", str); + Default::default() + } + } else { + tracing::error!("could not parse the matching string you gave {}", str); + Default::default() + } + } + PathMatcher::Def(PathMatcherDef { pathname, search }) => UrlPatternInit { + pathname: pathname.to_owned(), + search: search.to_owned(), + ..Default::default() + }, + }; + let opts = UrlPatternOptions::default(); + let Ok(pattern) = ::parse(matching_options, opts) else { + tracing::error!("could not parse the input"); + return false; + }; + // dbg!(&incoming); + match pattern.test(UrlPatternMatchInput::Init(incoming)) { + Ok(true) => { + tracing::trace!("matched!"); + true + } + Ok(false) => { + tracing::trace!("not matched!"); + false + } + Err(e) => { + tracing::error!("could not match {:?}", e); + false + } + } + } +} + +#[cfg(test)] +mod test { + use crate::path_matcher::PathMatcher; + use http::Uri; + + #[test] + fn test_url_pattern_pathname() { + let pm = PathMatcher::pathname("/"); + assert_eq!(pm.test_uri(&Uri::from_static("/")), true); + let pm = PathMatcher::pathname("/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::pathname("/here/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), false); + let pm = PathMatcher::pathname("/**/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::pathname("/**/*.css"); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.css")), + true + ); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.html")), + false + ); + } + #[test] + fn test_url_pattern_query() { + let pm = PathMatcher::str("/?abc=true"); + assert_eq!(pm.test_uri(&Uri::from_static("/")), false); + assert_eq!(pm.test_uri(&Uri::from_static("/?def=true")), false); + assert_eq!(pm.test_uri(&Uri::from_static("/?abc=true")), true); + assert_eq!(pm.test_uri(&Uri::from_static("/?abc=")), false); + + let pm2 = PathMatcher::str("/**/*?delayms"); + assert_eq!(pm2.test_uri(&Uri::from_static("/?delayms")), true); + + let pm2 = PathMatcher::query("?*a*b*c*foo=bar"); + assert_eq!( + pm2.test_uri(&Uri::from_static("/?delay.ms=2000&a-b-c-foo=bar")), + true + ); + } + #[test] + fn test_url_pattern_str() { + let pm = PathMatcher::str("/"); + assert_eq!(pm.test_uri(&Uri::from_static("/")), true); + let pm = PathMatcher::str("/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::str("/here/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), false); + let pm = PathMatcher::str("/**/*.css"); + assert_eq!(pm.test_uri(&Uri::from_static("/style.css")), true); + let pm = PathMatcher::str("/**/*.css"); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.css")), + true + ); + assert_eq!( + pm.test_uri(&Uri::from_static("/a/b/c/--oopasxstyle.html")), + false + ); + } +} diff --git a/crates/bsnext_guards/src/route_guard.rs b/crates/bsnext_guards/src/route_guard.rs new file mode 100644 index 0000000..b5f10f9 --- /dev/null +++ b/crates/bsnext_guards/src/route_guard.rs @@ -0,0 +1,7 @@ +use axum::extract::Request; +use http::Response; + +pub trait RouteGuard { + fn accept_req(&self, req: &Request) -> bool; + fn accept_res(&self, res: &Response) -> bool; +} diff --git a/crates/bsnext_html/src/html_writer.rs b/crates/bsnext_html/src/html_writer.rs index 62623d6..95b4ae3 100644 --- a/crates/bsnext_html/src/html_writer.rs +++ b/crates/bsnext_html/src/html_writer.rs @@ -1,5 +1,3 @@ -use bsnext_input::playground::Playground; -use bsnext_input::server_config::ServerConfig; use bsnext_input::{Input, InputWriter}; pub struct HtmlWriter; @@ -36,6 +34,8 @@ impl InputWriter for HtmlWriter { #[test] fn test_html_writer_for_playground() { + use bsnext_input::playground::Playground; + use bsnext_input::server_config::ServerConfig; let css = r#"body { background: red; }"#; diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index 66708c6..387f669 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -1,4 +1,5 @@ use bsnext_input::playground::Playground; +use bsnext_input::route::Route; use bsnext_input::server_config::{ServerConfig, ServerIdentity}; use bsnext_input::{Input, InputCreation, InputCtx, InputError}; use std::fs::read_to_string; @@ -17,7 +18,7 @@ impl InputCreation for HtmlFs { } fn from_input_str>(content: P, ctx: &InputCtx) -> Result> { - let input = playground_html_str_to_input(&content.as_ref(), ctx) + let input = playground_html_str_to_input(content.as_ref(), ctx) .map_err(|e| Box::new(InputError::HtmlError(e.to_string())))?; Ok(input) } @@ -31,9 +32,11 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result Result { + let joined = format!("{} {}", name, content); + let r = Route::from_cli_str(joined); + if let Ok(route) = r { + routes.push(route); + node_ids_to_remove.push(x.id()); + } + } + _ => todo!("not supported!"), + } + } + for node_id in node_ids_to_remove { document.tree.get_mut(node_id).unwrap().detach(); } @@ -86,6 +107,7 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result anyhow::Result<()> { let Some(server) = as_input.servers.get(0) else { return Err(anyhow::anyhow!("no server")); }; - let routes = server.routes(); + let routes = server.combined_routes(); let html = routes.get(0).unwrap(); let js = routes.get(1).unwrap(); let css = routes.get(2).unwrap(); @@ -80,3 +81,30 @@ fn test_html_playground_with_port() -> anyhow::Result<()> { assert_eq!(first.identity, ident); Ok(()) } + +const INPUT_WITH_META: &str = r#" + +
+

Test!

+ +
"#; + +#[test] +fn test_html_playground_with_meta() -> anyhow::Result<()> { + let ident = ServerIdentity::Address { + bind_address: String::from("0.0.0.0:8080"), + }; + let input_args = InputArgs { port: Some(8080) }; + let ctx = InputCtx::new(&[], Some(input_args)); + let as_input = HtmlFs::from_input_str(INPUT_WITH_META, &ctx)?; + let first = as_input.servers.get(0).unwrap(); + assert_eq!(first.identity, ident); + let routes = first.combined_routes(); + let found = routes + .iter() + .find(|x| matches!(x.kind, RouteKind::Dir(..))) + .expect("must find dir"); + assert_debug_snapshot!(found.path); + assert_debug_snapshot!(found.kind); + Ok(()) +} diff --git a/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap new file mode 100644 index 0000000..ba24976 --- /dev/null +++ b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/bsnext_html/tests/html_playground.rs +expression: found.kind +--- +Dir( + DirRoute { + dir: "examples/basic/public", + base: None, + }, +) diff --git a/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap new file mode 100644 index 0000000..c6f107d --- /dev/null +++ b/crates/bsnext_html/tests/snapshots/html_playground__html_playground_with_meta.snap @@ -0,0 +1,7 @@ +--- +source: crates/bsnext_html/tests/html_playground.rs +expression: found.path +--- +PathDef { + inner: "/", +} diff --git a/crates/bsnext_input/Cargo.toml b/crates/bsnext_input/Cargo.toml index 7a07ae0..24d0be4 100644 --- a/crates/bsnext_input/Cargo.toml +++ b/crates/bsnext_input/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] bsnext_resp = { path = "../bsnext_resp" } bsnext_tracing = { path = "../bsnext_tracing" } +bsnext_guards = { path = "../bsnext_guards" } +shell-words = { version = "1.1.0" } miette = { workspace = true } diff --git a/crates/bsnext_input/src/lib.rs b/crates/bsnext_input/src/lib.rs index ebc3efd..49e0cc0 100644 --- a/crates/bsnext_input/src/lib.rs +++ b/crates/bsnext_input/src/lib.rs @@ -12,6 +12,8 @@ pub mod input_test; pub mod path_def; pub mod playground; pub mod route; + +pub mod route_cli; pub mod route_manifest; pub mod server_config; pub mod startup; @@ -120,15 +122,15 @@ impl InputCtx { pub fn first_id_or_named(&self) -> ServerIdentity { self.prev_server_ids .as_ref() - .and_then(|x| x.get(0)) + .and_then(|x| x.first()) .map(ToOwned::to_owned) - .unwrap_or_else(|| ServerIdentity::named()) + .unwrap_or_else(ServerIdentity::named) } pub fn first_id(&self) -> Option { self.prev_server_ids .as_ref() - .and_then(|x| x.get(0)) + .and_then(|x| x.first()) .map(ToOwned::to_owned) } diff --git a/crates/bsnext_input/src/playground.rs b/crates/bsnext_input/src/playground.rs index 5258c16..8b64d71 100644 --- a/crates/bsnext_input/src/playground.rs +++ b/crates/bsnext_input/src/playground.rs @@ -1,9 +1,10 @@ use crate::path_def::PathDef; use crate::route::{FallbackRoute, Opts, Route, RouteKind}; +use bsnext_guards::path_matcher::PathMatcher; +use bsnext_guards::MatcherList; use bsnext_resp::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use bsnext_resp::inject_addition::{AdditionPosition, InjectAddition}; -use bsnext_resp::inject_opts::{InjectOpts, Injection, InjectionItem, MatcherList}; -use bsnext_resp::path_matcher::PathMatcher; +use bsnext_resp::inject_opts::{InjectOpts, Injection, InjectionItem}; #[derive(Debug, PartialEq, Default, Hash, Clone, serde::Deserialize, serde::Serialize)] pub struct Playground { diff --git a/crates/bsnext_input/src/route.rs b/crates/bsnext_input/src/route.rs index d3d7108..d9bffa9 100644 --- a/crates/bsnext_input/src/route.rs +++ b/crates/bsnext_input/src/route.rs @@ -1,4 +1,5 @@ use crate::path_def::PathDef; +use crate::route_cli::RouteCli; use crate::watch_opts::WatchOpts; use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::inject_opts::InjectOpts; @@ -78,6 +79,10 @@ impl Route { pub fn path(&self) -> &str { self.path.as_str() } + pub fn from_cli_str>(a: A) -> Result { + let cli = RouteCli::try_from_cli_str(a)?; + cli.try_into() + } } #[derive(Debug, Hash, PartialEq, Clone, serde::Deserialize, serde::Serialize)] diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs new file mode 100644 index 0000000..8931ae0 --- /dev/null +++ b/crates/bsnext_input/src/route_cli.rs @@ -0,0 +1,63 @@ +use crate::path_def::PathDef; +use crate::route::{DirRoute, Route, RouteKind}; +use clap::Parser; +use shell_words::split; + +#[derive(clap::Parser, Debug)] +#[command(version)] +pub struct RouteCli { + #[command(subcommand)] + command: SubCommands, +} + +impl RouteCli { + pub fn try_from_cli_str>(a: A) -> Result { + let as_args = split(a.as_ref())?; + RouteCli::try_parse_from(as_args).map_err(|e| anyhow::anyhow!(e)) + } +} + +impl TryInto for RouteCli { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + Ok(match self.command { + SubCommands::ServeDir { dir, path } => { + let mut route = Route::default(); + route.path = PathDef::try_new(path)?; + route.kind = RouteKind::Dir(DirRoute { dir, base: None }); + route + } + }) + } +} + +#[derive(Debug, clap::Subcommand)] +pub enum SubCommands { + /// does testing things + ServeDir { + /// lists test values + #[arg(short, long)] + path: String, + #[arg(short, long)] + dir: String, + }, +} + +#[cfg(test)] +mod test { + use super::*; + use clap::Parser; + + use shell_words::split; + #[test] + fn test_serve_dir() -> anyhow::Result<()> { + let input = "bslive serve-dir --path=/ --dir=examples/basic/public"; + let as_args = split(input)?; + let parsed = RouteCli::try_parse_from(as_args)?; + let as_route: Result = parsed.try_into(); + dbg!(&as_route); + // assert_debug_snapshot!(parsed); + Ok(()) + } +} diff --git a/crates/bsnext_input/src/server_config.rs b/crates/bsnext_input/src/server_config.rs index e008a48..313ec3a 100644 --- a/crates/bsnext_input/src/server_config.rs +++ b/crates/bsnext_input/src/server_config.rs @@ -25,13 +25,17 @@ impl ServerConfig { /// All regular routes, plus dynamically added ones (for example, through a playground) /// pub fn combined_routes(&self) -> Vec { - let mut routes = self.routes.clone(); - if let Some(playground) = &self.playground { - routes.extend(playground.as_routes()) + let routes = self.routes.clone(); + match &self.playground { + None => self.routes.clone(), + Some(playground) => { + let mut pg_routes = playground.as_routes(); + pg_routes.extend(routes); + pg_routes + } } - routes } - pub fn routes(&self) -> &[Route] { + pub fn raw_routes(&self) -> &[Route] { &self.routes } } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap new file mode 100644 index 0000000..6e262f6 --- /dev/null +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap @@ -0,0 +1,10 @@ +--- +source: crates/bsnext_input/src/route_cli.rs +expression: parsed +--- +RouteCli { + command: ServeDir { + path: "/", + dir: "examples/basic/public", + }, +} diff --git a/crates/bsnext_md/src/md_writer.rs b/crates/bsnext_md/src/md_writer.rs index a911f3d..694564d 100644 --- a/crates/bsnext_md/src/md_writer.rs +++ b/crates/bsnext_md/src/md_writer.rs @@ -43,7 +43,7 @@ fn _input_to_str(input: &Input) -> String { } } - for route in server_config.routes() { + for route in server_config.raw_routes() { let path_only = json!({"path": route.path.as_str()}); let route_yaml = serde_yaml::to_string(&path_only).expect("never fail here on route?"); chunks.push(fenced_route(&route_yaml)); diff --git a/crates/bsnext_resp/Cargo.toml b/crates/bsnext_resp/Cargo.toml index 62377bc..c71693e 100644 --- a/crates/bsnext_resp/Cargo.toml +++ b/crates/bsnext_resp/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -urlpattern = { version = "0.3.0" } +bsnext_guards = { path = "../bsnext_guards" } tracing = { workspace = true } axum = { workspace = true } diff --git a/crates/bsnext_resp/src/builtin_strings.rs b/crates/bsnext_resp/src/builtin_strings.rs index 9efc9c9..1dc8e3f 100644 --- a/crates/bsnext_resp/src/builtin_strings.rs +++ b/crates/bsnext_resp/src/builtin_strings.rs @@ -1,7 +1,8 @@ use crate::connector::Connector; use crate::debug::Debug; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -16,7 +17,7 @@ pub enum BuiltinStrings { Debug, } -impl InjectorGuard for BuiltinStringDef { +impl RouteGuard for BuiltinStringDef { fn accept_req(&self, req: &Request) -> bool { match self.name { BuiltinStrings::Connector => Connector.accept_req(req), diff --git a/crates/bsnext_resp/src/connector.rs b/crates/bsnext_resp/src/connector.rs index eec4cbd..cdbc8a6 100644 --- a/crates/bsnext_resp/src/connector.rs +++ b/crates/bsnext_resp/src/connector.rs @@ -1,12 +1,13 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use crate::RespMod; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, Default)] pub struct Connector; -impl InjectorGuard for Connector { +impl RouteGuard for Connector { fn accept_req(&self, req: &Request) -> bool { RespMod::accepts_html(req) } diff --git a/crates/bsnext_resp/src/debug.rs b/crates/bsnext_resp/src/debug.rs index 46aca4b..0bd3807 100644 --- a/crates/bsnext_resp/src/debug.rs +++ b/crates/bsnext_resp/src/debug.rs @@ -1,11 +1,12 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, Default)] pub struct Debug; -impl InjectorGuard for Debug { +impl RouteGuard for Debug { fn accept_req(&self, req: &Request) -> bool { req.uri().path().contains("core.css") } diff --git a/crates/bsnext_resp/src/inject_addition.rs b/crates/bsnext_resp/src/inject_addition.rs index d2958c8..a8e3250 100644 --- a/crates/bsnext_resp/src/inject_addition.rs +++ b/crates/bsnext_resp/src/inject_addition.rs @@ -1,5 +1,6 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -15,7 +16,7 @@ pub enum AdditionPosition { Prepend(String), } -impl InjectorGuard for InjectAddition { +impl RouteGuard for InjectAddition { fn accept_req(&self, _req: &Request) -> bool { true } diff --git a/crates/bsnext_resp/src/inject_opt_test/mod.rs b/crates/bsnext_resp/src/inject_opt_test/mod.rs index 1674dd8..fcd1476 100644 --- a/crates/bsnext_resp/src/inject_opt_test/mod.rs +++ b/crates/bsnext_resp/src/inject_opt_test/mod.rs @@ -3,6 +3,8 @@ use crate::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use crate::inject_addition::{AdditionPosition, InjectAddition}; use crate::inject_opts::{InjectOpts, Injection, InjectionItem, UnknownStringDef}; use crate::inject_replacement::{InjectReplacement, Pos}; +use bsnext_guards::path_matcher::PathMatcher; +use bsnext_guards::MatcherList; #[test] fn test_inject_opts_bool() { @@ -186,3 +188,54 @@ fn test_inject_append_prepend() { let actual: Result = serde_yaml::from_str(input); assert_eq!(actual.unwrap().inject, expected.inject); } + +#[test] +fn test_path_matchers() { + #[derive(Debug, serde::Deserialize)] + struct A { + inject: InjectOpts, + } + let input = r#" +inject: + append: lol + only: + - /*.css + - pathname: /*.css +"#; + let expected = A { + inject: InjectOpts::Item(InjectionItem { + inner: Injection::Addition(InjectAddition { + addition_position: AdditionPosition::Append("lol".to_string()), + }), + only: Some(MatcherList::Items(vec![ + PathMatcher::str("/*.css"), + PathMatcher::pathname("/*.css"), + ])), + }), + }; + let actual: Result = serde_yaml::from_str(input); + assert_eq!(actual.unwrap().inject, expected.inject); +} + +#[test] +fn test_path_matcher_single() { + #[derive(Debug, serde::Deserialize)] + struct A { + inject: InjectOpts, + } + let input = r#" + inject: + append: lol + only: /*.css + "#; + let expected = A { + inject: InjectOpts::Item(InjectionItem { + inner: Injection::Addition(InjectAddition { + addition_position: AdditionPosition::Append("lol".to_string()), + }), + only: Some(MatcherList::Item(PathMatcher::str("/*.css"))), + }), + }; + let actual: Result = serde_yaml::from_str(input); + assert_eq!(actual.unwrap().inject, expected.inject); +} diff --git a/crates/bsnext_resp/src/inject_opts.rs b/crates/bsnext_resp/src/inject_opts.rs index ddb324e..64dbf58 100644 --- a/crates/bsnext_resp/src/inject_opts.rs +++ b/crates/bsnext_resp/src/inject_opts.rs @@ -1,9 +1,10 @@ use crate::builtin_strings::{BuiltinStringDef, BuiltinStrings}; use crate::inject_addition::InjectAddition; use crate::inject_replacement::InjectReplacement; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; -use crate::path_matcher::PathMatcher; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; +use bsnext_guards::MatcherList; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -55,14 +56,6 @@ pub struct InjectionItem { pub only: Option, } -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] -pub enum MatcherList { - None, - Item(PathMatcher), - Items(Vec), -} - #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] #[serde(untagged)] pub enum Injection { @@ -76,20 +69,13 @@ pub enum Injection { pub struct UnknownStringDef { pub name: String, } - -impl InjectorGuard for InjectionItem { +impl RouteGuard for InjectionItem { fn accept_req(&self, req: &Request) -> bool { - // right now, we only support matching on the pathname - let path_and_query = req.uri().path_and_query().expect("?"); - let path = path_and_query.path(); - - let path_is_allowed = match self.only.as_ref() { + let uri_is_allowed = match self.only.as_ref() { None => true, - Some(MatcherList::None) => true, - Some(MatcherList::Item(matcher)) => matcher.test(path), - Some(MatcherList::Items(matchers)) => matchers.iter().any(|m| m.test(path)), + Some(ml) => ml.test_uri(req.uri()), }; - if path_is_allowed { + if uri_is_allowed { match &self.inner { Injection::BsLive(built_ins) => built_ins.accept_req(req), Injection::UnknownNamed(_) => todo!("accept_req Injection::UnknownNamed"), diff --git a/crates/bsnext_resp/src/inject_replacement.rs b/crates/bsnext_resp/src/inject_replacement.rs index fd1cfe6..e2db864 100644 --- a/crates/bsnext_resp/src/inject_replacement.rs +++ b/crates/bsnext_resp/src/inject_replacement.rs @@ -1,5 +1,6 @@ -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use http::Response; #[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] @@ -17,7 +18,7 @@ pub enum Pos { Replace(String), } -impl InjectorGuard for InjectReplacement { +impl RouteGuard for InjectReplacement { fn accept_req(&self, _req: &Request) -> bool { true } diff --git a/crates/bsnext_resp/src/injector_guard.rs b/crates/bsnext_resp/src/injector_guard.rs index fc3140d..45a0e03 100644 --- a/crates/bsnext_resp/src/injector_guard.rs +++ b/crates/bsnext_resp/src/injector_guard.rs @@ -1,13 +1,8 @@ -use axum::extract::Request; +use bsnext_guards::route_guard::RouteGuard; use bytes::Bytes; -use http::{HeaderMap, Response}; +use http::HeaderMap; -pub trait InjectorGuard { - fn accept_req(&self, req: &Request) -> bool; - fn accept_res(&self, res: &Response) -> bool; -} - -pub trait ByteReplacer: InjectorGuard { +pub trait ByteReplacer: RouteGuard { fn apply(&self, body: &'_ str) -> Option; fn replace_bytes( diff --git a/crates/bsnext_resp/src/lib.rs b/crates/bsnext_resp/src/lib.rs index 4d82a41..48d086b 100644 --- a/crates/bsnext_resp/src/lib.rs +++ b/crates/bsnext_resp/src/lib.rs @@ -6,17 +6,18 @@ pub mod inject_addition; pub mod inject_opts; pub mod inject_replacement; pub mod injector_guard; -pub mod path_matcher; + use crate::inject_opts::InjectionItem; #[cfg(test)] pub mod inject_opt_test; -use crate::injector_guard::{ByteReplacer, InjectorGuard}; +use crate::injector_guard::ByteReplacer; use axum::body::Body; use axum::extract::Request; use axum::middleware::Next; use axum::response::IntoResponse; use axum::Extension; +use bsnext_guards::route_guard::RouteGuard; use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; use http::{Response, StatusCode}; use http_body_util::BodyExt; diff --git a/crates/bsnext_resp/src/path_matcher.rs b/crates/bsnext_resp/src/path_matcher.rs deleted file mode 100644 index e9e58ff..0000000 --- a/crates/bsnext_resp/src/path_matcher.rs +++ /dev/null @@ -1,141 +0,0 @@ -use urlpattern::UrlPatternInit; -use urlpattern::UrlPatternMatchInput; -use urlpattern::{UrlPattern, UrlPatternOptions}; - -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] -pub enum PathMatcher { - Str(String), - Def(PathMatcherDef), -} - -impl PathMatcher { - pub fn pathname(str: impl Into) -> Self { - Self::Def(PathMatcherDef { - pathname: Some(str.into()), - }) - } -} - -#[derive(Debug, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] -pub struct PathMatcherDef { - pub pathname: Option, -} - -impl PathMatcher { - pub fn test(&self, uri: &str) -> bool { - let incoming = UrlPatternInit { - pathname: Some(uri.to_owned()), - ..Default::default() - }; - - let to_pathname = match self { - PathMatcher::Str(str) => str.as_str(), - PathMatcher::Def(PathMatcherDef { - pathname: Some(str), - }) => str.as_str(), - PathMatcher::Def(PathMatcherDef { pathname: None }) => { - unreachable!("how can this occur?") - } - }; - tracing::trace!(?to_pathname, ?uri, "PathMatcher::Str"); - let init = UrlPatternInit { - pathname: Some(to_pathname.to_owned()), - ..Default::default() - }; - let opts = UrlPatternOptions::default(); - let Ok(pattern) = ::parse(init, opts) else { - tracing::error!(?to_pathname, "could not parse the input"); - return false; - }; - match pattern.test(UrlPatternMatchInput::Init(incoming)) { - Ok(true) => { - tracing::trace!("matched!"); - true - } - Ok(false) => { - tracing::trace!("not matched!"); - false - } - Err(e) => { - tracing::error!("could not match {:?}", e); - false - } - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::inject_addition::{AdditionPosition, InjectAddition}; - use crate::inject_opts::{InjectOpts, Injection, InjectionItem, MatcherList}; - - #[test] - fn test_path_matchers() { - #[derive(Debug, serde::Deserialize)] - struct A { - inject: InjectOpts, - } - let input = r#" -inject: - append: lol - only: - - /*.css - - pathname: /*.css -"#; - let expected = A { - inject: InjectOpts::Item(InjectionItem { - inner: Injection::Addition(InjectAddition { - addition_position: AdditionPosition::Append("lol".to_string()), - }), - only: Some(MatcherList::Items(vec![ - PathMatcher::Str("/*.css".to_string()), - PathMatcher::Def(PathMatcherDef { - pathname: Some("/*.css".to_string()), - }), - ])), - }), - }; - let actual: Result = serde_yaml::from_str(input); - assert_eq!(actual.unwrap().inject, expected.inject); - } - - #[test] - fn test_path_matcher_single() { - #[derive(Debug, serde::Deserialize)] - struct A { - inject: InjectOpts, - } - let input = r#" - inject: - append: lol - only: /*.css - "#; - let expected = A { - inject: InjectOpts::Item(InjectionItem { - inner: Injection::Addition(InjectAddition { - addition_position: AdditionPosition::Append("lol".to_string()), - }), - only: Some(MatcherList::Item(PathMatcher::Str("/*.css".to_string()))), - }), - }; - let actual: Result = serde_yaml::from_str(input); - assert_eq!(actual.unwrap().inject, expected.inject); - } - - #[test] - fn test_url_pattern() { - let pm = PathMatcher::pathname("/"); - assert_eq!(pm.test("/"), true); - let pm = PathMatcher::pathname("/*.css"); - assert_eq!(pm.test("/style.css"), true); - let pm = PathMatcher::pathname("/here/*.css"); - assert_eq!(pm.test("/style.css"), false); - let pm = PathMatcher::pathname("/**/*.css"); - assert_eq!(pm.test("/style.css"), true); - let pm = PathMatcher::pathname("/**/*.css"); - assert_eq!(pm.test("/a/b/c/--oopasxstyle.css"), true); - assert_eq!(pm.test("/a/b/c/--oopasxstyle.html"), false); - } -} diff --git a/crates/bsnext_system/src/args.rs b/crates/bsnext_system/src/args.rs index 8baf3bd..07ce89e 100644 --- a/crates/bsnext_system/src/args.rs +++ b/crates/bsnext_system/src/args.rs @@ -2,6 +2,8 @@ use crate::Example; use bsnext_input::target::TargetKind; use bsnext_tracing::{LogLevel, OutputFormat}; +// bslive route --path=/ --dir= + #[derive(clap::Parser, Debug)] #[command(version, name = "Browsersync Live")] pub struct Args { diff --git a/examples/basic/handler_stack.yml b/examples/basic/handler_stack.yml index 64edd64..1be90ec 100644 --- a/examples/basic/handler_stack.yml +++ b/examples/basic/handler_stack.yml @@ -3,6 +3,12 @@ servers: routes: - path: /styles.css raw: 'body { background: red }' + - name: "raw+dir" + routes: + - path: / + html: hello world! + - path: / + dir: examples/basic/public - name: "2dirs" routes: - path: / diff --git a/examples/basic/playground.yml b/examples/basic/playground.yml index 8673ac2..f1797d6 100644 --- a/examples/basic/playground.yml +++ b/examples/basic/playground.yml @@ -1,5 +1,8 @@ servers: - name: playground + routes: + - path: / + dir: examples/basic/public playground: html: |
@@ -7,7 +10,6 @@ servers:
css: | @import url("/reset.css"); - :root { border: 50px solid pink; height: 100vh; diff --git a/examples/basic/public/bg-01.jpg b/examples/basic/public/bg-01.jpg new file mode 100644 index 0000000..a4af5d0 Binary files /dev/null and b/examples/basic/public/bg-01.jpg differ diff --git a/examples/html/playground.html b/examples/html/playground.html index a234b1f..22b3b93 100644 --- a/examples/html/playground.html +++ b/examples/html/playground.html @@ -1,3 +1,5 @@ + +