Skip to main content

codetether_browser/browser/offline/
explain_cors.rs

1//! Send a CORS preflight (OPTIONS) and classify whether the cross-origin call would be allowed.
2
3use anyhow::Result;
4use reqwest::blocking::Client;
5use std::collections::BTreeMap;
6
7use super::explain_cors_analyse::header_reasons;
8
9#[derive(Debug, serde::Serialize)]
10pub struct CorsExplanation {
11    pub allowed: bool,
12    pub target: String,
13    pub origin: String,
14    pub method: String,
15    pub preflight_status: u16,
16    pub reasons: Vec<String>,
17    pub response_headers: BTreeMap<String, String>,
18}
19
20pub fn run(url: &str, origin: &str, method: &str) -> Result<String> {
21    let client = Client::builder().build()?;
22    let resp = client
23        .request(reqwest::Method::OPTIONS, url)
24        .header("origin", origin)
25        .header("access-control-request-method", method)
26        .send()?;
27    let preflight_status = resp.status().as_u16();
28    let headers: BTreeMap<String, String> = resp
29        .headers()
30        .iter()
31        .map(|(k, v)| {
32            (
33                k.as_str().to_ascii_lowercase(),
34                String::from_utf8_lossy(v.as_bytes()).to_string(),
35            )
36        })
37        .collect();
38    let mut reasons = Vec::new();
39    if !(200..300).contains(&preflight_status) {
40        reasons.push(format!(
41            "preflight returned non-2xx status {preflight_status}"
42        ));
43    }
44    reasons.extend(header_reasons(&headers, origin, method));
45    Ok(serde_json::to_string_pretty(&CorsExplanation {
46        allowed: reasons.is_empty(),
47        target: url.into(),
48        origin: origin.into(),
49        method: method.into(),
50        preflight_status,
51        reasons,
52        response_headers: headers,
53    })?)
54}