/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;

use allocative::Allocative;
use anyhow::Context;
use buck2_core::is_open_source;
use bytes::Bytes;
use dice::UserComputationData;
use dupe::Dupe;
use futures::stream::BoxStream;
use gazebo::prelude::VecExt;
use http::Method;
use hyper::client::connect::Connect;
use hyper::client::ResponseFuture;
use hyper::Body;
use hyper::Request;
use hyper::Response;
use hyper::StatusCode;
use rustls::Certificate;
use rustls::ClientConfig;
use rustls::PrivateKey;
use rustls::RootCertStore;
use thiserror::Error;

use self::find_certs::find_tls_cert;

pub mod counting_client;
pub mod find_certs;
mod proxy;
mod redirect;
pub mod retries;
mod secure_client;
mod secure_proxied_client;
#[cfg(fbcode_build)]
mod x2p;
use counting_client::CountingHttpClient;
use proxy::http_proxy_from_env;
use proxy::https_proxy_from_env;
use secure_client::SecureHttpClient;
use secure_proxied_client::SecureProxiedClient;

const BUCK2_USER_AGENT: &str = "Buck2";

/// Support following up to 10 redirects, after which a redirected request will
/// error out.
const DEFAULT_MAX_REDIRECTS: usize = 10;

/// General-purpose function to get a regular HTTP client for use throughout the
/// buck2 codebase.
///
/// This should work for internal and OSS use cases.
/// TODO(skarlage): Remove `allow_vpnless` when vpnless becomes default.
pub fn http_client(allow_vpnless: bool) -> anyhow::Result<CountingHttpClient> {
    let http_client = if is_open_source() {
        http_client_for_oss()
    } else if allow_vpnless && supports_vpnless() {
        http_client_for_vpnless()
    } else {
        http_client_for_internal()
    }?;
    Ok(CountingHttpClient::new(http_client))
}

/// Returns a client suitable for OSS usecases. Supports standard Curl-like
/// proxy environment variables: $HTTP_PROXY, $HTTPS_PROXY.
pub fn http_client_for_oss() -> anyhow::Result<Arc<dyn HttpClient>> {
    // Add standard proxy variables if defined.
    // Ignores values that cannot be turned into valid URIs.
    let mut proxies = Vec::new();
    if let Some(proxy) = https_proxy_from_env()? {
        proxies.push(proxy);
    }
    if let Some(proxy) = http_proxy_from_env()? {
        proxies.push(proxy);
    }

    if !proxies.is_empty() {
        Ok(Arc::new(SecureProxiedClient::with_proxies(proxies)?))
    } else {
        let config = tls_config_with_system_roots()?;
        Ok(Arc::new(SecureHttpClient::new(
            config,
            DEFAULT_MAX_REDIRECTS,
        )))
    }
}

/// Returns a client suitable for Meta-internal usecases. Supports standard
/// $THRIFT_TLS_CL_* environment variables.
fn http_client_for_internal() -> anyhow::Result<Arc<dyn HttpClient>> {
    let tls_config = if let Ok(Some(cert_path)) = find_tls_cert() {
        tls_config_with_single_cert(cert_path.clone(), cert_path)?
    } else {
        tls_config_with_system_roots()?
    };

    Ok(Arc::new(SecureHttpClient::new(
        tls_config,
        DEFAULT_MAX_REDIRECTS,
    )))
}

/// Returns a client suitable for making http requests via the VPNless x2pagent
/// proxy running on the local machine. Supports both http proxy server and
/// unix domain socket proxy path.
#[cfg(fbcode_build)]
fn http_client_for_vpnless() -> anyhow::Result<Arc<dyn HttpClient>> {
    if let Some(port) = cpe::x2p::http1_proxy_port() {
        let client = x2p::X2PAgentProxyClient::new(port)?;
        Ok(Arc::new(client))
    } else {
        #[cfg(unix)]
        {
            let proxy_path = cpe::x2p::proxy_url_http1();
            let client = x2p::X2PAgentUnixSocketClient::new(proxy_path)?;
            Ok(Arc::new(client))
        }

        #[cfg(not(unix))]
        anyhow::bail!("VPNless unix domain socket http client not supported in non-unix");
    }
}

#[cfg(not(fbcode_build))]
fn http_client_for_vpnless() -> anyhow::Result<Arc<dyn HttpClient>> {
    anyhow::bail!("VPNless client is not supported for non-internal fbcode builds")
}

/// Dice implementations so we can pass along the HttpClient to various subsystems
/// that need to use it (Materializer, RunActions, etc).
pub trait HasHttpClient {
    fn get_http_client(&self) -> CountingHttpClient;
}

pub trait SetHttpClient {
    fn set_http_client(&mut self, client: CountingHttpClient);
}

impl HasHttpClient for UserComputationData {
    fn get_http_client(&self) -> CountingHttpClient {
        self.data
            .get::<CountingHttpClient>()
            .expect("HttpClient should be set")
            .dupe()
    }
}

impl SetHttpClient for UserComputationData {
    fn set_http_client(&mut self, client: CountingHttpClient) {
        self.data.set(client);
    }
}

/// Whether the machine buck is running on supports vpnless operation.
fn supports_vpnless() -> bool {
    #[cfg(fbcode_build)]
    return cpe::x2p::supports_vpnless();

    #[cfg(not(fbcode_build))]
    return false;
}

/// Load the system root certificates into rustls cert store.
fn load_system_root_certs() -> anyhow::Result<RootCertStore> {
    let mut roots = rustls::RootCertStore::empty();
    let native_certs: Vec<_> = rustls_native_certs::load_native_certs()
        .context("Error loading system root certificates")?
        .into_map(|cert| cert.0);

    // According to [`rustls` documentation](https://docs.rs/rustls/latest/rustls/struct.RootCertStore.html#method.add_parsable_certificates),
    // it's better to only add parseable certs when loading system certs because
    // there are typically many system certs and not all of them can be valid. This
    // is pertinent for e.g. macOS which may have a lot of old certificates that may
    // not parse correctly.
    let (valid, invalid) = roots.add_parsable_certificates(native_certs.as_slice());

    // But make sure we get at least _one_ valid cert, otherwise we legitimately won't be
    // able to make any connections via https.
    anyhow::ensure!(
        valid > 0,
        "Error loading system certs: unable to find any valid system certs"
    );
    tracing::debug!("Loaded {} valid system root certs", valid);
    tracing::debug!("Loaded {} invalid system root certs", invalid);
    Ok(roots)
}

/// Deserialize certificate pair at `cert` and `key` into structures that can
/// be inserted into rustls CertStore.
fn load_cert_pair<P: AsRef<Path>>(
    cert: P,
    key: P,
) -> anyhow::Result<(Vec<Certificate>, PrivateKey)> {
    let cert_file = File::open(cert).context("opening certificate file")?;
    let key_file = File::open(key).context("opening private key file")?;
    let mut cert_reader = BufReader::new(&cert_file);
    let mut key_reader = BufReader::new(&key_file);

    let certs = rustls_pemfile::certs(&mut cert_reader)
        .context("creating PEM from internal certificate and private key")?
        .into_map(Certificate);

    let private_key = rustls_pemfile::pkcs8_private_keys(&mut key_reader)
        .context("reading private key from internal certificate")?
        .pop()
        .ok_or_else(|| {
            anyhow::anyhow!("Expected internal certificate to contain at least one private key")
        })?;
    let key = PrivateKey(private_key);

    Ok((certs, key))
}

fn tls_config_with_system_roots() -> anyhow::Result<ClientConfig> {
    let system_roots = load_system_root_certs()?;
    Ok(ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(system_roots)
        .with_no_client_auth())
}

fn tls_config_with_single_cert<P: AsRef<Path>>(
    cert_path: P,
    key_path: P,
) -> anyhow::Result<ClientConfig> {
    let system_roots = load_system_root_certs()?;
    let (cert, key) = load_cert_pair(cert_path, key_path)?;
    ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(system_roots)
        .with_single_cert(cert, key)
        .context("Error creating TLS config with cert and key path")
}

fn http_error_label(status: StatusCode) -> &'static str {
    if status.is_server_error() {
        "Server"
    } else if status.is_client_error() {
        "Client"
    } else {
        "Unknown"
    }
}

