1use anyhow::Context as _;
6use fidl::prelude::*;
7use fidl_fuchsia_net_http as net_http;
8use fidl_fuchsia_pkg_http as fpkg_http;
9use fuchsia_async::{self as fasync, TimeoutExt as _};
10use fuchsia_component::escrow::EscrowOperation;
11use fuchsia_component::server::{Item, ServiceFs, ServiceFsDir};
12use fuchsia_hyper as fhyper;
13use fuchsia_inspect as finspect;
14use futures::StreamExt;
15use futures::prelude::*;
16use http_body_util::{BodyStream, Full};
17use http_client_config::Config;
18use hyper::header::{AUTHORIZATION, COOKIE, HeaderName, PROXY_AUTHORIZATION, WWW_AUTHENTICATE};
19
20pub type Body = Full<hyper::body::Bytes>;
21use log::{debug, error, info, trace};
22use std::str::FromStr as _;
23
24mod pkg;
25mod resuming_get;
26
27static MAX_REDIRECTS: u8 = 10;
28static DEFAULT_DEADLINE_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(15);
29
30fn to_status_line(version: hyper::Version, status: hyper::StatusCode) -> Vec<u8> {
31 match status.canonical_reason() {
32 None => format!("{:?} {}", version, status.as_str()),
33 Some(canonical_reason) => format!("{:?} {} {}", version, status.as_str(), canonical_reason),
34 }
35 .as_bytes()
36 .to_vec()
37}
38
39fn tcp_options() -> fhyper::TcpOptions {
40 let mut options: fhyper::TcpOptions = std::default::Default::default();
41
42 options.keepalive_idle = Some(std::time::Duration::from_secs(60));
45 options.keepalive_interval = Some(std::time::Duration::from_secs(15));
46 options.keepalive_count = Some(8);
48
49 options
50}
51
52struct RedirectInfo {
53 url: Option<hyper::Uri>,
54 referrer: Option<hyper::Uri>,
55 method: hyper::Method,
56}
57
58fn redirect_info(
59 old_uri: &hyper::Uri,
60 method: &hyper::Method,
61 hyper_response: &hyper::Response<impl hyper::body::Body>,
62) -> Option<RedirectInfo> {
63 if hyper_response.status().is_redirection() {
64 Some(RedirectInfo {
65 url: hyper_response
66 .headers()
67 .get(hyper::header::LOCATION)
68 .and_then(|loc| calculate_redirect(old_uri, loc)),
69 referrer: hyper_response
70 .headers()
71 .get(hyper::header::REFERER)
72 .and_then(|loc| calculate_redirect(old_uri, loc)),
73 method: if hyper_response.status() == hyper::StatusCode::SEE_OTHER {
74 hyper::Method::GET
75 } else {
76 method.clone()
77 },
78 })
79 } else {
80 None
81 }
82}
83
84async fn to_success_response(
85 current_url: &hyper::Uri,
86 current_method: &hyper::Method,
87 hyper_response: hyper::Response<hyper::body::Incoming>,
88 scope: vfs::execution_scope::ExecutionScope,
89) -> net_http::Response {
90 let redirect_info = redirect_info(current_url, current_method, &hyper_response);
91 let headers = hyper_response
92 .headers()
93 .iter()
94 .map(|(name, value)| net_http::Header {
95 name: name.as_str().as_bytes().to_vec(),
96 value: value.as_bytes().to_vec(),
97 })
98 .collect();
99
100 let (tx, rx) = zx::Socket::create_stream();
101 let response = net_http::Response {
102 error: None,
103 body: Some(rx),
104 final_url: Some(current_url.to_string()),
105 status_code: Some(hyper_response.status().as_u16() as u32),
106 status_line: Some(to_status_line(hyper_response.version(), hyper_response.status())),
107 headers: Some(headers),
108 redirect: redirect_info.and_then(|info| {
109 info.url.map(|url| net_http::RedirectTarget {
110 method: Some(info.method.to_string()),
111 url: Some(url.to_string()),
112 referrer: info.referrer.map(|r| r.to_string()),
113 ..Default::default()
114 })
115 }),
116 ..Default::default()
117 };
118
119 let _ = scope.spawn(async move {
120 let mut hyper_body = BodyStream::new(hyper_response.into_body());
121 while let Some(frame) = hyper_body.next().await {
122 if let Ok(frame) = frame {
123 if let Ok(chunk) = frame.into_data() {
124 let mut offset: usize = 0;
125 while offset < chunk.len() {
126 let pending = match tx.wait_one(
127 zx::Signals::SOCKET_PEER_CLOSED | zx::Signals::SOCKET_WRITABLE,
128 zx::MonotonicInstant::INFINITE,
129 ).to_result() {
130 Err(status) => {
131 error!("tx.wait() failed - status: {}", status);
132 return;
133 }
134 Ok(pending) => pending,
135 };
136 if pending.contains(zx::Signals::SOCKET_PEER_CLOSED) {
137 info!("tx.wait() saw signal SOCKET_PEER_CLOSED");
138 return;
139 }
140 assert!(pending.contains(zx::Signals::SOCKET_WRITABLE));
141 let written = match tx.write(&chunk[offset..]) {
142 Err(status) => {
143 if status == zx::Status::SHOULD_WAIT {
146 error!("Saw SHOULD_WAIT despite waiting first - expected now? - continuing");
147 continue;
148 }
149 info!("tx.write() failed - status: {}", status);
150 return;
151 }
152 Ok(written) => written,
153 };
154 offset += written;
155 }
156 }
157 }
158 }
159 });
160
161 response
162}
163
164fn to_fidl_error(error: &hyper_util::client::legacy::Error) -> net_http::Error {
165 use std::error::Error as _;
166
167 if error.is_connect() {
168 return net_http::Error::Connect;
169 }
170
171 let mut source = error.source();
172 while let Some(err) = source {
173 if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
174 if hyper_err.is_parse() || hyper_err.is_parse_status() || hyper_err.is_parse_too_large()
175 {
176 return net_http::Error::UnableToParse;
177 }
178
179 if hyper_err.is_closed() || hyper_err.is_canceled() || hyper_err.is_shutdown() {
180 return net_http::Error::ChannelClosed;
181 }
182
183 if hyper_err.is_timeout() {
184 return net_http::Error::DeadlineExceeded;
185 }
186 }
187 source = err.source();
188 }
189
190 net_http::Error::Internal
191}
192
193fn to_error_response(error: net_http::Error) -> net_http::Response {
194 net_http::Response {
195 error: Some(error),
196 body: None,
197 final_url: None,
198 status_code: None,
199 status_line: None,
200 headers: None,
201 redirect: None,
202 ..Default::default()
203 }
204}
205
206struct Loader {
207 method: hyper::Method,
208 url: hyper::Uri,
209 headers: hyper::HeaderMap,
210 body: Vec<u8>,
211 deadline: fasync::MonotonicInstant,
212 scope: vfs::execution_scope::ExecutionScope,
213}
214
215impl Loader {
216 async fn new(
217 req: net_http::Request,
218 scope: vfs::execution_scope::ExecutionScope,
219 ) -> Result<Self, anyhow::Error> {
220 let net_http::Request { method, url, headers, body, deadline, .. } = req;
221 let method = method.as_ref().map(|method| hyper::Method::from_str(method)).transpose()?;
222 let method = method.unwrap_or(hyper::Method::GET);
223 if let Some(url) = url {
224 let url = hyper::Uri::try_from(url)?;
225 let headers = headers
226 .unwrap_or_else(|| vec![])
227 .into_iter()
228 .map(|net_http::Header { name, value }| {
229 let name = hyper::header::HeaderName::from_bytes(&name)?;
230 let value = hyper::header::HeaderValue::from_bytes(&value)?;
231 Ok((name, value))
232 })
233 .collect::<Result<hyper::HeaderMap, anyhow::Error>>()?;
234
235 let body = match body {
236 Some(net_http::Body::Buffer(buffer)) => {
237 let mut bytes = vec![0; buffer.size as usize];
238 buffer.vmo.read(&mut bytes, 0)?;
239 bytes
240 }
241 Some(net_http::Body::Stream(socket)) => {
242 let mut stream = fasync::Socket::from_socket(socket)
243 .into_datagram_stream()
244 .map(|r| r.context("reading from datagram stream"));
245 let mut bytes = Vec::new();
246 while let Some(chunk) = stream.next().await {
247 bytes.extend(chunk?);
248 }
249 bytes
250 }
251 None => Vec::new(),
252 };
253
254 let deadline = deadline
255 .map(|deadline| fasync::MonotonicInstant::from_nanos(deadline))
256 .unwrap_or_else(|| fasync::MonotonicInstant::after(DEFAULT_DEADLINE_DURATION));
257
258 trace!("Starting request {} {}", method, url);
259
260 Ok(Loader { method, url, headers, body, deadline, scope })
261 } else {
262 Err(anyhow::Error::msg("Request missing URL"))
263 }
264 }
265
266 fn build_request(&self) -> hyper::Request<Body> {
267 let Self { method, url, headers, body, deadline: _, scope: _ } = self;
268 let mut request = hyper::Request::new(Full::new(body.clone().into()));
269 *request.method_mut() = method.clone();
270 *request.uri_mut() = url.clone();
271 *request.headers_mut() = headers.clone();
272 request
273 }
274
275 async fn start(mut self, loader_client: net_http::LoaderClientProxy) -> Result<(), zx::Status> {
276 let client = fhyper::new_https_client_from_tcp_options(tcp_options());
277 loop {
278 break match client.request(self.build_request()).await {
279 Ok(hyper_response) => {
280 if let Some((url, method)) =
281 handle_redirect(&self.url, &self.method, &hyper_response, &mut self.headers)
282 {
283 let response = to_success_response(
284 &self.url,
285 &self.method,
286 hyper_response,
287 self.scope.clone(),
288 )
289 .await;
290 self.url = url;
291 self.method = method;
292 trace!("Reporting redirect to OnResponse: {} {}", self.method, self.url);
293 match loader_client.on_response(response).await {
294 Ok(()) => {}
295 Err(e) => {
296 debug!("Not redirecting because: {}", e);
297 break Ok(());
298 }
299 };
300 trace!("Redirect allowed to {} {}", self.method, self.url);
301 continue;
302 }
303 let response = to_success_response(
304 &self.url,
305 &self.method,
306 hyper_response,
307 self.scope.clone(),
308 )
309 .await;
310 let _: Result<_, _> = loader_client.on_response(response).await;
313 Ok(())
314 }
315 Err(error) => {
316 info!("Received network level error from hyper: {}", error);
317 let _: Result<_, _> =
320 loader_client.on_response(to_error_response(to_fidl_error(&error))).await;
321 Ok(())
322 }
323 };
324 }
325 }
326
327 async fn fetch(
328 mut self,
329 ) -> Result<(hyper::Response<hyper::body::Incoming>, hyper::Uri, hyper::Method), net_http::Error>
330 {
331 let deadline = self.deadline;
332 if deadline < fasync::MonotonicInstant::now() {
333 return Err(net_http::Error::DeadlineExceeded);
334 }
335 let client = fhyper::new_https_client_from_tcp_options(tcp_options());
336
337 async move {
338 let mut redirects = 0;
339 loop {
340 break match client.request(self.build_request()).await {
341 Ok(hyper_response) => {
342 if redirects != MAX_REDIRECTS {
343 if let Some((url, method)) = handle_redirect(
344 &self.url,
345 &self.method,
346 &hyper_response,
347 &mut self.headers,
348 ) {
349 self.url = url;
350 self.method = method;
351 trace!("Redirecting to {} {}", self.method, self.url);
352 redirects += 1;
353 continue;
354 }
355 }
356 Ok((hyper_response, self.url, self.method))
357 }
358 Err(e) => {
359 info!("Received network level error from hyper: {}", e);
360 Err(to_fidl_error(&e))
361 }
362 };
363 }
364 }
365 .on_timeout(deadline, || Err(net_http::Error::DeadlineExceeded))
366 .await
367 }
368}
369
370fn calculate_redirect(
371 old_url: &hyper::Uri,
372 location: &hyper::header::HeaderValue,
373) -> Option<hyper::Uri> {
374 let old_parts = old_url.clone().into_parts();
375 let mut new_parts = hyper::Uri::try_from(location.as_bytes()).ok()?.into_parts();
376
377 if old_parts.scheme.as_ref().map(|s| s.as_str()) == Some("https")
379 && new_parts.scheme.as_ref().map(|s| s.as_str()) == Some("http")
380 {
381 error!("Not following insecure redirect downgrade");
382 return None;
383 }
384
385 if new_parts.scheme.is_none() {
386 new_parts.scheme = old_parts.scheme;
387 }
388 if new_parts.authority.is_none() {
389 new_parts.authority = old_parts.authority;
390 }
391 Some(hyper::Uri::from_parts(new_parts).ok()?)
392}
393
394fn is_cross_origin(old_url: &hyper::Uri, new_url: &hyper::Uri) -> bool {
397 old_url.scheme() != new_url.scheme() || old_url.authority() != new_url.authority()
398}
399
400fn sensitive_headers() -> [HeaderName; 5] {
401 [
402 AUTHORIZATION,
403 COOKIE,
404 HeaderName::from_static("cookie2"),
405 PROXY_AUTHORIZATION,
406 WWW_AUTHENTICATE,
407 ]
408}
409
410fn strip_sensitive_headers(headers: &mut hyper::HeaderMap) {
411 for header in sensitive_headers() {
412 let _ = headers.remove(header);
413 }
414}
415
416fn handle_redirect(
417 old_url: &hyper::Uri,
418 method: &hyper::Method,
419 hyper_response: &hyper::Response<impl hyper::body::Body>,
420 headers: &mut hyper::HeaderMap,
421) -> Option<(hyper::Uri, hyper::Method)> {
422 let redirect = redirect_info(old_url, method, hyper_response)?;
423 let url = redirect.url?;
424 if is_cross_origin(old_url, &url) {
425 strip_sensitive_headers(headers);
426 }
427 Some((url, redirect.method))
428}
429
430async fn loader_server(
431 stream: net_http::LoaderRequestStream,
432 idle_timeout: fasync::MonotonicDuration,
433) -> Result<(), anyhow::Error> {
434 let background_tasks = vfs::execution_scope::ExecutionScope::new();
435 let (stream, unbind_if_stalled) = detect_stall::until_stalled(stream, idle_timeout);
436
437 stream
438 .err_into::<anyhow::Error>()
439 .try_for_each_concurrent(None, |message| {
440 let scope = background_tasks.clone();
441 async move {
442 match message {
443 net_http::LoaderRequest::Fetch { request, responder } => {
444 debug!(
445 "Fetch request received (url: {}): {:?}",
446 request
447 .url
448 .as_ref()
449 .and_then(|url| Some(url.as_str()))
450 .unwrap_or_default(),
451 request
452 );
453 let result = Loader::new(request, scope.clone()).await?.fetch().await;
454 responder.send(match result {
455 Ok((hyper_response, final_url, final_method)) => {
456 to_success_response(
457 &final_url,
458 &final_method,
459 hyper_response,
460 scope.clone(),
461 )
462 .await
463 }
464 Err(error) => to_error_response(error),
465 })?;
466 }
467 net_http::LoaderRequest::Start { request, client, control_handle } => {
468 debug!(
469 "Start request received (url: {}): {:?}",
470 request
471 .url
472 .as_ref()
473 .and_then(|url| Some(url.as_str()))
474 .unwrap_or_default(),
475 request
476 );
477 Loader::new(request, scope).await?.start(client.into_proxy()).await?;
478 control_handle.shutdown();
479 }
480 }
481 Ok(())
482 }
483 })
484 .await?;
485
486 background_tasks.wait().await;
487
488 if let Ok(Some(server_end)) = unbind_if_stalled.await {
491 fuchsia_component::client::connect_channel_to_protocol_at::<net_http::LoaderMarker>(
492 server_end.into(),
493 "/escrow",
494 )?;
495 }
496
497 Ok(())
498}
499
500enum HttpServices {
501 Loader(net_http::LoaderRequestStream),
502 PkgClient(fpkg_http::ClientRequestStream),
503}
504
505#[fuchsia::main]
506pub async fn main() -> Result<(), anyhow::Error> {
507 log::info!("http-client starting");
508 fuchsia_trace_provider::trace_provider_create_with_fdio();
509 let inspector = finspect::Inspector::default();
510 let pkg_http_node = inspector.root().create_child("pkg-http");
511 let pkg_http_connections_node = pkg_http_node.create_child("connections");
512 let pkg_http_connection_count = std::sync::atomic::AtomicU64::new(0);
513 let _inspect_server_task =
514 inspect_runtime::publish(&inspector, inspect_runtime::PublishOptions::default());
515
516 let escrow_operation = EscrowOperation::new();
517 escrow_operation.watch_for_stop().expect("Failed to prep escrow operation");
518
519 let config = Config::take_from_startup_handle();
520 let tcp_receive_buffer_size = (config.tcp_receive_buffer_size_bytes > 0)
521 .then_some(config.tcp_receive_buffer_size_bytes.try_into().unwrap());
522 let idle_timeout = if config.stop_on_idle_timeout_millis >= 0 {
523 fasync::MonotonicDuration::from_millis(config.stop_on_idle_timeout_millis)
524 } else {
525 fasync::MonotonicDuration::INFINITE
526 };
527
528 let mut fs = ServiceFs::new();
529 let _: &mut ServiceFsDir<'_, _> = fs
530 .take_and_serve_directory_handle()?
531 .dir("svc")
532 .add_fidl_service(HttpServices::Loader)
533 .add_fidl_service(HttpServices::PkgClient);
534
535 let outgoing_dir_task = async move {
536 fs.until_stalled(idle_timeout)
537 .for_each_concurrent(None, |item| async {
538 match item {
539 Item::Request(services, _active_guard) => match services {
540 HttpServices::Loader(stream) => loader_server(stream, idle_timeout)
541 .await
542 .unwrap_or_else(|e: anyhow::Error| error!("{:?}", e)),
543 HttpServices::PkgClient(stream) => pkg::serve_client_request_stream(
544 stream,
545 idle_timeout,
546 tcp_receive_buffer_size,
547 pkg_http_connections_node.create_child(
548 pkg_http_connection_count
549 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
550 .to_string(),
551 ),
552 )
553 .await
554 .unwrap_or_else(|e: anyhow::Error| error!("{e:#}")),
555 },
556 Item::Stalled(outgoing_directory) => {
557 escrow_operation
558 .run(outgoing_directory.into())
559 .expect("failed to run escrow operation");
560 }
561 }
562 })
563 .await;
564 };
565 outgoing_dir_task.await;
566
567 Ok(())
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use hyper::header::{CONTENT_TYPE, HeaderMap, HeaderValue, LOCATION};
574
575 #[test]
576 fn test_is_cross_origin() {
577 let origin = hyper::Uri::from_static("https://example.com/path");
578
579 assert!(!is_cross_origin(&origin, &hyper::Uri::from_static("https://example.com/other")));
581
582 assert!(!is_cross_origin(
584 &origin,
585 &hyper::Uri::from_static("https://example.com/path?foo=bar")
586 ));
587
588 assert!(is_cross_origin(&origin, &hyper::Uri::from_static("https://test.com/path")));
590
591 assert!(is_cross_origin(&origin, &hyper::Uri::from_static("http://example.com/path")));
593
594 assert!(is_cross_origin(
596 &origin,
597 &hyper::Uri::from_static("https://example.com:8080/path")
598 ));
599 }
600
601 #[test]
602 fn test_strip_sensitive_headers() {
603 let mut headers = HeaderMap::new();
604 let (content_label, content_value) =
605 (CONTENT_TYPE, HeaderValue::from_static("application/json"));
606 assert!(!headers.append(&content_label, content_value.clone()));
607
608 for header in sensitive_headers() {
609 assert!(!headers.append(header, HeaderValue::from_static("value")));
610 }
611
612 strip_sensitive_headers(&mut headers);
613
614 let mut expected_headers = HeaderMap::new();
615 assert!(!expected_headers.append(content_label, content_value));
616 assert_eq!(headers, expected_headers);
617 }
618
619 #[test]
620 fn test_strip_sensitive_headers_multiple_values() {
621 let mut headers = HeaderMap::new();
622 assert!(!headers.append(COOKIE, HeaderValue::from_static("session1=123"),));
623 assert!(headers.append(COOKIE, HeaderValue::from_static("session2=456"),));
626
627 strip_sensitive_headers(&mut headers);
628
629 assert!(!headers.contains_key(COOKIE));
630 }
631
632 fn run_redirect_test(
633 redirect_url: &'static str,
634 initial_headers: &[(hyper::header::HeaderName, &'static str)],
635 expected_url: &'static str,
636 ) -> hyper::HeaderMap {
637 let old_url = hyper::Uri::from_static("https://example.com/path");
638 let method = hyper::Method::GET;
639
640 let mut response =
641 hyper::Response::new(http_body_util::Full::<hyper::body::Bytes>::default());
642 *response.status_mut() = hyper::StatusCode::MOVED_PERMANENTLY;
643 assert!(!response.headers_mut().append(LOCATION, HeaderValue::from_static(redirect_url),));
644
645 let mut headers = HeaderMap::new();
646 for (name, val) in initial_headers {
647 assert!(!headers.append(name, HeaderValue::from_static(val)));
648 }
649
650 let result = handle_redirect(&old_url, &method, &response, &mut headers);
651 assert_eq!(result, Some((hyper::Uri::from_static(expected_url), hyper::Method::GET)));
652 headers
653 }
654
655 #[test]
656 fn test_handle_redirect_same_origin() {
657 let (auth_key, auth_val) = (AUTHORIZATION, "Bearer token");
658 let headers = run_redirect_test(
659 "/new-path",
660 &[(auth_key.clone(), auth_val)],
661 "https://example.com/new-path",
662 );
663 assert_eq!(headers.get(&auth_key), Some(&HeaderValue::from_static(auth_val)));
665 }
666
667 #[test]
668 fn test_handle_redirect_cross_origin() {
669 let (auth_key, auth_val) = (AUTHORIZATION, "Bearer token");
670 let (content_type_key, content_type_val) = (CONTENT_TYPE, "application/json");
671 let headers = run_redirect_test(
672 "https://other.com/new-path",
673 &[(auth_key.clone(), auth_val), (content_type_key.clone(), content_type_val)],
674 "https://other.com/new-path",
675 );
676 assert!(!headers.contains_key(&auth_key));
678 assert_eq!(
680 headers.get(&content_type_key),
681 Some(&HeaderValue::from_static(content_type_val))
682 );
683 }
684
685 #[test]
686 fn test_calculate_redirect() {
687 let old_url = hyper::Uri::from_static("https://example.com/path");
688
689 let loc = hyper::header::HeaderValue::from_static("/new-path");
691 assert_eq!(
692 calculate_redirect(&old_url, &loc),
693 Some(hyper::Uri::from_static("https://example.com/new-path"))
694 );
695
696 let loc = hyper::header::HeaderValue::from_static("https://other.example.com/path");
698 assert_eq!(
699 calculate_redirect(&old_url, &loc),
700 Some(hyper::Uri::from_static("https://other.example.com/path"))
701 );
702
703 let loc = hyper::header::HeaderValue::from_static("http://example.com/path");
705 assert_eq!(calculate_redirect(&old_url, &loc), None);
706
707 let old_url_http = hyper::Uri::from_static("http://example.com/path");
709 let loc = hyper::header::HeaderValue::from_static("http://other.example.com/path");
710 assert_eq!(
711 calculate_redirect(&old_url_http, &loc),
712 Some(hyper::Uri::from_static("http://other.example.com/path"))
713 );
714
715 let loc = hyper::header::HeaderValue::from_static("HTTP://example.com/path");
717 assert_eq!(calculate_redirect(&old_url, &loc), None);
718
719 let loc = hyper::header::HeaderValue::from_bytes(b"https://\xffinvalid.com").unwrap();
721 assert_eq!(calculate_redirect(&old_url, &loc), None);
722 }
723}