termivar_scanner/http_evidence/
probe.rs1use std::{collections::BTreeMap, fmt};
2
3use reqwest::{
4 header::{HeaderName, HeaderValue},
5 Method, Url,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::DecisionExecutionRequest;
10
11use super::{
12 policy::{forbidden_request_header, validate_http_url},
13 HttpEvidenceError,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19#[non_exhaustive]
20pub enum HttpProbeMethod {
21 Get,
23 Head,
25 Options,
27}
28
29impl HttpProbeMethod {
30 pub(super) fn as_reqwest(self) -> Method {
31 match self {
32 Self::Get => Method::GET,
33 Self::Head => Method::HEAD,
34 Self::Options => Method::OPTIONS,
35 }
36 }
37
38 pub const fn as_str(self) -> &'static str {
40 match self {
41 Self::Get => "GET",
42 Self::Head => "HEAD",
43 Self::Options => "OPTIONS",
44 }
45 }
46}
47
48#[derive(Clone, PartialEq, Eq)]
50pub struct HttpProbe {
51 url: Url,
52 method: HttpProbeMethod,
53 headers: BTreeMap<String, String>,
54}
55
56impl fmt::Debug for HttpProbe {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 formatter
59 .debug_struct("HttpProbe")
60 .field("url", &"<redacted>")
61 .field("method", &self.method)
62 .field("header_names", &self.headers.keys().collect::<Vec<_>>())
63 .field("header_values", &"<redacted>")
64 .finish()
65 }
66}
67
68impl HttpProbe {
69 pub fn new(url: Url, method: HttpProbeMethod) -> Result<Self, HttpEvidenceError> {
71 validate_http_url(&url)?;
72 Ok(Self {
73 url,
74 method,
75 headers: BTreeMap::new(),
76 })
77 }
78
79 pub fn with_header(
86 mut self,
87 name: impl Into<String>,
88 value: impl Into<String>,
89 ) -> Result<Self, HttpEvidenceError> {
90 let name = name.into();
91 let parsed_name = HeaderName::from_bytes(name.as_bytes())
92 .map_err(|_| HttpEvidenceError::InvalidHeaderName { name: name.clone() })?;
93 if forbidden_request_header(&parsed_name) {
94 return Err(HttpEvidenceError::ForbiddenRequestHeader {
95 name: parsed_name.as_str().to_owned(),
96 });
97 }
98 let value = value.into();
99 HeaderValue::from_str(&value).map_err(|_| HttpEvidenceError::InvalidHeaderValue {
100 name: parsed_name.as_str().to_owned(),
101 })?;
102 self.headers.insert(parsed_name.as_str().to_owned(), value);
103 Ok(self)
104 }
105
106 pub fn url(&self) -> &Url {
108 &self.url
109 }
110
111 pub(super) fn url_mut(&mut self) -> &mut Url {
112 &mut self.url
113 }
114
115 pub fn method(&self) -> HttpProbeMethod {
117 self.method
118 }
119
120 pub fn headers(&self) -> &BTreeMap<String, String> {
122 &self.headers
123 }
124}
125
126pub trait HttpProbeProvider: Send + Sync {
128 fn probe_for(&self, request: &DecisionExecutionRequest)
130 -> Result<HttpProbe, HttpEvidenceError>;
131}
132
133impl<F> HttpProbeProvider for F
134where
135 F: Fn(&DecisionExecutionRequest) -> Result<HttpProbe, HttpEvidenceError> + Send + Sync,
136{
137 fn probe_for(
138 &self,
139 request: &DecisionExecutionRequest,
140 ) -> Result<HttpProbe, HttpEvidenceError> {
141 self(request)
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct SubjectHttpProbeProvider {
148 method: HttpProbeMethod,
149}
150
151impl SubjectHttpProbeProvider {
152 pub const fn new(method: HttpProbeMethod) -> Self {
154 Self { method }
155 }
156}
157
158impl Default for SubjectHttpProbeProvider {
159 fn default() -> Self {
160 Self::new(HttpProbeMethod::Get)
161 }
162}
163
164impl HttpProbeProvider for SubjectHttpProbeProvider {
165 fn probe_for(
166 &self,
167 request: &DecisionExecutionRequest,
168 ) -> Result<HttpProbe, HttpEvidenceError> {
169 let subject = request.case().subject().as_str();
170 let raw_url = subject.strip_prefix("endpoint:").ok_or_else(|| {
171 HttpEvidenceError::InvalidEndpointSubject {
172 subject: subject.to_owned(),
173 }
174 })?;
175 let url = Url::parse(raw_url).map_err(|source| HttpEvidenceError::InvalidUrl {
176 value: raw_url.to_owned(),
177 source,
178 })?;
179 HttpProbe::new(url, self.method)
180 }
181}