Skip to content

Commit

Permalink
Merge pull request #51 from BrowserSync/inject-params
Browse files Browse the repository at this point in the history
control injections
  • Loading branch information
shakyShane authored Dec 28, 2024
2 parents dea3d74 + 85c8b70 commit bf00a48
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 8 deletions.
70 changes: 69 additions & 1 deletion crates/bsnext_core/src/dynamic_query_params.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
use axum::extract::{Query, Request};
use axum::middleware::Next;
use axum::response::IntoResponse;
use bsnext_guards::path_matcher::PathMatcher;
use bsnext_guards::MatcherList;
use bsnext_resp::builtin_strings::{BuiltinStringDef, BuiltinStrings};
use bsnext_resp::cache_opts::CacheOpts;
use bsnext_resp::inject_opts::{Injection, InjectionItem};
use bsnext_resp::InjectHandling;
use std::convert::Infallible;
use std::time::Duration;
use tokio::time::sleep;
Expand All @@ -15,9 +20,40 @@ pub struct DynamicQueryParams {
/// Control if Browsersync will add cache-busting headers, or not.
#[serde(rename = "bslive.cache")]
pub cache: Option<CacheOpts>,
/// Control if Browsersync will add cache-busting headers, or not.
#[serde(rename = "bslive.inject")]
pub inject: Option<InjectParam>,
}

#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum InjectParam {
BuiltinStrings(BuiltinStrings),
Other(String),
}

#[cfg(test)]
mod test {
use super::*;
use http::Uri;
#[test]
fn test_deserializing() -> anyhow::Result<()> {
let input = Uri::from_static(
"/abc?bslive.delay.ms=2000&bslive.cache=prevent&bslive.inject=bslive:js-connector",
);
let Query(query_with_named): Query<DynamicQueryParams> =
Query::try_from_uri(&input).unwrap();
insta::assert_debug_snapshot!(query_with_named);

let input = Uri::from_static("/abc?bslive.inject=false");
let Query(query_with_bool): Query<DynamicQueryParams> =
Query::try_from_uri(&input).unwrap();
insta::assert_debug_snapshot!(query_with_bool);
Ok(())
}
}