#[derive(Debug, Error)]
pub enum HttpError {
    #[error("HTTP URI Error: URI {uri} is malformed: {source:?}")]
    InvalidUri {
        uri: String,
        #[source]
        source: http::uri::InvalidUri,
    },
    #[error("HTTP: Error building request: {0}")]
    BuildRequest(#[from] http::Error),
    #[error("HTTP: Error sending request: {0}")]
    SendRequest(#[from] hyper::Error),
    #[error("HTTP {} Error ({status}) when querying URI: {uri}. Response text: {text}", http_error_label(*.status))]
    Status {
        status: StatusCode,
        uri: String,
        text: String,
    },
    #[error("HTTP Error: Exceeded max redirects ({max_redirects}) while fetching URI: {uri}. ")]
    TooManyRedirects { uri: String, max_redirects: usize },
    #[error("HTTP: Error mutating request: {0}")]
    MutateRequest(#[from] anyhow::Error),
    #[error("HTTP: Testing client, http methods not supported")]
    Test,
}

/// Trait describe http client that can perform simple HEAD and GET requests.
#[async_trait::async_trait]
pub trait HttpClient: Allocative + Send + Sync {
    /// Send a HEAD request. Assumes no body will be returned. If one is returned, it will be ignored.
    async fn head(&self, uri: &str) -> Result<Response<()>, HttpError> {
        let req = Request::builder()
            .uri(uri)
            .method(Method::HEAD)
            .header(http::header::USER_AGENT, BUCK2_USER_AGENT)
            .body(Bytes::new())
            .map_err(HttpError::BuildRequest)?;
        self.request(req).await.map(|resp| resp.map(|_| ()))
    }

    /// Send a GET request.
    async fn get(&self, uri: &str) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        let req = Request::builder()
            .uri(uri)
            .method(Method::GET)
            .header(http::header::USER_AGENT, BUCK2_USER_AGENT)
            .body(Bytes::new())
            .map_err(HttpError::BuildRequest)?;
        self.request(req).await
    }

    async fn post(
        &self,
        uri: &str,
        body: Bytes,
        headers: Vec<(String, String)>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        let mut req = Request::builder()
            .uri(uri)
            .method(Method::POST)
            .header(http::header::USER_AGENT, BUCK2_USER_AGENT);
        for (name, value) in headers {
            req = req.header(name, value);
        }
        let req = req.body(body).map_err(HttpError::BuildRequest)?;
        self.request(req).await
    }

    async fn put(
        &self,
        uri: &str,
        body: Bytes,
        headers: Vec<(String, String)>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        let mut req = Request::builder()
            .uri(uri)
            .method(Method::PUT)
            .header(http::header::USER_AGENT, BUCK2_USER_AGENT);
        for (name, value) in headers {
            req = req.header(name, value);
        }
        let req = req.body(body).map_err(HttpError::BuildRequest)?;
        self.request(req).await
    }

    /// Send a generic request.
    async fn request(
        &self,
        request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError>;

    /// Whether this client supports vpnless operation. When set, will make requests
    /// to the `vpnless_url` attribute in the `download_file` action rather than the
    /// normal `url` attribute.
    fn supports_vpnless(&self) -> bool {
        // Most clients do not support vpnless.
        false
    }
}

/// Trait wrapper around a hyper::Client because hyper::Client is parameterized by
/// the connector. At runtime, we want to pick different connectors (e.g. HttpsConnector,
/// ProxyConnector<HttpsConnector<..>>, etc); thus wrap the client so we can switch
/// out the concrete type without exposing implementation details to callers.
trait RequestClient: Send + Sync {
    fn request(&self, request: Request<Bytes>) -> ResponseFuture;
}

impl<C> RequestClient for hyper::Client<C>
where
    C: Connect + Clone + Send + Sync + 'static,
{
    fn request(&self, request: Request<Bytes>) -> ResponseFuture {
        self.request(request.map(Body::from))
    }
}

/// Http client used for unit testing; errors on any calls to underlying http methods.
#[derive(Allocative)]
pub struct ClientForTest {}

#[async_trait::async_trait]
impl HttpClient for ClientForTest {
    async fn request(
        &self,
        _request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        Err(HttpError::Test)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::net::IpAddr;
use std::str::FromStr;

use anyhow::Context;
use http::uri::InvalidUri;
use http::uri::PathAndQuery;
use http::uri::Scheme;
use http::Uri;
use hyper_proxy::Intercept;
use hyper_proxy::Proxy;
use ipnetwork::IpNetwork;

/// Lookup environment variable and return string value. Checks first for uppercase
/// and falls back to lowercase if unset.
fn env_to_string(env: &'static str) -> anyhow::Result<Option<String>> {
    std::env::var_os(env)
        .or_else(|| std::env::var_os(env.to_lowercase()))
        .map(|s| s.into_string())
        .transpose()
        .map_err(|original| anyhow::anyhow!("Invalid utf8 string: '{:?}'", original))
}

fn noproxy_from_env(scheme: Scheme) -> anyhow::Result<Option<NoProxy>> {
    Ok(env_to_string("NO_PROXY")?.map(|no_proxy| NoProxy::new(scheme, no_proxy)))
}

/// Returns a hyper_proxy::Proxy struct that proxies connections to the uri at
/// $HTTPS_PROXY (or $https_proxy if the former is unset). Respects $NO_PROXY.
pub(super) fn https_proxy_from_env() -> anyhow::Result<Option<Proxy>> {
    if let Some(https_proxy) = env_to_string("HTTPS_PROXY")? {
        let uri: DefaultSchemeUri = https_proxy
            .parse()
            .with_context(|| format!("Invalid HTTPS_PROXY uri: {}", https_proxy))?;
        if let Some(no_proxy) = noproxy_from_env(Scheme::HTTPS)? {
            Ok(Some(Proxy::new(
                no_proxy.into_proxy_intercept(),
                uri.into(),
            )))
        } else {
            Ok(Some(Proxy::new(Intercept::Https, uri.into())))
        }
    } else {
        Ok(None)
    }
}

/// Returns a hyper_proxy::Proxy struct that proxies connections to the uri at
/// $HTTP_PROXY (or $http_proxy if the former is unset). Respects $NO_PROXY.
pub(super) fn http_proxy_from_env() -> anyhow::Result<Option<Proxy>> {
    if let Some(http_proxy) = env_to_string("HTTP_PROXY")? {
        let uri: DefaultSchemeUri = http_proxy
            .parse()
            .with_context(|| format!("Invalid HTTP_PROXY uri: {}", http_proxy))?;
        if let Some(no_proxy) = noproxy_from_env(Scheme::HTTP)? {
            Ok(Some(Proxy::new(
                no_proxy.into_proxy_intercept(),
                uri.into(),
            )))
        } else {
            Ok(Some(Proxy::new(Intercept::Http, uri.into())))
        }
    } else {
        Ok(None)
    }
}

/// A wrapped Uri that handles inserting a default scheme (http) if one is not present.
///
/// See https://everything.curl.dev/usingcurl/proxies/type for more information about
/// how curl treats default schemes for e.g. proxy env vars.
pub(super) struct DefaultSchemeUri(pub(super) Uri);

impl FromStr for DefaultSchemeUri {
    type Err = InvalidUri;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<Uri>().map(Self)
    }
}

impl From<DefaultSchemeUri> for Uri {
    fn from(default_scheme_uri: DefaultSchemeUri) -> Self {
        let mut parts = default_scheme_uri.0.into_parts();
        if parts.scheme.is_none() {
            parts.scheme = Some(Scheme::HTTP);
        }
        if parts.path_and_query.is_none() {
            parts.path_and_query = Some(PathAndQuery::from_static("/"));
        }
        Uri::from_parts(parts).expect("Got invalid uri from formerly valid uri")
    }
}

#[derive(Debug)]
struct Domain(String);

impl Domain {
    /// Returns whether this domain "matches" candidate according to Curl's rules
    /// for NO_PROXY.
    ///
    /// See https://github.com/curl/curl/issues/1208 for a bit of discussion about
    /// some of the particulars of subdomain matching.
    fn is_match<S: AsRef<str>>(&self, candidate: S) -> bool {
        let candidate = candidate.as_ref();
        // * unambiguously matches all domains.
        self.0 == "*"
            // Exact match
            || self.0 == candidate
            // .<domain> matches all subdomains, look for exact match
            || self.0.trim_start_matches('.') == candidate
            // Candidate suffixed by domain, only match if candidate is a subdomain of domain
            // Ex: domain=".facebook.com" matches "images.facebook.com" but not "www.thefacebook.com"
            || candidate.trim_end_matches(self.0.as_str().trim_start_matches('.')).ends_with('.')
    }
}

/// Wrapper for the parsed version of Curl's "no proxy" format from the standard
/// NO_PROXY / no_proxy environment variables.
///
/// Uses hyper-proxy's Intercept::Custom to drive matching logic for whether Uris
/// should be proxied or not.
///
/// Incorporates a scheme to proxy connections as well. Connections matching this
/// (scheme, no_proxy_spec) pair will *not* be proxied.
///
/// Matching logic derived from reqwest::NoProxy (e.g. [here](https://github.com/seanmonstar/reqwest/blob/master/src/proxy.rs#L467)).
pub(super) struct NoProxy {
    addresses: Vec<IpAddr>,
    networks: Vec<IpNetwork>,
    domains: Vec<Domain>,
    proxy_scheme: Scheme,
}

impl NoProxy {
    /// Constructs a new NoProxy struct. Connections made to a host matching the
    /// no_proxy spec with a scheme of `proxy_scheme` WILL NOT be proxied (e.g.
    /// requests to these hosts with `proxy_scheme` will go directly to the dest).
    pub(super) fn new<S: AsRef<str>>(proxy_scheme: Scheme, s: S) -> Self {
        let mut addresses = Vec::new();
        let mut networks = Vec::new();
        let mut domains = Vec::new();
        let s = s.as_ref();
        for entity in s.split(',').map(str::trim) {
            if let Ok(network) = entity.parse::<IpNetwork>() {
                networks.push(network);
            } else if let Ok(address) = entity.parse::<IpAddr>() {
                addresses.push(address);
            } else {
                domains.push(Domain(entity.to_owned()));
            }
        }

        Self {
            addresses,
            networks,
            domains,
            proxy_scheme,
        }
    }

    /// Returns whether `host` matches any of the NO_PROXY entities - i.e. that we
    /// should *not* proxy requests to this host.
    fn should_bypass_proxy_for_host<S: AsRef<str>>(&self, host: S) -> bool {
        let host = host.as_ref();
        if let Ok(host_address) = host.parse::<IpAddr>() {
            self.addresses
                .iter()
                .any(|address| host_address == *address)
                || self
                    .networks
                    .iter()
                    .any(|network| network.contains(host_address))
        } else {
            self.domains.iter().any(|domain| domain.is_match(host))
        }
    }

    /// Converts this NoProxy spec into a hyper_proxy::Intercept::Custom closure
    /// so it can be used to build a new hyper_proxy::Proxy.
    ///
    /// Note: There's a tricky bit of logic below. We explicitly *negate* the return
    /// condition of the closure because of the way hyper_proxy::Intercept::Custom's
    /// closure works; if it returns `true`, the connection is proxied.
    ///
    /// For NoProxy, we want to *negate* this logic - if a (scheme, host) pair
    /// match our NoProxy instance, we _don't want to proxy_. Therefore we negate
    /// the return conditions below.
    ///
    /// Some examples to clarify:
    ///
    /// NO_PROXY=".facebook.com" for HTTPS
    ///     does not proxy https://www.facebook.com
    ///     does not proxy https://images.facebook.com
    ///     does proxy https://www.thefacebook.com
    ///     does proxy http://www.thefacebook.com
    ///
    /// NO_PROXY="192.168.0.1" for HTTP
    ///     does not proxy http://192.168.0.1
    ///     does proxy https://192.168.0.1
    ///     does proxy http://192.168.0.2
    pub(super) fn into_proxy_intercept(self) -> Intercept {
        let should_proxy = move |scheme: Option<&str>, host: Option<&str>, _port: Option<u16>| {
            // IPv6 addresses are wrapped in [ ] so remove those for equality checks.
            let host = host.map(|h| h.trim_start_matches('[').trim_end_matches(']'));
            let should_bypass_proxy =
                host.map_or(false, |host| self.should_bypass_proxy_for_host(host));

            // Negation happens here - true means we're going to proxy the connection.
            !should_bypass_proxy
                && self.proxy_scheme.as_str() == scheme.unwrap_or(Scheme::HTTP.as_str())
        };
        should_proxy.into()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn uri(s: &'static str) -> Uri {
        s.parse().unwrap()
    }

    #[test]
    fn test_domain_match() {
        let d = Domain(".facebook.com".to_owned());
        assert!(d.is_match("www.facebook.com"));
        assert!(!d.is_match("boofacebook.com"));

        let d = Domain("facebook.com".to_owned());
        assert!(d.is_match("facebook.com"));
        assert!(d.is_match("www.facebook.com"));

        let d = Domain("photos.facebook.com".to_owned());
        assert!(!d.is_match("facebook.com"));
        assert!(d.is_match("jpg.photos.facebook.com"));

        let d = Domain("*".to_owned());
        assert!(d.is_match("www.facebook.com"));
        assert!(d.is_match("facebook.com"));
    }

    #[test]
    fn test_noproxy_empty_string_does_not_match() {
        let noproxy = NoProxy::new(Scheme::HTTP, "");
        assert!(!noproxy.should_bypass_proxy_for_host("facebook.com"));
    }

    #[test]
    fn test_noproxy_matches_ip_address() {
        let noproxy = NoProxy::new(Scheme::HTTP, "192.168.0.1");
        assert!(noproxy.should_bypass_proxy_for_host("192.168.0.1"));
    }

    #[test]
    fn test_noproxy_matches_ip_network() {
        let noproxy = NoProxy::new(Scheme::HTTP, "192.168.0.0/16");
        assert!(noproxy.should_bypass_proxy_for_host("192.168.0.1"));
    }

    #[test]
    fn test_noproxy_matches_subdomain() {
        let noproxy = NoProxy::new(Scheme::HTTP, ".facebook.com");
        assert!(noproxy.should_bypass_proxy_for_host("images.facebook.com"));
    }

    #[test]
    fn test_noproxy_matches_multiple() {
        let noproxy = NoProxy::new(Scheme::HTTP, ".facebook.com, 192.168.0.0/24, 28.0.0.1");
        assert!(noproxy.should_bypass_proxy_for_host("images.facebook.com"));
        assert!(noproxy.should_bypass_proxy_for_host("192.168.0.1"));
        assert!(!noproxy.should_bypass_proxy_for_host("28.0.0.2"));
    }

    #[test]
    fn test_noproxy_intercept_does_not_proxy_for_ip_addr_match() {
        let noproxy = NoProxy::new(Scheme::HTTPS, "192.168.0.1");
        let intercept = noproxy.into_proxy_intercept();
        // DON'T proxy https connections to 192.168.0.1 because it's an IP match
        assert!(!intercept.matches(&uri("https://192.168.0.1/foo")));
        // DON'T proxy http connections to 192.168.0.1 because it's a different scheme
        assert!(!intercept.matches(&uri("http://192.168.0.1/foo")));
        // DO proxy https connections to 192.168.0.2 because no IP match and schemes match
        assert!(intercept.matches(&uri("https://192.168.0.2/bar")));
    }

    #[test]
    fn test_noproxy_intercept_does_not_proxy_for_ip_net_match() {
        let noproxy = NoProxy::new(Scheme::HTTPS, "192.168.0.0/24");
        let intercept = noproxy.into_proxy_intercept();
        // DON'T proxy https to 192.168.0.1 because IP and scheme match
        assert!(!intercept.matches(&uri("https://192.168.0.1/foo")));
        // DO proxy https to 192.168.1.1 because IP mismatch and scheme match
        assert!(intercept.matches(&uri("https://192.168.1.1/foo")));
        // DON'T proxy http to 192.168.0.1 because scheme mismatch
        assert!(!intercept.matches(&uri("http://192.168.0.1/foo")));
    }

    #[test]
    fn test_noproxy_intercept_does_not_proxy_for_domain_match() {
        let noproxy = NoProxy::new(Scheme::HTTPS, ".facebook.com");
        let intercept = noproxy.into_proxy_intercept();
        // DON'T proxy because scheme matches and is subdomain.
        assert!(!intercept.matches(&uri("https://www.facebook.com/foo/bar")));
        // DO proxy because scheme matches but domain is different.
        assert!(intercept.matches(&uri("https://www.thefacebook.com/foo/bar")));
        // DON'T proxy because scheme mismatch
        assert!(!intercept.matches(&uri("http://www.facebook.com/foo/bar")));
    }

    #[test]
    fn test_noproxy_intercept_does_not_proxy_for_scheme_mismatch() {
        let noproxy = NoProxy::new(Scheme::HTTP, ".facebook.com");
        let intercept = noproxy.into_proxy_intercept();
        assert!(!intercept.matches(&uri("https://www.facebook.com/foo/bar")));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::future::Future;

use anyhow::Context;
use bytes::Bytes;
use http::HeaderMap;
use http::Method;
use http::Uri;
use hyper::Request;
use hyper::Response;
use hyper::StatusCode;

use crate::http::HttpError;

trait UriWithRedirect {
    fn with_redirect(&self, location: &Uri) -> anyhow::Result<Uri>;

    fn is_cross_host(&self, other: &Uri) -> bool;
}

impl UriWithRedirect for Uri {
    /// Converts this Uri into the redirect Uri by combining it with the URI
    /// obtained from the Location header of a response.
    fn with_redirect(&self, location: &Uri) -> anyhow::Result<Uri> {
        let mut redirected = Uri::builder();
        if let Some(scheme) = location.scheme().or_else(|| self.scheme()) {
            redirected = redirected.scheme(scheme.clone());
        };
        if let Some(authority) = location.authority().or_else(|| self.authority()) {
            redirected = redirected.authority(authority.clone());
        }
        if let Some(path_and_query) = location.path_and_query().or_else(|| self.path_and_query()) {
            redirected = redirected.path_and_query(path_and_query.clone());
        }
        redirected.build().context("Building redirected URI")
    }

    /// Returns whether this Uri is the same host as represented by 'other'.
    fn is_cross_host(&self, other: &Uri) -> bool {
        self.host() != other.host() || self.port_u16() != other.port_u16()
    }
}

/// Intermediate type to store aspects of a request we need to mutate during
/// redirect state machine.
/// This is mostly because hyper::Request cannot be cloned.
pub(super) struct PendingRequest {
    method: Method,
    uri: Uri,
    headers: HeaderMap,
    body: Bytes,
}

impl PendingRequest {
    pub(super) fn from_request(request: &Request<Bytes>) -> Self {
        Self {
            method: request.method().clone(),
            uri: request.uri().clone(),
            headers: request.headers().clone(),
            body: request.body().clone(),
        }
    }

    pub(super) fn to_request(&self) -> anyhow::Result<Request<Bytes>> {
        let mut builder = Request::builder()
            .method(self.method.clone())
            .uri(self.uri.clone());
        *builder
            .headers_mut()
            .expect("Request builder should not error here") = self.headers.clone();
        builder
            .body(self.body.clone())
            .context("building redirected request")
    }
}

/// A simple state machine that drives following redirects. Much of this is derived
/// from how [`reqwest` handles redirects](https://docs.rs/reqwest/latest/src/reqwest/redirect.rs.html#1-337)
/// as well as the [`follow-redirects`](https://github.com/srijs/rust-follow-redirects) crate.
/// Unfortunately, the latter is abandoned; until and unless it's maintained by someone
/// (preferably the hyper folks), let's roll our own.
pub(super) struct RedirectEngine<B> {
    processed_redirects: usize,
    max_redirects: usize,
    pending_request: PendingRequest,
    response: Response<B>,
}

impl<B> RedirectEngine<B> {
    pub(super) fn new(
        max_redirects: usize,
        pending_request: PendingRequest,
        response: Response<B>,
    ) -> Self {
        Self {
            processed_redirects: 0,
            max_redirects,
            pending_request,
            response,
        }
    }

    /// Handle any redirects we get in the course of sending the request (up to
    /// self.max_redirects).
    pub(super) async fn handle_redirects<S, F>(
        mut self,
        sender_func: S,
    ) -> Result<Response<B>, HttpError>
    where
        F: Future<Output = Result<Response<B>, HttpError>>,
        S: Fn(Request<Bytes>) -> F,
    {
        let initial_uri = self.pending_request.uri.clone();
        loop {
            if self.processed_redirects > self.max_redirects {
                return Err(HttpError::TooManyRedirects {
                    uri: initial_uri.to_string(),
                    max_redirects: self.max_redirects,
                });
            }
            if !self.should_redirect() {
                break;
            }
            tracing::debug!(
                "http: processing redirect request ({}) for {}",
                self.response.status(),
                self.pending_request.uri,
            );

            if let Some(redirect_request) = self
                .update_and_create_request()
                .map_err(HttpError::MutateRequest)?
            {
                self.response = sender_func(redirect_request).await?;
                self.processed_redirects += 1;
            } else {
                break;
            }
        }

        Ok(self.response)
    }

    /// Whether we should redirect this response.
    fn should_redirect(&self) -> bool {
        let got_redirect_status_code = matches!(
            self.response.status(),
            StatusCode::MOVED_PERMANENTLY
                | StatusCode::FOUND
                | StatusCode::SEE_OTHER
                | StatusCode::TEMPORARY_REDIRECT
                | StatusCode::PERMANENT_REDIRECT
        );

        got_redirect_status_code && self.extract_redirect_location_from_response().is_some()
    }

    /// Updates the request in place to send to the redirect location.
    fn update_and_create_request(&mut self) -> anyhow::Result<Option<Request<Bytes>>> {
        let redirect_location =
            if let Some(location) = self.extract_redirect_location_from_response() {
                location
            } else {
                return Ok(None);
            };

        let redirect_uri = self
            .pending_request
            .uri
            .clone()
            .with_redirect(&redirect_location)?;
        let is_cross_host = redirect_uri.is_cross_host(&self.pending_request.uri);
        self.pending_request.uri = redirect_uri;

        if is_cross_host {
            for sensitive_header in &[
                hyper::header::AUTHORIZATION,
                hyper::header::COOKIE,
                hyper::header::PROXY_AUTHORIZATION,
                hyper::header::WWW_AUTHENTICATE,
            ] {
                self.pending_request.headers.remove(sensitive_header);
            }
        }

        match self.response.status() {
            StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::SEE_OTHER => {
                for sensitive_header in &[
                    hyper::header::TRANSFER_ENCODING,
                    hyper::header::CONTENT_ENCODING,
                    hyper::header::CONTENT_TYPE,
                    hyper::header::CONTENT_LENGTH,
                ] {
                    self.pending_request.headers.remove(sensitive_header);
                }

                if self.pending_request.method == Method::HEAD {
                    self.pending_request.method = Method::GET;
                }
            }
            _ => {}
        }

        Some(self.pending_request.to_request()).transpose()
    }

    /// Extracts location header from the current response and tries to convert it
    /// to a URI.
    fn extract_redirect_location_from_response(&self) -> Option<Uri> {
        self.response
            .headers()
            .get(hyper::header::LOCATION)
            .and_then(|location| Uri::try_from(location.as_bytes()).ok())
    }
}

#[cfg(test)]
mod tests {
    use httptest::matchers::*;
    use httptest::responders;
    use httptest::Expectation;

    use super::*;
    use crate::http::secure_client::SecureHttpClient;
    use crate::http::tls_config_with_system_roots;
    use crate::http::HttpClient;

    #[tokio::test]
    async fn test_follows_redirects() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        // Chain of two redirects /foo -> /bar -> /baz.
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/foo"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/bar"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/bar"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/baz"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/baz"))
                .times(1)
                .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 10);
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_head_changes_to_get_on_redirect() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        // Chain of two redirects /foo -> /bar -> /baz.
        test_server.expect(
            Expectation::matching(request::method_path("HEAD", "/foo"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/bar"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/bar"))
                .times(1)
                .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 10);
        let resp = client.head(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_post_gets_redirected() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        // Redirect /foo -> /bar
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("POST", "/foo"),
                request::body("Hello, world!"),
            ])
            .times(1)
            .respond_with(
                responders::status_code(307).append_header(http::header::LOCATION, "/bar"),
            ),
        );
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("POST", "/bar"),
                request::body("Hello, world!"),
                request::headers(not(contains(key(hyper::header::ORIGIN.as_str())))),
                request::headers(not(contains(key(hyper::header::AUTHORIZATION.as_str())))),
                request::headers(not(contains(key(hyper::header::WWW_AUTHENTICATE.as_str())))),
                request::headers(not(contains(key(hyper::header::COOKIE.as_str())))),
                request::headers(not(contains(key(
                    hyper::header::PROXY_AUTHORIZATION.as_str()
                )))),
            ])
            .times(1)
            .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 10);
        let bytes = Bytes::from_static(b"Hello, world!");
        let resp = client
            .post(
                &test_server.url_str("/foo"),
                bytes,
                vec![("key".to_owned(), "value".to_owned())],
            )
            .await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_too_many_redirects_fails() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        // Chain of three redirects /foo -> /bar -> /baz -> /boo.
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/foo"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/bar"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/bar"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/baz"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/baz"))
                .times(1)
                .respond_with(
                    responders::status_code(302).append_header(http::header::LOCATION, "/boo"),
                ),
        );
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/boo"))
                .times(0)
                .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 1);
        let url = test_server.url_str("/foo");
        let result = client.get(&url).await;
        if let HttpError::TooManyRedirects { uri, max_redirects } = result.as_ref().err().unwrap() {
            assert_eq!(url.to_owned(), *uri);
            assert_eq!(1, *max_redirects);
        } else {
            unreachable!(
                "Expected HttpError::TooManyRedirects, got {:?}",
                result.err().unwrap()
            );
        }

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::time::Duration;

