@browsersync/bslive 0.0.5 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +32 -18
- package/bin.js +6 -0
- package/bslive/Cargo.toml +35 -0
- package/{build.rs → bslive/build.rs} +1 -1
- package/bslive/src/lib.rs +130 -0
- package/bsnext/Cargo.toml +21 -0
- package/bsnext/src/main.rs +110 -0
- package/crates/bsnext_client/Cargo.toml +8 -0
- package/crates/bsnext_client/build.rs +53 -0
- package/crates/bsnext_client/dist/index.js +6493 -0
- package/crates/bsnext_client/generated/dto.ts +116 -0
- package/crates/bsnext_client/generated/schema.ts +187 -0
- package/crates/bsnext_client/index.html +14 -0
- package/crates/bsnext_client/package-lock.json +2059 -0
- package/crates/bsnext_client/package.json +25 -0
- package/crates/bsnext_client/src/lib.rs +1 -0
- package/crates/bsnext_client/style.css +3 -0
- package/crates/bsnext_client/ts/console.ts +25 -0
- package/crates/bsnext_client/ts/index.ts +73 -0
- package/crates/bsnext_client/tsconfig.json +16 -0
- package/crates/bsnext_core/Cargo.toml +43 -0
- package/crates/bsnext_core/src/dir_loader.rs +230 -0
- package/crates/bsnext_core/src/dto.rs +281 -0
- package/crates/bsnext_core/src/handlers/mod.rs +1 -0
- package/crates/bsnext_core/src/handlers/proxy.rs +95 -0
- package/crates/bsnext_core/src/lib.rs +11 -0
- package/crates/bsnext_core/src/meta/mod.rs +5 -0
- package/crates/bsnext_core/src/not_found/mod.rs +2 -0
- package/crates/bsnext_core/src/not_found/not_found.html +20 -0
- package/crates/bsnext_core/src/not_found/not_found_service.rs +41 -0
- package/crates/bsnext_core/src/not_found/route_list.rs +49 -0
- package/crates/bsnext_core/src/panic_handler.rs +32 -0
- package/crates/bsnext_core/src/raw_loader.rs +226 -0
- package/crates/bsnext_core/src/server/actor.rs +92 -0
- package/crates/bsnext_core/src/server/error.rs +41 -0
- package/crates/bsnext_core/src/server/handler_change.rs +85 -0
- package/crates/bsnext_core/src/server/handler_listen.rs +157 -0
- package/crates/bsnext_core/src/server/handler_patch.rs +42 -0
- package/crates/bsnext_core/src/server/handler_routes_updated.rs +27 -0
- package/crates/bsnext_core/src/server/handler_stop.rs +38 -0
- package/crates/bsnext_core/src/server/mod.rs +10 -0
- package/crates/bsnext_core/src/server/router/mod.rs +112 -0
- package/crates/bsnext_core/src/server/router/tests.rs +204 -0
- package/crates/bsnext_core/src/server/signals.rs +11 -0
- package/crates/bsnext_core/src/server/state.rs +19 -0
- package/crates/bsnext_core/src/servers_supervisor/actor.rs +199 -0
- package/crates/bsnext_core/src/servers_supervisor/file_changed_handler.rs +41 -0
- package/crates/bsnext_core/src/servers_supervisor/get_servers_handler.rs +23 -0
- package/crates/bsnext_core/src/servers_supervisor/input_changed_handler.rs +21 -0
- package/crates/bsnext_core/src/servers_supervisor/mod.rs +6 -0
- package/crates/bsnext_core/src/servers_supervisor/start_handler.rs +82 -0
- package/crates/bsnext_core/src/servers_supervisor/stop_handler.rs +26 -0
- package/crates/bsnext_core/src/ws/mod.rs +164 -0
- package/crates/bsnext_example/Cargo.toml +10 -0
- package/crates/bsnext_example/src/basic.rs +51 -0
- package/crates/bsnext_example/src/lib.rs +26 -0
- package/crates/bsnext_example/src/lit.rs +37 -0
- package/crates/bsnext_example/src/md.rs +18 -0
- package/crates/bsnext_fs/Cargo.toml +22 -0
- package/crates/bsnext_fs/src/actor.rs +122 -0
- package/crates/bsnext_fs/src/buffered_debounce.rs +166 -0
- package/crates/bsnext_fs/src/filter.rs +30 -0
- package/crates/bsnext_fs/src/inner_fs_event_handler.rs +94 -0
- package/crates/bsnext_fs/src/lib.rs +141 -0
- package/crates/bsnext_fs/src/remove_path_handler.rs +46 -0
- package/crates/bsnext_fs/src/stop_handler.rs +15 -0
- package/crates/bsnext_fs/src/stream.rs +167 -0
- package/crates/bsnext_fs/src/test/mod.rs +213 -0
- package/crates/bsnext_fs/src/watch_path_handler.rs +67 -0
- package/crates/bsnext_fs/src/watcher.rs +348 -0
- package/crates/bsnext_input/Cargo.toml +22 -0
- package/crates/bsnext_input/src/input_test/mod.rs +151 -0
- package/crates/bsnext_input/src/lib.rs +153 -0
- package/crates/bsnext_input/src/md.rs +541 -0
- package/crates/bsnext_input/src/paths.rs +64 -0
- package/crates/bsnext_input/src/route.rs +185 -0
- package/crates/bsnext_input/src/route_manifest.rs +186 -0
- package/crates/bsnext_input/src/server_config.rs +88 -0
- package/crates/bsnext_input/src/target.rs +7 -0
- package/crates/bsnext_input/src/watch_opt_test/mod.rs +68 -0
- package/crates/bsnext_input/src/yml.rs +1 -0
- package/crates/bsnext_output/Cargo.toml +16 -0
- package/crates/bsnext_output/src/json.rs +11 -0
- package/crates/bsnext_output/src/lib.rs +25 -0
- package/crates/bsnext_output/src/pretty.rs +147 -0
- package/crates/bsnext_resp/Cargo.toml +13 -0
- package/crates/bsnext_resp/src/js/snippet.html +1 -0
- package/crates/bsnext_resp/src/js/ws.js +1 -0
- package/crates/bsnext_resp/src/lib.rs +79 -0
- package/crates/bsnext_system/Cargo.toml +29 -0
- package/crates/bsnext_system/src/args.rs +43 -0
- package/crates/bsnext_system/src/lib.rs +227 -0
- package/crates/bsnext_system/src/monitor.rs +241 -0
- package/crates/bsnext_system/src/monitor_any_watchables.rs +127 -0
- package/crates/bsnext_system/src/start_kind/snapshots/bsnext_system__start_kind__start_from_paths__test__test-2.snap +11 -0
- package/crates/bsnext_system/src/start_kind/snapshots/bsnext_system__start_kind__start_from_paths__test__test.snap +29 -0
- package/crates/bsnext_system/src/start_kind/start_from_example.rs +49 -0
- package/crates/bsnext_system/src/start_kind/start_from_inputs.rs +67 -0
- package/crates/bsnext_system/src/start_kind/start_from_paths.rs +51 -0
- package/crates/bsnext_system/src/start_kind.rs +56 -0
- package/crates/bsnext_system/src/startup.rs +51 -0
- package/crates/bsnext_tracing/Cargo.toml +10 -0
- package/crates/bsnext_tracing/src/lib.rs +89 -0
- package/examples/basic/input.yml +5 -0
- package/examples/basic/public/index.html +15 -0
- package/examples/basic/public/reset.css +52 -0
- package/examples/basic/public/script.js +1 -0
- package/examples/basic/public/styles.css +3 -0
- package/examples/kitchen-sink/a-file.md +1 -0
- package/examples/kitchen-sink/api/1.json +1 -0
- package/examples/kitchen-sink/api/2.json +3 -0
- package/examples/kitchen-sink/app.js +1 -0
- package/examples/kitchen-sink/input.html +185 -0
- package/examples/kitchen-sink/input.yml +133 -0
- package/examples/kitchen-sink/styles-2.css +3 -0
- package/examples/lit/index.html +21 -0
- package/examples/lit/input.yml +5 -0
- package/examples/lit/lit.js +82 -0
- package/examples/lit/package-lock.json +62 -0
- package/examples/lit/package.json +15 -0
- package/examples/md-single/frontmatter.md +35 -0
- package/examples/md-single/md-single.md +35 -0
- package/examples/openai/.nvm +1 -0
- package/examples/openai/build.mjs +21 -0
- package/examples/openai/index.html +13 -0
- package/examples/openai/input.yml +59 -0
- package/examples/openai/package-lock.json +720 -0
- package/examples/openai/package.json +21 -0
- package/examples/openai/src/index.js +21 -0
- package/examples/openai/sse/01.txt +8 -0
- package/examples/proxy-simple/input.yml +9 -0
- package/examples/single/input.yaml +26 -0
- package/index.d.ts +3 -1
- package/index.js +3 -2
- package/package.json +22 -19
- package/run.sh +6 -0
- package/src/lib.rs +0 -9
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
use anyhow::Context;
|
|
2
|
+
use axum::body::Body;
|
|
3
|
+
use axum::extract::Request;
|
|
4
|
+
use axum::Extension;
|
|
5
|
+
|
|
6
|
+
use axum::response::{IntoResponse, Response};
|
|
7
|
+
|
|
8
|
+
use http::{HeaderValue, StatusCode, Uri};
|
|
9
|
+
use hyper_tls::HttpsConnector;
|
|
10
|
+
use hyper_util::client::legacy::connect::HttpConnector;
|
|
11
|
+
use hyper_util::client::legacy::Client;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone)]
|
|
14
|
+
pub struct ProxyConfig {
|
|
15
|
+
pub target: String,
|
|
16
|
+
pub path: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Make our own error that wraps `anyhow::Error`.
|
|
20
|
+
pub struct AnyAppError(anyhow::Error);
|
|
21
|
+
|
|
22
|
+
// Tell axum how to convert `AppError` into a response.
|
|
23
|
+
impl IntoResponse for AnyAppError {
|
|
24
|
+
fn into_response(self) -> Response {
|
|
25
|
+
(
|
|
26
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
27
|
+
format!("Something went wrong: {}", self.0),
|
|
28
|
+
)
|
|
29
|
+
.into_response()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
impl<E> From<E> for AnyAppError
|
|
33
|
+
where
|
|
34
|
+
E: Into<anyhow::Error>,
|
|
35
|
+
{
|
|
36
|
+
fn from(err: E) -> Self {
|
|
37
|
+
Self(err.into())
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub async fn proxy_handler(
|
|
42
|
+
Extension(config): Extension<ProxyConfig>,
|
|
43
|
+
req: Request,
|
|
44
|
+
) -> Result<Response, AnyAppError> {
|
|
45
|
+
let client = {
|
|
46
|
+
req.extensions()
|
|
47
|
+
.get::<Client<HttpsConnector<HttpConnector>, Body>>()
|
|
48
|
+
.expect("must have a client, move this to an extractor?")
|
|
49
|
+
};
|
|
50
|
+
let client_c = client.clone();
|
|
51
|
+
|
|
52
|
+
let target = config.target.clone();
|
|
53
|
+
|
|
54
|
+
tracing::trace!(?config);
|
|
55
|
+
|
|
56
|
+
let path = req.uri().path();
|
|
57
|
+
|
|
58
|
+
tracing::trace!(req.uri = %path, config.path = config.path);
|
|
59
|
+
// tracing::trace!(req.headers = ?req.headers());
|
|
60
|
+
|
|
61
|
+
let path_query = req
|
|
62
|
+
.uri()
|
|
63
|
+
.path_and_query()
|
|
64
|
+
.map(|v| v.as_str())
|
|
65
|
+
.unwrap_or(path);
|
|
66
|
+
|
|
67
|
+
let uri = format!("{}{}", target, path_query);
|
|
68
|
+
let parsed = Uri::try_from(uri).context("tried to parse")?;
|
|
69
|
+
|
|
70
|
+
let target = match (parsed.host(), parsed.port()) {
|
|
71
|
+
(Some(host), Some(port)) => format!("{host}:{port}"),
|
|
72
|
+
(Some(host), None) => host.to_owned(),
|
|
73
|
+
_ => unreachable!("could not extract `host` from url"),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
tracing::trace!(outgoing.uri = %parsed);
|
|
77
|
+
|
|
78
|
+
let host_header_value = HeaderValue::from_str(&target).unwrap();
|
|
79
|
+
|
|
80
|
+
let (parts, body) = req.into_parts();
|
|
81
|
+
let mut req = Request::from_parts(parts, body);
|
|
82
|
+
|
|
83
|
+
*req.uri_mut() = parsed;
|
|
84
|
+
|
|
85
|
+
// todo: Which other headers to mod here?
|
|
86
|
+
req.headers_mut().insert("host", host_header_value);
|
|
87
|
+
|
|
88
|
+
let res = client_c
|
|
89
|
+
.request(req)
|
|
90
|
+
.await
|
|
91
|
+
.map_err(|_| StatusCode::BAD_REQUEST)
|
|
92
|
+
.into_response();
|
|
93
|
+
|
|
94
|
+
Ok(res)
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport"
|
|
6
|
+
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
8
|
+
<title>BSNEXT</title>
|
|
9
|
+
<style>
|
|
10
|
+
pre {
|
|
11
|
+
width: 100%;
|
|
12
|
+
overflow-x: auto;
|
|
13
|
+
}
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<h2>No routes were matched - here's what's available on this address:</h2>
|
|
18
|
+
{{route_list}}
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
use axum::extract::{Request, State};
|
|
2
|
+
use axum::middleware::Next;
|
|
3
|
+
use std::sync::Arc;
|
|
4
|
+
|
|
5
|
+
use axum::response::{IntoResponse, Response};
|
|
6
|
+
|
|
7
|
+
use http::header::CONTENT_TYPE;
|
|
8
|
+
use http::{HeaderValue, StatusCode};
|
|
9
|
+
use mime_guess::mime;
|
|
10
|
+
|
|
11
|
+
use crate::handlers::proxy::AnyAppError;
|
|
12
|
+
use crate::not_found::route_list::create_route_list_html;
|
|
13
|
+
use crate::server::state::ServerState;
|
|
14
|
+
use tracing::{span, Level};
|
|
15
|
+
|
|
16
|
+
pub async fn not_found_loader(
|
|
17
|
+
State(state): State<Arc<ServerState>>,
|
|
18
|
+
req: Request,
|
|
19
|
+
next: Next,
|
|
20
|
+
) -> Result<Response, AnyAppError> {
|
|
21
|
+
let span = span!(parent: None, Level::INFO, "not_found", path = req.uri().path());
|
|
22
|
+
let _guard = span.enter();
|
|
23
|
+
|
|
24
|
+
let r = next.run(req).await;
|
|
25
|
+
if r.status().as_u16() != 404 {
|
|
26
|
+
return Ok(r);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let routes = state.routes.read().await;
|
|
30
|
+
let markup = create_route_list_html(&routes);
|
|
31
|
+
|
|
32
|
+
Ok((
|
|
33
|
+
StatusCode::NOT_FOUND,
|
|
34
|
+
[(
|
|
35
|
+
CONTENT_TYPE,
|
|
36
|
+
HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
|
|
37
|
+
)],
|
|
38
|
+
markup,
|
|
39
|
+
)
|
|
40
|
+
.into_response())
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use bsnext_input::route::{DirRoute, ProxyRoute, Route, RouteKind};
|
|
2
|
+
use htmlescape::encode_minimal;
|
|
3
|
+
|
|
4
|
+
pub fn create_route_list_html(routes: &[Route]) -> String {
|
|
5
|
+
let wrapper = include_str!("not_found.html");
|
|
6
|
+
let mut markup = String::from("<table>");
|
|
7
|
+
markup.push_str(
|
|
8
|
+
r##"
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<td>Path</td>
|
|
12
|
+
<td>Kind</td>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
"##,
|
|
16
|
+
);
|
|
17
|
+
for x in routes.iter() {
|
|
18
|
+
let mut item = String::from("<tr>");
|
|
19
|
+
let kind = match &x.kind {
|
|
20
|
+
RouteKind::Html { .. } => String::from("RouteKind::Html"),
|
|
21
|
+
RouteKind::Json { .. } => String::from("RouteKind::Json"),
|
|
22
|
+
RouteKind::Raw { .. } => String::from("RouteKind::Raw"),
|
|
23
|
+
RouteKind::Sse { .. } => String::from("RouteKind::Sse"),
|
|
24
|
+
RouteKind::Proxy(ProxyRoute { proxy }) => {
|
|
25
|
+
format!("RouteKind::Proxy('{}')", proxy)
|
|
26
|
+
}
|
|
27
|
+
RouteKind::Dir(DirRoute { dir }) => {
|
|
28
|
+
format!("RouteKind::Dir('{}')", dir.clone())
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
item.push_str(
|
|
32
|
+
format!(
|
|
33
|
+
"<td><a href='{}'><code>{}</code></a></td>\
|
|
34
|
+
\
|
|
35
|
+
<td><small>{}</small></td>",
|
|
36
|
+
x.path, x.path, kind
|
|
37
|
+
)
|
|
38
|
+
.as_str(),
|
|
39
|
+
);
|
|
40
|
+
item.push_str("</tr>");
|
|
41
|
+
markup.push_str(item.as_str());
|
|
42
|
+
}
|
|
43
|
+
markup.push_str("</table>");
|
|
44
|
+
markup.push_str("<pre><code>");
|
|
45
|
+
let json = serde_json::to_string_pretty(routes).expect("serde");
|
|
46
|
+
markup.push_str(&encode_minimal(&json));
|
|
47
|
+
markup.push_str("</pre></code>");
|
|
48
|
+
wrapper.replace("{{route_list}}", markup.as_str())
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
use axum::http::{
|
|
2
|
+
header::{self},
|
|
3
|
+
Response, StatusCode,
|
|
4
|
+
};
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use http_body_util::Full;
|
|
7
|
+
use std::any::Any;
|
|
8
|
+
|
|
9
|
+
pub fn handle_panic(err: Box<dyn Any + Send + 'static>) -> Response<Full<Bytes>> {
|
|
10
|
+
let details = if let Some(s) = err.downcast_ref::<String>() {
|
|
11
|
+
s.clone()
|
|
12
|
+
} else if let Some(s) = err.downcast_ref::<&str>() {
|
|
13
|
+
s.to_string()
|
|
14
|
+
} else {
|
|
15
|
+
"Unknown panic message".to_string()
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let body = serde_json::json!({
|
|
19
|
+
"error": {
|
|
20
|
+
"kind": "panic",
|
|
21
|
+
"details": details,
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let body = serde_json::to_string(&body).unwrap();
|
|
26
|
+
|
|
27
|
+
Response::builder()
|
|
28
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
29
|
+
.header(header::CONTENT_TYPE, "application/json")
|
|
30
|
+
.body(Full::from(body))
|
|
31
|
+
.unwrap()
|
|
32
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
use std::convert::Infallible;
|
|
2
|
+
|
|
3
|
+
use axum::extract::{Request, State};
|
|
4
|
+
use axum::middleware::Next;
|
|
5
|
+
use axum::response::{Html, IntoResponse, Response, Sse};
|
|
6
|
+
use axum::routing::any;
|
|
7
|
+
use axum::{middleware, Json, Router};
|
|
8
|
+
use http::header::CONTENT_TYPE;
|
|
9
|
+
|
|
10
|
+
use crate::meta::MetaData;
|
|
11
|
+
use crate::server::state::ServerState;
|
|
12
|
+
use axum::body::Body;
|
|
13
|
+
use axum::response::sse::Event;
|
|
14
|
+
use bsnext_input::route::{CorsOpts, DelayKind, DelayOpts, DirRoute, ProxyRoute, Route, RouteKind};
|
|
15
|
+
use bsnext_resp::response_modifications_layer;
|
|
16
|
+
use bytes::Bytes;
|
|
17
|
+
use http::StatusCode;
|
|
18
|
+
use http_body_util::BodyExt;
|
|
19
|
+
use std::sync::Arc;
|
|
20
|
+
use std::time::Duration;
|
|
21
|
+
use tokio::time::sleep;
|
|
22
|
+
use tokio_stream::StreamExt;
|
|
23
|
+
use tower::ServiceExt;
|
|
24
|
+
use tower_http::cors::CorsLayer;
|
|
25
|
+
use tracing::{span, Level};
|
|
26
|
+
|
|
27
|
+
// use futures_util::stream::{self, Stream};
|
|
28
|
+
|
|
29
|
+
pub async fn raw_loader(
|
|
30
|
+
State(app): State<Arc<ServerState>>,
|
|
31
|
+
req: Request,
|
|
32
|
+
next: Next,
|
|
33
|
+
) -> impl IntoResponse {
|
|
34
|
+
let span = span!(parent: None, Level::INFO, "raw_loader", path = req.uri().path());
|
|
35
|
+
let _guard = span.enter();
|
|
36
|
+
|
|
37
|
+
let routes = app.routes.read().await;
|
|
38
|
+
let mut temp_router = matchit::Router::new();
|
|
39
|
+
// let mut app = Router::new();
|
|
40
|
+
|
|
41
|
+
// which route kinds can be hard-matched first
|
|
42
|
+
for route in routes.iter() {
|
|
43
|
+
let path = route.path();
|
|
44
|
+
match &route.kind {
|
|
45
|
+
RouteKind::Html { .. }
|
|
46
|
+
| RouteKind::Json { .. }
|
|
47
|
+
| RouteKind::Raw { .. }
|
|
48
|
+
| RouteKind::Sse { .. } => {
|
|
49
|
+
let existing = temp_router.at_mut(path);
|
|
50
|
+
if let Ok(prev) = existing {
|
|
51
|
+
*prev.value = route.clone();
|
|
52
|
+
tracing::trace!("updated mutable route at {}", path)
|
|
53
|
+
} else if let Err(err) = existing {
|
|
54
|
+
match temp_router.insert(path, route.clone()) {
|
|
55
|
+
Ok(_) => {
|
|
56
|
+
tracing::trace!(path, "+")
|
|
57
|
+
}
|
|
58
|
+
Err(_) => {
|
|
59
|
+
tracing::error!("❌ {:?}", err.to_string())
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
_ => {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
drop(routes);
|
|
69
|
+
|
|
70
|
+
let matched = temp_router.at(req.uri().path());
|
|
71
|
+
|
|
72
|
+
let Ok(matched) = matched else {
|
|
73
|
+
return next.run(req).await;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let route = matched.value;
|
|
77
|
+
let _params = matched.params;
|
|
78
|
+
|
|
79
|
+
let mut app = Router::new();
|
|
80
|
+
match &route.kind {
|
|
81
|
+
RouteKind::Sse { sse } => {
|
|
82
|
+
let raw = sse.to_owned();
|
|
83
|
+
app = app.route(
|
|
84
|
+
req.uri().path(),
|
|
85
|
+
any(|| async move {
|
|
86
|
+
let l = raw
|
|
87
|
+
.lines()
|
|
88
|
+
.map(|l| l.to_owned())
|
|
89
|
+
.map(|l| l.strip_prefix("data:").unwrap_or(&l).to_owned())
|
|
90
|
+
.filter(|l| !l.trim().is_empty())
|
|
91
|
+
.collect::<Vec<_>>();
|
|
92
|
+
|
|
93
|
+
tracing::trace!(lines.count = l.len(), "sending EventStream");
|
|
94
|
+
|
|
95
|
+
let stream = tokio_stream::iter(l)
|
|
96
|
+
.throttle(Duration::from_millis(500))
|
|
97
|
+
.map(|chu| Event::default().data(chu))
|
|
98
|
+
.map(Ok::<_, Infallible>);
|
|
99
|
+
|
|
100
|
+
Sse::new(stream)
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
RouteKind::Raw { raw } => {
|
|
105
|
+
tracing::trace!("-> served Route::Raw {} {} bytes", route.path, raw.len());
|
|
106
|
+
let moved = raw.clone();
|
|
107
|
+
let p = req.uri().path().to_owned();
|
|
108
|
+
app = app.route(
|
|
109
|
+
req.uri().path(),
|
|
110
|
+
any(|| async move { text_asset_response(&p, &moved) }),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
RouteKind::Html { html } => {
|
|
114
|
+
tracing::trace!("-> served Route::Html {} {} bytes", route.path, html.len());
|
|
115
|
+
let moved = html.clone();
|
|
116
|
+
app = app.route(req.uri().path(), any(|| async { Html(moved) }));
|
|
117
|
+
}
|
|
118
|
+
RouteKind::Json { json } => {
|
|
119
|
+
tracing::trace!("-> served Route::Json {} {}", route.path, json);
|
|
120
|
+
let moved = json.to_owned();
|
|
121
|
+
app = app.route(req.uri().path(), any(|| async move { Json(moved) }));
|
|
122
|
+
}
|
|
123
|
+
RouteKind::Dir(DirRoute { dir: _ }) => {
|
|
124
|
+
unreachable!("should never reach RouteKind::Dir")
|
|
125
|
+
}
|
|
126
|
+
RouteKind::Proxy(ProxyRoute { proxy: _ }) => {
|
|
127
|
+
unreachable!("should never reach RouteKind::Proxy")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
app = add_route_layers(app, route);
|
|
132
|
+
|
|
133
|
+
app.layer(middleware::from_fn(tag_raw))
|
|
134
|
+
.layer(middleware::from_fn(response_modifications_layer))
|
|
135
|
+
.oneshot(req)
|
|
136
|
+
.await
|
|
137
|
+
.into_response()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async fn tag_raw(req: Request, next: Next) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
141
|
+
let (mut parts, body) = next.run(req).await.into_parts();
|
|
142
|
+
if parts.status.as_u16() == 200 {
|
|
143
|
+
parts.extensions.insert(MetaData::ServedRaw);
|
|
144
|
+
}
|
|
145
|
+
Ok(Response::from_parts(parts, body))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn add_route_layers(app: Router, route: &Route) -> Router {
|
|
149
|
+
let mut app = app;
|
|
150
|
+
|
|
151
|
+
if route
|
|
152
|
+
.cors_opts
|
|
153
|
+
.as_ref()
|
|
154
|
+
.is_some_and(|v| *v == CorsOpts::Cors(true))
|
|
155
|
+
{
|
|
156
|
+
tracing::trace!(to = route.path, "adding permissive cors");
|
|
157
|
+
app = app.layer(CorsLayer::permissive());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if let Some(DelayOpts::Delay(DelayKind::Ms(ms))) = route.delay_opts.as_ref() {
|
|
161
|
+
tracing::trace!(to = route.path, ?ms, "adding delay");
|
|
162
|
+
let ms = *ms;
|
|
163
|
+
app = app.layer(middleware::from_fn(
|
|
164
|
+
move |req: Request, next: Next| async move {
|
|
165
|
+
let res = next.run(req).await;
|
|
166
|
+
sleep(Duration::from_millis(ms)).await;
|
|
167
|
+
Ok::<_, Infallible>(res)
|
|
168
|
+
},
|
|
169
|
+
));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// if route.opts.as_ref().is_some_and(|v| v.buff) {
|
|
173
|
+
// app = app.layer(middleware::from_fn(print_request_response));
|
|
174
|
+
// }
|
|
175
|
+
|
|
176
|
+
app
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[allow(dead_code)]
|
|
180
|
+
async fn print_request_response(
|
|
181
|
+
req: Request,
|
|
182
|
+
next: Next,
|
|
183
|
+
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
184
|
+
let res = next.run(req).await;
|
|
185
|
+
|
|
186
|
+
let (parts, body) = res.into_parts();
|
|
187
|
+
let bytes = buffer_and_print("response", body).await?;
|
|
188
|
+
let lines = bytes.chunks(150).map(|c| c.to_owned()).collect::<Vec<_>>();
|
|
189
|
+
|
|
190
|
+
let stream = tokio_stream::iter(lines)
|
|
191
|
+
.throttle(Duration::from_millis(500))
|
|
192
|
+
.map(Ok::<_, Infallible>);
|
|
193
|
+
|
|
194
|
+
let res = Response::from_parts(parts, Body::from_stream(stream));
|
|
195
|
+
|
|
196
|
+
Ok(res)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async fn buffer_and_print<B>(direction: &str, body: B) -> Result<Bytes, (StatusCode, String)>
|
|
200
|
+
where
|
|
201
|
+
B: axum::body::HttpBody<Data = Bytes>,
|
|
202
|
+
B::Error: std::fmt::Display,
|
|
203
|
+
{
|
|
204
|
+
let bytes = match body.collect().await {
|
|
205
|
+
Ok(collected) => collected.to_bytes(),
|
|
206
|
+
Err(err) => {
|
|
207
|
+
return Err((
|
|
208
|
+
StatusCode::BAD_REQUEST,
|
|
209
|
+
format!("failed to read {direction} body: {err}"),
|
|
210
|
+
));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if let Ok(body) = std::str::from_utf8(&bytes) {
|
|
215
|
+
tracing::debug!("▶️ {direction} body = {body:?}");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Ok(bytes)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pub fn text_asset_response(path: &str, css: &str) -> Response {
|
|
222
|
+
let mime = mime_guess::from_path(path);
|
|
223
|
+
let aas_str = mime.first_or_text_plain();
|
|
224
|
+
let cloned = css.to_owned();
|
|
225
|
+
([(CONTENT_TYPE, aas_str.to_string())], cloned).into_response()
|
|
226
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
use crate::dto::ClientEvent;
|
|
2
|
+
use crate::server::handler_stop::Stop;
|
|
3
|
+
use crate::server::signals::ServerSignals;
|
|
4
|
+
use crate::server::state::ServerState;
|
|
5
|
+
use actix::{ActorContext, AsyncContext, Running};
|
|
6
|
+
use axum_server::Handle;
|
|
7
|
+
use bsnext_input::route_manifest::RoutesManifest;
|
|
8
|
+
use bsnext_input::server_config::ServerConfig;
|
|
9
|
+
use std::net::SocketAddr;
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
use tokio::sync::oneshot::Sender;
|
|
12
|
+
use tokio::sync::{broadcast, oneshot};
|
|
13
|
+
use tracing::{span, Level};
|
|
14
|
+
|
|
15
|
+
pub struct ServerActor {
|
|
16
|
+
pub config: ServerConfig,
|
|
17
|
+
pub routes_manifest: RoutesManifest,
|
|
18
|
+
pub signals: Option<ServerSignals>,
|
|
19
|
+
pub app_state: Option<Arc<ServerState>>,
|
|
20
|
+
pub addr: Option<SocketAddr>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl ServerActor {
|
|
24
|
+
pub fn new_from_config(config: ServerConfig) -> Self {
|
|
25
|
+
let routes_manifest = RoutesManifest::new(&config.routes);
|
|
26
|
+
Self {
|
|
27
|
+
config,
|
|
28
|
+
signals: None,
|
|
29
|
+
app_state: None,
|
|
30
|
+
addr: None,
|
|
31
|
+
routes_manifest,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
pub fn install_signals(&mut self) -> (Sender<()>, Handle, broadcast::Sender<ClientEvent>) {
|
|
35
|
+
// todo: make this an enum for more messages
|
|
36
|
+
let (client_sender, client_receiver) = broadcast::channel::<ClientEvent>(5);
|
|
37
|
+
let (shutdown_complete, shutdown_complete_receiver) = oneshot::channel();
|
|
38
|
+
let axum_server_handle = Handle::new();
|
|
39
|
+
let axum_server_handle_clone = axum_server_handle.clone();
|
|
40
|
+
|
|
41
|
+
self.signals = Some(ServerSignals {
|
|
42
|
+
complete_mdg_receiver: Some(shutdown_complete_receiver),
|
|
43
|
+
axum_server_handle: Some(axum_server_handle),
|
|
44
|
+
client_sender: Some(client_sender.clone()),
|
|
45
|
+
client_receiver: Some(client_receiver),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
(shutdown_complete, axum_server_handle_clone, client_sender)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl actix::Actor for ServerActor {
|
|
53
|
+
type Context = actix::Context<Self>;
|
|
54
|
+
|
|
55
|
+
fn started(&mut self, _ctx: &mut Self::Context) {
|
|
56
|
+
tracing::trace!(actor.name = "ServerActor", actor.lifecyle = "started");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
|
60
|
+
let span = span!(
|
|
61
|
+
Level::TRACE,
|
|
62
|
+
"stopping",
|
|
63
|
+
actor.name = "ServerActor",
|
|
64
|
+
actor.lifecyle = "stopping",
|
|
65
|
+
identity = ?self.config.identity
|
|
66
|
+
);
|
|
67
|
+
let _guard = span.enter();
|
|
68
|
+
let addr = ctx.address();
|
|
69
|
+
let s = ctx.state();
|
|
70
|
+
|
|
71
|
+
// this handles crashes. If the server actor crashes we want to ensure we're
|
|
72
|
+
// cleaning up any server we've started. So in a crash we return 'Running::Continue'
|
|
73
|
+
// and send ourselves the `Stop2` message (which will cleanly close the server)
|
|
74
|
+
// `Stop2` will then eventually also try to close this actor, at which point we'll allow it
|
|
75
|
+
if s.alive() {
|
|
76
|
+
tracing::trace!("actor was ❤️, sending `Stop2` to SELF",);
|
|
77
|
+
addr.do_send(Stop);
|
|
78
|
+
Running::Continue
|
|
79
|
+
} else {
|
|
80
|
+
tracing::trace!("Server actor was already 💀");
|
|
81
|
+
Running::Stop
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
|
86
|
+
tracing::trace!(
|
|
87
|
+
actor.name = "ServerActor",
|
|
88
|
+
actor.lifecyle = "stopped",
|
|
89
|
+
identity = ?self.config.identity
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
use std::net::SocketAddr;
|
|
2
|
+
use thiserror::Error;
|
|
3
|
+
|
|
4
|
+
// We derive `thiserror::Error`
|
|
5
|
+
#[derive(serde::Serialize, serde::Deserialize, Debug, Error)]
|
|
6
|
+
pub enum ServerError {
|
|
7
|
+
// The `#[from]` attribute generates `From<JsonRejection> for ApiError`
|
|
8
|
+
// implementation. See `thiserror` docs for more information
|
|
9
|
+
#[error("address in use {socket_addr}")]
|
|
10
|
+
AddrInUse { socket_addr: SocketAddr },
|
|
11
|
+
#[error("invalid bind address: {addr_parse_error}")]
|
|
12
|
+
InvalidAddress { addr_parse_error: String },
|
|
13
|
+
#[error("could not determine the reason")]
|
|
14
|
+
Unknown,
|
|
15
|
+
#[error("server was closed")]
|
|
16
|
+
Closed,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn test_api_error() {
|
|
21
|
+
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
22
|
+
struct Output {
|
|
23
|
+
error: ServerError,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let e = ServerError::AddrInUse {
|
|
27
|
+
socket_addr: "127.0.0.1:3000".parse().unwrap(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let output = Output { error: e };
|
|
31
|
+
let _as_yaml = serde_yaml::to_string(&output).unwrap();
|
|
32
|
+
let _as_json = serde_json::to_string_pretty(&output).unwrap();
|
|
33
|
+
// println!("{}", as_yaml);
|
|
34
|
+
// println!("{}", as_json);
|
|
35
|
+
|
|
36
|
+
let input = r#"
|
|
37
|
+
error: !AddrInUse
|
|
38
|
+
socket_addr: 127.0.0.1:3000
|
|
39
|
+
"#;
|
|
40
|
+
let _input: Output = serde_yaml::from_str(&input).unwrap();
|
|
41
|
+
}
|