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_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    // Use TCP keepalive to notice stuck connections.
43    // After 60s with no data received send a probe every 15s.
44    options.keepalive_idle = Some(std::time::Duration::from_secs(60));
45    options.keepalive_interval = Some(std::time::Duration::from_secs(15));
46    // After 8 probes go unacknowledged treat the connection as dead.
47    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                            // Because of the wait above, we shouldn't ever see SHOULD_WAIT here, but to avoid
144                            // brittle-ness, continue and wait again in that case.
145                            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                    // We don't care if on_response returns an error since this is the last
311                    // callback.
312                    let _: Result<_, _> = loader_client.on_response(response).await;
313                    Ok(())
314                }
315                Err(error) => {
316                    info!("Received network level error from hyper: {}", error);
317                    // We don't care if on_response returns an error since this is the last
318                    // callback.
319                    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    // Prevent insecure redirect downgrade (https -> http)
378    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
394// A request is considered cross-origin if the scheme or the authority differs
395// between the old and new url.
396fn 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 the connection did not close or receive new messages within the timeout, send it
489    // over to component manager to wait for it on our behalf.
490    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        // Same origin, different path = same origin
580        assert!(!is_cross_origin(&origin, &hyper::Uri::from_static("https://example.com/other")));
581
582        // Same origin, same path with query = same origin
583        assert!(!is_cross_origin(
584            &origin,
585            &hyper::Uri::from_static("https://example.com/path?foo=bar")
586        ));
587
588        // Different host = cross-origin
589        assert!(is_cross_origin(&origin, &hyper::Uri::from_static("https://test.com/path")));
590
591        // Different scheme = cross-origin
592        assert!(is_cross_origin(&origin, &hyper::Uri::from_static("http://example.com/path")));
593
594        // Different port = cross-origin
595        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        // Append will return true and add the header value to the list of
624        // values for COOKIE.
625        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        // Headers must be preserved on same-origin redirect
664        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        // Authorization must be stripped on cross-origin redirect
677        assert!(!headers.contains_key(&auth_key));
678        // Content-Type must be preserved
679        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        // Same scheme, relative path = Perform redirect
690        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        // Same scheme, different host under example namespace = Perform redirect
697        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        // Insecure redirect downgrade (https -> http) = Block redirect
704        let loc = hyper::header::HeaderValue::from_static("http://example.com/path");
705        assert_eq!(calculate_redirect(&old_url, &loc), None);
706
707        // Insecure to insecure redirect = Perform redirect
708        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        // Insecure redirect downgrade with uppercase scheme (https -> HTTP) = Block redirect
716        let loc = hyper::header::HeaderValue::from_static("HTTP://example.com/path");
717        assert_eq!(calculate_redirect(&old_url, &loc), None);
718
719        // Invalid URL characters = Block redirect
720        let loc = hyper::header::HeaderValue::from_bytes(b"https://\xffinvalid.com").unwrap();
721        assert_eq!(calculate_redirect(&old_url, &loc), None);
722    }
723}