use futures::future::Future;
use http::StatusCode;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum HttpError {
    #[error("HTTP Client error: {0}")]
    Client(#[from] crate::http::HttpError),

    #[error("HTTP Transfer Error when querying URL: {}. Failed after {} bytes", .url, .received)]
    Transfer {
        received: u64,
        url: String,
        #[source]
        source: hyper::Error,
    },
}

impl HttpError {
    fn is_retryable(&self) -> bool {
        match self {
            Self::Client(client_error) => match client_error {
                crate::http::HttpError::Status { status, .. } => {
                    status.is_server_error() || *status == StatusCode::TOO_MANY_REQUESTS
                }
                crate::http::HttpError::SendRequest(..) => true,
                _ => false,
            },
            Self::Transfer { source, .. } => !source.is_connect(),
        }
    }
}
pub trait AsHttpError {
    fn as_http_error(&self) -> Option<&HttpError>;
}

pub async fn http_retry<Exec, F, T, E>(exec: Exec, mut intervals: Vec<Duration>) -> Result<T, E>
where
    Exec: Fn() -> F,
    E: AsHttpError + std::fmt::Display,
    F: Future<Output = Result<T, E>>,
{
    intervals.insert(0, Duration::from_secs(0));
    let mut backoff = intervals.into_iter().peekable();

    while let Some(duration) = backoff.next() {
        tokio::time::sleep(duration).await;

        let res = exec().await;

        let http_error = res.as_ref().err().and_then(|err| err.as_http_error());

        if let Some(http_error) = http_error {
            if http_error.is_retryable() {
                if let Some(b) = backoff.peek() {
                    tracing::warn!(
                        "Retrying a HTTP error after {} seconds: {:#}",
                        b.as_secs(),
                        http_error
                    );
                    continue;
                }
            }
        }

        return res;
    }

    unreachable!("The loop above will exit before we get to the end")
}

#[cfg(test)]
mod tests {
    use super::*;

    struct Mock {
        responses: std::cell::RefCell<Vec<Result<String, HttpTestError>>>,
    }

    impl Mock {
        fn new(responses: Vec<Result<String, HttpTestError>>) -> Self {
            Self {
                responses: std::cell::RefCell::new(responses),
            }
        }

        async fn exec(&self) -> Result<String, HttpTestError> {
            // This function is expected to be of type Fn and not FnMut
            // We use internal mutability (RefCell) here to ensure that
            let mut responses = self.responses.borrow_mut();
            responses.remove(0)
        }
    }

    #[derive(Debug, Error)]
    enum HttpTestError {
        #[error("Error in test")]
        Client(#[from] HttpError),
    }

    impl AsHttpError for HttpTestError {
        fn as_http_error(&self) -> Option<&HttpError> {
            match self {
                Self::Client(e) => Some(e),
            }
        }
    }

    fn ok_response() -> Result<String, HttpTestError> {
        Ok("Success".to_owned()).map_err(|_: anyhow::Error| test_error(StatusCode::IM_A_TEAPOT))
    }

    fn retryable() -> Result<String, HttpTestError> {
        Err(test_error(StatusCode::INTERNAL_SERVER_ERROR))
    }

    fn non_retryable() -> Result<String, HttpTestError> {
        Err(test_error(StatusCode::NOT_FOUND))
    }

    fn test_error(status: StatusCode) -> HttpTestError {
        HttpTestError::Client(HttpError::Client(crate::http::HttpError::Status {
            status,
            uri: "something".to_owned(),
            text: "something else".to_owned(),
        }))
    }

    fn retries(n: usize) -> Vec<Duration> {
        vec![Duration::from_secs(0); n]
    }

    #[tokio::test]
    async fn test_http_retry_success() {
        let mock = Mock::new(vec![ok_response()]);
        let result = http_retry(|| mock.exec(), retries(0)).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_http_retry_retryable() {
        let mock = Mock::new(vec![retryable(), ok_response()]);
        let result = http_retry(|| mock.exec(), retries(1)).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_http_retry_exhaust_retries() {
        let mock = Mock::new(vec![retryable(), ok_response()]);
        let result = http_retry(|| mock.exec(), retries(0)).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_http_retry_non_retryable() {
        let mock = Mock::new(vec![non_retryable(), ok_response()]);
        let result = http_retry(|| mock.exec(), retries(1)).await;
        assert!(result.is_err());
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use allocative::Allocative;
use bytes::Bytes;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use futures::TryStreamExt;
use hyper::client::connect::Connect;
use hyper::Body;
use hyper::Request;
use hyper::Response;
use hyper_rustls::HttpsConnectorBuilder;
use rustls::ClientConfig;
use tokio::io::AsyncReadExt;
use tokio_util::io::StreamReader;

use crate::http::redirect::PendingRequest;
use crate::http::redirect::RedirectEngine;
use crate::http::HttpClient;
use crate::http::HttpError;
use crate::http::RequestClient;

/// A simple client that can make requests to HTTPS or HTTP endpoints. Handles
/// redirects (up to max_redirects).
#[derive(Allocative)]
pub(super) struct SecureHttpClient {
    // hyper::Client doesn't implement Allocative.
    #[allocative(skip)]
    inner: Arc<dyn RequestClient>,
    max_redirects: usize,
}

impl SecureHttpClient {
    /// Constructs a client that uses default system roots to setup TLS.
    pub(super) fn new(tls_config: ClientConfig, max_redirects: usize) -> Self {
        let connector = HttpsConnectorBuilder::new()
            .with_tls_config(tls_config)
            .https_or_http()
            .enable_http1()
            .enable_http2()
            .build();
        Self::with_connector(connector, max_redirects)
    }

    pub(super) fn with_connector<C: Connect + Clone + Send + Sync + 'static>(
        connector: C,
        max_redirects: usize,
    ) -> Self {
        Self {
            inner: Arc::new(hyper::Client::builder().build::<_, Body>(connector)),
            max_redirects,
        }
    }

    async fn send_request_impl(
        &self,
        request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        self.inner
            .request(request)
            .await
            .map_err(HttpError::SendRequest)
            .map(|res| res.map(|body| body.boxed()))
    }
}

#[async_trait::async_trait]
impl HttpClient for SecureHttpClient {
    async fn request(
        &self,
        request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        let pending_request = PendingRequest::from_request(&request);
        let uri = request.uri().to_string();
        tracing::debug!("http: request: {:?}", request);
        let resp = self.send_request_impl(request).await?;
        tracing::debug!("http: response: {:?}", resp.status());

        // Handle redirects up to self.max_redirects times.
        let redirect_engine = RedirectEngine::new(self.max_redirects, pending_request, resp);
        let resp = redirect_engine
            .handle_redirects(|req| self.send_request_impl(req))
            .await?;

        if !resp.status().is_success() {
            let status = resp.status();
            let text = read_truncated_error_response(resp).await;
            return Err(HttpError::Status { status, uri, text });
        }

        Ok(resp)
    }
}

async fn read_truncated_error_response(
    mut resp: Response<BoxStream<'_, hyper::Result<Bytes>>>,
) -> String {
    let read = StreamReader::new(
        resp.body_mut()
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)),
    );
    let mut buf = Vec::with_capacity(1024);
    read.take(1024).read_to_end(&mut buf).await.map_or_else(
        |e| format!("Error decoding response: {:?}", e),
        |_| String::from_utf8_lossy(buf.as_ref()).into_owned(),
    )
}

#[cfg(test)]
mod tests {
    use httptest::matchers::*;
    use httptest::responders;
    use httptest::Expectation;
    use hyper::StatusCode;

    use super::*;
    use crate::http::tls_config_with_system_roots;

    #[tokio::test]
    async fn test_simple_get_success() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/foo"))
                .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 10);
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_simple_put_success() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("PUT", "/foo"),
                request::body("Hello, world!")
            ])
            .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 1);
        let bytes = Bytes::from_static(b"Hello, world!");
        let resp = client
            .put(
                &test_server.url_str("/foo"),
                bytes,
                vec![("key".to_owned(), "value".to_owned())],
            )
            .await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_simple_post_success() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("POST", "/foo"),
                request::body("Hello, world!")
            ])
            .respond_with(responders::status_code(200)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 1);
        let bytes = Bytes::from_static(b"Hello, world!");
        let resp = client
            .post(
                &test_server.url_str("/foo"),
                bytes,
                vec![("key".to_owned(), "value".to_owned())],
            )
            .await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_404_not_found_is_error() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(request::method_path("GET", "/foo"))
                .respond_with(responders::status_code(404)),
        );

        let client = SecureHttpClient::new(tls_config_with_system_roots()?, 10);
        let url = test_server.url_str("/foo");
        let result = client.get(&url).await;
        assert!(result.is_err());
        if let HttpError::Status { status, uri, text } = result.as_ref().err().unwrap() {
            assert_eq!(StatusCode::NOT_FOUND, *status);
            assert_eq!(url.to_owned(), *uri);
            assert!(text.is_empty());
        } else {
            unreachable!(
                "Expected HttpError::Status, got {:?}",
                result.err().unwrap()
            );
        }

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::sync::Arc;

