Skip to main content

http_client/
main.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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    // Use TCP keepalive to notice stuck connections.
40    // After 60s with no data received send a probe every 15s.
41    options.keepalive_idle = Some(std::time::Duration::from_secs(60));
42    options.keepalive_interval = Some(std::time::Duration::from_secs(15));
43    // After 8 probes go unacknowledged treat the connection as dead.
44    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                            // Because of the wait above, we shouldn't ever see SHOULD_WAIT here, but to avoid
140                            // brittle-ness, continue and wait again in that case.
141                            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)] // TODO(https://fxbug.dev/42176989)
161    if error.is_parse() {
162        net_http::Error::UnableToParse
163    } else if error.is_user() {
164        //TODO(zmbush): handle this case.
165        net_http::Error::Internal
166    } else if error.is_canceled() {
167        //TODO(zmbush): handle this case.
168        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        //TODO(zmbush): handle this case.
175        net_http::Error::Internal
176    } else if error.is_body_write_aborted() {
177        //TODO(zmbush): handle this case.
178        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                    // We don't care if on_response returns an error since this is the last
302                    // callback.
303                    let _: Result<_, _> = loader_client.on_response(response).await;
304                    Ok(())
305                }
306                Err(error) => {
307                    info!("Received network level error from hyper: {}", error);
308                    // We don't care if on_response returns an error since this is the last
309                    // callback.
310                    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    // Prevent insecure redirect downgrade (https -> http)
368    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
384// A request is considered cross-origin if the scheme or the authority differs
385// between the old and new url.
386fn 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 the connection did not close or receive new messages within the timeout, send it
479    // over to component manager to wait for it on our behalf.
480    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        // Same origin, different path = same origin
567        assert!(!is_cross_origin(&origin, &hyper::Uri::from_static("https://example.com/other")));
568
569        // Same origin, same path with query = same origin
570        assert!(!is_cross_origin(
571            &origin,
572            &hyper::Uri::from_static("https://example.com/path?foo=bar")
573        ));
574
575        // Different host = cross-origin
576        assert!(is_cross_origin(&origin, &hyper::Uri::from_static("https://test.com/path")));
577
578        // Different scheme = cross-origin
579        assert!(is_cross_origin(&origin, &hyper::Uri::from_static("http://example.com/path")));
580
581        // Different port = cross-origin
582        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        // Append will return true and add the header value to the list of
611        // values for COOKIE.
612        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        // Headers must be preserved on same-origin redirect
650        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        // Authorization must be stripped on cross-origin redirect
663        assert!(!headers.contains_key(&auth_key));
664        // Content-Type must be preserved
665        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        // Same scheme, relative path = Perform redirect
676        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        // Same scheme, different host under example namespace = Perform redirect
683        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        // Insecure redirect downgrade (https -> http) = Block redirect
690        let loc = hyper::header::HeaderValue::from_static("http://example.com/path");
691        assert_eq!(calculate_redirect(&old_url, &loc), None);
692
693        // Insecure to insecure redirect = Perform redirect
694        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        // Insecure redirect downgrade with uppercase scheme (https -> HTTP) = Block redirect
702        let loc = hyper::header::HeaderValue::from_static("HTTP://example.com/path");
703        assert_eq!(calculate_redirect(&old_url, &loc), None);
704
705        // Invalid URL characters = Block redirect
706        let loc = hyper::header::HeaderValue::from_bytes(b"https://\xffinvalid.com").unwrap();
707        assert_eq!(calculate_redirect(&old_url, &loc), None);
708    }
709}