Skip to main content

ordinary_config/http/
csp.rs

1// Copyright (C) 2026 The Ordinary Authors.
2//
3// SPDX-License-Identifier: BSD-3-Clause
4
5use serde::{Deserialize, Serialize};
6
7#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
8#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
9#[derive(Deserialize, Serialize, Debug, Clone, Default)]
10pub struct HttpCsp {
11    /// defaults to `'self'` (`default-src ` does not need to be included)
12    #[serde(skip_serializing_if = "Option::is_none")]
13    #[serde(default)]
14    pub default_src: Option<String>,
15
16    /// defaults to unset unless inline hashes are included,
17    /// in which case the directive will start with `'self'` (`script-src ` does not need to be included).
18    #[serde(skip_serializing_if = "Option::is_none")]
19    #[serde(default)]
20    pub script_src: Option<String>,
21
22    /// defaults to unset unless inline hashes are included,
23    /// in which case the directive will start with `'self'` (`style-src ` does not need to be included).
24    #[serde(skip_serializing_if = "Option::is_none")]
25    #[serde(default)]
26    pub style_src: Option<String>,
27
28    /// defaults to unset.
29    ///
30    /// (`font-src ` does not need to be included).
31    #[serde(skip_serializing_if = "Option::is_none")]
32    #[serde(default)]
33    pub font_src: Option<String>,
34
35    /// defaults to unset.
36    ///
37    /// (`img-src ` does not need to be included).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    #[serde(default)]
40    pub img_src: Option<String>,
41
42    /// defaults to unset.
43    ///
44    /// (`frame-src ` does not need to be included).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    #[serde(default)]
47    pub frame_src: Option<String>,
48
49    /// defaults to `true`.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[serde(default)]
52    pub include_inline_hashes: Option<bool>,
53}
54
55impl HttpCsp {
56    #[must_use]
57    #[allow(clippy::too_many_lines)]
58    pub fn build_string(
59        &self,
60        base: &Self,
61        inline_style_hashes: Option<Vec<String>>,
62        inline_script_hashes: Option<Vec<String>>,
63        secure: bool,
64        has_wasm: bool,
65    ) -> String {
66        let mut out = String::new();
67
68        let include_inline_hashes = self
69            .include_inline_hashes
70            .unwrap_or(base.include_inline_hashes.unwrap_or(true));
71
72        let default_src = self
73            .default_src
74            .clone()
75            .unwrap_or(base.default_src.clone().unwrap_or("'self'".to_string()));
76
77        if !default_src.is_empty() {
78            out.push_str("default-src ");
79            out.push_str(default_src.as_str());
80            out.push_str("; ");
81        }
82
83        let mut script_src = self
84            .script_src
85            .clone()
86            .unwrap_or(base.script_src.clone().unwrap_or_default());
87
88        if include_inline_hashes
89            && let Some(script_hashes) = inline_script_hashes
90            && !script_hashes.is_empty()
91        {
92            if script_src.is_empty() {
93                script_src.push_str("'self'");
94                if has_wasm {
95                    script_src.push_str(" 'wasm-unsafe-eval'");
96                }
97            }
98
99            for hash in script_hashes {
100                script_src.push_str(" '");
101                script_src.push_str(hash.as_str());
102                script_src.push('\'');
103            }
104        }
105
106        if !script_src.is_empty() {
107            out.push_str("script-src ");
108            out.push_str(script_src.as_str());
109            out.push_str("; ");
110        }
111
112        let mut style_src = self
113            .style_src
114            .clone()
115            .unwrap_or(base.style_src.clone().unwrap_or_default());
116
117        if include_inline_hashes
118            && let Some(style_hashes) = inline_style_hashes
119            && !style_hashes.is_empty()
120        {
121            if style_src.is_empty() {
122                style_src.push_str("'self'");
123            }
124
125            for hash in style_hashes {
126                style_src.push_str(" '");
127                style_src.push_str(hash.as_str());
128                style_src.push('\'');
129            }
130        }
131
132        if !style_src.is_empty() {
133            out.push_str("style-src ");
134            out.push_str(style_src.as_str());
135            out.push_str("; ");
136        }
137
138        let font_src = self
139            .font_src
140            .clone()
141            .unwrap_or(base.font_src.clone().unwrap_or_default());
142        if !font_src.is_empty() {
143            out.push_str("font-src ");
144            out.push_str(font_src.as_str());
145            out.push_str("; ");
146        }
147
148        let img_src = self
149            .img_src
150            .clone()
151            .unwrap_or(base.img_src.clone().unwrap_or_default());
152        if !img_src.is_empty() {
153            out.push_str("img-src ");
154            out.push_str(img_src.as_str());
155            out.push_str("; ");
156        }
157
158        let frame_src = self
159            .frame_src
160            .clone()
161            .unwrap_or(base.frame_src.clone().unwrap_or_default());
162        if !frame_src.is_empty() {
163            out.push_str("frame-src ");
164            out.push_str(frame_src.as_str());
165            out.push_str("; ");
166        }
167
168        if secure {
169            out.push_str("upgrade-insecure-requests; ");
170        }
171
172        out.push_str("report-to csp");
173
174        out = out.trim().to_string();
175        out
176    }
177}