use allocative::Allocative;
use bytes::Bytes;
use futures::stream::BoxStream;
use hyper::Request;
use hyper::Response;
use hyper_proxy::Proxy;
use hyper_proxy::ProxyConnector;
use hyper_rustls::HttpsConnectorBuilder;
use tokio_rustls::TlsConnector;

use crate::http::secure_client::SecureHttpClient;
use crate::http::tls_config_with_system_roots;
use crate::http::HttpClient;
use crate::http::HttpError;
use crate::http::DEFAULT_MAX_REDIRECTS;

#[derive(Allocative)]
pub(super) struct SecureProxiedClient {
    inner: SecureHttpClient,
}

impl SecureProxiedClient {
    pub(super) fn with_proxies<I: IntoIterator<Item = Proxy>>(proxies: I) -> anyhow::Result<Self> {
        let config = tls_config_with_system_roots()?;

        // This connector establishes a secure connection from client -> dest
        let https_connector = HttpsConnectorBuilder::new()
            .with_tls_config(config.clone())
            .https_or_http()
            .enable_http1()
            .enable_http2()
            .build();

        // Note: we use the `unsecured()` constructor here, but all that does is not load TLS config. We pass our own TLS config below:
        let mut proxy_connector = ProxyConnector::unsecured(https_connector);

        // This connector wraps the above and _also_ establishes a secure connection to
        // the proxy, re-using the same TLS config for the above connector.
        //
        // The net effect is that we can establish a secure connection to the proxy and
        // have that tunnel our secure connection to the destination.

        proxy_connector.set_tls(Some(TlsConnector::from(Arc::new(config))));
        proxy_connector.extend_proxies(proxies);

        Ok(Self {
            inner: SecureHttpClient::with_connector(proxy_connector, DEFAULT_MAX_REDIRECTS),
        })
    }
}

