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