junobuild_storage/http/
headers.rs

1use crate::constants::ASSET_ENCODING_NO_COMPRESSION;
2use crate::http::types::HeaderField;
3use crate::types::config::{StorageConfig, StorageConfigIFrame};
4use crate::types::store::{Asset, AssetEncoding, EncodingType};
5use crate::url::matching_urls;
6use hex::encode;
7use std::collections::HashMap;
8
9pub fn build_headers(
10    asset: &Asset,
11    encoding: &AssetEncoding,
12    encoding_type: &EncodingType,
13    config: &StorageConfig,
14) -> Vec<HeaderField> {
15    // Starts with the headers build from the configuration
16    let mut headers: HashMap<String, String> = build_config_headers(&asset.key.full_path, config)
17        .into_iter()
18        .map(|HeaderField(key, value)| (key.to_lowercase(), value))
19        .collect();
20
21    // Asset-level headers take precedence on the config - a specific header is more important than default configuration.
22    for HeaderField(key, value) in &asset.headers {
23        headers.insert(key.to_lowercase(), value.clone());
24    }
25
26    // If the asset is accessible only via a query token,
27    // we set a few headers to prevent indexing and caching by crawlers or intermediaries.
28    if asset.key.token.is_some() {
29        for HeaderField(key, value) in token_headers() {
30            headers.insert(key.to_lowercase(), value);
31        }
32    }
33
34    // The Accept-Ranges HTTP response header is a marker used by the server to advertise its support for partial requests from the client for file downloads.
35    headers.insert("accept-ranges".to_string(), "bytes".to_string());
36
37    headers.insert(
38        "etag".to_string(),
39        format!("\"{}\"", encode(encoding.sha256)),
40    );
41
42    // Headers for security
43    for HeaderField(key, value) in security_headers() {
44        headers.insert(key.to_lowercase(), value);
45    }
46
47    // iFrame with default to DENY for security reason
48    if let Some(HeaderField(key, value)) = iframe_headers(&config.unwrap_iframe()) {
49        headers.insert(key.to_lowercase(), value);
50    }
51
52    if encoding_type.clone() != *ASSET_ENCODING_NO_COMPRESSION {
53        headers.insert("content-encoding".to_string(), encoding_type.to_string());
54    }
55
56    headers
57        .into_iter()
58        .map(|(key, value)| HeaderField(key, value))
59        .collect()
60}
61
62pub fn build_redirect_headers(location: &str, iframe: &StorageConfigIFrame) -> Vec<HeaderField> {
63    let mut headers = Vec::new();
64
65    // Headers for security
66    headers.extend(security_headers());
67
68    // iFrame with default to none
69    if let Some(iframe_header) = iframe_headers(iframe) {
70        headers.push(iframe_header);
71    }
72
73    headers.push(HeaderField("Location".to_string(), location.to_string()));
74
75    headers
76}
77
78// Source: NNS-dapp
79/// List of recommended security headers as per https://owasp.org/www-satellite-secure-headers/
80/// These headers enable browser security features (like limit access to platform apis and set
81/// iFrame policies, etc.).
82fn security_headers() -> Vec<HeaderField> {
83    vec![
84        HeaderField("X-Content-Type-Options".to_string(), "nosniff".to_string()),
85        HeaderField(
86            "Strict-Transport-Security".to_string(),
87            "max-age=31536000 ; includeSubDomains".to_string(),
88        ),
89        // "Referrer-Policy: no-referrer" would be more strict, but breaks local dev deployment
90        // same-origin is still ok from a security perspective
91        HeaderField("Referrer-Policy".to_string(), "same-origin".to_string()),
92    ]
93}
94
95fn iframe_headers(iframe: &StorageConfigIFrame) -> Option<HeaderField> {
96    match iframe {
97        StorageConfigIFrame::Deny => Some(HeaderField(
98            "X-Frame-Options".to_string(),
99            "DENY".to_string(),
100        )),
101        StorageConfigIFrame::SameOrigin => Some(HeaderField(
102            "X-Frame-Options".to_string(),
103            "SAMEORIGIN".to_string(),
104        )),
105        StorageConfigIFrame::AllowAny => None,
106    }
107}
108
109fn token_headers() -> Vec<HeaderField> {
110    vec![
111        // Prevents search engines from indexing the asset or following links from it.
112        HeaderField("X-Robots-Tag".to_string(), "noindex, nofollow".to_string()),
113        // Ensures the asset is not cached anywhere (neither in shared caches nor in the browser),
114        // and is considered private to the requesting user.
115        HeaderField("Cache-Control".to_string(), "private, no-store".to_string()),
116    ]
117}
118
119pub fn build_config_headers(
120    requested_path: &str,
121    StorageConfig {
122        headers: config_headers,
123        ..
124    }: &StorageConfig,
125) -> Vec<HeaderField> {
126    matching_urls(requested_path, config_headers)
127        .iter()
128        .flat_map(|(_, headers)| headers.clone())
129        .collect()
130}