diff --git a/Cargo.lock b/Cargo.lock
index 9d10a8be..9f5c3c4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -514,6 +514,7 @@ dependencies = [
"hyper-tls",
"hyper-util",
"insta",
+ "log",
"mime_guess",
"serde",
"serde_json",
@@ -587,9 +588,11 @@ dependencies = [
name = "bsnext_guards"
version = "0.18.0"
dependencies = [
+ "anyhow",
"axum",
"http",
"serde",
+ "thiserror",
"tracing",
"urlpattern",
]
diff --git a/Cargo.toml b/Cargo.toml
index c910e646..1b445f61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.15", features = ["sync"] }
futures = "0.3.30"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
-tower = { version = "0.4", features = ['steer'] }
+tower = { version = "0.4", features = ['steer', 'util'] }
tower-http = { version = "0.6.0", features = ['trace', 'fs', 'compression-full', 'decompression-full', 'catch-panic', 'cors', 'timeout', 'set-header', ] }
tracing = { version = "0.1.35", default-features = false, features = ["std", "attributes"] }
actix = "0.13.1"
diff --git a/_bslive.yml b/_bslive.yml
index 0cad30d1..e31de230 100644
--- a/_bslive.yml
+++ b/_bslive.yml
@@ -1,23 +1,45 @@
servers:
- - port: 3000
- clients:
- log: trace
- watchers:
- - dir: crates
- run:
- - sh: cargo build
+ - port: 3003
+ routes:
+ - path: /api
+ proxy: https://example.com
+ rewrite_uri: false
+ inject:
+ - append: 'abc'
+ only: /index.html
+ - port: 3002
routes:
- path: /
+ html: lol
+ - path: /api
+ json: [ 1,2 ]
+ when:
+ query:
+ has: 'mock'
+ - path: /api
+ proxy: https://duckduckgo.github.io
+ - port: 3001
+ routes:
+ - path: /a/really/:long/path
+ dir: examples/basic/public
+ - path: /shane
+ raw: "here2"
+ when:
+ - query:
+ has: 'abc'
+ - path: /shane
+ raw: "here"
+ when:
+ - query:
+ has: 'def'
+ - path: /shane
dir: examples/basic/public
- watch:
- debounce:
- ms: 300
- run:
- - all:
- - sh: echo 2
- - sh: exit 0
- - sh: sleep 1 && echo '->hehe'
- name: '[another attempt--->]'
- - seq:
- - sleep 1
- - echo 'after?'
\ No newline at end of file
+ - path: /shane
+ dir: examples/html
+ - path: /
+ html: "4"
+ when:
+ - query:
+ is: 'a=b'
+ - path: /
+ html: "3"
diff --git a/crates/bsnext_client/src/lib.rs b/crates/bsnext_client/src/lib.rs
index 18f223ba..f48aaaff 100644
--- a/crates/bsnext_client/src/lib.rs
+++ b/crates/bsnext_client/src/lib.rs
@@ -8,7 +8,7 @@ pub const WS_PATH: &str = "/__bs_ws";
pub fn html_with_base(base_override: &str) -> String {
let base = UI_HTML;
- let next = format!("", base_override);
+ let next = format!("");
let replaced = base.replace("", next.as_str());
replaced
}
diff --git a/crates/bsnext_core/Cargo.toml b/crates/bsnext_core/Cargo.toml
index 3156a89a..606ffb6e 100644
--- a/crates/bsnext_core/Cargo.toml
+++ b/crates/bsnext_core/Cargo.toml
@@ -45,6 +45,7 @@ serde_yaml = { workspace = true }
serde_json = { workspace = true }
bytes = { workspace = true }
http = { workspace = true }
+log = "0.4.21"
[[example]]
name = "abc"
diff --git a/crates/bsnext_core/src/export.rs b/crates/bsnext_core/src/export.rs
index e4881a14..9a451210 100644
--- a/crates/bsnext_core/src/export.rs
+++ b/crates/bsnext_core/src/export.rs
@@ -38,14 +38,14 @@ impl OutputWriterTrait for ExportError {
"kind": named,
"error": error_string
});
- writeln!(_sink, "{}", v)?;
+ writeln!(_sink, "{v}")?;
Ok(())
}
fn write_pretty(&self, sink: &mut W) -> anyhow::Result<()> {
match self {
ExportError::Fs(fs_write_error) => {
- writeln!(sink, "[export]: Error! {}", fs_write_error)?;
+ writeln!(sink, "[export]: Error! {fs_write_error}")?;
}
}
Ok(())
@@ -55,7 +55,7 @@ impl OutputWriterTrait for ExportError {
impl OutputWriterTrait for ExportEvent {
fn write_json(&self, _sink: &mut W) -> anyhow::Result<()> {
let str = serde_json::to_string(&self)?;
- writeln!(_sink, "{}", str)?;
+ writeln!(_sink, "{str}")?;
Ok(())
}
diff --git a/crates/bsnext_core/src/handler_stack.rs b/crates/bsnext_core/src/handler_stack.rs
index 50dd2c70..7bcf5fa6 100644
--- a/crates/bsnext_core/src/handler_stack.rs
+++ b/crates/bsnext_core/src/handler_stack.rs
@@ -1,108 +1,41 @@
-use crate::handlers::proxy::{proxy_handler, ProxyConfig};
-use crate::not_found::not_found_service::not_found_loader;
+use crate::handlers::proxy::{proxy_handler, ProxyConfig, RewriteKind};
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::body::Body;
+use axum::extract::{Request, State};
use axum::handler::Handler;
-use axum::middleware::{from_fn, from_fn_with_state};
+use axum::middleware::{from_fn, map_response, Next};
+use axum::response::IntoResponse;
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 axum::{middleware, Extension, Router};
+use axum_extra::middleware::option_layer;
+use bsnext_guards::route_guard::RouteGuard;
+use bsnext_guards::{uri_extension, OuterUri};
+use bsnext_input::route::{ListOrSingle, ProxyRoute, Route, RouteKind};
+use bsnext_input::when_guard::{HasGuard, JsonGuard, JsonPropGuard, WhenBodyGuard, WhenGuard};
+use bsnext_resp::InjectHandling;
+use bytes::Bytes;
+use http::header::{ACCEPT, CONTENT_TYPE};
+use http::request::Parts;
+use http::uri::PathAndQuery;
+use http::{Method, Response, StatusCode, Uri};
+use http_body_util::BodyExt;
+use serde_json::Value;
use std::collections::HashMap;
+use std::ffi::OsStr;
+use std::io;
+use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
+use std::time::Duration;
+use tokio::fs::{create_dir_all, File};
+use tokio::io::{AsyncWriteExt, BufWriter};
+use tokio_stream::wrappers::ReceiverStream;
+use tokio_stream::StreamExt;
+use tower::ServiceExt;
+use tower_http::decompression::DecompressionLayer;
use tower_http::services::{ServeDir, ServeFile};
-
-#[derive(Debug, PartialEq)]
-pub enum HandlerStack {
- None,
- // todo: make this a separate thing
- Raw(RawRouteOpts),
- RawAndDirs {
- raw: RawRouteOpts,
- dirs: Vec,
- },
- Dirs(Vec),
- Proxy {
- proxy: ProxyRoute,
- opts: Opts,
- },
- DirsProxy {
- dirs: Vec,
- proxy: ProxyRoute,
- opts: Opts,
- },
-}
-
-#[derive(Debug, PartialEq)]
-pub struct DirRouteOpts {
- dir_route: DirRoute,
- opts: Opts,
- fallback_route: Option,
-}
-
-#[derive(Debug, PartialEq)]
-pub struct RawRouteOpts {
- raw_route: RawRoute,
- opts: Opts,
-}
-
-impl DirRouteOpts {
- pub fn as_serve_dir(&self, cwd: &Path) -> ServeDir {
- match &self.dir_route.base {
- Some(base_dir) => {
- tracing::trace!(
- "combining root: `{}` with given path: `{}`",
- base_dir.display(),
- self.dir_route.dir
- );
- ServeDir::new(base_dir.join(&self.dir_route.dir))
- }
- None => {
- 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)
- }
- pub fn as_serve_file(&self) -> ServeFile {
- match &self.dir_route.base {
- Some(base_dir) => {
- tracing::trace!(
- "combining root: `{}` with given path: `{}`",
- base_dir.display(),
- self.dir_route.dir
- );
- ServeFile::new(base_dir.join(&self.dir_route.dir))
- }
- None => {
- tracing::trace!("no root given, using `{}` directly", self.dir_route.dir);
- ServeFile::new(&self.dir_route.dir)
- }
- }
- }
-}
-
-impl DirRouteOpts {
- fn new(p0: DirRoute, p1: Opts, fallback_route: Option) -> DirRouteOpts {
- Self {
- dir_route: p0,
- opts: p1,
- fallback_route,
- }
- }
-}
+use tracing::{debug, error, trace, trace_span};
pub struct RouteMap {
pub mapping: HashMap>,
@@ -122,187 +55,543 @@ impl RouteMap {
}
}
+ #[tracing::instrument(skip(self))]
pub fn into_router(self, ctx: &RuntimeCtx) -> Router {
let mut router = Router::new();
- tracing::trace!("processing `{}` different routes", self.mapping.len());
+ trace!("processing `{}` different routes", self.mapping.len());
- for (path, route_list) in self.mapping {
- tracing::trace!(
- "processing path: `{}` with `{}` routes",
- path,
- route_list.len()
- );
+ for (index, (path, route_list)) in self.mapping.into_iter().enumerate() {
+ trace!(?index, ?path, "creating for `{}` routes", route_list.len());
- let stack = routes_to_stack(route_list);
- let path_router = stack_to_router(&path, stack, ctx);
+ // let stack = routes_to_stack(route_list);
+ // let path_router = stack_to_router(&path, stack, ctx);
+ let stack = route_list_for_path(path.as_str(), route_list, ctx.clone());
- tracing::trace!("will merge router at path: `{path}`");
- router = router.merge(path_router);
+ trace!(?index, ?path, "will merge router");
+ router = router.merge(stack);
}
router
}
}
-pub fn append_stack(state: HandlerStack, route: Route) -> HandlerStack {
- match state {
- HandlerStack::None => match route.kind {
- 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,
- },
- RouteKind::Dir(dir) => {
- HandlerStack::Dirs(vec![DirRouteOpts::new(dir, route.opts, route.fallback)])
+#[tracing::instrument(skip_all)]
+pub fn route_list_for_path(path: &str, routes: Vec, ctx: RuntimeCtx) -> Router {
+ // let r1 = from_fn_with_state((path.to_string(), routes, ctx), try_one);
+ let svc = any_service(try_one.with_state((path.to_string(), routes, ctx)));
+ tracing::trace!("nest_service");
+ Router::new()
+ .nest_service(path, svc)
+ .layer(from_fn(uri_extension))
+}
+
+pub async fn try_one(
+ State((path, routes, ctx)): State<(String, Vec, RuntimeCtx)>,
+ Extension(OuterUri(outer_uri)): Extension,
+ parts: Parts,
+ uri: Uri,
+ req: Request,
+) -> impl IntoResponse {
+ let span = trace_span!("try_one", outer_uri = ?outer_uri, path = path, local_uri = ?uri);
+ let _g = span.enter();
+
+ let pq = outer_uri.path_and_query();
+
+ trace!(?parts);
+ debug!("will try {} candidate routes", routes.len());
+
+ let candidates: Vec = routes
+ .iter()
+ .enumerate()
+ .filter(|(index, route)| {
+ let span = trace_span!("early filter for candidates", index = index);
+ let _g = span.enter();
+
+ trace!(?route.kind);
+
+ // early checks from parts only
+ let can_serve: bool = route
+ .when
+ .as_ref()
+ .map(|when| match &when {
+ ListOrSingle::WhenOne(when) => match_one(when, &outer_uri, &path, pq, &parts),
+ ListOrSingle::WhenMany(many) => many
+ .iter()
+ .all(|when| match_one(when, &outer_uri, &path, pq, &parts)),
+ })
+ .unwrap_or(true);
+
+ // if this routes wants to inspect the body, check it was a POST
+ let can_consume = match &route.when_body {
+ None => true,
+ Some(when_body) => {
+ let consuming = NeedsJsonGuard(when_body).accept_req(&req, &outer_uri);
+ trace!(route.when_body.json = consuming);
+ consuming
+ }
+ };
+
+ trace!(?can_serve);
+ trace!(?can_consume);
+
+ if !can_serve || !can_consume {
+ return false;
}
- },
- 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(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(RawRouteOpts { raw_route, opts }),
- },
- HandlerStack::RawAndDirs { .. } => {
- todo!("support RawAndDirs")
+
+ true
+ })
+ .map(|(index, route)| {
+ let consume = route
+ .when_body
+ .as_ref()
+ .is_some_and(|body| NeedsJsonGuard(body).accept_req(&req, &outer_uri));
+
+ let css_req = req
+ .headers()
+ .get(ACCEPT)
+ .and_then(|h| h.to_str().ok())
+ .map(|c| c.contains("text/css"))
+ .unwrap_or(false);
+
+ let js_req = Path::new(req.uri().path())
+ .extension()
+ .is_some_and(|ext| ext == OsStr::new("js"));
+ let mirror = if (css_req || js_req) {
+ RouteHelper(&route).mirror().map(|v| v.to_path_buf())
+ } else {
+ None
+ };
+
+ let injections = route.opts.inject.as_injections();
+ let req_accepted = injections
+ .items
+ .iter()
+ .any(|item| item.accept_req(&req, &outer_uri));
+
+ RouteCandidate {
+ index,
+ consume,
+ route,
+ mirror,
+ inject: req_accepted,
+ }
+ })
+ .collect::>();
+
+ debug!("{} candidates passed early checks", candidates.len());
+
+ let mut body: Option = Some(req.into_body());
+
+ 'find_candidates: for candidate in &candidates {
+ let span = trace_span!("index", index = candidate.index);
+ let _g = span.enter();
+
+ trace!(?parts);
+
+ let (next_body, control_flow) = candidate.try_exec(&mut body).await;
+ if next_body.is_some() {
+ body = next_body
}
- HandlerStack::Dirs(mut dirs) => match route.kind {
- RouteKind::Dir(next_dir) => {
- dirs.push(DirRouteOpts::new(next_dir, route.opts, route.fallback));
- HandlerStack::Dirs(dirs)
+
+ if control_flow.is_break() {
+ continue 'find_candidates;
+ }
+
+ trace!(mirror = ?candidate.mirror);
+
+ let mut method_router = to_method_router(&path, &candidate.route.kind, &ctx);
+
+ // decompress if needed
+ if candidate.mirror.is_some() || candidate.inject {
+ method_router = method_router.layer(DecompressionLayer::new());
+ }
+
+ if let Some(mirror_path) = &candidate.mirror {
+ method_router = method_router.layer(middleware::from_fn_with_state(
+ mirror_path.to_owned(),
+ mirror_handler,
+ ));
+ }
+
+ let raw_out: MethodRouter = optional_layers(method_router, &candidate.route.opts);
+ let req_clone = match candidate.route.kind {
+ RouteKind::Raw(_) => Request::from_parts(parts.clone(), Body::empty()),
+ RouteKind::Proxy(_) => {
+ if let Some(body) = body.take() {
+ Request::from_parts(parts.clone(), body)
+ } else {
+ Request::from_parts(parts.clone(), Body::empty())
+ }
}
- RouteKind::Proxy(proxy) => HandlerStack::DirsProxy {
- dirs,
- proxy,
- opts: route.opts,
- },
- _ => HandlerStack::Dirs(dirs),
- },
- HandlerStack::Proxy { proxy, opts } => match route.kind {
- RouteKind::Dir(dir) => HandlerStack::DirsProxy {
- dirs: vec![DirRouteOpts::new(dir, route.opts, route.fallback)],
- proxy,
- opts,
+ RouteKind::Dir(_) => Request::from_parts(parts.clone(), Body::empty()),
+ };
+
+ // MAKE THE REQUEST
+ let result = raw_out.oneshot(req_clone).await;
+
+ match result {
+ Ok(result) => match result.status().as_u16() {
+ // todo: this is way too simplistic, it should allow 404 being deliberately returned etc
+ 404 | 405 => {
+ if let Some(fallback) = &candidate.route.fallback {
+ let mr = to_method_router(&path, &fallback.kind, &ctx);
+ let raw_out: MethodRouter = optional_layers(mr, &fallback.opts);
+ let raw_fb = Request::from_parts(parts.clone(), Body::empty());
+ return raw_out.oneshot(raw_fb).await.into_response();
+ }
+ }
+ _ => {
+ trace!(
+ ?candidate.index,
+ " - ✅ a non-404 response was given {}",
+ result.status()
+ );
+ return result.into_response();
+ }
},
- _ => HandlerStack::Proxy { proxy, opts },
- },
- HandlerStack::DirsProxy {
- mut dirs,
- proxy,
- opts,
- } => match route.kind {
- RouteKind::Dir(dir) => {
- dirs.push(DirRouteOpts::new(dir, route.opts, route.fallback));
- HandlerStack::DirsProxy { dirs, proxy, opts }
+ Err(e) => {
+ tracing::error!(?e);
+ return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
- // todo(alpha): how to handle multiple proxies? should it just override for now?
- _ => HandlerStack::DirsProxy { dirs, proxy, opts },
- },
+ }
}
+
+ tracing::trace!("StatusCode::NOT_FOUND");
+ StatusCode::NOT_FOUND.into_response()
}
-pub fn fallback_to_layered_method_router(route: FallbackRoute) -> MethodRouter {
- match route.kind {
- RouteKind::Raw(raw_route) => {
- let svc = any_service(serve_raw_one.with_state(raw_route));
- optional_layers(svc, &route.opts)
- }
- RouteKind::Proxy(_new_proxy_route) => {
- // todo(alpha): make a decision proxy as a fallback
- todo!("add support for RouteKind::Proxy as a fallback?")
- }
- RouteKind::Dir(dir) => {
- tracing::trace!("creating fallback for dir {:?}", dir);
- let item = DirRouteOpts::new(dir, route.opts, None);
- let serve_dir_service = item.as_serve_file();
- let service = get_service(serve_dir_service);
- optional_layers(service, &item.opts)
+struct RouteHelper<'a>(pub &'a Route);
+
+impl<'a> RouteHelper<'a> {
+ fn mirror(&self) -> Option<&Path> {
+ match &self.0.kind {
+ RouteKind::Proxy(ProxyRoute {
+ unstable_mirror, ..
+ }) => unstable_mirror.as_ref().map(|s| Path::new(s)),
+ _ => None,
}
}
}
-pub fn routes_to_stack(routes: Vec) -> HandlerStack {
- routes.into_iter().fold(HandlerStack::None, append_stack)
+#[derive(Debug)]
+struct RouteCandidate<'a> {
+ index: usize,
+ route: &'a Route,
+ consume: bool,
+ mirror: Option,
+ inject: bool,
}
-pub fn stack_to_router(path: &str, stack: HandlerStack, ctx: &RuntimeCtx) -> Router {
- match stack {
- HandlerStack::None => unreachable!(),
- 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(), ctx);
- Router::new()
- .nest_service(path, service)
- .layer(from_fn(not_found_loader))
+impl RouteCandidate<'_> {
+ pub async fn try_exec(&self, body: &mut Option) -> (Option, ControlFlow<()>) {
+ if self.consume {
+ trace!("trying to collect body because candidate needs it");
+ match_json_body(body, self.route).await
+ } else {
+ (None, ControlFlow::Continue(()))
}
- HandlerStack::Proxy { proxy, opts } => {
+ }
+}
+
+fn match_one(
+ when_guard: &WhenGuard,
+ outer_uri: &Uri,
+ path: &str,
+ pq: Option<&PathAndQuery>,
+ parts: &Parts,
+) -> bool {
+ match when_guard {
+ WhenGuard::Always => true,
+ WhenGuard::Never => false,
+ WhenGuard::ExactUri { exact_uri: true } => path == pq.map(|pq| pq.as_str()).unwrap_or("/"),
+ WhenGuard::ExactUri { exact_uri: false } => path != pq.map(|pq| pq.as_str()).unwrap_or("/"),
+ WhenGuard::Query { query } => QueryHasGuard(query).accept_req_parts(parts, outer_uri),
+ WhenGuard::Accept { accept } => AcceptHasGuard(accept).accept_req_parts(parts, outer_uri),
+ }
+}
+
+fn to_method_router(path: &str, route_kind: &RouteKind, ctx: &RuntimeCtx) -> MethodRouter {
+ match route_kind {
+ RouteKind::Raw(raw) => any_service(serve_raw_one.with_state(raw.clone())),
+ RouteKind::Proxy(proxy) => {
let proxy_config = ProxyConfig {
target: proxy.proxy.clone(),
path: path.to_string(),
+ headers: proxy.proxy_headers.clone().unwrap_or_default(),
+ rewrite_kind: RewriteKind::from(proxy.rewrite_uri),
};
-
let proxy_with_decompression = proxy_handler.layer(Extension(proxy_config.clone()));
- let as_service = any(proxy_with_decompression);
+ any(proxy_with_decompression)
+ }
+ RouteKind::Dir(dir_route) => {
+ tracing::trace!(?dir_route);
+ match &dir_route.base {
+ Some(base_dir) => {
+ tracing::trace!(
+ "combining root: `{}` with given path: `{}`",
+ base_dir.display(),
+ dir_route.dir
+ );
+ get_service(ServeDir::new(base_dir.join(&dir_route.dir)))
+ }
+ None => {
+ let pb = PathBuf::from(&dir_route.dir);
+ if pb.is_file() {
+ get_service(ServeFile::new(pb))
+ } else if pb.is_absolute() {
+ trace!("no root given, using `{}` directly", dir_route.dir);
+ get_service(
+ ServeDir::new(&dir_route.dir).append_index_html_on_directories(true),
+ )
+ } else {
+ let joined = ctx.cwd().join(pb);
+ trace!(?joined, "serving");
+ get_service(ServeDir::new(joined).append_index_html_on_directories(true))
+ }
+ }
+ }
+ }
+ }
+}
+
+async fn mirror_handler(
+ State(path): State,
+ req: Request,
+ next: Next,
+) -> impl IntoResponse {
+ let (mut sender, receiver) = tokio::sync::mpsc::channel::>(100);
+ let as_stream = ReceiverStream::from(receiver);
+ let c = req.uri().clone();
+ let p = path.join(c.path().strip_prefix("/").unwrap());
+
+ let r = next.run(req).await;
+ let s = r.into_body().into_data_stream();
+
+ tokio::spawn(async move {
+ let s = s.throttle(Duration::from_millis(10));
+ tokio::pin!(s);
+ create_dir_all(&p.parent().unwrap()).await.unwrap();
+ let mut file = BufWriter::new(File::create(p).await.unwrap());
+
+ while let Some(Ok(b)) = s.next().await {
+ match file.write(&b).await {
+ Ok(_) => {}
+ Err(e) => error!(?e, "could not write"),
+ };
+ // match file.write("\n".as_bytes()).await {
+ // Ok(_) => {}
+ // Err(e) => error!(?e, "could not new line"),
+ // };
+ match file.flush().await {
+ Ok(_) => {}
+ Err(e) => error!(?e, "could not flush"),
+ };
+ match sender.send(Ok(b)).await {
+ Ok(_) => {}
+ Err(e) => {
+ error!(?e, "sender was dropped before reading was finished");
+ error!("will break");
+ break;
+ }
+ };
+ }
+ });
+
+ Body::from_stream(as_stream).into_response()
+}
+
+struct QueryHasGuard<'a>(pub &'a HasGuard);
- Router::new().nest_service(path, optional_layers(as_service, &opts))
+impl RouteGuard for QueryHasGuard<'_> {
+ fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool {
+ true
+ }
+
+ fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool {
+ true
+ }
+
+ fn accept_req_parts(&self, parts: &Parts, _outer_uri: &Uri) -> bool {
+ let Some(query) = parts.uri.query() else {
+ return false;
+ };
+ match &self.0 {
+ HasGuard::Is { is } | HasGuard::Literal(is) => is == query,
+ HasGuard::Has { has } => query.contains(has),
+ HasGuard::NotHas { not_has } => !query.contains(not_has),
+ }
+ }
+}
+struct AcceptHasGuard<'a>(pub &'a HasGuard);
+
+impl RouteGuard for AcceptHasGuard<'_> {
+ fn accept_req(&self, _req: &Request, _outer_uri: &Uri) -> bool {
+ true
+ }
+
+ fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool {
+ true
+ }
+
+ fn accept_req_parts(&self, parts: &Parts, _outer_uri: &Uri) -> bool {
+ let Some(query) = parts.headers.get("accept") else {
+ return false;
+ };
+ let Ok(str) = std::str::from_utf8(query.as_bytes()) else {
+ tracing::error!("bytes incorrrect");
+ return false;
+ };
+ match &self.0 {
+ HasGuard::Literal(is) | HasGuard::Is { is } => is == str,
+ HasGuard::Has { has } => str.contains(has),
+ HasGuard::NotHas { not_has } => !str.contains(not_has),
}
- HandlerStack::DirsProxy { dirs, proxy, opts } => {
- 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)
+ }
+}
+
+struct NeedsJsonGuard<'a>(pub &'a ListOrSingle);
+impl RouteGuard for NeedsJsonGuard<'_> {
+ #[tracing::instrument(skip_all, name = "NeedsJsonGuard.accept_req")]
+ fn accept_req(&self, req: &Request, _outer_uri: &Uri) -> bool {
+ let exec = match self.0 {
+ ListOrSingle::WhenOne(WhenBodyGuard::Json { .. }) => true,
+ ListOrSingle::WhenMany(items) => items
+ .iter()
+ .any(|item| matches!(item, WhenBodyGuard::Json { .. })),
+ _ => false,
+ };
+ trace!(?exec);
+ if !exec {
+ return false;
}
+ let headers = req.headers();
+ let method = req.method();
+ let json = headers.get(CONTENT_TYPE).is_some_and(|header| {
+ header
+ .to_str()
+ .ok()
+ .map(|h| h.contains("application/json"))
+ .unwrap_or(false)
+ });
+ trace!(?json, ?method, ?headers);
+ json && method == Method::POST
+ }
+
+ fn accept_res(&self, _res: &Response, _outer_uri: &Uri) -> bool {
+ true
}
}
-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(ctx.cwd());
- let service = get_service(serve_dir_service);
- optional_layers(service, &dir_route.opts)
+impl NeedsJsonGuard<'_> {
+ pub fn match_body(&self, value: &Value) -> bool {
+ let matches: Vec<(&'_ WhenBodyGuard, bool)> = match self.0 {
+ ListOrSingle::WhenOne(one) => vec![(one, match_one_json(value, one))],
+ ListOrSingle::WhenMany(many) => many
+ .iter()
+ .map(|guard| (guard, match_one_json(value, guard)))
+ .collect(),
+ };
+ matches.iter().all(|(_item, result)| *result)
+ }
+}
+
+pub fn match_one_json(value: &Value, when_body_guard: &WhenBodyGuard) -> bool {
+ match when_body_guard {
+ WhenBodyGuard::Json { json } => match json {
+ JsonGuard::ArrayLast { items, last } => match value.pointer(items) {
+ Some(Value::Array(arr)) => match arr.last() {
+ None => false,
+ Some(last_item) => last.iter().all(|prop_guard| match prop_guard {
+ JsonPropGuard::PathIs { path, is } => match last_item.pointer(path) {
+ Some(Value::String(val_string)) => val_string == is,
+ _ => false,
+ },
+ JsonPropGuard::PathHas { path, has } => match last_item.pointer(path) {
+ Some(Value::String(val_string)) => val_string.contains(has),
+ _ => false,
+ },
+ }),
+ },
+ _ => false,
+ },
+ JsonGuard::ArrayAny { items, any } => match value.pointer(items) {
+ Some(Value::Array(arr)) if arr.is_empty() => false,
+ Some(Value::Array(val)) => val
+ .iter()
+ .any(|val| any.iter().any(|prop_guard| match_prop(val, prop_guard))),
+ _ => false,
+ },
+ JsonGuard::ArrayAll { items, all } => match value.pointer(items) {
+ Some(Value::Array(arr)) if arr.is_empty() => false,
+ Some(Value::Array(arr)) => arr
+ .iter()
+ .any(|one_val| all.iter().all(|guard| match_prop(one_val, guard))),
+ _ => false,
+ },
+ JsonGuard::Path(pg) => match_prop(value, pg),
+ },
+ WhenBodyGuard::Never => false,
+ }
+}
+
+async fn match_json_body(
+ body: &mut Option,
+ route: &Route,
+) -> (Option, ControlFlow<()>) {
+ if let Some(inner_body) = body.take() {
+ let collected = inner_body.collect();
+ let bytes = match collected.await {
+ Ok(collected) => collected.to_bytes(),
+ Err(err) => {
+ tracing::error!(?err, "could not collect bytes...");
+ Bytes::new()
}
- Some(fallback) => {
- let stack = fallback_to_layered_method_router(fallback.clone());
- let serve_dir_service = dir_route
- .as_serve_dir(ctx.cwd())
- .fallback(stack)
- .call_fallback_on_method_not_allowed(true);
- let service = any_service(serve_dir_service);
- optional_layers(service, &dir_route.opts)
+ };
+
+ trace!("did collect {} bytes", bytes.len());
+
+ match serde_json::from_slice(bytes.iter().as_slice()) {
+ Ok(value) => {
+ let result = route
+ .when_body
+ .as_ref()
+ .map(|when_body| NeedsJsonGuard(when_body).match_body(&value));
+ if result.is_some_and(|res| !res) {
+ trace!("ignoring, `when_body` was present, but didn't match the guards");
+ trace!("restoring body from clone");
+ (Some(Body::from(bytes)), ControlFlow::Break(()))
+ } else {
+ if result.is_some() {
+ trace!("✅ when_body produced a valid match");
+ } else {
+ trace!("when_body didn't produce a value");
+ }
+ (Some(Body::from(bytes)), ControlFlow::Continue(()))
+ }
}
- })
- .collect::>();
+ Err(err) => {
+ tracing::error!(?err, "could not deserialize into Value");
+ (Some(Body::from(bytes)), ControlFlow::Continue(()))
+ }
+ }
+ } else {
+ trace!("could not .take() body");
+ (None, ControlFlow::Continue(()))
+ }
+}
- initial.layer(from_fn_with_state(serve_dir_items, try_many_services_dir))
+pub fn match_prop(value: &Value, prop_guard: &JsonPropGuard) -> bool {
+ match prop_guard {
+ JsonPropGuard::PathIs { path, is } => match value.pointer(path) {
+ Some(Value::String(val_string)) => val_string == is,
+ _ => false,
+ },
+ JsonPropGuard::PathHas { path, has } => match value.pointer(path) {
+ Some(Value::String(val_string)) => val_string.contains(has),
+ _ => false,
+ },
+ }
}
#[cfg(test)]
@@ -314,59 +603,8 @@ mod test {
use bsnext_input::Input;
use http::Request;
- use insta::assert_debug_snapshot;
-
use tower::ServiceExt;
- #[test]
- fn test_handler_stack_01() -> 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"))
- .map(ToOwned::to_owned)
- .unwrap();
-
- let actual = routes_to_stack(first.routes);
- assert_debug_snapshot!(actual);
- Ok(())
- }
-
- #[test]
- fn test_handler_stack_02() -> 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("2dirs+proxy"))
- .map(ToOwned::to_owned)
- .unwrap();
-
- let actual = routes_to_stack(first.routes);
-
- assert_debug_snapshot!(actual);
- Ok(())
- }
- #[test]
- fn test_handler_stack_03() -> anyhow::Result<()> {
- let yaml = include_str!("../../../examples/basic/handler_stack.yml");
- let input = serde_yaml::from_str::(&yaml)?;
- let first = input
- .servers
- .iter()
- .find(|s| s.identity.is_named("raw+opts"))
- .map(ToOwned::to_owned)
- .unwrap();
-
- let actual = routes_to_stack(first.routes);
-
- assert_debug_snapshot!(actual);
- Ok(())
- }
-
#[tokio::test]
async fn test_routes_to_router() -> anyhow::Result<()> {
let yaml = include_str!("../../../examples/basic/handler_stack.yml");
@@ -386,8 +624,9 @@ mod test {
// Define the request
// Make a one-shot request on the router
let response = router.oneshot(request).await?;
- let (_parts, body) = to_resp_parts_and_body(response).await;
+ let (parts, body) = to_resp_parts_and_body(response).await;
+ assert_eq!(parts.status.as_u16(), 200);
assert_eq!(body, "body { background: red }");
}
diff --git a/crates/bsnext_core/src/handlers/proxy.rs b/crates/bsnext_core/src/handlers/proxy.rs
index c905a814..7ecf0cda 100644
--- a/crates/bsnext_core/src/handlers/proxy.rs
+++ b/crates/bsnext_core/src/handlers/proxy.rs
@@ -6,18 +6,40 @@ use axum::response::{IntoResponse, Response};
use axum::routing::any;
use axum::Extension;
use bsnext_guards::route_guard::RouteGuard;
+use bsnext_guards::OuterUri;
use bsnext_resp::InjectHandling;
-use http::{HeaderValue, StatusCode, Uri};
+use http::uri::{Parts, PathAndQuery};
+use http::{HeaderName, HeaderValue, StatusCode, Uri};
use hyper_tls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
+use std::collections::BTreeMap;
+use std::str::FromStr;
use tower::ServiceExt;
use tower_http::decompression::DecompressionLayer;
+use tracing::{trace_span, Instrument};
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub target: String,
pub path: String,
+ pub headers: BTreeMap,
+ pub rewrite_kind: RewriteKind,
+}
+
+#[derive(Debug, Clone)]
+pub enum RewriteKind {
+ Alias,
+ Nested,
+}
+
+impl From