Skip to main content

brooks_lib/verifying/cors/
verify.rs

1// brooks, Copyright 2026, Will Hawkins
2//
3// This file is part of brooks.
4
5// This file is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! CORS Verification
19
20use regex::Regex;
21
22use crate::{
23    cdni::{
24        cors::spec::{
25            AccessControlAllowOrigin, AccessControlAllowOriginPattern, CrossoriginPolicy,
26            TypedCrossoriginPolicy,
27        },
28        gmd::spec::TypedGenericMetadata,
29    },
30    verifying::verify::{
31        AnalysisResult, CdniVerificationKey, MetadataAnalysisContext, MetadataVerificationError,
32        Verifier,
33    },
34};
35use std::{
36    error::Error,
37    fmt::{Debug, Display},
38};
39
40#[derive(Debug, Clone, Default)]
41pub struct CrossoriginPolicyVerificationKey {}
42
43#[derive(Debug)]
44pub enum AccessControlAllowOriginVerificationError {
45    BadPattern(Box<dyn Error>),
46}
47
48impl Display for AccessControlAllowOriginVerificationError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            AccessControlAllowOriginVerificationError::BadPattern(error) => {
52                write!(f, "Bad pattern in allow-origin: {error}")
53            }
54        }
55    }
56}
57
58impl Error for AccessControlAllowOriginVerificationError {}
59
60#[derive(Debug)]
61pub enum CrossoriginPolicyVerificationError {
62    AccessControlAllowOriginVerificationError(AccessControlAllowOriginVerificationError),
63}
64
65impl Display for CrossoriginPolicyVerificationError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            CrossoriginPolicyVerificationError::AccessControlAllowOriginVerificationError(
69                access_control_allow_origin_verification_error,
70            ) => write!(f, "{access_control_allow_origin_verification_error}"),
71        }
72    }
73}
74
75impl Error for CrossoriginPolicyVerificationError {}
76
77fn verify_allow_origin(
78    allow_origin: &AccessControlAllowOrigin<()>,
79) -> Result<AccessControlAllowOrigin<CrossoriginPolicyVerificationKey>, MetadataVerificationError> {
80    let mut analyzed_allow_origin: Vec<AccessControlAllowOriginPattern> = vec![];
81    for pattern in &allow_origin.allow_origin {
82        let r = Regex::new(&pattern.pattern).map_err(|e| {
83            MetadataVerificationError::CrossoriginPolicyVerificationError(
84                CrossoriginPolicyVerificationError::AccessControlAllowOriginVerificationError(
85                    AccessControlAllowOriginVerificationError::BadPattern(e.into()),
86                )
87                .into(),
88            )
89        })?;
90        analyzed_allow_origin.push(AccessControlAllowOriginPattern {
91            pattern: pattern.pattern.clone(),
92            aug: Some(r),
93        });
94    }
95
96    Ok(AccessControlAllowOrigin {
97        allow_origin: analyzed_allow_origin,
98        wildcard_return: allow_origin.wildcard_return,
99        aug: CrossoriginPolicyVerificationKey::default(),
100    })
101}
102
103#[derive(Debug)]
104pub struct CrossoriginPolicyMetadataAnalyzer {}
105
106fn analyze_crossoriginpolicy(
107    typed_cop: &TypedCrossoriginPolicy<()>,
108    input: MetadataAnalysisContext,
109) -> AnalysisResult<MetadataAnalysisContext, MetadataVerificationError> {
110    let verified_allow_origin = verify_allow_origin(&typed_cop.value.allow_origin)?;
111
112    let res = TypedCrossoriginPolicy::<CrossoriginPolicyVerificationKey> {
113        tpe: TypedCrossoriginPolicy::<()>::typed_cdni_metadata_name(),
114        value: CrossoriginPolicy::<CrossoriginPolicyVerificationKey> {
115            allow_origin: verified_allow_origin,
116            preflight_only: typed_cop.value.preflight_only,
117            aug: CrossoriginPolicyVerificationKey::default(),
118        },
119    };
120
121    Ok((input, CdniVerificationKey::CrossoriginPolicy(Box::new(res))))
122}
123
124impl Verifier<MetadataAnalysisContext, MetadataVerificationError>
125    for CrossoriginPolicyMetadataAnalyzer
126{
127    fn analyze(
128        &self,
129        v: &TypedGenericMetadata<()>,
130        input: MetadataAnalysisContext,
131    ) -> AnalysisResult<MetadataAnalysisContext, MetadataVerificationError> {
132        let typed_cop = serde_json::from_value::<TypedCrossoriginPolicy<()>>(
133            serde_json::to_value(v)
134                .map_err(|e| MetadataVerificationError::JsonError(e.to_string()))?,
135        )
136        .map_err(|e| MetadataVerificationError::JsonError(e.to_string()))?;
137
138        // Verify the allow-list.
139        analyze_crossoriginpolicy(&typed_cop, input)
140    }
141
142    fn type_name(&self, candidate: &str) -> bool {
143        candidate == TypedCrossoriginPolicy::<()>::typed_cdni_metadata_name()
144    }
145}
146
147#[cfg(test)]
148mod test {
149    use std::{assert_matches, path::Path};
150
151    use crate::{
152        cdni::cors::spec::{CrossoriginPolicy, TypedCrossoriginPolicy},
153        environment::scope::Scopes,
154        mel::types::Type,
155        tests::read_test_file,
156        verifying::{
157            cors::verify::analyze_crossoriginpolicy,
158            verify::{CdniVerificationKey, MetadataAnalysisContext},
159        },
160    };
161
162    #[test]
163    fn test_verify_draft_figure1() {
164        let json = read_test_file(Path::new("./src/cdni/tests/cors/figure1.json"));
165        let cop_metadata = serde_json::from_str::<TypedCrossoriginPolicy<()>>(&json)
166            .expect("Could not parse JSON test file");
167        assert_eq!(
168            cop_metadata.tpe,
169            TypedCrossoriginPolicy::<()>::typed_cdni_metadata_name()
170        );
171
172        let (_, result) = analyze_crossoriginpolicy(
173            &cop_metadata,
174            MetadataAnalysisContext {
175                scopes: Scopes::<Type>::default(),
176            },
177        )
178        .expect("Could not verify valid MI.CrossoriginPolicy metadata");
179
180        let result = match result {
181            CdniVerificationKey::CrossoriginPolicy(r) => r,
182            _ => todo!(),
183        };
184
185        assert_matches!(
186            *result,
187            TypedCrossoriginPolicy {
188                tpe: _,
189                value: CrossoriginPolicy {
190                    allow_origin: ao,
191                    preflight_only: None,
192                    aug: _,
193                }
194            } if ao.allow_origin.len() == 1
195        );
196    }
197
198    #[test]
199    fn test_verify_bad_pattern() {
200        let json = read_test_file(Path::new("./src/cdni/tests/cors/bad_pattern.json"));
201        let cop_metadata = serde_json::from_str::<TypedCrossoriginPolicy<()>>(&json)
202            .expect("Could not parse JSON test file");
203        assert_eq!(
204            cop_metadata.tpe,
205            TypedCrossoriginPolicy::<()>::typed_cdni_metadata_name()
206        );
207
208        let result = analyze_crossoriginpolicy(
209            &cop_metadata,
210            MetadataAnalysisContext {
211                scopes: Scopes::<Type>::default(),
212            },
213        );
214
215        assert_matches!(result, Err(_));
216    }
217}