#[async_trait::async_trait]
impl HttpClient for SecureProxiedClient {
    async fn request(
        &self,
        request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        self.inner.request(request).await
    }
}

#[cfg(test)]
#[cfg(any(fbcode_build, cargo_internal_build))]
mod tests {
    use anyhow::Context as _;
    use httptest::matchers::*;
    use httptest::responders;
    use httptest::Expectation;
    use hyper::Body;

    use super::*;

    /// Barebones proxy server implementation that simply forwards requests onto
    /// the destination server.
    struct ProxyServer {
        addr: std::net::SocketAddr,
        // Need to hold a ref to the task so when Drop runs on Self we cancel
        // the task.
        #[allow(dead_code)]
        handle: tokio::task::JoinHandle<()>,
    }

    impl ProxyServer {
        async fn new() -> anyhow::Result<Self> {
            use std::convert::Infallible;
            use std::net::TcpListener;
            use std::net::ToSocketAddrs;

            use hyper::service::make_service_fn;
            use hyper::service::service_fn;
            use hyper::Server;

            let proxy_server_addr = "[::1]:0".to_socket_addrs().unwrap().next().unwrap();
            let listener =
                TcpListener::bind(proxy_server_addr).context("failed to bind to local address")?;
            let proxy_server_addr = listener.local_addr()?;

            let make_proxy_service = make_service_fn(|_conn| async move {
                Ok::<_, Infallible>(service_fn(|mut req: Request<Body>| async move {
                    let client = hyper::Client::new();
                    req.headers_mut().insert(
                        http::header::VIA,
                        http::HeaderValue::from_static("testing-proxy-server"),
                    );
                    println!("Proxying request: {:?}", req);
                    client
                        .request(req)
                        .await
                        .context("Failed sending requeest to destination")
                }))
            });

            let handle = tokio::task::spawn(async move {
                println!("started proxy server");
                Server::from_tcp(listener)
                    .unwrap()
                    .serve(make_proxy_service)
                    .await
                    .expect("Proxy server exited unexpectedly");
            });

            Ok(Self {
                addr: proxy_server_addr,
                handle,
            })
        }

        fn uri(&self) -> anyhow::Result<http::Uri> {
            http::Uri::builder()
                .scheme("http")
                .authority(self.addr.to_string().as_str())
                .path_and_query("/")
                .build()
                .context("failed to build proxy server URI")
        }
    }

    #[tokio::test]
    async fn test_uses_http_proxy() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/foo"),
                request::headers(contains(("via", "testing-proxy-server")))
            ])
            .times(1)
            .respond_with(responders::status_code(200)),
        );

        let proxy_server = ProxyServer::new().await?;
        println!("proxy_server uri: {}", proxy_server.uri()?);

        let client = SecureProxiedClient::with_proxies([Proxy::new(
            hyper_proxy::Intercept::Http,
            proxy_server.uri()?,
        )])?;
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_uses_http_proxy_with_no_scheme_in_proxy_uri() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/foo"),
                request::headers(contains(("via", "testing-proxy-server")))
            ])
            .times(1)
            .respond_with(responders::status_code(200)),
        );

        let proxy_server = ProxyServer::new().await?;

        let authority = proxy_server.uri()?.authority().unwrap().clone();
        let proxy_uri = format!("{}:{}", authority.host(), authority.port().unwrap());
        println!("proxy_uri: {}", proxy_uri);
        let client = SecureProxiedClient::with_proxies([Proxy::new(
            hyper_proxy::Intercept::Http,
            crate::http::proxy::DefaultSchemeUri(proxy_uri.try_into()?).into(),
        )])?;
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    #[cfg(any(fbcode_build, cargo_internal_build))] // TODO(@skarlage): Debug why this fails on CircleCI
    async fn test_does_not_proxy_when_no_proxy_matches() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![request::method_path("GET", "/foo")])
                .times(1)
                .respond_with(responders::status_code(200)),
        );

        let proxy_server = ProxyServer::new().await?;
        println!("proxy_server uri: {}", proxy_server.uri()?);

        let test_server_host = test_server
            .url("/")
            .authority()
            .unwrap()
            .clone()
            .host()
            .to_owned();
        let no_proxy = crate::http::proxy::NoProxy::new(http::uri::Scheme::HTTP, test_server_host);

        // Don't proxy connections to test_server.
        let client = SecureProxiedClient::with_proxies([Proxy::new(
            no_proxy.into_proxy_intercept(),
            proxy_server.uri()?,
        )])?;
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }

    #[tokio::test]
    async fn test_proxies_when_no_proxy_does_not_match() -> anyhow::Result<()> {
        let test_server = httptest::Server::run();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/foo"),
                request::headers(contains(("via", "testing-proxy-server")))
            ])
            .times(1)
            .respond_with(responders::status_code(200)),
        );

        let proxy_server = ProxyServer::new().await?;
        println!("proxy_server uri: {}", proxy_server.uri()?);

        // Don't proxy HTTPS connections to *.foobar.com
        let no_proxy = crate::http::proxy::NoProxy::new(http::uri::Scheme::HTTP, ".foobar.com");

        let client = SecureProxiedClient::with_proxies([Proxy::new(
            no_proxy.into_proxy_intercept(),
            proxy_server.uri()?,
        )])?;
        let resp = client.get(&test_server.url_str("/foo")).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::path::Path;

use allocative::Allocative;
use anyhow::Context;
use bytes::Bytes;
use futures::stream::BoxStream;
use http::uri::Scheme;
use http::Uri;
use hyper::Request;
use hyper::Response;
use hyper_proxy::Intercept;
use hyper_proxy::Proxy;
use hyper_proxy::ProxyConnector;

use crate::http::HttpClient;
use crate::http::HttpError;
use crate::http::SecureHttpClient;
use crate::http::SecureProxiedClient;
use crate::http::DEFAULT_MAX_REDIRECTS;

/// x2pagent proxies only speak plain HTTP, so we need to mutate requests prior
/// to sending them off.
fn change_scheme_to_http(request: &mut Request<Bytes>) {
    let uri = request.uri().clone();
    let mut parts = uri.into_parts();
    parts.scheme = Some(Scheme::HTTP);
    *request.uri_mut() = Uri::from_parts(parts).expect("Unexpected invalid URI from request");
}

#[derive(Allocative)]
pub(super) struct X2PAgentProxyClient {
    inner: SecureProxiedClient,
}

impl X2PAgentProxyClient {
    pub(super) fn new(http1_proxy_port: u16) -> anyhow::Result<Self> {
        let proxy = Proxy::new(
            Intercept::All,
            format!("http://localhost:{}", http1_proxy_port)
                .try_into()
                .context("Error converting x2pagent proxy address into URI")?,
        );
        let client = SecureProxiedClient::with_proxies([proxy])?;
        Ok(Self { inner: client })
    }

    async fn request_impl(
        &self,
        mut request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        change_scheme_to_http(&mut request);
        self.inner.request(request).await
    }
}

#[async_trait::async_trait]
impl HttpClient for X2PAgentProxyClient {
    async fn request(
        &self,
        request: Request<Bytes>,
    ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
        self.request_impl(request).await
    }

    fn supports_vpnless(&self) -> bool {
        true
    }
}

// TODO(skarlage): Remove this client once x2pagent http proxy is deployed and works.
#[cfg(unix)]
mod unix {
    use hyper_unix_connector::UnixClient;

    use super::*;

    /// Wraps a secure client so that we can talk to the x2pagent-provided unix socket
    /// proxy server.
    ///
    /// This server has a few requirements that are reflected in the methods below:
    /// 1. Requests are http-only. The proxy server handles TLS encapsulation + picking
    ///    correct certs.
    /// 2. Requests must be sent to a unix domain socket
    #[derive(Allocative)]
    pub struct X2PAgentUnixSocketClient {
        inner: SecureHttpClient,
    }

    impl X2PAgentUnixSocketClient {
        pub fn new<P: AsRef<Path>>(socket_path: P) -> anyhow::Result<Self> {
            let proxy = Proxy::new(
                Intercept::All,
                hyper_unix_connector::Uri::new(socket_path, "/").into(),
            );

            // X2PAgent does all TLS for us, hence the _unsecured bit.
            let proxy_connector = ProxyConnector::from_proxy_unsecured(UnixClient, proxy);

            Ok(Self {
                inner: SecureHttpClient::with_connector(proxy_connector, DEFAULT_MAX_REDIRECTS),
            })
        }

