Skip to content

Commit 0bde26d

Browse files
authored
feat: simplify core + add actix web + update ntex (#3)
* feat: simplify core + add actix web + update ntex * chore: update versions and readme
1 parent 29b9fa4 commit 0bde26d

File tree

12 files changed

+494
-181
lines changed

12 files changed

+494
-181
lines changed

.github/workflows/test-publish.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ jobs:
3232
- uses: actions-rs/cargo@v1
3333
with:
3434
command: test
35-
all-features: true
3635
args: --verbose
3736
publish:
3837
name: Publish

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ members = [
33
"packages/ntex-helmet",
44
"packages/helmet-core",
55
"packages/axum-helmet",
6+
"packages/actix-web-helmet",
67
]
8+
resolver = "2"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A security middleware library for popular Rust web frameworks.
55
## Packages
66

77
- `ntex-helmet` is a security middleware for the `ntex` web framework.
8-
- `actix-web-helmet` is a security middleware for the `actix-web` web framework. **_Coming Soon_**
8+
- `actix-web-helmet` is a security middleware for the `actix-web` web framework.
99
- `rocket-helmet` is a security middleware for the `rocket` web framework. **_Coming Soon_**
1010
- `warp-helmet` is a security middleware for the `warp` web framework. **_Coming Soon_**
1111
- `axum-helmet` is a security middleware for the `axum` web framework.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "actix-web-helmet"
3+
version = "0.2.0"
4+
edition = "2021"
5+
authors = ["Daniel Kovacs <[email protected]>"]
6+
description = "HTTP security headers middleware for actix-web"
7+
readme = "README.md"
8+
license = "MIT"
9+
homepage = "https://github.com/danielkov/rust-helmet"
10+
repository = "https://github.com/danielkov/rust-helmet"
11+
keywords = ["actix", "actix-web", "helmet", "security", "middleware"]
12+
categories = ["web-programming", "http", "middleware"]
13+
14+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15+
16+
[dependencies]
17+
actix-web = { version = "4.10" }
18+
helmet-core = { path = "../helmet-core", version = "0.2.0" }
19+
futures = { version = "0.3" }
20+
21+
[dev-dependencies]
22+
actix-web = { version = "4.10" }
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# `actix-web-helmet` - Security Middleware for `ntex` web framework
2+
3+
[![crate](https://img.shields.io/crates/v/actix-web-helmet.svg)](https://crates.io/crates/actix-web-helmet)
4+
[![docs](https://docs.rs/actix-web-helmet/badge.svg)](https://docs.rs/actix-web-helmet)
5+
6+
`actix-web-helmet` is a security middleware for the `actix-web` web framework. It's based on the [helmet](https://helmetjs.github.io/) middleware for Node.js.
7+
8+
It works by setting HTTP headers for you. These headers can help protect your app from some well-known web vulnerabilities:
9+
10+
- [Cross-Origin-Embedder-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy)
11+
- [Cross-Origin-Opener-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy)
12+
- [Cross-Origin-Resource-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy)
13+
- [Origin-Agent-Cluster](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster)
14+
- [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)
15+
- [Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)
16+
- [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options)
17+
- [X-DNS-Prefetch-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control)
18+
- [X-Download-Options](<https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/ms537628(v=vs.85)?redirectedfrom=MSDN>)
19+
- [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options)
20+
- [X-Permitted-Cross-Domain-Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Permitted-Cross-Domain-Policies)
21+
- [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection)
22+
- [X-Powered-By](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Powered-By)
23+
- [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
24+
25+
## Usage
26+
27+
Add this to your `Cargo.toml`:
28+
29+
```toml
30+
[dependencies]
31+
actix-web-helmet = "0.1"
32+
```
33+
34+
## Example
35+
36+
```rust
37+
use actix_web::{web, App, HttpResponse};
38+
use actix_web_helmet::Helmet;
39+
40+
#[actix_web::main]
41+
async fn main() {
42+
let app = App::new()
43+
.wrap(Helmet::default())
44+
.service(web::resource("/").to(|| HttpResponse::Ok()));
45+
46+
// ...
47+
}
48+
```
49+
50+
## License
51+
52+
This project is licensed under the [MIT license](LICENSE).
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
//! `actix-web-helmet` is a middleware for securing your Actix-Web application with various HTTP headers.
2+
//!
3+
//! `actix_web_helmet::Helmet` is a middleware that can be used to set various HTTP headers that can help protect your app from well-known web vulnerabilities.
4+
//!
5+
//! It is based on the [Helmet](https://helmetjs.github.io/) middleware for Express.js.
6+
//!
7+
//! # Usage
8+
//!
9+
//! ```no_run
10+
//! use actix_web::{web, App, HttpServer, Responder, get};
11+
//! use actix_web_helmet::Helmet;
12+
//!
13+
//! #[get("/")]
14+
//! async fn index() -> impl Responder {
15+
//! "Hello, World!"
16+
//! }
17+
//!
18+
//! #[actix_web::main]
19+
//! async fn main() -> std::io::Result<()> {
20+
//! HttpServer::new(|| App::new().wrap(Helmet::default()).service(index))
21+
//! .bind(("127.0.0.1", 8080))?
22+
//! .run()
23+
//! .await
24+
//! }
25+
//! ```
26+
//!
27+
//! By default Helmet will set the following headers:
28+
//!
29+
//! ```text
30+
//! Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests
31+
//! Cross-Origin-Opener-Policy: same-origin
32+
//! Cross-Origin-Resource-Policy: same-origin
33+
//! Origin-Agent-Cluster: ?1
34+
//! Referrer-Policy: no-referrer
35+
//! Strict-Transport-Security: max-age=15552000; includeSubDomains
36+
//! X-Content-Type-Options: nosniff
37+
//! X-DNS-Prefetch-Control: off
38+
//! X-Download-Options: noopen
39+
//! X-Frame-Options: sameorigin
40+
//! X-Permitted-Cross-Domain-Policies: none
41+
//! X-XSS-Protection: 0
42+
//! ```
43+
//!
44+
//! This might be a good starting point for most users, but it is highly recommended to spend some time with the documentation for each header, and adjust them to your needs.
45+
//!
46+
//! # Configuration
47+
//!
48+
//! By default if you construct a new instance of `Helmet` it will not set any headers.
49+
//!
50+
//! It is possible to configure `Helmet` to set only the headers you want, by using the `add` method to add headers.
51+
//!
52+
//! ```no_run
53+
//! use actix_web::{get, web, App, HttpServer, Responder};
54+
//! use actix_web_helmet::{Helmet, ContentSecurityPolicy, CrossOriginOpenerPolicy};
55+
//!
56+
//! #[get("/")]
57+
//! async fn index() -> impl Responder {
58+
//! "Hello, World!"
59+
//! }
60+
//!
61+
//! #[actix_web::main]
62+
//! async fn main() -> std::io::Result<()> {
63+
//! HttpServer::new(|| {
64+
//! {
65+
//! App::new().wrap(
66+
//! Helmet::new()
67+
//! .add(
68+
//! ContentSecurityPolicy::new()
69+
//! .child_src(vec!["'self'"])
70+
//! .child_src(vec!["'self'", "https://youtube.com"])
71+
//! .connect_src(vec!["'self'", "https://youtube.com"])
72+
//! .default_src(vec!["'self'", "https://youtube.com"])
73+
//! .font_src(vec!["'self'", "https://youtube.com"]),
74+
//! )
75+
//! .add(CrossOriginOpenerPolicy::same_origin_allow_popups()),
76+
//! )
77+
//! }
78+
//! .service(index)
79+
//! })
80+
//! .bind(("127.0.0.1", 8080))?
81+
//! .run()
82+
//! .await
83+
//! }
84+
//! ```
85+
use std::future::Future;
86+
use std::pin::Pin;
87+
88+
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
89+
use actix_web::http::header::{HeaderName, HeaderValue};
90+
use actix_web::Error;
91+
use futures::future::{ok, Ready};
92+
93+
use helmet_core::Helmet as HelmetCore;
94+
95+
// re-export helmet_core::*, except for the `Helmet` struct
96+
pub use helmet_core::*;
97+
98+
pub struct HelmetMiddleware<S> {
99+
inner: HelmetCore,
100+
service: S,
101+
}
102+
103+
/// Helmet middleware
104+
/// ```rust
105+
/// use actix_web::{web, App, HttpServer};
106+
/// use actix_web_helmet::Helmet;
107+
/// ```
108+
pub struct Helmet(HelmetCore);
109+
110+
impl Helmet {
111+
/// Create a new instance of `Helmet` with no headers set.
112+
pub fn new() -> Self {
113+
Self(HelmetCore::new())
114+
}
115+
116+
/// Add a header to the middleware.
117+
pub fn add(self, middleware: impl Into<helmet_core::Header>) -> Self {
118+
Self(self.0.add(middleware))
119+
}
120+
}
121+
122+
impl<S, B> Transform<S, ServiceRequest> for Helmet
123+
where
124+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, // Add 'static bound
125+
S::Future: 'static,
126+
B: 'static,
127+
{
128+
type Response = ServiceResponse<B>;
129+
type Error = Error;
130+
type InitError = ();
131+
// The actual middleware service that will be created
132+
type Transform = HelmetMiddleware<S>;
133+
// The future that resolves to the middleware service
134+
type Future = Ready<Result<Self::Transform, Self::InitError>>;
135+
136+
fn new_transform(&self, service: S) -> Self::Future {
137+
// Create the middleware service instance HelmetMiddleware
138+
// Clone the inner configuration (HelmetCore).
139+
// HelmetCore should derive Clone or you might need to wrap it in Rc/Arc.
140+
// Assuming HelmetCore is Clone:
141+
ok(HelmetMiddleware {
142+
inner: self.0.clone(), // Clone the configuration from the factory
143+
service, // Pass the next service in the chain
144+
})
145+
146+
// If HelmetCore is large and not Clone, you might wrap it in Rc in the Helmet struct:
147+
// pub struct Helmet(Rc<HelmetCore>);
148+
// And then clone the Rc here:
149+
// ok(HelmetMiddleware {
150+
// inner: self.0.clone(),
151+
// service,
152+
// })
153+
}
154+
}
155+
156+
impl Default for Helmet {
157+
fn default() -> Self {
158+
Self(HelmetCore::default())
159+
}
160+
}
161+
162+
type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
163+
164+
impl<S, B> Service<ServiceRequest> for HelmetMiddleware<S>
165+
where
166+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
167+
S::Future: 'static,
168+
B: 'static,
169+
{
170+
type Response = ServiceResponse<B>;
171+
type Error = Error;
172+
type Future = LocalBoxFuture<Result<Self::Response, Self::Error>>;
173+
174+
// This service is ready when its next service is ready
175+
forward_ready!(service);
176+
177+
fn call(&self, req: ServiceRequest) -> Self::Future {
178+
let fut = self.service.call(req);
179+
180+
let headers_vec = self
181+
.inner
182+
.headers
183+
.iter()
184+
.map(|header| (header.0, header.1.clone()))
185+
.collect::<Vec<_>>();
186+
187+
Box::pin(async move {
188+
let mut res = fut.await?;
189+
190+
// Set the headers
191+
for (name, value) in &headers_vec {
192+
res.headers_mut().insert(
193+
HeaderName::from_bytes(name.as_bytes()).unwrap(),
194+
HeaderValue::from_str(value).unwrap(),
195+
);
196+
}
197+
Ok(res)
198+
})
199+
}
200+
}
201+
202+
#[cfg(test)]
203+
mod tests {
204+
use actix_web::http::header::{HeaderName, HeaderValue};
205+
// Make sure HttpResponse is imported if not already
206+
use actix_web::{http, test, web, App, HttpResponse}; // Added test, http, HttpResponse
207+
208+
use super::*; // Keep this
209+
210+
#[actix_web::test]
211+
async fn test_helmet() {
212+
// 1. Create the middleware *factory* instance
213+
let helmet_factory =
214+
Helmet::new().add(ContentSecurityPolicy::new().child_src(vec!["'self'"]));
215+
216+
// 2. Initialize the service using the factory with .wrap()
217+
let app = test::init_service(
218+
// Use test::init_service
219+
App::new()
220+
.wrap(helmet_factory) // Use the factory here
221+
// Define a simple async route correctly
222+
.route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
223+
)
224+
.await;
225+
226+
// 3. Create a request
227+
let req = test::TestRequest::get().uri("/").to_request(); // Use test::TestRequest
228+
229+
// 4. Call the service
230+
let res = test::call_service(&app, req).await; // Use test::call_service and the app
231+
232+
// 5. Assertions
233+
assert!(res.status().is_success()); // Check status code idiomatically
234+
assert_eq!(
235+
res.headers()
236+
.get(HeaderName::from_static("content-security-policy")),
237+
Some(&HeaderValue::from_static("child-src 'self'"))
238+
);
239+
}
240+
241+
// Optional: Add a test for the default configuration
242+
#[actix_web::test]
243+
async fn test_helmet_default() {
244+
let app = test::init_service(
245+
App::new()
246+
.wrap(Helmet::default()) // Use the default factory
247+
.route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
248+
)
249+
.await;
250+
251+
let req = test::TestRequest::get().uri("/").to_request();
252+
let resp = test::call_service(&app, req).await;
253+
254+
assert!(resp.status().is_success());
255+
// Check one or two default headers to confirm it works
256+
assert_eq!(
257+
resp.headers().get(http::header::X_FRAME_OPTIONS), // Use constants from http::header
258+
Some(&HeaderValue::from_static("SAMEORIGIN"))
259+
);
260+
assert_eq!(
261+
resp.headers().get(http::header::X_XSS_PROTECTION),
262+
Some(&HeaderValue::from_static("0"))
263+
);
264+
}
265+
}

packages/axum-helmet/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "axum-helmet"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
authors = ["Daniel Kovacs <[email protected]>"]
66
description = "HTTP security headers middleware core for axum web framework"
@@ -15,7 +15,7 @@ categories = ["web-programming", "http", "middleware"]
1515

1616
[dependencies]
1717
axum = "0.8"
18-
helmet-core = { path = "../helmet-core", version = "0.1.0" }
18+
helmet-core = { path = "../helmet-core", version = "0.2.0" }
1919
tower = "0.5"
2020
tower-service = "0.3"
2121
http = "1.0"

packages/axum-helmet/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ impl HelmetLayer {
7373
.iter()
7474
.map(|header| {
7575
(
76-
HeaderName::try_from(header.name()).expect("invalid header name"),
77-
HeaderValue::try_from(header.value()).expect("invalid header value"),
76+
HeaderName::try_from(header.0).expect("invalid header name"),
77+
HeaderValue::try_from(&header.1).expect("invalid header value"),
7878
)
7979
})
8080
.collect();

0 commit comments

Comments
 (0)