axum_js_advice/
lib.rs

1//!
2//! # Axum JS Advice Middleware
3//! 
4//! This project allows you to includes a custom middleware that injects a JavaScript alert in your axum application, advising users to disable JavaScript for enhanced security.
5//! 
6//! ## Features
7//! 
8//! - **Custom Middleware**: Adds an HTML `<script>` tag to responses, displaying a warning pop-up if JavaScript is enabled.
9//! - **Request and Response Buffering**: Reads and modifies HTTP request/response bodies to include the custom script.
10//! - **Minimal Setup**: Built with simplicity in mind using `axum` and `tokio`.
11//! 
12//! ## How It Works
13//! 
14//! The middleware intercepts responses and appends a JavaScript alert to the body. The alert advises users to disable JavaScript for their safety.
15//! 
16//! ### Example Warning Script Injected:
17//! ```html
18//! <script>
19//! alert("Warning!\nYou have JavaScript enabled, you are putting yourself at risk!\nPlease disable it immediately!");
20//! </script>
21//! ```
22//! 
23//! ### Usage
24//!
25//! ```bash
26//! cargo add axum_js_advice
27//! ```
28//! 
29//! Then, add axum_js_advice() in your middleware layer, for example:
30//! 
31//! ```rust
32//! use axum::{middleware, Router};
33//! use axum_js_advice::js_advice;
34//!
35//! #[tokio::main]
36//! async fn main() {
37//!     let app = Router::new()
38//!         .route(
39//!             "/",
40//!             axum::routing::get(|| async move { axum::response::Html("Hello from `POST /`") }),
41//!         )
42//!         //.layer(middleware::from_fn(OTHER_MIDDLEWARE_RULE))
43//!         .layer(middleware::from_fn(js_advice));
44//!
45//!     let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
46//!         .await
47//!         .unwrap();
48//!     println!("listening on {}", listener.local_addr().unwrap());
49//!     axum::serve(listener, app).await.unwrap();
50//! }
51//! ```
52//!
53//! ## Example Output
54//! 
55//! ```bash
56//! cargo run --example middleware
57//! ```
58//!
59//! When visiting `http://127.0.0.1:3000/`, the response will include:
60//!
61//! ```html
62//! <script>
63//! alert("Warning!\nYou have JavaScript enabled, you are putting yourself at risk!\nPlease disable it immediately!");
64//! </script>
65//! Hello from `POST /`
66//! ```
67//! 
68//! If you have JavaScript enabled, a reminder will pop up asking you to disable it; otherwise, you can browse freely.
69//! 
70//! ## License
71//! 
72//! This project is licensed under the GPL License. See the [LICENSE](./LICENSE) file for details.
73//!
74
75use http_body_util::BodyExt;
76
77/// Middleware fn to add a pop-up of JS advice
78pub async fn js_advice(
79    req: axum::extract::Request,
80    next: axum::middleware::Next,
81) -> Result<impl axum::response::IntoResponse, (axum::http::StatusCode, String)> {
82    let (parts, body) = req.into_parts();
83    let bytes = buffer("request", body).await?;
84    let req = axum::extract::Request::from_parts(parts, axum::body::Body::from(bytes));
85    let res = next.run(req).await;
86    let (parts, body) = res.into_parts();
87    let raw_response = buffer("response", body).await?;
88    if parts
89        .headers
90        .get("content-type")
91        .unwrap_or(&axum::http::header::HeaderValue::from_str("content-type").unwrap())
92        .to_str()
93        .unwrap_or("")
94        .contains("text/html")
95    {
96        let bytes_response = [js_advice_html().into(), raw_response.clone()].concat();
97        let res =
98            axum::response::Response::from_parts(parts, axum::body::Body::from(bytes_response));
99        return Ok(res);
100    }
101    let res = axum::response::Response::from_parts(parts, axum::body::Body::from(raw_response));
102    Ok(res)
103}
104
105/// Return buffer | StatusCode from a given body
106async fn buffer<B>(
107    direction: &str,
108    body: B,
109) -> Result<axum::body::Bytes, (axum::http::StatusCode, String)>
110where
111    B: axum::body::HttpBody<Data = axum::body::Bytes>,
112    B::Error: std::fmt::Display,
113{
114    let bytes = match body.collect().await {
115        Ok(collected) => collected.to_bytes(),
116        Err(err) => {
117            return Err((
118                axum::http::StatusCode::BAD_REQUEST,
119                format!("failed to read {direction} body: {err}"),
120            ));
121        }
122    };
123    Ok(bytes)
124}
125
126/// HTML minified
127fn js_advice_html() -> &'static str {
128    r#"<script>alert("Warning!\nYou have JavaScript enabled, you are putting yourself at risk!\nPlease disable it immediately!")</script>"#
129}