453 lines · 16.9 KB · Rust
RawDownload
use std::{    any::Any,    borrow::Cow,    io::{BufRead, Read},    sync::{Arc, Mutex},}; use bstr::BStr;use git_packetline::PacketLineRef;pub use traits::{Error, GetResponse, Http, PostResponse}; use crate::client::blocking_io::bufread_ext::ReadlineBufRead;use crate::{    client::{self, capabilities, Capabilities, ExtendedBufRead, HandleProgress, MessageKind, RequestWriter},    Protocol, Service,}; #[cfg(all(feature = "http-client-reqwest", feature = "http-client-curl"))]compile_error!("Cannot set both 'http-client-reqwest' and 'http-client-curl' features as they are mutually exclusive"); #[cfg(feature = "http-client-curl")]mod curl; /// The experimental `reqwest` backend.////// It doesn't support any of the shared http options yet, but can be seen as example on how to integrate blocking `http` backends./// There is also nothing that would prevent it from becoming a fully-featured HTTP backend except for demand and time.#[cfg(feature = "http-client-reqwest")]pub mod reqwest; mod traits; ///pub mod options {    /// A function to authenticate a URL.    pub type AuthenticateFn =        dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + Send + Sync;     /// Possible settings for the `http.followRedirects` configuration option.    #[derive(Debug, Copy, Clone, PartialEq, Eq)]    pub enum FollowRedirects {        /// Follow only the first redirect request, most suitable for typical git requests.        Initial,        /// Follow all redirect requests from the server unconditionally        All,        /// Follow no redirect request.        None,    }     impl Default for FollowRedirects {        fn default() -> Self {            FollowRedirects::Initial        }    }     /// The way to configure a proxy for authentication if a username is present in the configured proxy.    #[derive(Debug, Copy, Clone, PartialEq, Eq)]    pub enum ProxyAuthMethod {        /// Automatically pick a suitable authentication method.        AnyAuth,        ///HTTP basic authentication.        Basic,        /// Http digest authentication to prevent a password to be passed in clear text.        Digest,        /// GSS negotiate authentication.        Negotiate,        /// NTLM authentication        Ntlm,    }     impl Default for ProxyAuthMethod {        fn default() -> Self {            ProxyAuthMethod::AnyAuth        }    }} /// Options to configure http requests.// TODO: testing most of these fields requires a lot of effort, unless special flags to introspect ongoing requests are added.#[derive(Default, Clone)]pub struct Options {    /// Headers to be added to every request.    /// They are applied unconditionally and are expected to be valid as they occour in an HTTP request, like `header: value`, without newlines.    ///    /// Refers to `http.extraHeader` multi-var.    pub extra_headers: Vec<String>,    /// How to handle redirects.    ///    /// Refers to `http.followRedirects`.    pub follow_redirects: options::FollowRedirects,    /// Used in conjunction with `low_speed_time_seconds`, any non-0 value signals the amount of bytes per second at least to avoid    /// aborting the connection.    ///    /// Refers to `http.lowSpeedLimit`.    pub low_speed_limit_bytes_per_second: u32,    /// Used in conjunction with `low_speed_bytes_per_second`, any non-0 value signals the amount seconds the minimal amount    /// of bytes per second isn't reached.    ///    /// Refers to `http.lowSpeedTime`.    pub low_speed_time_seconds: u64,    /// A curl-style proxy declaration of the form `[protocol://][user[:password]@]proxyhost[:port]`.    ///    /// Refers to `http.proxy`.    pub proxy: Option<String>,    /// The comma-separated list of hosts to not send through the `proxy`, or `*` to entirely disable all proxying.    pub no_proxy: Option<String>,    /// The way to authenticate against the proxy if the `proxy` field contains a username.    ///    /// Refers to `http.proxyAuthMethod`.    pub proxy_auth_method: options::ProxyAuthMethod,    /// If authentication is needed for the proxy as its URL contains a username, this method must be set to provide a password    /// for it before making the request, and to store it if the connection succeeds.    pub proxy_authenticate: Option<(        git_credentials::helper::Action,        Arc<std::sync::Mutex<options::AuthenticateFn>>,    )>,    /// The `HTTP` `USER_AGENT` string presented to an `HTTP` server, notably not the user agent present to the `git` server.    ///    /// If not overridden, it defaults to the user agent provided by `curl`, which is a deviation from how `git` handles this.    /// Thus it's expected from the callers to set it to their application, or use higher-level crates which make it easy to do this    /// more correctly.    ///    /// Using the correct user-agent might affect how the server treats the request.    ///    /// Refers to `http.userAgent`.    pub user_agent: Option<String>,    /// The amount of time we wait until aborting a connection attempt.    ///    /// If `None`, this typically defaults to 2 minutes to 5 minutes.    /// Refers to `gitoxide.http.connectTimeout`.    pub connect_timeout: Option<std::time::Duration>,    /// If enabled, emit additional information about connections and possibly the data received or written.    pub verbose: bool,    /// Backend specific options, if available.    pub backend: Option<Arc<Mutex<dyn Any + Send + Sync + 'static>>>,} /// The actual http client implementation, using curl#[cfg(feature = "http-client-curl")]pub type Impl = curl::Curl;/// The actual http client implementation, using reqwest#[cfg(feature = "http-client-reqwest")]pub type Impl = reqwest::Remote; /// A transport for supporting arbitrary http clients by abstracting interactions with them into the [Http] trait.pub struct Transport<H: Http> {    url: String,    user_agent_header: &'static str,    desired_version: Protocol,    actual_version: Protocol,    http: H,    service: Option<Service>,    line_provider: Option<git_packetline::StreamingPeekableIter<H::ResponseBody>>,    identity: Option<git_sec::identity::Account>,} impl<H: Http> Transport<H> {    /// Create a new instance with `http` as implementation to communicate to `url` using the given `desired_version`.    /// Note that we will always fallback to other versions as supported by the server.    pub fn new_http(http: H, url: &str, desired_version: Protocol) -> Self {        Transport {            url: url.to_owned(),            user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")),            desired_version,            actual_version: Default::default(),            service: None,            http,            line_provider: None,            identity: None,        }    }} #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]impl Transport<Impl> {    /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol.    ///    /// Note that the actual implementation depends on feature toggles.    pub fn new(url: &str, desired_version: Protocol) -> Self {        Self::new_http(Impl::default(), url, desired_version)    }} impl<H: Http> Transport<H> {    fn check_content_type(service: Service, kind: &str, headers: <H as Http>::Headers) -> Result<(), client::Error> {        let wanted_content_type = format!("application/x-{}-{}", service.as_str(), kind);        if !headers.lines().collect::<Result<Vec<_>, _>>()?.iter().any(|l| {            let mut tokens = l.split(':');            tokens.next().zip(tokens.next()).map_or(false, |(name, value)| {                name.eq_ignore_ascii_case("content-type") && value.trim() == wanted_content_type            })        }) {            return Err(client::Error::Http(Error::Detail {                description: format!(                    "Didn't find '{}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported.",                    wanted_content_type                ),            }));        }        Ok(())    }     #[allow(clippy::unnecessary_wraps, unknown_lints)]    fn add_basic_auth_if_present(&self, headers: &mut Vec<Cow<'_, str>>) -> Result<(), client::Error> {        if let Some(git_sec::identity::Account { username, password }) = &self.identity {            #[cfg(not(debug_assertions))]            if self.url.starts_with("http://") {                return Err(client::Error::AuthenticationRefused(                    "Will not send credentials in clear text over http",                ));            }            headers.push(Cow::Owned(format!(                "Authorization: Basic {}",                base64::encode(format!("{}:{}", username, password))            )))        }        Ok(())    }} fn append_url(base: &str, suffix: &str) -> String {    let mut buf = base.to_owned();    if base.as_bytes().last() != Some(&b'/') {        buf.push('/');    }    buf.push_str(suffix);    buf} impl<H: Http> client::TransportWithoutIO for Transport<H> {    fn set_identity(&mut self, identity: git_sec::identity::Account) -> Result<(), client::Error> {        self.identity = Some(identity);        Ok(())    }     fn request(        &mut self,        write_mode: client::WriteMode,        on_into_read: MessageKind,    ) -> Result<RequestWriter<'_>, client::Error> {        let service = self.service.expect("handshake() must have been called first");        let url = append_url(&self.url, service.as_str());        let static_headers = &[            Cow::Borrowed(self.user_agent_header),            Cow::Owned(format!("Content-Type: application/x-{}-request", service.as_str())),            format!("Accept: application/x-{}-result", service.as_str()).into(),        ];        let mut dynamic_headers = Vec::new();        self.add_basic_auth_if_present(&mut dynamic_headers)?;        if self.actual_version != Protocol::V1 {            dynamic_headers.push(Cow::Owned(format!(                "Git-Protocol: version={}",                self.actual_version as usize            )));        }         let PostResponse {            headers,            body,            post_body,        } = self.http.post(            &url,            &self.url,            static_headers.iter().chain(&dynamic_headers),            write_mode.into(),        )?;        let line_provider = self            .line_provider            .as_mut()            .expect("handshake to have been called first");        line_provider.replace(body);        Ok(RequestWriter::new_from_bufread(            post_body,            Box::new(HeadersThenBody::<H, _> {                service,                headers: Some(headers),                body: line_provider.as_read_without_sidebands(),            }),            write_mode,            on_into_read,        ))    }     fn to_url(&self) -> Cow<'_, BStr> {        Cow::Borrowed(self.url.as_str().into())    }     fn connection_persists_across_multiple_requests(&self) -> bool {        false    }     fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {        self.http.configure(config)    }} impl<H: Http> client::Transport for Transport<H> {    fn handshake<'a>(        &mut self,        service: Service,        extra_parameters: &'a [(&'a str, Option<&'a str>)],    ) -> Result<client::SetServiceResponse<'_>, client::Error> {        let url = append_url(self.url.as_ref(), &format!("info/refs?service={}", service.as_str()));        let static_headers = [Cow::Borrowed(self.user_agent_header)];        let mut dynamic_headers = Vec::<Cow<'_, str>>::new();        if self.desired_version != Protocol::V1 || !extra_parameters.is_empty() {            let mut parameters = if self.desired_version != Protocol::V1 {                let mut p = format!("version={}", self.desired_version as usize);                if !extra_parameters.is_empty() {                    p.push(':');                }                p            } else {                String::new()            };            parameters.push_str(                &extra_parameters                    .iter()                    .map(|(key, value)| match value {                        Some(value) => format!("{}={}", key, value),                        None => key.to_string(),                    })                    .collect::<Vec<_>>()                    .join(":"),            );            dynamic_headers.push(format!("Git-Protocol: {}", parameters).into());        }        self.add_basic_auth_if_present(&mut dynamic_headers)?;        let GetResponse { headers, body } =            self.http                .get(url.as_ref(), &self.url, static_headers.iter().chain(&dynamic_headers))?;        <Transport<H>>::check_content_type(service, "advertisement", headers)?;         let line_reader = self            .line_provider            .get_or_insert_with(|| git_packetline::StreamingPeekableIter::new(body, &[PacketLineRef::Flush]));         // the service announcement is only sent sometimes depending on the exact server/protocol version/used protocol (http?)        // eat the announcement when its there to avoid errors later (and check that the correct service was announced).        // Ignore the announcement otherwise.        let line_ = line_reader            .peek_line()            .ok_or(client::Error::ExpectedLine("capabilities, version or service"))???;        let line = line_.as_text().ok_or(client::Error::ExpectedLine("text"))?;         if let Some(announced_service) = line.as_bstr().strip_prefix(b"# service=") {            if announced_service != service.as_str().as_bytes() {                return Err(client::Error::Http(Error::Detail {                    description: format!(                        "Expected to see service {:?}, but got {:?}",                        service.as_str(),                        announced_service                    ),                }));            }             line_reader.as_read().read_to_end(&mut Vec::new())?;        }         let capabilities::recv::Outcome {            capabilities,            refs,            protocol: actual_protocol,        } = Capabilities::from_lines_with_version_detection(line_reader)?;        self.actual_version = actual_protocol;        self.service = Some(service);        Ok(client::SetServiceResponse {            actual_protocol,            capabilities,            refs,        })    }} struct HeadersThenBody<H: Http, B: Unpin> {    service: Service,    headers: Option<H::Headers>,    body: B,} impl<H: Http, B: Unpin> HeadersThenBody<H, B> {    fn handle_headers(&mut self) -> std::io::Result<()> {        if let Some(headers) = self.headers.take() {            <Transport<H>>::check_content_type(self.service, "result", headers)                .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?        }        Ok(())    }} impl<H: Http, B: Read + Unpin> Read for HeadersThenBody<H, B> {    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {        self.handle_headers()?;        self.body.read(buf)    }} impl<H: Http, B: BufRead + Unpin> BufRead for HeadersThenBody<H, B> {    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {        self.handle_headers()?;        self.body.fill_buf()    }     fn consume(&mut self, amt: usize) {        self.body.consume(amt)    }} impl<H: Http, B: ReadlineBufRead + Unpin> ReadlineBufRead for HeadersThenBody<H, B> {    fn readline(&mut self) -> Option<std::io::Result<Result<PacketLineRef<'_>, git_packetline::decode::Error>>> {        if let Err(err) = self.handle_headers() {            return Some(Err(err));        }        self.body.readline()    }} impl<H: Http, B: ExtendedBufRead + Unpin> ExtendedBufRead for HeadersThenBody<H, B> {    fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {        self.body.set_progress_handler(handle_progress)    }     fn peek_data_line(&mut self) -> Option<std::io::Result<Result<&[u8], client::Error>>> {        if let Err(err) = self.handle_headers() {            return Some(Err(err));        }        self.body.peek_data_line()    }     fn reset(&mut self, version: Protocol) {        self.body.reset(version)    }     fn stopped_at(&self) -> Option<MessageKind> {        self.body.stopped_at()    }} /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> {    Transport::new_http(http, url, desired_version)} /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {    Transport::new(url, desired_version)} ///#[cfg(feature = "http-client-curl")]pub mod redirect;