junobuild_storage/http/
headers.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use crate::constants::ASSET_ENCODING_NO_COMPRESSION;
use crate::http::types::HeaderField;
use crate::types::config::{StorageConfig, StorageConfigIFrame};
use crate::types::store::{Asset, AssetEncoding, EncodingType};
use crate::url::matching_urls;
use hex::encode;

pub fn build_headers(
    asset: &Asset,
    encoding: &AssetEncoding,
    encoding_type: &EncodingType,
    config: &StorageConfig,
) -> Vec<HeaderField> {
    let mut headers = asset.headers.clone();

    // 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.
    headers.push(HeaderField(
        "accept-ranges".to_string(),
        "bytes".to_string(),
    ));

    headers.push(HeaderField(
        "etag".to_string(),
        format!("\"{}\"", encode(encoding.sha256)),
    ));

    // Headers for security
    headers.extend(security_headers());

    // iFrame with default to DENY for security reason
    if let Some(iframe_header) = iframe_headers(&config.unwrap_iframe()) {
        headers.push(iframe_header);
    }

    if encoding_type.clone() != *ASSET_ENCODING_NO_COMPRESSION {
        headers.push(HeaderField(
            "Content-Encoding".to_string(),
            encoding_type.to_string(),
        ));
    }

    // Headers build from the configuration
    let config_headers = build_config_headers(&asset.key.full_path, config);
    headers.extend(config_headers);

    headers
}

pub fn build_redirect_headers(location: &str, iframe: &StorageConfigIFrame) -> Vec<HeaderField> {
    let mut headers = Vec::new();

    // Headers for security
    headers.extend(security_headers());

    // iFrame with default to none
    if let Some(iframe_header) = iframe_headers(iframe) {
        headers.push(iframe_header);
    }

    headers.push(HeaderField("Location".to_string(), location.to_string()));

    headers
}

// Source: NNS-dapp
/// List of recommended security headers as per https://owasp.org/www-satellite-secure-headers/
/// These headers enable browser security features (like limit access to platform apis and set
/// iFrame policies, etc.).
fn security_headers() -> Vec<HeaderField> {
    vec![
        HeaderField("X-Content-Type-Options".to_string(), "nosniff".to_string()),
        HeaderField(
            "Strict-Transport-Security".to_string(),
            "max-age=31536000 ; includeSubDomains".to_string(),
        ),
        // "Referrer-Policy: no-referrer" would be more strict, but breaks local dev deployment
        // same-origin is still ok from a security perspective
        HeaderField("Referrer-Policy".to_string(), "same-origin".to_string()),
    ]
}

fn iframe_headers(iframe: &StorageConfigIFrame) -> Option<HeaderField> {
    match iframe {
        StorageConfigIFrame::Deny => Some(HeaderField(
            "X-Frame-Options".to_string(),
            "DENY".to_string(),
        )),
        StorageConfigIFrame::SameOrigin => Some(HeaderField(
            "X-Frame-Options".to_string(),
            "SAMEORIGIN".to_string(),
        )),
        StorageConfigIFrame::AllowAny => None,
    }
}

pub fn build_config_headers(
    requested_path: &str,
    StorageConfig {
        headers: config_headers,
        ..
    }: &StorageConfig,
) -> Vec<HeaderField> {
    matching_urls(requested_path, config_headers)
        .iter()
        .flat_map(|(_, headers)| headers.clone())
        .collect()
}