warp_helmet/lib.rs
1//! `warp-helmet` is a security middleware for the Warp web framework that sets various HTTP headers to help protect your app.
2//!
3//! `warp_helmet::Helmet` wraps a Warp filter to automatically set 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 warp::Filter;
11//! use warp_helmet::{Helmet, HelmetFilter};
12//!
13//! #[tokio::main]
14//! async fn main() {
15//! let helmet: HelmetFilter = Helmet::default().try_into().unwrap();
16//!
17//! let route = helmet.wrap(
18//! warp::path::end().map(|| "Hello, world!")
19//! );
20//!
21//! warp::serve(route).run(([127, 0, 0, 1], 3000)).await;
22//! }
23//! ```
24//!
25//! By default Helmet will set the following headers:
26//!
27//! ```text
28//! 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
29//! Cross-Origin-Opener-Policy: same-origin
30//! Cross-Origin-Resource-Policy: same-origin
31//! Origin-Agent-Cluster: ?1
32//! Referrer-Policy: no-referrer
33//! Strict-Transport-Security: max-age=15552000; includeSubDomains
34//! X-Content-Type-Options: nosniff
35//! X-DNS-Prefetch-Control: off
36//! X-Download-Options: noopen
37//! X-Frame-Options: sameorigin
38//! X-Permitted-Cross-Domain-Policies: none
39//! X-XSS-Protection: 0
40//! ```
41//!
42//! 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.
43//!
44//! # Configuration
45//!
46//! By default if you construct a new instance of `Helmet` it will not set any headers.
47//!
48//! It is possible to configure `Helmet` to set only the headers you want, by using the `add` method.
49//!
50//! ```no_run
51//! use warp::Filter;
52//! use warp_helmet::{Helmet, HelmetFilter, ContentSecurityPolicy, CrossOriginOpenerPolicy};
53//!
54//! #[tokio::main]
55//! async fn main() {
56//! let helmet: HelmetFilter = Helmet::new()
57//! .add(
58//! ContentSecurityPolicy::new()
59//! .default_src(vec!["'self'"])
60//! .script_src(vec!["'self'", "https://cdn.example.com"]),
61//! )
62//! .add(CrossOriginOpenerPolicy::same_origin_allow_popups())
63//! .try_into()
64//! .unwrap();
65//!
66//! let route = helmet.wrap(
67//! warp::path::end().map(|| "Hello, world!")
68//! );
69//!
70//! warp::serve(route).run(([127, 0, 0, 1], 3000)).await;
71//! }
72//! ```
73use http::header::{HeaderMap, HeaderName, HeaderValue};
74use warp::reply::{Reply, Response};
75use warp::Filter;
76
77use helmet_core::Helmet as HelmetCore;
78
79// re-export helmet_core::*, except for the `Helmet` struct
80pub use helmet_core::*;
81
82/// Helmet header configuration wrapper.
83///
84/// Use `Helmet::default()` for a sensible set of default security headers,
85/// or `Helmet::new()` to start with no headers and add only the ones you need.
86///
87/// Convert to [`HelmetFilter`] via `try_into()` to use with Warp.
88///
89/// ```rust
90/// use warp_helmet::{Helmet, HelmetFilter};
91///
92/// let filter: HelmetFilter = Helmet::default().try_into().unwrap();
93/// ```
94#[derive(Default)]
95pub struct Helmet(HelmetCore);
96
97impl Helmet {
98 /// Create a new instance of `Helmet` with no headers set.
99 pub fn new() -> Self {
100 Self(HelmetCore::new())
101 }
102
103 /// Add a header.
104 #[allow(clippy::should_implement_trait)]
105 pub fn add(self, header: impl Into<helmet_core::Header>) -> Self {
106 Self(self.0.add(header))
107 }
108
109 pub fn into_filter(self) -> Result<HelmetFilter, HelmetError> {
110 self.try_into()
111 }
112}
113
114/// The Warp filter wrapper created by converting a [`Helmet`] configuration.
115#[derive(Clone)]
116pub struct HelmetFilter {
117 headers: HeaderMap,
118}
119
120impl HelmetFilter {
121 /// Wrap a filter to add security headers to all its responses.
122 pub fn wrap<F, R>(
123 self,
124 filter: F,
125 ) -> impl Filter<Extract = (Response,), Error = F::Error> + Clone
126 where
127 F: Filter<Extract = (R,), Error: Send> + Clone + Send,
128 R: Reply + Send,
129 {
130 let headers = self.headers;
131 filter.map(move |reply: R| {
132 let mut resp = reply.into_response();
133 for (name, value) in headers.iter() {
134 resp.headers_mut().insert(name, value.clone());
135 }
136 resp
137 })
138 }
139}
140
141impl TryFrom<Helmet> for HelmetFilter {
142 type Error = HelmetError;
143
144 fn try_from(helmet: Helmet) -> Result<Self, Self::Error> {
145 let mut headers = HeaderMap::new();
146 for header in helmet.0.headers.iter() {
147 let name = HeaderName::try_from(header.0)
148 .map_err(|_| HelmetError::InvalidHeaderName(header.0.to_string()))?;
149 let value = HeaderValue::from_str(&header.1)
150 .map_err(|_| HelmetError::InvalidHeaderValue(header.1.clone()))?;
151 headers.insert(name, value);
152 }
153 Ok(Self { headers })
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[tokio::test]
162 async fn test_helmet() {
163 let route = Helmet::new()
164 .add(helmet_core::XContentTypeOptions::nosniff())
165 .add(helmet_core::XFrameOptions::same_origin())
166 .add(helmet_core::XXSSProtection::on().mode_block())
167 .into_filter()
168 .unwrap()
169 .wrap(warp::path::end().map(|| "Hello, world!"));
170
171 let res = warp::test::request().path("/").reply(&route).await;
172
173 assert_eq!(res.status(), 200);
174 assert_eq!(
175 res.headers()
176 .get("X-Content-Type-Options")
177 .map(|v| v.to_str().unwrap()),
178 Some("nosniff")
179 );
180 assert_eq!(
181 res.headers()
182 .get("X-Frame-Options")
183 .map(|v| v.to_str().unwrap()),
184 Some("SAMEORIGIN")
185 );
186 assert_eq!(
187 res.headers()
188 .get("X-XSS-Protection")
189 .map(|v| v.to_str().unwrap()),
190 Some("1; mode=block")
191 );
192 }
193
194 #[tokio::test]
195 async fn test_helmet_default() {
196 let helmet: HelmetFilter = Helmet::default().try_into().unwrap();
197 let route = helmet.wrap(warp::path::end().map(|| "Hello, world!"));
198
199 let res = warp::test::request().path("/").reply(&route).await;
200
201 assert_eq!(res.status(), 200);
202 assert_eq!(
203 res.headers()
204 .get("X-Frame-Options")
205 .map(|v| v.to_str().unwrap()),
206 Some("SAMEORIGIN")
207 );
208 assert_eq!(
209 res.headers()
210 .get("X-XSS-Protection")
211 .map(|v| v.to_str().unwrap()),
212 Some("0")
213 );
214 assert_eq!(
215 res.headers()
216 .get("Referrer-Policy")
217 .map(|v| v.to_str().unwrap()),
218 Some("no-referrer")
219 );
220 }
221}