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}