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