        async fn request_impl(
            &self,
            mut request: Request<Bytes>,
        ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
            change_scheme_to_http(&mut request);
            self.inner.request(request).await
        }
    }

    #[async_trait::async_trait]
    impl HttpClient for X2PAgentUnixSocketClient {
        async fn request(
            &self,
            request: Request<Bytes>,
        ) -> Result<Response<BoxStream<hyper::Result<Bytes>>>, HttpError> {
            self.request_impl(request).await
        }

        fn supports_vpnless(&self) -> bool {
            true
        }
    }
}

#[cfg(unix)]
pub(super) use unix::X2PAgentUnixSocketClient;

#[cfg(test)]
mod tests {
    use http::Method;

    use super::*;

    #[test]
    fn test_change_scheme_to_http_succeeds() -> anyhow::Result<()> {
        let mut request = Request::builder()
            .method(Method::GET)
            .uri("https://some.site/foo")
            .body(Bytes::new())?;
        change_scheme_to_http(&mut request);

        assert_eq!(
            Scheme::HTTP,
            *request
                .uri()
                .scheme()
                .expect("should have scheme after mutating request")
        );
        Ok(())
    }

    #[test]
    fn test_change_scheme_to_http_no_effect() -> anyhow::Result<()> {
        let uri: Uri = "http://some.site/foo".try_into()?;
        let mut request = Request::builder()
            .method(Method::GET)
            .uri(uri.clone())
            .body(Bytes::new())?;
        change_scheme_to_http(&mut request);

        assert_eq!(&uri, request.uri());
        Ok(())
    }
}

#[cfg(all(test, unix))]
mod unix_tests {
    use std::convert::Infallible;
    use std::path::PathBuf;

    use http::HeaderValue;
    use httptest::matchers::*;
    use httptest::responders;
    use httptest::Expectation;
    use hyper::service::make_service_fn;
    use hyper::service::service_fn;
    use hyper::Body;
    use hyper::Server;
    use hyper_unix_connector::UnixConnector;
    use tokio::task::JoinHandle;

    use super::*;

    /// Conceptually similar to crate::http::tests::ProxyServer, but sets up a
    /// local unix domain socket instead.
    struct UnixSocketProxyServer {
        pub socket: PathBuf,
        // Need to hold a ref so when Drop runs on Self we cancel the task.
        #[allow(dead_code)]
        handle: JoinHandle<()>,
        // Need to hold ref so socket doesn't get removed.
        #[allow(dead_code)]
        tempdir: tempfile::TempDir,
    }

    impl UnixSocketProxyServer {
        async fn new() -> anyhow::Result<Self> {
            let tempdir = tempfile::tempdir()?;
            let socket = tempdir.path().join("test-uds.sock");

            let listener: UnixConnector = tokio::net::UnixListener::bind(&socket)
                .context("binding to unix socket")?
                .into();
            let handler_func = make_service_fn(|_conn| async move {
                Ok::<_, Infallible>(service_fn(|mut req: Request<Body>| async move {
                    let client = hyper::Client::new();
                    req.headers_mut().insert(
                        http::header::VIA,
                        HeaderValue::from_static("testing-proxy-server"),
                    );
                    println!("Proxying request: {:?}", req);
                    client
                        .request(req.map(Body::from))
                        .await
                        .context("Failed sending requeest to destination")
                }))
            });

            let handle = tokio::task::spawn(async move {
                println!("started proxy server");
                Server::builder(listener)
                    .serve(handler_func)
                    .await
                    .expect("Proxy server exited unexpectedly");
            });

            Ok(Self {
                socket,
                handle,
                tempdir,
            })
        }
    }

    #[tokio::test]
    async fn test_proxy_mutates_request_and_succeeds() -> anyhow::Result<()> {
        let proxy_server = UnixSocketProxyServer::new().await?;

        let test_server = httptest::Server::run();
        let url = test_server.url("/foo");
        let host = url.authority().unwrap().to_string();
        test_server.expect(
            Expectation::matching(all_of![
                request::method_path("GET", "/foo"),
                request::headers(contains(("via", "testing-proxy-server"))),
                request::headers(contains(("host", host))),
            ])
            .times(1)
            .respond_with(responders::status_code(200)),
        );

        let client = X2PAgentUnixSocketClient::new(&proxy_server.socket)?;
        let resp = client.get(&url.to_string()).await?;
        assert_eq!(200, resp.status().as_u16());

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::collections::HashMap;
use std::sync::Arc;

use allocative::Allocative;
use async_trait::async_trait;
use buck2_core::cells::name::CellName;
use buck2_core::cells::unchecked_cell_rel_path::UncheckedCellRelativePath;
use dice::DiceComputations;
use itertools::Itertools;

use crate::dice::cells::HasCellResolver;
use crate::ignores::file_ignores::FileIgnoreResult;
use crate::ignores::file_ignores::FileIgnores;
use crate::legacy_configs::dice::HasLegacyConfigs;

/// Ignored path configurations for all cells.
#[derive(Allocative, Debug, Eq, PartialEq)]
pub(crate) struct AllCellIgnores {
    ignores: HashMap<CellName, FileIgnores>,
}

impl AllCellIgnores {
    pub(crate) fn check_ignored(
        &self,
        cell: CellName,
        path: &UncheckedCellRelativePath,
    ) -> anyhow::Result<FileIgnoreResult> {
        Ok(self
            .ignores
            .get(&cell)
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "Internal error: Should've had an ignore spec for `{}`. Had `{}`",
                    cell,
                    self.ignores.keys().join(", ")
                )
            })?
            .check(path))
    }
}

#[async_trait]
pub(crate) trait HasAllCellIgnores {
    async fn new_all_cell_ignores(&self) -> anyhow::Result<Arc<AllCellIgnores>>;
}

#[async_trait]
impl HasAllCellIgnores for DiceComputations {
    async fn new_all_cell_ignores(&self) -> anyhow::Result<Arc<AllCellIgnores>> {
        let cells = self.get_cell_resolver().await?;
        let configs = self.get_legacy_configs_on_dice().await?;

        let mut ignores = HashMap::new();

        for (cell_name, instance) in cells.cells() {
            let config = configs.get(cell_name).unwrap();
            let ignore_spec = config.get("project", "ignore")?;
            let ignore_spec = ignore_spec.as_ref().map_or("", |s| &**s);

            let cell_ignores = FileIgnores::new_for_interpreter(
                ignore_spec,
                instance.nested_cells().clone(),
                cells.is_root_cell(cell_name),
            )?;
            ignores.insert(cell_name, cell_ignores);
        }

        Ok(Arc::new(AllCellIgnores { ignores }))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use allocative::Allocative;
use buck2_core::cells::name::CellName;
use buck2_core::cells::nested::NestedCells;
use buck2_core::cells::unchecked_cell_rel_path::UncheckedCellRelativePath;

use crate::ignores::ignore_set::IgnoreSet;

#[derive(Debug, thiserror::Error)]
enum FileOpsError {
    #[error("Tried to read ignored dir `{0}` (reason: {1}).")]
    ReadIgnoredDir(String, String),
}

pub(crate) enum FileIgnoreResult {
    Ok,
    IgnoredByPattern(String, String),
    IgnoredByCell(String, CellName),
}

impl FileIgnoreResult {
    /// Converts the FileIgnoreResult to a Result<()> where any ignored case is converted to an Err
    /// with appropriate message. This should be used when it would be an error to interact with an
    /// ignored file.
    pub(crate) fn into_result(self) -> anyhow::Result<()> {
        match self {
            FileIgnoreResult::Ok => Ok(()),
            FileIgnoreResult::IgnoredByPattern(path, pattern) => {
                Err(anyhow::anyhow!(FileOpsError::ReadIgnoredDir(
                    path,
                    format!("file is matched by pattern `{}`", pattern)
                )))
            }
            FileIgnoreResult::IgnoredByCell(path, cell_name) => Err(anyhow::anyhow!(
                FileOpsError::ReadIgnoredDir(path, format!("file is part of cell `{}`", cell_name))
            )),
        }
    }

    /// Returns true if the file is ignored, false otherwise.
    pub(crate) fn is_ignored(&self) -> bool {
        match self {
            FileIgnoreResult::Ok => false,
            _ => true,
        }
    }
}

/// Ignores files based on configured ignore patterns and cell paths.
#[derive(PartialEq, Eq, Allocative, Debug)]
pub struct FileIgnores {
    ignores: IgnoreSet,
    cell_ignores: NestedCells,
}

impl FileIgnores {
    /// Creates a new FileIgnores intended for use by the interpreter.
    ///
    /// This will ignore files/dirs in the ignore spec and those in other cells.
    pub fn new_for_interpreter(
        ignore_spec: &str,
        nested_cells: NestedCells,
        root_cell: bool,
    ) -> anyhow::Result<FileIgnores> {
        Ok(FileIgnores {
            ignores: IgnoreSet::from_ignore_spec(ignore_spec, root_cell)?,
            cell_ignores: nested_cells,
        })
    }

    pub(crate) fn check(&self, path: &UncheckedCellRelativePath) -> FileIgnoreResult {
        let candidate = globset::Candidate::new(path.as_str());

        if let Some(pattern) = self.ignores.matches_candidate(&candidate) {
            return FileIgnoreResult::IgnoredByPattern(
                path.as_str().to_owned(),
                pattern.to_owned(),
            );
        }

        if let Some((_, cell_name, _)) = self.cell_ignores.matches(path) {
            return FileIgnoreResult::IgnoredByCell(path.as_str().to_owned(), cell_name);
        }

        FileIgnoreResult::Ok
    }
}

#[cfg(test)]
mod tests {
    use buck2_core::cells::cell_root_path::CellRootPath;
    use buck2_core::cells::name::CellName;
    use buck2_core::cells::nested::NestedCells;
    use buck2_core::cells::unchecked_cell_rel_path::UncheckedCellRelativePath;
    use buck2_core::fs::project_rel_path::ProjectRelativePath;

    use crate::ignores::file_ignores::FileIgnores;

    #[test]
    fn file_ignores() -> anyhow::Result<()> {
        let cells = &[
            (
                CellName::testing_new("root"),
                CellRootPath::new(ProjectRelativePath::unchecked_new("root")),
            ),
            (
                CellName::testing_new("other"),
                CellRootPath::new(ProjectRelativePath::unchecked_new("root/other_cell")),
            ),
            (
                CellName::testing_new("third"),
                CellRootPath::new(ProjectRelativePath::unchecked_new("third")),
            ),
        ];
        let nested_cells = NestedCells::from_cell_roots(cells, CellRootPath::testing_new("root"));
        let ignores = FileIgnores::new_for_interpreter(
            "**/*.java , some/dir/**, one/*, \n    recursive, trailing_slash/",
            nested_cells,
            true,
        )?;

        assert_eq!(
            true,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new(
                    "some/long/path/Class.java"
                ))
                .is_ignored()
        );

        assert_eq!(
            true,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new("other_cell"))
                .is_ignored()
        );

        assert_eq!(
            true,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new(
                    "other_cell/some/lib"
                ))
                .is_ignored()
        );

