Sebastian Thielrefactorba1d8836 years agoHistory
261 lines · 8.5 KB · Rust
RawDownload
use crate::{    client::{self, capabilities, ExtendedBufRead, HandleProgress, RequestWriter},    Protocol, Service,};use git_packetline::PacketLine;use std::{    borrow::Cow,    convert::Infallible,    io::{self, BufRead, Read},}; #[cfg(feature = "http-client-curl")]pub(crate) mod curl; mod traits;use crate::client::MessageKind;pub use traits::{Error, GetResponse, Http, PostResponse}; #[cfg(feature = "http-client-curl")]pub type Impl = curl::Curl; pub struct Transport<H: Http> {    url: String,    user_agent_header: &'static str,    desired_version: crate::Protocol,    actual_version: crate::Protocol,    http: H,    service: Option<Service>,    line_provider: Option<git_packetline::Provider<H::ResponseBody>>,    identity: Option<client::Identity>,} impl Transport<Impl> {    pub fn new(url: &str, version: crate::Protocol) -> Self {        Transport {            url: url.to_owned(),            user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")),            desired_version: version,            actual_version: version,            service: None,            http: Impl::default(),            line_provider: None,            identity: None,        }    }} 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!("Content-Type: application/x-{}-{}", service.as_str(), kind);        if !headers            .lines()            .collect::<Result<Vec<_>, _>>()?            .iter()            .any(|l| l == &wanted_content_type)        {            return Err(client::Error::Http(Error::Detail(format!(                "Didn't find '{}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported.",                wanted_content_type            ))));        }        Ok(())    }     fn add_basic_auth_if_present(&self, headers: &mut Vec<Cow<'_, str>>) -> Result<(), client::Error> {        if let Some(identity) = &self.identity {            match identity {                client::Identity::Account { username, password } => {                    #[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 {    if base.ends_with('/') {        format!("{}{}", base, suffix)    } else {        format!("{}/{}", base, suffix)    }} impl<H: Http> client::Transport for Transport<H> {    fn handshake(&mut self, service: Service) -> Result<client::SetServiceResponse<'_>, client::Error> {        let url = append_url(&self.url, &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 {            dynamic_headers.push(Cow::Owned(format!(                "Git-Protocol: version={}",                self.desired_version as usize            )));        }        self.add_basic_auth_if_present(&mut dynamic_headers)?;        let GetResponse { headers, body } = self.http.get(&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::Provider::new(body, &[PacketLine::Flush]));         let mut announced_service = String::new();        line_reader.as_read().read_to_string(&mut announced_service)?;        let expected_service_announcement = format!("# service={}", service.as_str());        if announced_service.trim() != expected_service_announcement {            return Err(client::Error::Http(Error::Detail(format!(                "Expected to see {:?}, but got {:?}",                expected_service_announcement,                announced_service.trim()            ))));        }         let capabilities::recv::Outcome {            capabilities,            refs,            protocol: actual_protocol,        } = capabilities::recv::v1_or_v2_as_detected(line_reader)?;        self.actual_version = actual_protocol;        self.service = Some(service);        Ok(client::SetServiceResponse {            actual_protocol,            capabilities,            refs,        })    }     fn set_identity(&mut self, identity: client::Identity) -> Result<(), client::Error> {        self.identity = Some(identity);        Ok(())    }     fn request(        &mut self,        write_mode: client::WriteMode,        on_into_read: client::MessageKind,    ) -> Result<client::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(),            "Expect:".into(), // needed to avoid sending Expect: 100-continue, which adds another response and only CURL wants that        ];        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, static_headers.iter().chain(&dynamic_headers))?;        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 close(&mut self) -> Result<(), client::Error> {        Ok(())    }     fn to_url(&self) -> String {        self.url.to_owned()    }     fn desired_protocol_version(&self) -> Protocol {        self.desired_version    }     fn is_stateful(&self) -> bool {        false    }} struct HeadersThenBody<H: Http, B> {    service: Service,    headers: Option<H::Headers>,    body: B,} impl<H: Http, B> HeadersThenBody<H, B> {    fn handle_headers(&mut self) -> io::Result<()> {        if let Some(headers) = self.headers.take() {            <Transport<H>>::check_content_type(self.service, "result", headers)                .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?        }        Ok(())    }} impl<H: Http, B: ExtendedBufRead> io::Read for HeadersThenBody<H, B> {    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {        self.handle_headers()?;        self.body.read(buf)    }} impl<H: Http, B: ExtendedBufRead> io::BufRead for HeadersThenBody<H, B> {    fn fill_buf(&mut self) -> io::Result<&[u8]> {        self.handle_headers()?;        self.body.fill_buf()    }     fn consume(&mut self, amt: usize) {        self.body.consume(amt)    }} impl<H: Http, B: ExtendedBufRead> 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<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()    }} pub fn connect(url: &str, version: crate::Protocol) -> Result<Transport<Impl>, Infallible> {    Ok(Transport::new(url, version))}