Skip to main content

coil_runtime/http/
execution.rs

1use super::*;
2use crate::{FlashMessage, RuntimeBrowserError};
3use coil_cache::{CacheModelError, CachePlan};
4use std::collections::{BTreeMap, HashSet};
5use thiserror::Error;
6
7pub type RequestFieldMap = BTreeMap<String, Vec<String>>;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RequestInput {
11    pub method: HttpMethod,
12    pub host: String,
13    pub path: String,
14    pub headers: BTreeMap<String, String>,
15    pub query_params: RequestFieldMap,
16    pub form_fields: RequestFieldMap,
17    pub content_type: Option<String>,
18    pub raw_body: Vec<u8>,
19    pub scheme: String,
20    pub forwarded_proto: Option<String>,
21    pub request_id: Option<String>,
22    pub session_id: Option<String>,
23    pub session_cookie: Option<String>,
24    pub flash_cookie: Option<String>,
25    pub csrf_token: Option<String>,
26    pub csrf_action: Option<String>,
27    pub maintenance_bypass_token: Option<String>,
28    pub principal_id: Option<String>,
29    pub principal_kind: RequestPrincipalKind,
30    pub granted_capabilities: HashSet<coil_auth::Capability>,
31}
32
33impl RequestInput {
34    pub fn new(
35        method: HttpMethod,
36        host: impl Into<String>,
37        path: impl Into<String>,
38    ) -> Result<Self, RouteBuildError> {
39        Ok(Self {
40            method,
41            host: validate_host(host.into())?,
42            path: validate_route_path(path.into())?,
43            headers: BTreeMap::new(),
44            query_params: RequestFieldMap::new(),
45            form_fields: RequestFieldMap::new(),
46            content_type: None,
47            raw_body: Vec::new(),
48            scheme: "https".to_string(),
49            forwarded_proto: None,
50            request_id: None,
51            session_id: None,
52            session_cookie: None,
53            flash_cookie: None,
54            csrf_token: None,
55            csrf_action: None,
56            maintenance_bypass_token: None,
57            principal_id: None,
58            principal_kind: RequestPrincipalKind::Anonymous,
59            granted_capabilities: HashSet::new(),
60        })
61    }
62
63    pub fn with_scheme(mut self, scheme: impl Into<String>) -> Self {
64        self.scheme = scheme.into();
65        self
66    }
67
68    pub fn with_forwarded_proto(mut self, proto: impl Into<String>) -> Self {
69        self.forwarded_proto = Some(proto.into());
70        self
71    }
72
73    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
74        self.request_id = Some(request_id.into());
75        self
76    }
77
78    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
79        self.session_id = Some(session_id.into());
80        self
81    }
82
83    pub fn with_session_cookie(mut self, session_cookie: impl Into<String>) -> Self {
84        self.session_cookie = Some(session_cookie.into());
85        self
86    }
87
88    pub fn with_flash_cookie(mut self, flash_cookie: impl Into<String>) -> Self {
89        self.flash_cookie = Some(flash_cookie.into());
90        self
91    }
92
93    pub fn with_csrf_token(mut self, csrf_token: impl Into<String>) -> Self {
94        self.csrf_token = Some(csrf_token.into());
95        self
96    }
97
98    pub fn with_csrf_action(mut self, csrf_action: impl Into<String>) -> Self {
99        self.csrf_action = Some(csrf_action.into());
100        self
101    }
102
103    pub fn with_maintenance_bypass_token(mut self, bypass_token: impl Into<String>) -> Self {
104        self.maintenance_bypass_token = Some(bypass_token.into());
105        self
106    }
107
108    pub fn with_principal(mut self, principal_id: impl Into<String>) -> Self {
109        self.principal_id = Some(principal_id.into());
110        self.principal_kind = RequestPrincipalKind::User;
111        self
112    }
113
114    pub fn with_service_account_principal(mut self, principal_id: impl Into<String>) -> Self {
115        self.principal_id = Some(principal_id.into());
116        self.principal_kind = RequestPrincipalKind::ServiceAccount;
117        self
118    }
119
120    pub fn with_query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
121        push_request_field(&mut self.query_params, name.into(), value.into());
122        self
123    }
124
125    pub fn with_form_field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
126        push_request_field(&mut self.form_fields, name.into(), value.into());
127        self
128    }
129
130    pub fn with_query_params(mut self, params: RequestFieldMap) -> Self {
131        for (name, values) in params {
132            for value in values {
133                push_request_field(&mut self.query_params, name.clone(), value);
134            }
135        }
136        self
137    }
138
139    pub fn with_form_fields(mut self, fields: RequestFieldMap) -> Self {
140        for (name, values) in fields {
141            for value in values {
142                push_request_field(&mut self.form_fields, name.clone(), value);
143            }
144        }
145        self
146    }
147
148    pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
149        self.headers = headers;
150        self
151    }
152
153    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
154        self.content_type = Some(content_type.into());
155        self
156    }
157
158    pub fn with_raw_body(mut self, raw_body: Vec<u8>) -> Self {
159        self.raw_body = raw_body;
160        self
161    }
162
163    pub fn query_param(&self, name: &str) -> Option<&str> {
164        self.query_params
165            .get(name)
166            .and_then(|values| values.first().map(String::as_str))
167    }
168
169    pub fn form_field(&self, name: &str) -> Option<&str> {
170        self.form_fields
171            .get(name)
172            .and_then(|values| values.first().map(String::as_str))
173    }
174
175    pub fn grant_capability(mut self, capability: coil_auth::Capability) -> Self {
176        self.granted_capabilities.insert(capability);
177        self
178    }
179}
180
181pub(crate) fn push_request_field(fields: &mut RequestFieldMap, name: String, value: String) {
182    if !request_field_name_is_valid(&name) || !request_field_value_is_valid(&value) {
183        return;
184    }
185
186    fields.entry(name).or_default().push(value);
187}
188
189fn request_field_name_is_valid(value: &str) -> bool {
190    !value.is_empty() && !value.chars().any(|ch| ch.is_control())
191}
192
193fn request_field_value_is_valid(value: &str) -> bool {
194    !value
195        .chars()
196        .any(|ch| ch == '\0' || (ch.is_control() && !matches!(ch, '\n' | '\r' | '\t')))
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum RequestPrincipalKind {
201    Anonymous,
202    User,
203    ServiceAccount,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct RequestTraceContext {
208    pub request_id: String,
209    pub transport_scheme: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct SessionContext {
214    pub session_id: Option<String>,
215    pub resolved_from_cookie: bool,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct PrincipalContext {
220    pub principal_id: Option<String>,
221    pub principal_kind: RequestPrincipalKind,
222    pub granted_capabilities: HashSet<coil_auth::Capability>,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum CacheDisposition {
227    Public,
228    Private,
229    Uncacheable,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct RequestExecution {
234    pub customer_app: String,
235    pub site_id: Option<String>,
236    pub site_display_name: Option<String>,
237    pub brand_name: Option<String>,
238    pub method: HttpMethod,
239    pub host: String,
240    pub path: String,
241    pub headers: BTreeMap<String, String>,
242    pub query_params: RequestFieldMap,
243    pub form_fields: RequestFieldMap,
244    pub content_type: Option<String>,
245    pub raw_body: Vec<u8>,
246    pub route: ResolvedRoute,
247    pub route_area: RouteArea,
248    pub locale: String,
249    pub trace: RequestTraceContext,
250    pub session: SessionContext,
251    pub principal: PrincipalContext,
252    pub cache: CacheDisposition,
253    pub cache_plan: ExecutedCachePlan,
254    pub middleware: Vec<MiddlewareStage>,
255    pub response: HandlerResponse,
256    pub flash_messages: Vec<FlashMessage>,
257    pub response_cookies: Vec<String>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct ExecutedCachePlan {
262    pub plan: CachePlan,
263    pub headers: BTreeMap<String, String>,
264}
265
266#[derive(Debug, Error, PartialEq, Eq)]
267pub enum RequestExecutionError {
268    #[error("no route matches {method:?} {host}{path}")]
269    RouteNotFound {
270        method: HttpMethod,
271        host: String,
272        path: String,
273    },
274    #[error("route `{route}` requires a resolved session")]
275    SessionRequired { route: String },
276    #[error("route `{route}` requires capability `{capability}`")]
277    CapabilityRequired {
278        route: String,
279        capability: coil_auth::Capability,
280    },
281    #[error("route `{route}` requires a CSRF token")]
282    MissingCsrfToken { route: String },
283    #[error("route `{route}` requires a session before CSRF can be validated")]
284    MissingSessionForCsrf { route: String },
285    #[error("route `{route}` supplied an invalid CSRF token")]
286    InvalidCsrfToken { route: String },
287    #[error("session cookie failed validation: {0}")]
288    InvalidSessionCookie(String),
289    #[error("flash cookie failed validation: {0}")]
290    InvalidFlashCookie(String),
291    #[error("session `{session_id}` is not present in the server-side store")]
292    UnknownSession { session_id: String },
293    #[error("session `{session_id}` has expired")]
294    ExpiredSession { session_id: String },
295    #[error("session `{session_id}` has been revoked")]
296    RevokedSession { session_id: String },
297    #[error("route `{route}` is disabled by maintenance mode")]
298    MaintenanceMode { route: String },
299    #[error("route `{route}` is disabled because feature flag `{feature_flag}` is not enabled")]
300    FeatureFlagDisabled { route: String, feature_flag: String },
301    #[error("route `{route}` has no registered handler")]
302    HandlerNotRegistered { route: String },
303    #[error(transparent)]
304    Cache(#[from] CacheModelError),
305}
306
307impl RequestExecutionError {
308    pub(crate) fn from_browser_error(error: RuntimeBrowserError) -> Self {
309        match error {
310            RuntimeBrowserError::InvalidSessionCookie { reason } => {
311                Self::InvalidSessionCookie(reason)
312            }
313            RuntimeBrowserError::InvalidFlashCookie { reason } => Self::InvalidFlashCookie(reason),
314            RuntimeBrowserError::UnknownSession { session_id } => {
315                Self::UnknownSession { session_id }
316            }
317            RuntimeBrowserError::ExpiredSession { session_id } => {
318                Self::ExpiredSession { session_id }
319            }
320            RuntimeBrowserError::RevokedSession { session_id } => {
321                Self::RevokedSession { session_id }
322            }
323            other => Self::InvalidFlashCookie(other.to_string()),
324        }
325    }
326}