        assert_eq!(
            false,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new("third"))
                .is_ignored()
        );

        assert_eq!(
            false,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new("one/two/three"))
                .is_ignored()
        );

        assert_eq!(
            true,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new(
                    "recursive/two/three"
                ))
                .is_ignored()
        );

        assert_eq!(
            true,
            ignores
                .check(UncheckedCellRelativePath::unchecked_new(
                    "trailing_slash/BUCK"
                ))
                .is_ignored()
        );

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use allocative::Allocative;
use buck2_core::cells::paths::CellRelativePath;
use globset::Candidate;
use globset::GlobSetBuilder;
use once_cell::sync::Lazy;
use regex::Regex;

#[derive(Debug, Allocative)]
pub struct IgnoreSet {
    #[allocative(skip)]
    globset: globset::GlobSet,
    // We keep patterns so that error messages can refer to the specific pattern that was matched.
    // This should be in the same order as the strings were added to the GlobSet to match the indices returned from it.
    patterns: Vec<String>,
}

impl PartialEq for IgnoreSet {
    fn eq(&self, other: &Self) -> bool {
        // Only compare patterns because globset is derived from patterns.
        self.patterns == other.patterns
    }
}

impl Eq for IgnoreSet {}

impl IgnoreSet {
    /// Creates an IgnoreSet from an "ignore spec".
    ///
    /// This is modeled after buck1's parsing of project.ignores.
    ///
    /// An ignore spec is a comma-separated list of ignore patterns. If an ignore pattern
    /// contains a glob character, then it uses java.nio.file.FileSystem.getPathMatcher,
    /// otherwise it creates a com.facebook.buck.io.filesystem.RecursivePathMatcher
    ///
    /// Java's path matcher does not allow  '*' to cross directory boundaries. We get
    /// the RecursivePathMatcher behavior by identifying non-globby things and appending
    /// a '/**'.
    ///
    /// Always ignores `buck-out` if it is a `root_cell`.
    pub fn from_ignore_spec(spec: &str, root_cell: bool) -> anyhow::Result<Self> {
        // TODO(cjhopman): There's opportunity to greatly improve the performance of IgnoreSet by
        // constructing special cases for a couple of common patterns we see in ignore specs. We
        // know that these can get large wins in some places where we've done this same ignore (watchman, buck1's ignores).
        // `**/filename`: a filename filter. These can all be merged into one hashset lookup.
        // `**/*.ext`: an extension filter. These can all be merged into one hashset lookup.
        // `**/*x*x*`: just some general glob on the filename alone, can merge these into one GlobSet that just needs to check against the filename.
        // `some/prefix/**`: a directory prefix. These can all be merged into one trie lookup.
        let mut patterns_builder = GlobSetBuilder::new();
        let mut patterns = Vec::new();
        let buck_out = if root_cell { Some("buck-out") } else { None };
        for val in buck_out.into_iter().chain(spec.split(',')) {
            let val = val.trim();
            if val.is_empty() {
                continue;
            }

            let val = val.trim_end_matches('/');

            static GLOB_CHARS: Lazy<Regex> = Lazy::new(|| Regex::new(r"[*?{\[]").unwrap());

            if GLOB_CHARS.is_match(val) {
                patterns_builder.add(
                    globset::GlobBuilder::new(val)
                        .literal_separator(true)
                        .build()?,
                );
            } else {
                patterns_builder.add(globset::Glob::new(&format!("{{{},{}/**}}", val, val))?);
            }
            patterns.push(val.to_owned());
        }

        Ok(Self {
            globset: patterns_builder.build()?,
            patterns,
        })
    }

    /// Returns a pattern that matches the candidate if there is one.
    pub(crate) fn matches_candidate(&self, candidate: &Candidate) -> Option<&str> {
        match self.globset.matches_candidate(candidate).as_slice() {
            [] => None,
            [v, ..] => Some(&self.patterns[*v]),
        }
    }

