1use std::{
8 borrow::Cow,
9 convert::Infallible,
10 future::Future,
11 pin::Pin,
12 sync::Arc,
13 task::{Context, Poll},
14};
15
16use bytes::Bytes;
17use custom_protocol_http::{
18 Origins,
19 tower::{CanonicalOriginLayer, RefuseUnsupportedLayer, follow_redirects},
20};
21use http::{HeaderMap, HeaderValue, Request, Response, StatusCode, header};
22use http_body_util::{BodyExt, Full};
23use topcoat::router::{Body, Router, to_bytes};
24use tower::{Service, ServiceBuilder, ServiceExt};
25
26type ReqBody = Full<Bytes>;
31
32pub(crate) struct Bridge {
34 origins: Origins,
35 router: Arc<Router>,
36 #[cfg(feature = "session")]
40 webviews: Arc<crate::session::Webviews>,
41}
42
43impl Bridge {
44 pub(crate) fn new(
45 origins: Origins,
46 router: Router,
47 #[cfg(feature = "session")] webviews: Arc<crate::session::Webviews>,
48 ) -> Bridge {
49 Bridge {
50 origins,
51 router: Arc::new(router),
52 #[cfg(feature = "session")]
53 webviews,
54 }
55 }
56
57 pub(crate) const fn origins(&self) -> &Origins {
58 &self.origins
59 }
60
61 pub(crate) fn observe_navigation(&self, label: &str, ours: bool) {
63 #[cfg(feature = "session")]
64 self.webviews.observe(label, ours);
65 #[cfg(not(feature = "session"))]
66 let _ = (label, ours);
67 }
68
69 #[cfg_attr(
77 feature = "tracing",
78 tracing::instrument(
79 name = "serve",
80 skip_all,
81 fields(
82 webview = label,
83 method = %request.method(),
84 path = request.uri().path(),
85 status = tracing::field::Empty,
86 ),
87 )
88 )]
89 pub(crate) async fn serve(
90 &self,
91 label: &str,
92 request: Request<Vec<u8>>,
93 ) -> Response<Cow<'static, [u8]>> {
94 let (parts, body) = request.into_parts();
95 let request = Request::from_parts(parts, Full::new(Bytes::from(body)));
96
97 let service = ServiceBuilder::new()
100 .layer(CanonicalOriginLayer::new(self.origins.clone()))
101 .layer(follow_redirects::<ReqBody, Infallible>())
102 .layer(RefuseUnsupportedLayer::new())
103 .service(TopcoatService {
104 router: Arc::clone(&self.router),
105 #[cfg(feature = "session")]
106 label: label.to_owned(),
107 });
108
109 #[cfg(not(feature = "session"))]
110 let _ = label;
111
112 match service.oneshot(request).await {
113 Ok(response) => {
114 let response = deliver(response).await;
115 crate::trace::record_status(response.status());
116 response
117 }
118 Err(infallible) => match infallible {},
119 }
120 }
121}
122
123#[derive(Clone)]
141struct TopcoatService {
142 router: Arc<Router>,
143 #[cfg(feature = "session")]
146 label: String,
147}
148
149impl Service<Request<ReqBody>> for TopcoatService {
150 type Response = Response<Body>;
151 type Error = Infallible;
152 type Future = Pin<Box<dyn Future<Output = Result<Response<Body>, Infallible>> + Send>>;
153
154 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
155 Poll::Ready(Ok(()))
156 }
157
158 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
159 let router = Arc::clone(&self.router);
160 #[cfg(feature = "session")]
161 let label = self.label.clone();
162
163 Box::pin(async move {
164 let (parts, body) = request.into_parts();
165 let bytes = body
166 .collect()
167 .await
168 .map(http_body_util::Collected::to_bytes)
169 .unwrap_or_default();
170 let request = Request::from_parts(parts, Body::from(bytes.to_vec()));
171 #[cfg(feature = "session")]
172 let request = crate::session::attach(request, &label);
173 Ok(router.handle(request).await)
174 })
175 }
176}
177
178async fn deliver(response: Response<Body>) -> Response<Cow<'static, [u8]>> {
180 let (mut parts, body) = response.into_parts();
181 let Ok(bytes) = to_bytes(body, usize::MAX).await else {
182 return status_only(
183 StatusCode::INTERNAL_SERVER_ERROR,
184 "the response body could not be read",
185 );
186 };
187 harden(&mut parts.headers);
188 Response::from_parts(parts, Cow::Owned(bytes.to_vec()))
189}
190
191fn harden(headers: &mut HeaderMap) {
204 headers
205 .entry(header::REFERRER_POLICY)
206 .or_insert(HeaderValue::from_static("same-origin"));
207 headers
208 .entry(header::X_CONTENT_TYPE_OPTIONS)
209 .or_insert(HeaderValue::from_static("nosniff"));
210}
211
212pub(crate) fn unavailable() -> Response<Cow<'static, [u8]>> {
215 crate::trace::served_before_ready();
216 status_only(
217 StatusCode::SERVICE_UNAVAILABLE,
218 "the topcoat plugin has not finished starting",
219 )
220}
221
222fn status_only(status: StatusCode, reason: &str) -> Response<Cow<'static, [u8]>> {
223 let body = format!("{status}\n\n{reason}\n");
224 let mut response = Response::new(Cow::Owned(body.into_bytes()));
225 *response.status_mut() = status;
226 response.headers_mut().insert(
227 header::CONTENT_TYPE,
228 HeaderValue::from_static("text/plain; charset=utf-8"),
229 );
230 harden(response.headers_mut());
231 response
232}
233
234#[cfg(test)]
239mod tests {
240 use custom_protocol_http::Platform;
241 use http::{HeaderName, Method};
242 use topcoat::{
243 context::Cx,
244 router::{
245 IntoResponse, Path, Response as RouterResponse, RouteFn, RouteFuture,
246 headers as request_headers,
247 },
248 };
249
250 use super::*;
251
252 fn hello(cx: &Cx, _body: Body) -> RouteFuture<'_> {
253 Box::pin(async move { "hello".into_response(cx) })
254 }
255
256 fn add_todo(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
260 Box::pin(async move {
261 let mut response = RouterResponse::new(Body::empty());
262 *response.status_mut() = StatusCode::SEE_OTHER;
263 response
264 .headers_mut()
265 .insert(header::LOCATION, HeaderValue::from_static("/whoami"));
266 Ok(response)
267 })
268 }
269
270 fn teapot(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
274 Box::pin(async move {
275 let mut response = RouterResponse::new(Body::empty());
276 *response.status_mut() = StatusCode::IM_A_TEAPOT;
277 Ok(response)
278 })
279 }
280
281 fn login(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
284 Box::pin(async move {
285 let mut response = RouterResponse::new(Body::from("logged in"));
286 response.headers_mut().insert(
287 header::SET_COOKIE,
288 HeaderValue::from_static("__Host-session=t0ken; Path=/; Secure; HttpOnly"),
289 );
290 Ok(response)
291 })
292 }
293
294 fn add_todo_then_login(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
297 Box::pin(async move {
298 let mut response = RouterResponse::new(Body::empty());
299 *response.status_mut() = StatusCode::SEE_OTHER;
300 response
301 .headers_mut()
302 .insert(header::LOCATION, HeaderValue::from_static("/login"));
303 Ok(response)
304 })
305 }
306
307 fn sign_in_then_home(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
311 Box::pin(async move {
312 let mut response = RouterResponse::new(Body::empty());
313 *response.status_mut() = StatusCode::SEE_OTHER;
314 let headers = response.headers_mut();
315 headers.insert(header::LOCATION, HeaderValue::from_static("/"));
316 headers.insert(
317 header::SET_COOKIE,
318 HeaderValue::from_static("__Host-session=t0ken; Path=/; Secure; HttpOnly"),
319 );
320 Ok(response)
321 })
322 }
323
324 fn events(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
326 Box::pin(async move {
327 let mut response = RouterResponse::new(Body::from("data: hello\n\n"));
328 response.headers_mut().insert(
329 header::CONTENT_TYPE,
330 HeaderValue::from_static("text/event-stream"),
331 );
332 Ok(response)
333 })
334 }
335
336 fn echo_cookie(cx: &Cx, _body: Body) -> RouteFuture<'_> {
338 Box::pin(async move {
339 let value = request_headers(cx)
340 .get(header::COOKIE)
341 .and_then(|value| value.to_str().ok())
342 .unwrap_or("<none>")
343 .to_owned();
344 value.into_response(cx)
345 })
346 }
347
348 fn echo_host(cx: &Cx, _body: Body) -> RouteFuture<'_> {
349 Box::pin(async move {
350 let headers = request_headers(cx);
351 let host = headers
352 .get(header::HOST)
353 .and_then(|value| value.to_str().ok())
354 .unwrap_or("<none>");
355 let encoding = headers
356 .get(header::ACCEPT_ENCODING)
357 .and_then(|value| value.to_str().ok())
358 .unwrap_or("<none>");
359 format!("{host} {encoding}").into_response(cx)
360 })
361 }
362
363 fn offsite(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
365 Box::pin(async move {
366 let mut response = RouterResponse::new(Body::empty());
367 *response.status_mut() = StatusCode::SEE_OTHER;
368 response.headers_mut().insert(
369 header::LOCATION,
370 HeaderValue::from_static("https://evil.example/"),
371 );
372 Ok(response)
373 })
374 }
375
376 fn bridge() -> Bridge {
377 let router = Router::builder()
378 .route(RouteFn::new(Method::GET, path("/"), hello))
379 .route(RouteFn::new(Method::POST, path("/todos"), add_todo))
380 .route(RouteFn::new(Method::GET, path("/login"), login))
381 .route(RouteFn::new(
382 Method::POST,
383 path("/todos-then-login"),
384 add_todo_then_login,
385 ))
386 .route(RouteFn::new(
387 Method::POST,
388 path("/sign-in-then-home"),
389 sign_in_then_home,
390 ))
391 .route(RouteFn::new(Method::GET, path("/events"), events))
392 .route(RouteFn::new(Method::GET, path("/whoami"), echo_cookie))
393 .route(RouteFn::new(Method::GET, path("/headers"), echo_host))
394 .route(RouteFn::new(Method::POST, path("/offsite"), offsite))
395 .route(RouteFn::new(Method::POST, path("/must-not-run"), teapot))
396 .build();
397 let origins =
398 Origins::new("topcoat", Platform::Scheme).expect("`topcoat` is a valid scheme");
399 Bridge::new(
400 origins,
401 router,
402 #[cfg(feature = "session")]
403 Arc::new(crate::session::Webviews::new()),
404 )
405 }
406
407 fn path(literal: &'static str) -> Cow<'static, Path> {
408 Cow::Borrowed(Path::new(literal))
409 }
410
411 fn request(method: Method, path: &str, headers: &[(HeaderName, &str)]) -> Request<Vec<u8>> {
412 let mut builder = Request::builder()
413 .method(method)
414 .uri(format!("topcoat://localhost{path}"));
415 for (name, value) in headers {
416 builder = builder.header(name, *value);
417 }
418 builder.body(Vec::new()).expect("a valid request")
419 }
420
421 const WEBKIT_POST: &[(HeaderName, &str)] = &[(header::REFERER, "topcoat://localhost/")];
423
424 const UNSEEN: &str = "never-navigated";
428
429 async fn send(bridge: &Bridge, request: Request<Vec<u8>>) -> (StatusCode, String, HeaderMap) {
430 let response = bridge.serve(UNSEEN, request).await;
431 let (parts, body) = response.into_parts();
432 (
433 parts.status,
434 String::from_utf8_lossy(&body).into_owned(),
435 parts.headers,
436 )
437 }
438
439 #[tokio::test]
440 async fn a_page_is_served() {
441 let (status, body, headers) = send(&bridge(), request(Method::GET, "/", &[])).await;
442 assert_eq!(status, StatusCode::OK);
443 assert_eq!(body, "hello");
444 assert_eq!(
445 headers
446 .get(header::REFERRER_POLICY)
447 .and_then(|v| v.to_str().ok()),
448 Some("same-origin"),
449 "our URLs are not withheld from other origins"
450 );
451 }
452
453 #[tokio::test]
454 async fn the_router_sees_one_origin_and_no_accept_encoding() {
455 let (_, body, _) = send(
456 &bridge(),
457 request(
458 Method::GET,
459 "/headers",
460 &[(header::ACCEPT_ENCODING, "gzip, br")],
461 ),
462 )
463 .await;
464 assert_eq!(body, "topcoat.localhost <none>");
465 }
466
467 #[tokio::test]
468 async fn a_mutation_with_nothing_attributing_it_reaches_the_route() {
469 let (status, body, _) = send(&bridge(), request(Method::POST, "/must-not-run", &[])).await;
470 assert_eq!(status, StatusCode::IM_A_TEAPOT, "{body}");
471 }
472
473 #[tokio::test]
474 async fn a_request_for_a_foreign_authority_is_refused() {
475 let outbound = Request::builder()
476 .method(Method::GET)
477 .uri("https://evil.example/")
478 .body(Vec::new())
479 .expect("a valid request");
480 let (status, body, _) = send(&bridge(), outbound).await;
481
482 assert_eq!(status, StatusCode::FORBIDDEN);
483 assert!(body.contains("evil.example"), "{body}");
484 }
485
486 #[tokio::test]
487 async fn no_cookie_header_is_attached_to_anything() {
488 let (_, body, _) = send(&bridge(), request(Method::GET, "/whoami", &[])).await;
489 assert_eq!(body, "<none>");
490 }
491
492 #[tokio::test]
493 async fn post_redirect_get_is_followed_in_process() {
494 let (status, body, _) = send(&bridge(), request(Method::POST, "/todos", WEBKIT_POST)).await;
495
496 assert_eq!(status, StatusCode::OK, "the webview was handed a redirect");
497 assert_eq!(body, "<none>", "the redirect target was not fetched");
498 }
499
500 #[tokio::test]
501 async fn a_redirect_off_our_origin_is_handed_over_unfollowed() {
502 let (status, _, headers) =
503 send(&bridge(), request(Method::POST, "/offsite", WEBKIT_POST)).await;
504 assert_eq!(status, StatusCode::SEE_OTHER);
505 assert_eq!(
506 headers.get(header::LOCATION).and_then(|v| v.to_str().ok()),
507 Some("https://evil.example/")
508 );
509 }
510
511 #[tokio::test]
512 async fn a_set_cookie_is_refused_with_the_cookie_named() {
513 let (status, body, headers) = send(&bridge(), request(Method::GET, "/login", &[])).await;
514
515 assert_eq!(status, StatusCode::BAD_GATEWAY);
516 assert!(body.contains("__Host-session"), "{body}");
517 assert!(
518 headers.get(header::SET_COOKIE).is_none(),
519 "the cookie was passed on to a webview that will not keep it"
520 );
521 }
522
523 #[tokio::test]
524 async fn a_streaming_response_is_refused_with_the_capability_named() {
525 let (status, body, _) = send(&bridge(), request(Method::GET, "/events", &[])).await;
526
527 assert_eq!(status, StatusCode::BAD_GATEWAY);
528 assert!(body.contains("streams"), "{body}");
529 }
530
531 #[tokio::test]
532 async fn a_cookie_on_a_followed_hop_is_refused_not_swallowed() {
533 let (status, body, _) =
534 send(&bridge(), request(Method::POST, "/sign-in-then-home", &[])).await;
535
536 assert_eq!(
537 status,
538 StatusCode::BAD_GATEWAY,
539 "the redirect was followed past a Set-Cookie: {body}"
540 );
541 assert!(body.contains("__Host-session"), "{body}");
542 }
543
544 #[tokio::test]
545 async fn an_unsupported_redirect_hop_is_refused_rather_than_followed() {
546 let (status, body, _) =
547 send(&bridge(), request(Method::POST, "/todos-then-login", &[])).await;
548
549 assert_eq!(status, StatusCode::BAD_GATEWAY);
550 assert!(body.contains("__Host-session"), "{body}");
551 }
552
553 #[tokio::test]
554 async fn an_unknown_path_is_the_routers_own_404() {
555 let (status, _, _) = send(&bridge(), request(Method::GET, "/nope", &[])).await;
556 assert_eq!(status, StatusCode::NOT_FOUND);
557 }
558}