Skip to main content

tide_helmet/
lib.rs

1//! `tide-helmet` is a security middleware for the Tide web framework that sets various HTTP headers to help protect your app.
2//!
3//! `tide_helmet::Helmet` is a middleware that automatically sets security headers on all responses.
4//!
5//! It is based on the [Helmet](https://helmetjs.github.io/) library for Node.js and is highly configurable.
6//!
7//! # Usage
8//!
9//! ```no_run
10//! use tide_helmet::Helmet;
11//!
12//! #[async_std::main]
13//! async fn main() -> tide::Result<()> {
14//!     let mut app = tide::new();
15//!     app.with(Helmet::default());
16//!     app.at("/").get(|_| async { Ok("Hello, world!") });
17//!     app.listen("0.0.0.0:3000").await?;
18//!     Ok(())
19//! }
20//! ```
21//!
22//! By default Helmet will set the following headers:
23//!
24//! ```text
25//! 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
26//! Cross-Origin-Opener-Policy: same-origin
27//! Cross-Origin-Resource-Policy: same-origin
28//! Origin-Agent-Cluster: ?1
29//! Referrer-Policy: no-referrer
30//! Strict-Transport-Security: max-age=15552000; includeSubDomains
31//! X-Content-Type-Options: nosniff
32//! X-DNS-Prefetch-Control: off
33//! X-Download-Options: noopen
34//! X-Frame-Options: sameorigin
35//! X-Permitted-Cross-Domain-Policies: none
36//! X-XSS-Protection: 0
37//! ```
38//!
39//! 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.
40//!
41//! # Configuration
42//!
43//! By default if you construct a new instance of `Helmet` it will not set any headers.
44//!
45//! It is possible to configure `Helmet` to set only the headers you want, by using the `add` method to add headers.
46//!
47//! ```no_run
48//! use tide_helmet::{Helmet, ContentSecurityPolicy, CrossOriginOpenerPolicy};
49//!
50//! #[async_std::main]
51//! async fn main() -> tide::Result<()> {
52//!     let mut app = tide::new();
53//!     app.with(
54//!         Helmet::new()
55//!             .add(
56//!                 ContentSecurityPolicy::new()
57//!                     .default_src(vec!["'self'"])
58//!                     .script_src(vec!["'self'", "https://cdn.example.com"]),
59//!             )
60//!             .add(CrossOriginOpenerPolicy::same_origin_allow_popups()),
61//!     );
62//!     app.at("/").get(|_| async { Ok("Hello, world!") });
63//!     app.listen("0.0.0.0:3000").await?;
64//!     Ok(())
65//! }
66//! ```
67use tide::{Middleware, Next, Request};
68
69use helmet_core::Helmet as HelmetCore;
70
71// re-export helmet_core::*, except for the `Helmet` struct
72pub use helmet_core::*;
73
74/// Helmet middleware for Tide.
75///
76/// Use `Helmet::default()` for a sensible set of default security headers,
77/// or `Helmet::new()` to start with no headers and add only the ones you need.
78///
79/// ```rust
80/// use tide_helmet::Helmet;
81///
82/// let mut app = tide::new();
83/// app.with(Helmet::default());
84/// ```
85#[derive(Clone, Debug)]
86pub struct Helmet {
87    headers: Vec<(&'static str, String)>,
88}
89
90impl Default for Helmet {
91    fn default() -> Self {
92        Self::from(HelmetCore::default())
93    }
94}
95
96impl Helmet {
97    /// Create a new instance of `Helmet` with no headers set.
98    pub fn new() -> Self {
99        Self::from(HelmetCore::new())
100    }
101
102    /// Add a header to the middleware.
103    #[allow(clippy::should_implement_trait)]
104    pub fn add(mut self, header: impl Into<helmet_core::Header>) -> Self {
105        self.headers.push(header.into());
106        self
107    }
108}
109
110impl From<HelmetCore> for Helmet {
111    fn from(core: HelmetCore) -> Self {
112        Self {
113            headers: core
114                .headers
115                .iter()
116                .map(|header| (header.0, header.1.clone()))
117                .collect(),
118        }
119    }
120}
121
122#[async_trait::async_trait]
123impl<State: Clone + Send + Sync + 'static> Middleware<State> for Helmet {
124    async fn handle(&self, req: Request<State>, next: Next<'_, State>) -> tide::Result {
125        let mut res = next.run(req).await;
126        for (name, value) in &self.headers {
127            res.insert_header(*name, value.as_str());
128        }
129        Ok(res)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[async_std::test]
138    async fn test_helmet() {
139        let mut app = tide::new();
140        app.with(
141            Helmet::new()
142                .add(helmet_core::XContentTypeOptions::nosniff())
143                .add(helmet_core::XFrameOptions::same_origin())
144                .add(helmet_core::XXSSProtection::on().mode_block()),
145        );
146        app.at("/").get(|_| async { Ok("Hello, world!") });
147
148        let req = tide::http::Request::new(
149            tide::http::Method::Get,
150            tide::http::Url::parse("http://localhost/").unwrap(),
151        );
152        let res: tide::http::Response = app.respond(req).await.unwrap();
153
154        assert_eq!(res.status(), tide::StatusCode::Ok);
155        assert_eq!(
156            res.header("X-Content-Type-Options").map(|v| v.as_str()),
157            Some("nosniff")
158        );
159        assert_eq!(
160            res.header("X-Frame-Options").map(|v| v.as_str()),
161            Some("SAMEORIGIN")
162        );
163        assert_eq!(
164            res.header("X-XSS-Protection").map(|v| v.as_str()),
165            Some("1; mode=block")
166        );
167    }
168
169    #[async_std::test]
170    async fn test_helmet_default() {
171        let mut app = tide::new();
172        app.with(Helmet::default());
173        app.at("/").get(|_| async { Ok("Hello, world!") });
174
175        let req = tide::http::Request::new(
176            tide::http::Method::Get,
177            tide::http::Url::parse("http://localhost/").unwrap(),
178        );
179        let res: tide::http::Response = app.respond(req).await.unwrap();
180
181        assert_eq!(res.status(), tide::StatusCode::Ok);
182        assert_eq!(
183            res.header("X-Frame-Options").map(|v| v.as_str()),
184            Some("SAMEORIGIN")
185        );
186        assert_eq!(
187            res.header("X-XSS-Protection").map(|v| v.as_str()),
188            Some("0")
189        );
190        assert_eq!(
191            res.header("Referrer-Policy").map(|v| v.as_str()),
192            Some("no-referrer")
193        );
194    }
195}