1use std::{
51 future::Future,
52 pin::Pin,
53 task::{Context, Poll},
54};
55
56use http::{HeaderValue, Request, Response, StatusCode, header};
57use tower_layer::Layer;
58use tower_service::Service;
59
60pub use tower_http::follow_redirect::{self, FollowRedirect, FollowRedirectLayer};
61
62use crate::{Denial, Origins, Outcome, Unsupported, unsupported};
63
64pub const MAX_REDIRECTS: usize = 20;
68
69type BoxFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
77
78fn same_origin_policy<B, E>()
89-> impl follow_redirect::policy::Policy<B, E> + Clone + Send + Sync + 'static
90where
91 B: Clone,
92{
93 use follow_redirect::policy::{Limited, PolicyExt, SameOrigin, clone_body_fn};
94
95 SameOrigin::new()
96 .and::<_, B, E>(Limited::new(MAX_REDIRECTS))
97 .and::<_, B, E>(clone_body_fn(|body: &B| Some(body.clone())))
98}
99
100pub fn follow_redirects<B, E>()
110-> FollowRedirectLayer<impl follow_redirect::policy::Policy<B, E> + Clone + Send + Sync + 'static>
111where
112 B: Clone,
113{
114 FollowRedirectLayer::with_policy(same_origin_policy::<B, E>()).preserve_extensions(false)
115}
116
117#[derive(Debug, Clone)]
120pub struct CanonicalOriginLayer {
121 origins: Origins,
122}
123
124impl CanonicalOriginLayer {
125 #[must_use]
127 pub const fn new(origins: Origins) -> CanonicalOriginLayer {
128 CanonicalOriginLayer { origins }
129 }
130}
131
132impl<S> Layer<S> for CanonicalOriginLayer {
133 type Service = CanonicalOrigin<S>;
134
135 fn layer(&self, inner: S) -> CanonicalOrigin<S> {
136 CanonicalOrigin {
137 inner,
138 origins: self.origins.clone(),
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct CanonicalOrigin<S> {
146 inner: S,
147 origins: Origins,
148}
149
150impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for CanonicalOrigin<S>
155where
156 S: Service<Request<ReqBody>, Response = Response<ResBody>>,
157 S::Future: Send + 'static,
158 ResBody: From<Vec<u8>> + Send + 'static,
159{
160 type Response = Response<ResBody>;
161 type Error = S::Error;
162 type Future = BoxFuture<Response<ResBody>, S::Error>;
163
164 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
165 self.inner.poll_ready(cx)
166 }
167
168 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
169 match self.origins.accept(request) {
170 Outcome::Serve(request) => {
171 let future = self.inner.call(request.into_inner());
172 Box::pin(future)
173 }
174 Outcome::Deny(denial) => {
175 crate::trace::refused_foreign_origin(&denial);
176 Box::pin(async move { Ok(refused(&denial)) })
177 }
178 }
179 }
180}
181
182#[derive(Debug, Clone, Copy, Default)]
184pub struct RefuseUnsupportedLayer;
185
186impl RefuseUnsupportedLayer {
187 #[must_use]
189 pub const fn new() -> RefuseUnsupportedLayer {
190 RefuseUnsupportedLayer
191 }
192}
193
194impl<S> Layer<S> for RefuseUnsupportedLayer {
195 type Service = RefuseUnsupported<S>;
196
197 fn layer(&self, inner: S) -> RefuseUnsupported<S> {
198 RefuseUnsupported { inner }
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct RefuseUnsupported<S> {
205 inner: S,
206}
207
208impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RefuseUnsupported<S>
209where
210 S: Service<Request<ReqBody>, Response = Response<ResBody>>,
211 S::Future: Send + 'static,
212 ResBody: From<Vec<u8>> + Send + 'static,
213{
214 type Response = Response<ResBody>;
215 type Error = S::Error;
216 type Future = BoxFuture<Response<ResBody>, S::Error>;
217
218 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
219 self.inner.poll_ready(cx)
220 }
221
222 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
223 let future = self.inner.call(request);
224 Box::pin(async move {
225 let response = future.await?;
226 match unsupported(&response) {
227 Some(unsupported) => {
228 crate::trace::refused_unsupported(&unsupported);
229 Ok(unsupported_response(&unsupported))
230 }
231 None => Ok(response),
232 }
233 })
234 }
235}
236
237fn refused<B: From<Vec<u8>>>(denial: &Denial) -> Response<B> {
239 status_only(StatusCode::FORBIDDEN, &denial.to_string())
240}
241
242fn unsupported_response<B: From<Vec<u8>>>(unsupported: &Unsupported) -> Response<B> {
247 status_only(StatusCode::BAD_GATEWAY, &unsupported.to_string())
248}
249
250fn status_only<B: From<Vec<u8>>>(status: StatusCode, reason: &str) -> Response<B> {
251 let body = format!("{status}\n\n{reason}\n");
252 let mut response = Response::new(B::from(body.into_bytes()));
253 *response.status_mut() = status;
254 response.headers_mut().insert(
255 header::CONTENT_TYPE,
256 HeaderValue::from_static("text/plain; charset=utf-8"),
257 );
258 response
259}
260
261#[cfg(test)]
262mod tests {
263 use std::sync::{Arc, Mutex};
264
265 use bytes::Bytes;
266 use http::Method;
267 use http_body_util::{BodyExt, Full};
268 use tower::{ServiceBuilder, ServiceExt, service_fn};
269
270 use super::*;
271 use crate::Platform;
272
273 type ReqBody = Full<Bytes>;
274 type ResBody = Full<Bytes>;
275
276 fn origins() -> Origins {
277 Origins::new("topcoat", Platform::Scheme).expect("`topcoat` is a valid scheme")
278 }
279
280 type Seen = Arc<Mutex<Vec<(Method, String)>>>;
282
283 fn request(method: Method, uri: &str) -> Request<ReqBody> {
284 Request::builder()
285 .method(method)
286 .uri(uri)
287 .body(Full::new(Bytes::from_static(b"body")))
288 .expect("a valid request")
289 }
290
291 async fn body_of(response: Response<ResBody>) -> String {
292 let bytes = BodyExt::collect(response.into_body())
293 .await
294 .expect("a full body never fails")
295 .to_bytes();
296 String::from_utf8_lossy(&bytes).into_owned()
297 }
298
299 fn recording(
302 responses: Vec<Response<ResBody>>,
303 ) -> (
304 Seen,
305 impl Service<
306 Request<ReqBody>,
307 Response = Response<ResBody>,
308 Error = std::convert::Infallible,
309 Future: Send,
310 > + Clone
311 + Send
312 + 'static,
313 ) {
314 let seen: Seen = Arc::new(Mutex::new(Vec::new()));
315 let remaining = Arc::new(Mutex::new(responses));
316 let recorded = Arc::clone(&seen);
317 let service = service_fn(move |request: Request<ReqBody>| {
318 let recorded = Arc::clone(&recorded);
319 let remaining = Arc::clone(&remaining);
320 async move {
321 recorded
322 .lock()
323 .expect("the recorder is not poisoned")
324 .push((request.method().clone(), request.uri().to_string()));
325 let mut remaining = remaining.lock().expect("the queue is not poisoned");
326 let response = if remaining.len() > 1 {
327 remaining.remove(0)
328 } else {
329 let last = remaining.first().cloned();
330 last.unwrap_or_else(|| Response::new(Full::default()))
331 };
332 Ok::<_, std::convert::Infallible>(response)
333 }
334 });
335 (seen, service)
336 }
337
338 fn ok() -> Response<ResBody> {
339 Response::new(Full::new(Bytes::from_static(b"LANDED")))
340 }
341
342 fn redirect(status: StatusCode, location: &'static str) -> Response<ResBody> {
343 let mut response = Response::new(Full::default());
344 *response.status_mut() = status;
345 response
346 .headers_mut()
347 .insert(header::LOCATION, HeaderValue::from_static(location));
348 response
349 }
350
351 async fn serve(
353 responses: Vec<Response<ResBody>>,
354 request: Request<ReqBody>,
355 ) -> (Seen, Response<ResBody>) {
356 let (seen, inner) = recording(responses);
357 let service = ServiceBuilder::new()
358 .layer(CanonicalOriginLayer::new(origins()))
359 .layer(follow_redirects::<ReqBody, std::convert::Infallible>())
360 .layer(RefuseUnsupportedLayer::new())
361 .service(inner);
362 let response = service
363 .oneshot(request)
364 .await
365 .expect("the inner service is infallible");
366 (seen, response)
367 }
368
369 #[tokio::test]
370 async fn the_inner_service_sees_one_canonical_origin() {
371 let (seen, _) = serve(
372 vec![ok()],
373 request(Method::GET, "topcoat://localhost/a?b=c"),
374 )
375 .await;
376 let seen = seen.lock().expect("not poisoned");
377 assert_eq!(seen[0].1, "https://topcoat.localhost/a?b=c");
378 }
379
380 #[tokio::test]
381 async fn a_foreign_authority_is_refused_without_reaching_the_service() {
382 let (seen, response) =
383 serve(vec![ok()], request(Method::GET, "https://evil.example/")).await;
384
385 assert_eq!(response.status(), StatusCode::FORBIDDEN);
386 assert!(
387 seen.lock().expect("not poisoned").is_empty(),
388 "the service ran"
389 );
390 }
391
392 #[tokio::test]
393 async fn a_set_cookie_becomes_a_502_naming_the_cookie() {
394 let mut response = ok();
395 response.headers_mut().insert(
396 header::SET_COOKIE,
397 HeaderValue::from_static("__Host-session=t0ken; Secure"),
398 );
399 let (_, response) =
400 serve(vec![response], request(Method::GET, "topcoat://localhost/")).await;
401
402 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
403 assert!(body_of(response).await.contains("__Host-session"));
404 }
405
406 #[tokio::test]
407 async fn a_cookie_on_a_hop_that_redirects_is_refused() {
408 let mut hop = redirect(StatusCode::SEE_OTHER, "/landed");
409 hop.headers_mut().insert(
410 header::SET_COOKIE,
411 HeaderValue::from_static("__Host-session=t0ken; Secure"),
412 );
413 let (seen, response) = serve(
414 vec![hop, ok()],
415 request(Method::POST, "topcoat://localhost/sign-in"),
416 )
417 .await;
418
419 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
420 assert!(body_of(response).await.contains("__Host-session"));
421 assert_eq!(
422 seen.lock().expect("not poisoned").len(),
423 1,
424 "the redirect was followed past the cookie"
425 );
426 }
427
428 #[cfg(feature = "tracing")]
430 #[derive(Clone, Default)]
431 struct Captured(Arc<Mutex<Vec<u8>>>);
432
433 #[cfg(feature = "tracing")]
434 impl Captured {
435 fn text(&self) -> String {
436 String::from_utf8_lossy(&self.0.lock().expect("not poisoned")).into_owned()
437 }
438 }
439
440 #[cfg(feature = "tracing")]
441 impl std::io::Write for Captured {
442 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
443 self.0.lock().expect("not poisoned").extend_from_slice(buf);
444 Ok(buf.len())
445 }
446
447 fn flush(&mut self) -> std::io::Result<()> {
448 Ok(())
449 }
450 }
451
452 #[cfg(feature = "tracing")]
453 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for Captured {
454 type Writer = Captured;
455
456 fn make_writer(&'a self) -> Captured {
457 self.clone()
458 }
459 }
460
461 #[cfg(feature = "tracing")]
462 #[tokio::test]
463 async fn a_refusal_says_why_where_a_subscriber_can_hear_it() {
464 let captured = Captured::default();
465 let subscriber = tracing_subscriber::fmt()
466 .with_writer(captured.clone())
467 .with_max_level(tracing::Level::DEBUG)
468 .without_time()
469 .finish();
470
471 tracing::subscriber::with_default(subscriber, || {
472 let outcome = origins().accept(request(Method::GET, "https://evil.example/"));
473 let Outcome::Deny(denial) = outcome else {
474 panic!("a foreign authority is always denied");
475 };
476 crate::trace::refused_foreign_origin(&denial);
477 });
478
479 let text = captured.text();
480 assert!(
481 text.contains("evil.example"),
482 "the refusal did not name the origin it refused: {text}"
483 );
484 assert!(
485 text.contains("WARN"),
486 "a request meant for somebody else is not a debug detail: {text}"
487 );
488 }
489
490 #[tokio::test]
493 async fn a_csrf_check_needs_the_rewrite_underneath_it() {
494 use tower_http::csrf::CsrfLayer;
495
496 async fn form_post(origin: &str, host: &str) -> StatusCode {
497 let service = ServiceBuilder::new()
498 .layer(CsrfLayer::new())
499 .service(service_fn(|_: Request<ReqBody>| async {
500 Ok::<_, std::convert::Infallible>(ok())
501 }));
502 let request = Request::builder()
503 .method(Method::POST)
504 .uri("/todos")
505 .header(header::ORIGIN, origin)
506 .header(header::HOST, host)
507 .body(Full::new(Bytes::from_static(b"title=milk")))
508 .expect("a valid request");
509 service
510 .oneshot(request)
511 .await
512 .expect("the inner service is infallible")
513 .status()
514 }
515
516 assert_eq!(
517 form_post("topcoat://localhost", "localhost").await,
518 StatusCode::FORBIDDEN,
519 "a custom scheme is unreadable to an ordinary origin check, so this \
520 is what the application's own form post would get above the rewrite"
521 );
522 assert_eq!(
523 form_post("https://topcoat.localhost", "topcoat.localhost").await,
524 StatusCode::OK,
525 "the same request, rewritten, is the one the check was written for"
526 );
527 assert_eq!(
528 form_post("https://evil.example", "topcoat.localhost").await,
529 StatusCode::FORBIDDEN,
530 "the rewrite must not launder a foreign origin into a passing one"
531 );
532 }
533
534 #[derive(Clone, Copy)]
536 struct ShellMarker;
537
538 #[tokio::test]
542 async fn a_redirected_request_inherits_no_extensions() {
543 let seen: Arc<Mutex<Vec<bool>>> = Arc::new(Mutex::new(Vec::new()));
544 let recorded = Arc::clone(&seen);
545 let hops = Arc::new(Mutex::new(vec![
546 redirect(StatusCode::SEE_OTHER, "/landed"),
547 ok(),
548 ]));
549 let inner = service_fn(move |request: Request<ReqBody>| {
550 let recorded = Arc::clone(&recorded);
551 let hops = Arc::clone(&hops);
552 async move {
553 recorded
554 .lock()
555 .expect("not poisoned")
556 .push(request.extensions().get::<ShellMarker>().is_some());
557 let mut hops = hops.lock().expect("not poisoned");
558 let response = if hops.len() > 1 {
559 hops.remove(0)
560 } else {
561 hops.first().cloned().unwrap_or_else(ok)
562 };
563 Ok::<_, std::convert::Infallible>(response)
564 }
565 });
566 let service = ServiceBuilder::new()
567 .layer(CanonicalOriginLayer::new(origins()))
568 .layer(follow_redirects::<ReqBody, std::convert::Infallible>())
569 .service(inner);
570
571 let mut request = request(Method::POST, "topcoat://localhost/sign-in");
572 request.extensions_mut().insert(ShellMarker);
573 let _ = service.oneshot(request).await.expect("infallible");
574
575 assert_eq!(
576 seen.lock().expect("not poisoned").as_slice(),
577 [true, false],
578 "the shell's own state was replayed onto a request the application asked for"
579 );
580 }
581
582 #[tokio::test]
583 async fn see_other_becomes_a_get_and_drops_the_body() {
584 let (seen, response) = serve(
585 vec![redirect(StatusCode::SEE_OTHER, "/landed"), ok()],
586 request(Method::POST, "topcoat://localhost/todos"),
587 )
588 .await;
589
590 assert_eq!(response.status(), StatusCode::OK);
591 let seen = seen.lock().expect("not poisoned");
592 assert_eq!(seen[0].0, Method::POST);
593 assert_eq!(seen[1].0, Method::GET, "the hop kept the method");
594 assert!(seen[1].1.ends_with("/landed"));
595 }
596
597 #[tokio::test]
598 async fn moved_and_found_rewrite_a_post_and_leave_other_methods_alone() {
599 for status in [StatusCode::MOVED_PERMANENTLY, StatusCode::FOUND] {
600 let (seen, _) = serve(
601 vec![redirect(status, "/landed"), ok()],
602 request(Method::POST, "topcoat://localhost/todos"),
603 )
604 .await;
605 assert_eq!(seen.lock().expect("not poisoned")[1].0, Method::GET);
606
607 let (seen, _) = serve(
608 vec![redirect(status, "/landed"), ok()],
609 request(Method::PUT, "topcoat://localhost/todos"),
610 )
611 .await;
612 assert_eq!(
613 seen.lock().expect("not poisoned")[1].0,
614 Method::PUT,
615 "{status} rewrote a PUT"
616 );
617 }
618 }
619
620 #[tokio::test]
621 async fn temporary_and_permanent_preserve_the_method() {
622 for status in [
623 StatusCode::TEMPORARY_REDIRECT,
624 StatusCode::PERMANENT_REDIRECT,
625 ] {
626 let (seen, _) = serve(
627 vec![redirect(status, "/landed"), ok()],
628 request(Method::POST, "topcoat://localhost/todos"),
629 )
630 .await;
631 assert_eq!(
632 seen.lock().expect("not poisoned")[1].0,
633 Method::POST,
634 "{status} rewrote the method"
635 );
636 }
637 }
638
639 #[tokio::test]
640 async fn a_redirect_off_our_origin_is_handed_over_unfollowed() {
641 for location in [
642 "https://evil.example/x",
643 "//evil.example/x",
644 "https://topcoat.localhost.evil.example/x",
645 ] {
646 let (seen, response) = serve(
647 vec![redirect(StatusCode::SEE_OTHER, location), ok()],
648 request(Method::POST, "topcoat://localhost/todos"),
649 )
650 .await;
651
652 assert_eq!(
653 response.status(),
654 StatusCode::SEE_OTHER,
655 "{location} was followed"
656 );
657 assert_eq!(seen.lock().expect("not poisoned").len(), 1);
658 }
659 }
660
661 #[tokio::test]
662 async fn a_relative_location_resolves_against_the_request() {
663 let (seen, _) = serve(
664 vec![redirect(StatusCode::SEE_OTHER, "done"), ok()],
665 request(Method::POST, "topcoat://localhost/lists/mine"),
666 )
667 .await;
668
669 assert!(
670 seen.lock().expect("not poisoned")[1]
671 .1
672 .ends_with("/lists/done"),
673 "a relative reference did not resolve against the request"
674 );
675 }
676
677 #[tokio::test]
678 async fn a_redirect_loop_stops_at_the_hop_limit() {
679 let (seen, response) = serve(
680 vec![redirect(StatusCode::SEE_OTHER, "/loop")],
681 request(Method::POST, "topcoat://localhost/todos"),
682 )
683 .await;
684
685 assert_eq!(response.status(), StatusCode::SEE_OTHER);
686 assert_eq!(
687 seen.lock().expect("not poisoned").len(),
688 MAX_REDIRECTS + 1,
689 "the hop limit is not what this crate names"
690 );
691 }
692}