Skip to main content

coil_runtime/plan/
execution.rs

1use super::*;
2
3impl RuntimePlan {
4    pub fn execute_request(
5        &self,
6        request: RequestInput,
7        cookie_secret: &[u8],
8        csrf_secret: &[u8],
9    ) -> Result<RequestExecution, RequestExecutionError> {
10        let matched = self
11            .http
12            .resolve_match(&self.config, request.method, &request.host, &request.path)
13            .ok_or_else(|| RequestExecutionError::RouteNotFound {
14                method: request.method,
15                host: request.host.clone(),
16                path: request.path.clone(),
17            })?;
18
19        let trace = RequestTraceContext {
20            request_id: request.request_id.clone().unwrap_or_else(|| {
21                format!(
22                    "req:{:?}:{}:{}",
23                    request.method, request.host, matched.resolved.route_name
24                )
25            }),
26            transport_scheme: request
27                .forwarded_proto
28                .clone()
29                .unwrap_or_else(|| request.scheme.clone()),
30        };
31
32        let mut resolved_from_cookie = false;
33        let session_id = if let Some(session_id) = request.session_id.clone() {
34            Some(session_id)
35        } else if let Some(cookie) = request.session_cookie.as_deref() {
36            resolved_from_cookie = true;
37            Some(self.verify_session_cookie(cookie_secret, cookie)?)
38        } else {
39            None
40        };
41
42        let session = SessionContext {
43            session_id: session_id.clone(),
44            resolved_from_cookie,
45        };
46        let principal = PrincipalContext {
47            principal_id: request.principal_id.clone(),
48            principal_kind: request.principal_kind,
49            granted_capabilities: request.granted_capabilities.clone(),
50        };
51        let csrf_token = request.csrf_token.clone().or_else(|| {
52            request
53                .form_field(self.browser.csrf.field_name.as_str())
54                .map(str::to_string)
55        });
56
57        self.enforce_maintenance_mode(&matched.route, request.method, &request)?;
58        self.enforce_feature_flags(&matched.route, &matched.resolved)?;
59        self.enforce_route_auth(&matched.resolved, &session, &principal)?;
60        self.enforce_browser_policy(
61            &matched.route,
62            &matched.resolved,
63            request.method,
64            request.csrf_action.as_deref(),
65            csrf_token.as_deref(),
66            &session,
67            csrf_secret,
68        )?;
69        let response = self
70            .handlers
71            .get(&matched.resolved.route_name)
72            .cloned()
73            .map(|handler| handler.response)
74            .ok_or_else(|| RequestExecutionError::HandlerNotRegistered {
75                route: matched.resolved.route_name.clone(),
76            })?;
77        let cache = cache_disposition_for_route(request.method, &matched.resolved.auth, &session);
78        let cache_plan = build_execution_cache_plan(
79            self,
80            &request,
81            &matched.route,
82            &matched.resolved,
83            &session,
84            &principal,
85            cache,
86        )?;
87
88        Ok(RequestExecution {
89            customer_app: self.config.app.name.clone(),
90            site_id: matched.resolved.site_id.clone(),
91            site_display_name: matched
92                .resolved
93                .site_id
94                .as_deref()
95                .and_then(|site_id| self.config.site_for_id(site_id))
96                .map(|site| site.display_name.clone()),
97            brand_name: matched
98                .resolved
99                .site_id
100                .as_deref()
101                .and_then(|site_id| self.config.site_for_id(site_id))
102                .and_then(|site| site.brand_name.clone()),
103            method: request.method,
104            host: request.host,
105            path: request.path,
106            headers: request.headers,
107            query_params: request.query_params,
108            form_fields: request.form_fields,
109            content_type: request.content_type,
110            raw_body: request.raw_body,
111            route: matched.resolved.clone(),
112            route_area: matched.route.area,
113            locale: matched.resolved.locale.clone().unwrap_or_else(|| {
114                matched
115                    .resolved
116                    .site_id
117                    .as_deref()
118                    .and_then(|site_id| self.config.site_for_id(site_id))
119                    .map(|site| site.default_locale.clone())
120                    .unwrap_or_else(|| self.config.i18n.default_locale.clone())
121            }),
122            trace,
123            session: session.clone(),
124            principal,
125            cache,
126            cache_plan,
127            middleware: self.http.middleware.clone(),
128            response,
129            flash_messages: Vec::new(),
130            response_cookies: Vec::new(),
131        })
132    }
133
134    pub fn execute_browser_request(
135        &self,
136        browser: &mut BrowserHost,
137        mut request: RequestInput,
138        cookie_secret: &[u8],
139        csrf_secret: &[u8],
140        now: BrowserInstant,
141    ) -> Result<RequestExecution, RequestExecutionError> {
142        let resolved = browser
143            .resolve_request(&request, cookie_secret, now)
144            .map_err(RequestExecutionError::from_browser_error)?;
145
146        request.session_id = resolved.session.session_id.clone();
147        request.session_cookie = None;
148        request.flash_cookie = None;
149
150        if request.principal_id.is_none() {
151            request.principal_id = resolved.principal_id.clone();
152            if request.principal_id.is_some() {
153                request.principal_kind = RequestPrincipalKind::User;
154            }
155        }
156
157        let mut execution = self.execute_request(request, cookie_secret, csrf_secret)?;
158        execution.session = resolved.session;
159        if execution.principal.principal_id.is_none() {
160            execution.principal.principal_id = resolved.principal_id;
161            if execution.principal.principal_id.is_some() {
162                execution.principal.principal_kind = RequestPrincipalKind::User;
163            }
164        }
165        execution.flash_messages = resolved.flash_messages;
166        execution.response_cookies = resolved.response_cookies;
167        Ok(execution)
168    }
169
170    fn verify_session_cookie(
171        &self,
172        cookie_secret: &[u8],
173        cookie: &str,
174    ) -> Result<String, RequestExecutionError> {
175        self.browser
176            .sessions
177            .session_cookie
178            .unprotect(cookie_secret, cookie)
179            .map_err(|error| RequestExecutionError::InvalidSessionCookie(error.to_string()))
180    }
181
182    fn enforce_route_auth(
183        &self,
184        route: &ResolvedRoute,
185        session: &SessionContext,
186        principal: &PrincipalContext,
187    ) -> Result<(), RequestExecutionError> {
188        match route.auth {
189            RouteAuthGate::Public => Ok(()),
190            RouteAuthGate::Session => {
191                if session.session_id.is_some() {
192                    Ok(())
193                } else {
194                    Err(RequestExecutionError::SessionRequired {
195                        route: route.route_name.clone(),
196                    })
197                }
198            }
199            RouteAuthGate::Capability(capability) => {
200                if session.session_id.is_none() {
201                    return Err(RequestExecutionError::SessionRequired {
202                        route: route.route_name.clone(),
203                    });
204                }
205
206                if principal.granted_capabilities.contains(&capability) {
207                    Ok(())
208                } else {
209                    Err(RequestExecutionError::CapabilityRequired {
210                        route: route.route_name.clone(),
211                        capability,
212                    })
213                }
214            }
215        }
216    }
217
218    fn enforce_feature_flags(
219        &self,
220        route: &RouteDefinition,
221        resolved: &ResolvedRoute,
222    ) -> Result<(), RequestExecutionError> {
223        let Some(feature_flag) = route.feature_flag.as_deref() else {
224            return Ok(());
225        };
226
227        let Some(feature_flag_id) = FeatureFlagId::new(feature_flag.to_string()).ok() else {
228            return Err(RequestExecutionError::FeatureFlagDisabled {
229                route: route.name.clone(),
230                feature_flag: feature_flag.to_string(),
231            });
232        };
233        let context = FeatureFlagContext {
234            environment: self.config.app.environment,
235            customer_app: CustomerAppId::new(self.config.app.name.clone()).ok(),
236            site: resolved
237                .site_id
238                .as_deref()
239                .and_then(|site| SiteId::new(site.to_string()).ok()),
240            brand: resolved
241                .site_id
242                .as_deref()
243                .and_then(|site_id| self.config.site_for_id(site_id))
244                .and_then(|site| site.brand_name.as_deref())
245                .and_then(|brand| BrandId::new(brand.to_string()).ok()),
246            cohorts: BTreeSet::new(),
247        };
248
249        match self.observability.flags.get(&feature_flag_id) {
250            Some(flag) if flag.enabled_for(&context) => Ok(()),
251            _ => Err(RequestExecutionError::FeatureFlagDisabled {
252                route: route.name.clone(),
253                feature_flag: feature_flag.to_string(),
254            }),
255        }
256    }
257
258    fn enforce_maintenance_mode(
259        &self,
260        route: &RouteDefinition,
261        method: HttpMethod,
262        request: &RequestInput,
263    ) -> Result<(), RequestExecutionError> {
264        let customer_app = CustomerAppId::new(self.config.app.name.clone()).ok();
265        let blocked = self.observability.maintenance.blocks_request(
266            customer_app.as_ref(),
267            method.is_state_changing(),
268            request.maintenance_bypass_token.as_deref(),
269        );
270
271        if blocked {
272            Err(RequestExecutionError::MaintenanceMode {
273                route: route.name.clone(),
274            })
275        } else {
276            Ok(())
277        }
278    }
279
280    fn enforce_browser_policy(
281        &self,
282        route: &RouteDefinition,
283        resolved: &ResolvedRoute,
284        method: HttpMethod,
285        csrf_action: Option<&str>,
286        csrf_token: Option<&str>,
287        session: &SessionContext,
288        csrf_secret: &[u8],
289    ) -> Result<(), RequestExecutionError> {
290        let requires_csrf = method.is_state_changing()
291            && route.area != RouteArea::Api
292            && self.browser.csrf.enabled
293            && !matches!(resolved.auth, RouteAuthGate::Public);
294
295        if !requires_csrf {
296            return Ok(());
297        }
298
299        let session_id = session.session_id.as_deref().ok_or_else(|| {
300            RequestExecutionError::MissingSessionForCsrf {
301                route: resolved.route_name.clone(),
302            }
303        })?;
304        let token = csrf_token.ok_or_else(|| RequestExecutionError::MissingCsrfToken {
305            route: resolved.route_name.clone(),
306        })?;
307        let action = csrf_action.unwrap_or(&resolved.route_name);
308        let valid = self
309            .browser
310            .csrf
311            .verify_token(csrf_secret, session_id, action, token)
312            .map_err(|_| RequestExecutionError::InvalidCsrfToken {
313                route: resolved.route_name.clone(),
314            })?;
315
316        if valid {
317            Ok(())
318        } else {
319            Err(RequestExecutionError::InvalidCsrfToken {
320                route: resolved.route_name.clone(),
321            })
322        }
323    }
324}