    /// Returns whether any pattern matches.
    pub fn is_match(&self, path: &CellRelativePath) -> bool {
        self.globset.is_match(path.as_str())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ignore_set_defaults() {
        let set = IgnoreSet::from_ignore_spec("", true).unwrap();
        assert!(set.is_match(CellRelativePath::testing_new("buck-out/gen/src/file.txt")));
        assert!(!set.is_match(CellRelativePath::testing_new("src/file.txt")));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub(crate) mod all_cells;
pub mod file_ignores;
pub mod ignore_set;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;

use allocative::Allocative;
use anyhow::Context as _;
use async_trait::async_trait;
use buck2_core;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_path::AbsPath;
use buck2_core::fs::paths::abs_path::AbsPathBuf;
use buck2_core::fs::paths::file_name::FileName;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePath;
use buck2_core::fs::paths::forward_rel_path::ForwardRelativePathBuf;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;
use compact_str::CompactString;
use dupe::Dupe;
use once_cell::sync::Lazy;
use thiserror::Error;
use tokio::sync::Semaphore;

use crate::cas_digest::CasDigestConfig;
use crate::external_symlink::ExternalSymlink;
use crate::file_ops::FileDigest;
use crate::file_ops::FileDigestConfig;
use crate::file_ops::FileMetadata;
use crate::file_ops::RawDirEntry;
use crate::file_ops::RawPathMetadata;
use crate::file_ops::RawSymlink;
use crate::file_ops::TrackedFileDigest;
use crate::io::IoProvider;

#[derive(Clone, Dupe, Allocative)]
pub struct FsIoProvider {
    fs: ProjectRoot,
    cas_digest_config: CasDigestConfig,
}

impl FsIoProvider {
    pub fn new(fs: ProjectRoot, cas_digest_config: CasDigestConfig) -> Self {
        Self {
            fs,
            cas_digest_config,
        }
    }

    pub fn cas_digest_config(&self) -> CasDigestConfig {
        self.cas_digest_config
    }
}

#[derive(Error, Debug)]
enum ReadSymlinkAtExactPathError {
    #[error("The path does not exist")]
    DoesNotExist,
    #[error("The path is not a symlink")]
    NotASymlink,
}

impl FsIoProvider {
    /// Read a symlink at a given path. This method DOES NOT check whether there are any symlinks
    /// somewhere on intermediary components of `path`, so the expectation is that the caller
    /// already has ascertained that there are not.
    pub async fn read_unchecked_symlink(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<RawPathMetadata<ProjectRelativePathBuf>> {
        let fs = self.fs.dupe();
        let path = path.into_forward_relative_path_buf();
        tokio::task::spawn_blocking(move || {
            Ok(read_unchecked_symlink(fs.root(), path)?.map(ProjectRelativePathBuf::from))
        })
        .await?
    }
}

#[derive(Debug, Error)]
enum ReadDirError {
    #[error("File name `{0:?}` is not UTF-8")]
    NotUtf8(OsString),
}

/// i/o operations use tokio's blocking threads to not block the cpu threads on i/o. This avoids a lot of bottlenecks
/// and is especially important when fs operations are particularly slow (when using a network fs or something like
/// edenfs, for example).
#[async_trait]
impl IoProvider for FsIoProvider {
    async fn read_file_if_exists(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<Option<String>> {
        let path = self.fs.resolve(&path);

        // Don't want to totally saturate the executor with these so that some other work can progress.
        // For normal fs (or warm eden), something smaller would probably be fine, for eden this is probably
        // good (current plan in that impl is to allow multiple batches of 32 files at a time).
        static SEMAPHORE: Lazy<Semaphore> = Lazy::new(|| Semaphore::new(100));
        let _permit = SEMAPHORE.acquire().await.unwrap();

        tokio::task::spawn_blocking(move || fs_util::read_to_string_opt(path)).await?
    }

    async fn read_dir(&self, path: ProjectRelativePathBuf) -> anyhow::Result<Vec<RawDirEntry>> {
        // Don't want to totally saturate the executor with these so that some other work can progress.
        // For normal fs (or warm eden), something smaller would probably be fine, for eden couple hundred is probably
        // good (current plan in that impl is to allow multiple batches of 128 dirs at a time).
        static SEMAPHORE: Lazy<Semaphore> = Lazy::new(|| Semaphore::new(400));
        let _permit = SEMAPHORE.acquire().await.unwrap();

        let path = self.fs.resolve(&path);

        tokio::task::spawn_blocking(move || {
            let dir_entries = fs_util::read_dir(path)?;

            let mut entries = Vec::new();

            for entry in dir_entries {
                let e = entry.context("Error accessing directory entry")?;
                let file_name = e.file_name();
                let file_name = file_name
                    .to_str()
                    .ok_or_else(|| ReadDirError::NotUtf8(file_name.clone()))?;
                entries.push(RawDirEntry {
                    file_type: e.file_type()?.into(),
                    file_name: CompactString::from(file_name),
                });
            }

            anyhow::Ok(entries)
        })
        .await?
        .context("Error listing directory")
    }

    async fn read_path_metadata_if_exists(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<Option<RawPathMetadata<ProjectRelativePathBuf>>> {
        let fs = self.fs.dupe();
        let path = path.into_forward_relative_path_buf();
        let file_digest_config = FileDigestConfig::source(self.cas_digest_config);

        tokio::task::spawn_blocking(move || {
            let meta = read_path_metadata(fs.root(), &path, file_digest_config)?.map(
                |raw_meta_or_redirection| raw_meta_or_redirection.map(ProjectRelativePathBuf::from),
            );

            Ok(meta)
        })
        .await?
    }

    async fn settle(&self) -> anyhow::Result<()> {
        Ok(())
    }

    fn name(&self) -> &'static str {
        "fs"
    }

    async fn eden_version(&self) -> anyhow::Result<Option<String>> {
        Ok(None)
    }

    fn project_root(&self) -> &ProjectRoot {
        &self.fs
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// A path and the corresponding absolute path.
struct PathAndAbsPath {
    path: ForwardRelativePathBuf,
    abspath: AbsPathBuf,
}

impl PathAndAbsPath {
    fn push(&mut self, name: &FileName) {
        self.path.push(name);
        self.abspath.push(name);
    }
}

fn read_path_metadata<P: AsRef<AbsPath>>(
    root: P,
    relpath: &ForwardRelativePath,
    file_digest_config: FileDigestConfig,
) -> anyhow::Result<Option<RawPathMetadata<ForwardRelativePathBuf>>> {
    let root = root.as_ref();

    let mut relpath_components = relpath.iter();
    let mut meta = None;

    let curr_path_capacity = relpath.as_str().len();

    let mut curr_abspath = root.to_owned();
    curr_abspath.reserve(relpath.as_path().as_os_str().len());

    let curr_abspath_capacity = curr_abspath.capacity();

    let curr_path = ForwardRelativePathBuf::with_capacity(curr_path_capacity);

    let mut curr = PathAndAbsPath {
        path: curr_path,
        abspath: curr_abspath,
    };

    while let Some(c) = relpath_components.next() {
        // We track both paths so we don't need to convert the abspath back to a relative path if
        // we hit a symlink.
        curr.push(c);

        match ExactPathMetadata::from_exact_path(&curr)? {
            ExactPathMetadata::DoesNotExist => return Ok(None),
            ExactPathMetadata::Symlink(symlink) => {
                return Ok(Some(
                    symlink.to_raw_path_metadata(curr, relpath_components.collect())?,
                ));
            }
            ExactPathMetadata::FileOrDirectory(path_meta) => {
                meta = Some(path_meta);
            }
        };
    }

    // If we get here that means we never hit a symlink. So, the metadata we have
    let meta = meta.context("Attempted to access empty path")?;

    let meta = if meta.is_dir() {
        RawPathMetadata::Directory
    } else {
        let digest = FileDigest::from_file(&curr.abspath, file_digest_config)
            .with_context(|| format!("Error collecting file digest for `{}`", curr.path))?;
        let digest = TrackedFileDigest::new(digest, file_digest_config.as_cas_digest_config());
        RawPathMetadata::File(FileMetadata {
            digest,
            is_executable: is_executable(&meta),
        })
    };

    if cfg!(test) {
        assert!(curr.abspath.as_os_str().len() <= curr_abspath_capacity);
        assert!(curr.path.as_str().len() <= curr_path_capacity);
    }

    Ok(Some(meta))
}

enum ExactPathMetadata {
    DoesNotExist,
    Symlink(ExactPathSymlinkMetadata),
    FileOrDirectory(std::fs::Metadata),
}

impl ExactPathMetadata {
    fn from_exact_path(curr: &PathAndAbsPath) -> anyhow::Result<Self> {
        Ok(match fs_util::symlink_metadata_if_exists(&curr.abspath)? {
            Some(meta) if meta.file_type().is_symlink() => {
                let dest = fs_util::read_link(&curr.abspath)?;

                let out = if dest.is_absolute() {
                    ExactPathSymlinkMetadata::ExternalSymlink(dest)
                } else {
                    // Remove the symlink name.
                    let link_path = curr
                        .path
                        .parent()
                        .expect("We pushed a component to this so it cannot be empty")
                        .join_system(&dest)
                        .with_context(|| {
                            format!("Invalid symlink at `{}`: `{}`", curr.path, dest.display())
                        })?;

                    ExactPathSymlinkMetadata::InternalSymlink(link_path)
                };

                ExactPathMetadata::Symlink(out)
            }
            Some(meta) => ExactPathMetadata::FileOrDirectory(meta),
            None => ExactPathMetadata::DoesNotExist,
        })
    }
}

enum ExactPathSymlinkMetadata {
    ExternalSymlink(PathBuf),
    InternalSymlink(ForwardRelativePathBuf),
}

impl ExactPathSymlinkMetadata {
    fn to_raw_path_metadata(
        self,
        curr: PathAndAbsPath,
        rest: Option<ForwardRelativePathBuf>,
    ) -> anyhow::Result<RawPathMetadata<ForwardRelativePathBuf>> {
        Ok(match self {
            Self::ExternalSymlink(link_path) => RawPathMetadata::Symlink {
                at: curr.path,
                to: RawSymlink::External(Arc::new(ExternalSymlink::new(link_path, rest)?)),
            },
            Self::InternalSymlink(mut link_path) => {
                if let Some(rest) = rest {
                    link_path.push(&rest);
                }
                RawPathMetadata::Symlink {
                    at: curr.path,
                    to: RawSymlink::Relative(link_path),
                }
            }
        })
    }
}

fn read_unchecked_symlink<P: AsRef<AbsPath>>(
    root: P,
    relpath: ForwardRelativePathBuf,
) -> anyhow::Result<RawPathMetadata<ForwardRelativePathBuf>> {
    let abspath = root.as_ref().join(relpath.as_path());

    let curr = PathAndAbsPath {
        path: relpath,
        abspath,
    };

    match ExactPathMetadata::from_exact_path(&curr)? {
        ExactPathMetadata::DoesNotExist => Err(ReadSymlinkAtExactPathError::DoesNotExist.into()),
        ExactPathMetadata::FileOrDirectory(..) => {
            Err(ReadSymlinkAtExactPathError::NotASymlink.into())
        }
        ExactPathMetadata::Symlink(link) => link.to_raw_path_metadata(curr, None),
    }
}

#[cfg(unix)]
fn is_executable(meta: &std::fs::Metadata) -> bool {
    use std::os::unix::fs::PermissionsExt;
    // We check 0o111 (user,group,other) instead of 0o100 (user) because even if the user
    // doesn't have permission, if ANYONE does we assume the file is an executable
    meta.permissions().mode() & 0o111 > 0
}

#[cfg(not(unix))]
fn is_executable(_meta: &std::fs::Metadata) -> bool {
    false
}

#[cfg(all(test, unix))]
mod tests {
    use std::os::unix;

    use assert_matches::assert_matches;
    use buck2_core::fs::paths::abs_path::AbsPath;
    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_read_not_symlink() -> anyhow::Result<()> {
        let t = TempDir::new()?;
        let root = AbsPath::new(t.path())?;

        fs_util::write(root.join("x"), "xx")?;

        assert_matches!(
            read_path_metadata(
                AbsPath::new(t.path())?,
                ForwardRelativePath::new("x")?,
                FileDigestConfig::source(CasDigestConfig::testing_default())
            ),
            Ok(Some(RawPathMetadata::File(..)))
        );

        Ok(())
    }

    #[test]
    fn test_read_symlink() -> anyhow::Result<()> {
        let t = TempDir::new()?;

        unix::fs::symlink("y/z", t.path().join("x"))?;

        assert_matches!(
            read_path_metadata(AbsPath::new(t.path())?, ForwardRelativePath::new("x")?, FileDigestConfig::source(CasDigestConfig::testing_default())),
            Ok(Some(RawPathMetadata::Symlink{at:_, to: RawSymlink::Relative(r)})) => {
                assert_eq!(r, "y/z");
            }
        );

        Ok(())
    }

    #[test]
    fn test_read_symlink_in_dir() -> anyhow::Result<()> {
        let t = TempDir::new()?;
        let t = AbsPath::new(t.path())?;

        fs_util::create_dir_all(t.join("x/xx"))?;
        unix::fs::symlink("../y", t.join("x/xx/xxx"))?;

        assert_matches!(
            read_path_metadata(AbsPath::new(t)?, ForwardRelativePath::new("x/xx/xxx")?, FileDigestConfig::source(CasDigestConfig::testing_default())),
            Ok(Some(RawPathMetadata::Symlink{at:_, to: RawSymlink::Relative(r)})) => {
                assert_eq!(r, "x/y");
            }
        );

        Ok(())
    }

    #[test]
    fn test_read_symlink_remaining_path() -> anyhow::Result<()> {
        let t = TempDir::new()?;

        unix::fs::symlink("y", t.path().join("x"))?;

        assert_matches!(
            read_path_metadata(AbsPath::new(t.path())?, ForwardRelativePath::new("x/z/zz")?, FileDigestConfig::source(CasDigestConfig::testing_default())),
            Ok(Some(RawPathMetadata::Symlink{at:_, to: RawSymlink::Relative(r)})) => {
                assert_eq!(r, "y/z/zz");
            }
        );

        Ok(())
    }

    #[test]
    fn test_read_symlink_out_of_project() -> anyhow::Result<()> {
        let t = TempDir::new()?;

        unix::fs::symlink("../y", t.path().join("x"))?;

        assert_matches!(
            read_path_metadata(AbsPath::new(t.path())?, ForwardRelativePath::new("x/xx/xxx")?, FileDigestConfig::source(CasDigestConfig::testing_default())),
            Err(e) if format!("{:#}", e).contains("Invalid symlink")
        );

        Ok(())
    }

    #[test]
    fn test_read_unchecked_symlink() -> anyhow::Result<()> {
        let t = TempDir::new()?;
        let root = AbsPath::new(t.path())?;

        unix::fs::symlink("x", t.path().join("link"))?;
        unix::fs::symlink("/x", t.path().join("abs_link"))?;
        fs_util::write(root.join("file"), "xx")?;

        assert_matches!(
            read_unchecked_symlink(root, ForwardRelativePathBuf::new("link".to_owned())?),
            Ok(RawPathMetadata::Symlink {
                to: RawSymlink::Relative(..),
                ..
            })
        );

        assert_matches!(
            read_unchecked_symlink(root, ForwardRelativePathBuf::new("abs_link".to_owned())?),
            Ok(RawPathMetadata::Symlink {
                to: RawSymlink::External(..),
                ..
            })
        );

        assert_matches!(
            read_unchecked_symlink(root, ForwardRelativePathBuf::new("file".to_owned())?),
            Err(..)
        );

        assert_matches!(
            read_unchecked_symlink(
                root,
                ForwardRelativePathBuf::new("does_not_exist".to_owned())?
            ),
            Err(..)
        );

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

pub mod fs;
pub mod trace;

use allocative::Allocative;
use async_trait::async_trait;
use buck2_core::fs::project::ProjectRoot;
use buck2_core::fs::project_rel_path::ProjectRelativePathBuf;

use crate::file_ops::RawDirEntry;
use crate::file_ops::RawPathMetadata;

#[async_trait]
pub trait IoProvider: Allocative + Send + Sync {
    async fn read_file_if_exists(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<Option<String>>;

    async fn read_dir(&self, path: ProjectRelativePathBuf) -> anyhow::Result<Vec<RawDirEntry>>;

    async fn read_path_metadata_if_exists(
        &self,
        path: ProjectRelativePathBuf,
    ) -> anyhow::Result<Option<RawPathMetadata<ProjectRelativePathBuf>>>;

    /// Request that this I/O provider be up to date with whatever I/O operations the user might
    /// have done until this point.
    async fn settle(&self) -> anyhow::Result<()>;

    fn name(&self) -> &'static str;

    /// Returns the Eden version of the underlying system of the IoProvider, if available.
    async fn eden_version(&self) -> anyhow::Result<Option<String>>;

    fn project_root(&self) -> &ProjectRoot;

    fn as_any(&self) -> &dyn std::any::Any;
}