pub async fn dynamic_query_params_handler(req: Request, next: Next) -> impl IntoResponse {
pub async fn dynamic_query_params_handler(mut req: Request, next: Next) -> impl IntoResponse {
let Ok(Query(query_params)) = Query::try_from_uri(req.uri()) else {
let res = next.run(req).await;
return Ok::<_, Infallible>(res);
Expand All @@ -34,6 +70,38 @@ pub async fn dynamic_query_params_handler(req: Request, next: Next) -> impl Into
_ => {}
}

// Other things to apply *before*
#[allow(clippy::single_match)]
match &query_params {
DynamicQueryParams {
inject: Some(inject_append),
..
} => {
let uri = req.uri().clone();
let ex = req.extensions_mut();
if let Some(inject) = ex.get_mut::<InjectHandling>() {
tracing::info!(
"Adding an item to the injection handling on the fly {} {}",
uri.to_string(),
uri.path()
);
match inject_append {
InjectParam::Other(other) if other == "false" => {
inject.items = vec![];
}
InjectParam::BuiltinStrings(str) => inject.items.push(InjectionItem {
inner: Injection::BsLive(BuiltinStringDef {
name: str.to_owned(),
}),
only: Some(MatcherList::Item(PathMatcher::pathname(uri.path()))),
}),
InjectParam::Other(_) => todo!("other?"),
}
}
}
_ => {}
}

let mut res = next.run(req).await;

// things to apply *after*
Expand Down
5 changes: 1 addition & 4 deletions crates/bsnext_core/src/optional_layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter {
.map(|delay| middleware::from_fn_with_state(delay.clone(), delay_mw));

let injections = opts.inject.as_injections();
let inject_layer = Some(injections.items.len())
.filter(|inj| *inj > 0)
.map(|_| middleware::from_fn(response_modifications_layer));

let set_response_headers_layer = opts
.headers
Expand All @@ -47,7 +44,7 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter {

let optional_stack = ServiceBuilder::new()
.layer(middleware::from_fn(dynamic_query_params_handler))
.layer(option_layer(inject_layer))
.layer(middleware::from_fn(response_modifications_layer))
.layer(prevent_cache_headers_layer)
.layer(option_layer(set_response_headers_layer))
.layer(option_layer(cors_enabled_layer))
Expand Down
37 changes: 36 additions & 1 deletion crates/bsnext_core/src/query-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ query params.

- [delay](#delay-example) - simulate a delay in TTFB.
- [cache](#cache-example) - add or remove the headers that Browsersync to control cache
- [inject](#inject-example) - dynamically inject content into responses

---

Expand Down Expand Up @@ -137,4 +138,38 @@ fn main() -> anyhow::Result<()> {
assert_eq!(pairs, expected);
Ok(())
}
```
```

## Inject example

If you're serving Browsersync assets, but not using it to serve HTML, it makes it impossible
to inject our scripts.

Assuming you're running Browsersync on port 3000 - then you can use that to serve some assets like this:

```bash
# start a static file server from the current folder
bslive . --cors
```

```html
<-- Now in your HTML, reference a file -->
<script src="http://localhost:3000/dist/index.js">
```
This works, but we don't have Browsersync loaded in the page - to solve this problem append the following query param
and an auto-connecting script will be appended to the JS file.
- `?bslive.inject=bslive:js-connector`
```html
<-- Now in your HTML, reference a file -->
<script src="http://localhost:3000/dist/index.js?bslive.inject=bslive:js-connector">
```
## Inject example 2
If you want to prevent Browsersync from appending a script tag (or anything else), you can
use a query param to remove all injections.
Just add `?bslive.inject=false` as a query param to any page - that will prevent HTML injections
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/bsnext_core/src/dynamic_query_params.rs
expression: query_with_bool
---
DynamicQueryParams {
delay: None,
cache: None,
inject: Some(
Other(
"false",
),
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: crates/bsnext_core/src/dynamic_query_params.rs
expression: query_with_named
---
DynamicQueryParams {
delay: Some(
2000,
),
cache: Some(
Prevent,
),
inject: Some(
BuiltinStrings(
JsConnector,
),
),
}
1 change: 1 addition & 0 deletions crates/bsnext_resp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ anyhow = "1.0.86"

[dev-dependencies]
tokio = { workspace = true }
http = { workspace = true }
tower = { workspace = true }
6 changes: 6 additions & 0 deletions crates/bsnext_resp/src/builtin_strings.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::connector::Connector;
use crate::debug::Debug;
use crate::injector_guard::ByteReplacer;
use crate::js_connector::JsConnector;
use axum::extract::Request;
use bsnext_guards::route_guard::RouteGuard;
use http::Response;
Expand All @@ -13,6 +14,8 @@ pub struct BuiltinStringDef {
pub enum BuiltinStrings {
#[serde(rename = "bslive:connector")]
Connector,
#[serde(rename = "bslive:js-connector")]
JsConnector,
#[serde(rename = "bslive:debug")]
Debug,
}
Expand All @@ -22,13 +25,15 @@ impl RouteGuard for BuiltinStringDef {
match self.name {
BuiltinStrings::Connector => Connector.accept_req(req),
BuiltinStrings::Debug => Debug.accept_req(req),
BuiltinStrings::JsConnector => JsConnector.accept_req(req),
}
}

fn accept_res<T>(&self, res: &Response<T>) -> bool {
match self.name {
BuiltinStrings::Connector => Connector.accept_res(res),
BuiltinStrings::Debug => Debug.accept_res(res),
BuiltinStrings::JsConnector => JsConnector.accept_res(res),
}
}
}
Expand All @@ -38,6 +43,7 @@ impl ByteReplacer for BuiltinStringDef {
match self.name {
BuiltinStrings::Connector => Connector.apply(body),
BuiltinStrings::Debug => Debug.apply(body),
BuiltinStrings::JsConnector => JsConnector.apply(body),
}
}
}
31 changes: 31 additions & 0 deletions crates/bsnext_resp/src/js_connector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 JsConnector;

impl RouteGuard for JsConnector {
fn accept_req(&self, _req: &Request) -> bool {
true
}

fn accept_res<T>(&self, res: &Response<T>) -> bool {
let is_js = RespMod::is_js(res);
tracing::trace!("is_js: {}", is_js);
is_js
}
}
impl ByteReplacer for JsConnector {
fn apply(&self, body: &'_ str) -> Option<String> {
let footer = format!(
r#"{body};
// this was injected by the Browsersync Live Js Connector
;import('/__bs_js').catch(console.error);
"#
);
Some(footer)
}
}
11 changes: 11 additions & 0 deletions crates/bsnext_resp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod injector_guard;
use crate::inject_opts::InjectionItem;
#[cfg(test)]
pub mod inject_opt_test;
pub mod js_connector;

use crate::injector_guard::ByteReplacer;
use axum::body::Body;
Expand Down Expand Up @@ -38,6 +39,16 @@ impl RespMod {
.and_then(|v| v.to_str().ok().map(|s| s.contains("text/html")))
.unwrap_or(false)
}
pub fn is_js<T>(res: &Response<T>) -> bool {
res.headers()
.get(CONTENT_TYPE)
.and_then(|v| {
v.to_str()
.ok()
.map(|s| s.contains("application/javascript"))
})
.unwrap_or(false)
}
}

#[derive(Debug, Clone)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: crates/bsnext_system/src/start_kind/start_from_paths.rs
assertion_line: 125
expression: input
---
Input {
Expand Down
7 changes: 6 additions & 1 deletion examples/basic/inject.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ servers:
raw: 'body{}'
inject:
- append: lol
only: '/*.css'
only: '/*.css'
- name: 'no-inject'
routes:
- path: /
dir: examples/basic/public
inject: false
1 change: 1 addition & 0 deletions examples/basic/public/script2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("hello from script2.js");
17 changes: 17 additions & 0 deletions tests/inject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,22 @@ test.describe(
expect(body.toString()).toMatchSnapshot();
}
});
test("injects with bslive:js-connector query param", async ({
page,
bs,
}) => {
await page.goto(bs.named("no-inject", "/"));
const waiter = page.waitForRequest((req) =>
new URL(req.url()).pathname.startsWith("/__bs_js"),
);
await page.addScriptTag({
url: bs.named(
"no-inject",
"/script2.js?bslive.inject=bslive:js-connector",
),
type: "module",
});
await waiter;
});
},
);

0 comments on commit bf00a48

Please sign in to comment.