diff options
Diffstat (limited to 'vendor/gix-transport/src')
29 files changed, 4421 insertions, 0 deletions
diff --git a/vendor/gix-transport/src/client/async_io/bufread_ext.rs b/vendor/gix-transport/src/client/async_io/bufread_ext.rs new file mode 100644 index 000000000..abe206b8a --- /dev/null +++ b/vendor/gix-transport/src/client/async_io/bufread_ext.rs @@ -0,0 +1,126 @@ +use std::{ + io, + ops::{Deref, DerefMut}, +}; + +use async_trait::async_trait; +use futures_io::{AsyncBufRead, AsyncRead}; +use gix_packetline::PacketLineRef; + +use crate::{ + client::{Error, MessageKind}, + Protocol, +}; + +/// A function `f(is_error, text)` receiving progress or error information. +/// As it is not a future itself, it must not block. If IO is performed within the function, be sure to spawn +/// it onto an executor. +pub type HandleProgress = Box<dyn FnMut(bool, &[u8])>; + +/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters, +/// but leave support for reading lines directly without forcing them through `String`. +/// +/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to +/// read pack files while keeping open the option to read individual lines with low overhead. +#[async_trait(?Send)] +pub trait ReadlineBufRead: AsyncBufRead { + /// Read a packet line into the internal buffer and return it. + /// + /// Returns `None` if the end of iteration is reached because of one of the following: + /// + /// * natural EOF + /// * ERR packet line encountered + /// * A `delimiter` packet line encountered + async fn readline( + &mut self, + ) -> Option<io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>>; +} + +/// Provide even more access to the underlying packet reader. +#[async_trait(?Send)] +pub trait ExtendedBufRead: ReadlineBufRead { + /// Set the handler to which progress will be delivered. + /// + /// Note that this is only possible if packet lines are sent in side band mode. + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>); + /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using + /// [`stopped_at()`][ExtendedBufRead::stopped_at()]. + async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>>; + /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the + /// given protocol. + fn reset(&mut self, version: Protocol); + /// Return the kind of message at which the reader stopped. + fn stopped_at(&self) -> Option<MessageKind>; +} + +#[async_trait(?Send)] +impl<'a, T: ReadlineBufRead + ?Sized + 'a + Unpin> ReadlineBufRead for Box<T> { + async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + self.deref_mut().readline().await + } +} + +#[async_trait(?Send)] +impl<'a, T: ExtendedBufRead + ?Sized + 'a + Unpin> ExtendedBufRead for Box<T> { + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) { + self.deref_mut().set_progress_handler(handle_progress) + } + + async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> { + self.deref_mut().peek_data_line().await + } + + fn reset(&mut self, version: Protocol) { + self.deref_mut().reset(version) + } + + fn stopped_at(&self) -> Option<MessageKind> { + self.deref().stopped_at() + } +} + +#[async_trait(?Send)] +impl<T: AsyncRead + Unpin> ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, for<'b> fn(bool, &'b [u8])> { + async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + self.read_data_line().await + } +} + +#[async_trait(?Send)] +impl<'a, T: AsyncRead + Unpin> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> { + async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + self.read_data_line().await + } +} + +#[async_trait(?Send)] +impl<'a, T: AsyncRead + Unpin> ExtendedBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> { + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) { + self.set_progress_handler(handle_progress) + } + async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> { + match self.peek_data_line().await { + Some(Ok(Ok(line))) => Some(Ok(Ok(line))), + Some(Ok(Err(err))) => Some(Ok(Err(err.into()))), + Some(Err(err)) => Some(Err(err)), + None => None, + } + } + fn reset(&mut self, version: Protocol) { + match version { + Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]), + Protocol::V2 => self.reset_with(&[ + gix_packetline::PacketLineRef::Delimiter, + gix_packetline::PacketLineRef::Flush, + ]), + } + } + fn stopped_at(&self) -> Option<MessageKind> { + self.stopped_at().map(|l| match l { + gix_packetline::PacketLineRef::Flush => MessageKind::Flush, + gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter, + gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd, + gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"), + }) + } +} diff --git a/vendor/gix-transport/src/client/async_io/connect.rs b/vendor/gix-transport/src/client/async_io/connect.rs new file mode 100644 index 000000000..fe2a5808e --- /dev/null +++ b/vendor/gix-transport/src/client/async_io/connect.rs @@ -0,0 +1,47 @@ +pub use crate::client::non_io_types::connect::{Error, Options}; + +#[cfg(any(feature = "async-std"))] +pub(crate) mod function { + use std::convert::TryInto; + + use crate::client::{git, non_io_types::connect::Error}; + + /// A general purpose connector connecting to a repository identified by the given `url`. + /// + /// This includes connections to + /// [git daemons][crate::client::git::connect()] only at the moment. + /// + /// Use `options` to further control specifics of the transport resulting from the connection. + pub async fn connect<Url, E>( + url: Url, + options: super::Options, + ) -> Result<Box<dyn crate::client::Transport + Send>, Error> + where + Url: TryInto<gix_url::Url, Error = E>, + gix_url::parse::Error: From<E>, + { + let mut url = url.try_into().map_err(gix_url::parse::Error::from)?; + Ok(match url.scheme { + gix_url::Scheme::Git => { + if url.user().is_some() { + return Err(Error::UnsupportedUrlTokens { + url: url.to_bstring(), + scheme: url.scheme, + }); + } + let path = std::mem::take(&mut url.path); + Box::new( + git::Connection::new_tcp( + url.host().expect("host is present in url"), + url.port, + path, + options.version, + ) + .await + .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?, + ) + } + scheme => return Err(Error::UnsupportedScheme(scheme)), + }) + } +} diff --git a/vendor/gix-transport/src/client/async_io/mod.rs b/vendor/gix-transport/src/client/async_io/mod.rs new file mode 100644 index 000000000..6cb1a500e --- /dev/null +++ b/vendor/gix-transport/src/client/async_io/mod.rs @@ -0,0 +1,13 @@ +mod bufread_ext; +pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead}; + +mod request; +pub use request::RequestWriter; + +mod traits; +pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; + +/// +pub mod connect; +#[cfg(any(feature = "async-std"))] +pub use connect::function::connect; diff --git a/vendor/gix-transport/src/client/async_io/request.rs b/vendor/gix-transport/src/client/async_io/request.rs new file mode 100644 index 000000000..1121db356 --- /dev/null +++ b/vendor/gix-transport/src/client/async_io/request.rs @@ -0,0 +1,103 @@ +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_io::AsyncWrite; +use pin_project_lite::pin_project; + +use crate::client::{ExtendedBufRead, MessageKind, WriteMode}; + +pin_project! { + /// A [`Write`][io::Write] implementation optimized for writing packet lines. + /// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for + /// obtaining the response. + pub struct RequestWriter<'a> { + on_into_read: MessageKind, + #[pin] + writer: gix_packetline::Writer<Box<dyn AsyncWrite + Unpin + 'a>>, + reader: Box<dyn ExtendedBufRead + Unpin + 'a>, + } +} +impl<'a> futures_io::AsyncWrite for RequestWriter<'a> { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> { + self.project().writer.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + self.project().writer.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + self.project().writer.poll_close(cx) + } +} + +/// methods with bonds to IO +impl<'a> RequestWriter<'a> { + /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the + /// writes are finished, along with configuration for the `write_mode` and information about which message to write + /// when this instance is converted [into a `reader`][RequestWriter::into_read()] to read the request's response. + pub fn new_from_bufread<W: AsyncWrite + Unpin + 'a>( + writer: W, + reader: Box<dyn ExtendedBufRead + Unpin + 'a>, + write_mode: WriteMode, + on_into_read: MessageKind, + ) -> Self { + let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box<dyn AsyncWrite + Unpin>); + match write_mode { + WriteMode::Binary => writer.enable_binary_mode(), + WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), + } + RequestWriter { + on_into_read, + writer, + reader, + } + } + + /// Write the given message as packet line. + pub async fn write_message(&mut self, message: MessageKind) -> io::Result<()> { + match message { + MessageKind::Flush => { + gix_packetline::PacketLineRef::Flush + .write_to(self.writer.inner_mut()) + .await + } + MessageKind::Delimiter => { + gix_packetline::PacketLineRef::Delimiter + .write_to(self.writer.inner_mut()) + .await + } + MessageKind::ResponseEnd => { + gix_packetline::PacketLineRef::ResponseEnd + .write_to(self.writer.inner_mut()) + .await + } + MessageKind::Text(t) => gix_packetline::TextRef::from(t).write_to(self.writer.inner_mut()).await, + } + .map(|_| ()) + } + /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response. + /// + /// Doing so will also write the message type this instance was initialized with. + pub async fn into_read(mut self) -> std::io::Result<Box<dyn ExtendedBufRead + Unpin + 'a>> { + self.write_message(self.on_into_read).await?; + Ok(self.reader) + } + + /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [RequestWriter::into_read()]. + /// + /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the + /// underlying channel. + /// + /// # Note + /// + /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on + /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism + /// is implemented. + pub fn into_parts(self) -> (Box<dyn AsyncWrite + Unpin + 'a>, Box<dyn ExtendedBufRead + Unpin + 'a>) { + (self.writer.into_inner(), self.reader) + } +} diff --git a/vendor/gix-transport/src/client/async_io/traits.rs b/vendor/gix-transport/src/client/async_io/traits.rs new file mode 100644 index 000000000..ea73f5e09 --- /dev/null +++ b/vendor/gix-transport/src/client/async_io/traits.rs @@ -0,0 +1,113 @@ +use std::ops::DerefMut; + +use async_trait::async_trait; +use bstr::BString; +use futures_lite::io::AsyncWriteExt; + +use crate::{ + client::{Capabilities, Error, ExtendedBufRead, MessageKind, TransportWithoutIO, WriteMode}, + Protocol, Service, +}; + +/// The response of the [`handshake()`][Transport::handshake()] method. +pub struct SetServiceResponse<'a> { + /// The protocol the service can provide. May be different from the requested one + pub actual_protocol: Protocol, + /// The capabilities parsed from the server response. + pub capabilities: Capabilities, + /// In protocol version one, this is set to a list of refs and their peeled counterparts. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + Unpin + 'a>>, +} + +/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol] +/// used to connect to them. +/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible. +/// Thus the consumer of this trait will not have to deal with packet lines at all. +/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted. +#[async_trait(?Send)] +pub trait Transport: TransportWithoutIO { + /// Initiate connection to the given service and send the given `extra_parameters` along with it. + /// + /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key` + /// if it is None. + /// + /// Returns the service capabilities according according to the actual [Protocol] it supports, + /// and possibly a list of refs to be obtained. + /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one + /// if [TransportWithoutIO::supported_protocol_versions()] includes it or is empty. + /// Exhaust the returned [BufReader][SetServiceResponse::refs] for a list of references in case of protocol V1 + /// before making another request. + async fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error>; +} + +// Would be nice if the box implementation could auto-forward to all implemented traits. +#[async_trait(?Send)] +impl<T: Transport + ?Sized> Transport for Box<T> { + async fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error> { + self.deref_mut().handshake(service, extra_parameters).await + } +} + +// Would be nice if the box implementation could auto-forward to all implemented traits. +#[async_trait(?Send)] +impl<T: Transport + ?Sized> Transport for &mut T { + async fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error> { + self.deref_mut().handshake(service, extra_parameters).await + } +} + +/// An extension trait to add more methods to everything implementing [`Transport`]. +#[async_trait(?Send)] +pub trait TransportV2Ext { + /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`. + /// The `capabilities` were communicated during the handshake. + /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. + async fn invoke<'a>( + &mut self, + command: &str, + capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a, + arguments: Option<impl Iterator<Item = bstr::BString> + 'a>, + ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error>; +} + +#[async_trait(?Send)] +impl<T: Transport> TransportV2Ext for T { + async fn invoke<'a>( + &mut self, + command: &str, + capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a, + arguments: Option<impl Iterator<Item = BString> + 'a>, + ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error> { + let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush)?; + writer.write_all(format!("command={}", command).as_bytes()).await?; + for (name, value) in capabilities { + match value { + Some(value) => { + writer + .write_all(format!("{}={}", name, value.as_ref()).as_bytes()) + .await + } + None => writer.write_all(name.as_bytes()).await, + }?; + } + if let Some(arguments) = arguments { + writer.write_message(MessageKind::Delimiter).await?; + for argument in arguments { + writer.write_all(argument.as_ref()).await?; + } + } + Ok(writer.into_read().await?) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs b/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs new file mode 100644 index 000000000..5842ddd3d --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs @@ -0,0 +1,114 @@ +use std::{ + io, + ops::{Deref, DerefMut}, +}; + +use gix_packetline::PacketLineRef; + +use crate::{ + client::{Error, MessageKind}, + Protocol, +}; +/// A function `f(is_error, text)` receiving progress or error information. +pub type HandleProgress = Box<dyn FnMut(bool, &[u8])>; + +/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters, +/// but leave support for reading lines directly without forcing them through `String`. +/// +/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to +/// read pack files while keeping open the option to read individual lines with low overhead. +pub trait ReadlineBufRead: io::BufRead { + /// Read a packet line into the internal buffer and return it. + /// + /// Returns `None` if the end of iteration is reached because of one of the following: + /// + /// * natural EOF + /// * ERR packet line encountered + /// * A `delimiter` packet line encountered + fn readline( + &mut self, + ) -> Option<io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>>; +} + +/// Provide even more access to the underlying packet reader. +pub trait ExtendedBufRead: ReadlineBufRead { + /// Set the handler to which progress will be delivered. + /// + /// Note that this is only possible if packet lines are sent in side band mode. + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>); + /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using + /// [`stopped_at()`][ExtendedBufRead::stopped_at()]. + fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>>; + /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the + /// given protocol. + fn reset(&mut self, version: Protocol); + /// Return the kind of message at which the reader stopped. + fn stopped_at(&self) -> Option<MessageKind>; +} + +impl<'a, T: ReadlineBufRead + ?Sized + 'a> ReadlineBufRead for Box<T> { + fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + ReadlineBufRead::readline(self.deref_mut()) + } +} + +impl<'a, T: ExtendedBufRead + ?Sized + 'a> ExtendedBufRead for Box<T> { + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) { + self.deref_mut().set_progress_handler(handle_progress) + } + + fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> { + self.deref_mut().peek_data_line() + } + + fn reset(&mut self, version: Protocol) { + self.deref_mut().reset(version) + } + + fn stopped_at(&self) -> Option<MessageKind> { + self.deref().stopped_at() + } +} + +impl<T: io::Read> ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, fn(bool, &[u8])> { + fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + self.read_data_line() + } +} + +impl<'a, T: io::Read> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> { + fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> { + self.read_data_line() + } +} + +impl<'a, T: io::Read> ExtendedBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> { + fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) { + self.set_progress_handler(handle_progress) + } + fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> { + match self.peek_data_line() { + Some(Ok(Ok(line))) => Some(Ok(Ok(line))), + Some(Ok(Err(err))) => Some(Ok(Err(err.into()))), + Some(Err(err)) => Some(Err(err)), + None => None, + } + } + fn reset(&mut self, version: Protocol) { + match version { + Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]), + Protocol::V2 => self.reset_with(&[ + gix_packetline::PacketLineRef::Delimiter, + gix_packetline::PacketLineRef::Flush, + ]), + } + } + fn stopped_at(&self) -> Option<MessageKind> { + self.stopped_at().map(|l| match l { + gix_packetline::PacketLineRef::Flush => MessageKind::Flush, + gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter, + gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd, + gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"), + }) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/connect.rs b/vendor/gix-transport/src/client/blocking_io/connect.rs new file mode 100644 index 000000000..de3334f18 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/connect.rs @@ -0,0 +1,68 @@ +pub use crate::client::non_io_types::connect::{Error, Options}; + +pub(crate) mod function { + use std::convert::TryInto; + + use crate::client::{non_io_types::connect::Error, Transport}; + + /// A general purpose connector connecting to a repository identified by the given `url`. + /// + /// This includes connections to + /// [local repositories][crate::client::file::connect()], + /// [repositories over ssh][crate::client::ssh::connect()], + /// [git daemons][crate::client::git::connect()], + /// and if compiled in connections to [git repositories over https][crate::client::http::connect()]. + /// + /// Use `options` to further control specifics of the transport resulting from the connection. + pub fn connect<Url, E>(url: Url, options: super::Options) -> Result<Box<dyn Transport + Send>, Error> + where + Url: TryInto<gix_url::Url, Error = E>, + gix_url::parse::Error: From<E>, + { + let mut url = url.try_into().map_err(gix_url::parse::Error::from)?; + Ok(match url.scheme { + gix_url::Scheme::Ext(_) => return Err(Error::UnsupportedScheme(url.scheme)), + gix_url::Scheme::File => { + if url.user().is_some() || url.host().is_some() || url.port.is_some() { + return Err(Error::UnsupportedUrlTokens { + url: url.to_bstring(), + scheme: url.scheme, + }); + } + Box::new( + crate::client::blocking_io::file::connect(url.path, options.version) + .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?, + ) + } + gix_url::Scheme::Ssh => Box::new({ + crate::client::blocking_io::ssh::connect(url, options.version, options.ssh) + .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)? + }), + gix_url::Scheme::Git => { + if url.user().is_some() { + return Err(Error::UnsupportedUrlTokens { + url: url.to_bstring(), + scheme: url.scheme, + }); + } + Box::new({ + let path = std::mem::take(&mut url.path); + crate::client::git::connect( + url.host().expect("host is present in url"), + path, + options.version, + url.port, + ) + .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)? + }) + } + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + gix_url::Scheme::Https | gix_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)), + #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] + gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new(crate::client::http::connect( + &url.to_bstring().to_string(), + options.version, + )), + }) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/file.rs b/vendor/gix-transport/src/client/blocking_io/file.rs new file mode 100644 index 000000000..d80fe55df --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/file.rs @@ -0,0 +1,288 @@ +use std::{ + any::Any, + borrow::Cow, + error::Error, + ffi::{OsStr, OsString}, + io::Write, + process::{self, Stdio}, +}; + +use bstr::{io::BufReadExt, BStr, BString, ByteSlice}; + +use crate::{ + client::{self, git, ssh, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, + Protocol, Service, +}; + +// from https://github.com/git/git/blob/20de7e7e4f4e9ae52e6cc7cfaa6469f186ddb0fa/environment.c#L115:L115 +const ENV_VARS_TO_REMOVE: &[&str] = &[ + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CONFIG", + "GIT_CONFIG_PARAMETERS", + "GIT_OBJECT_DIRECTORY", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_IMPLICIT_WORK_TREE", + "GIT_GRAFT_FILE", + "GIT_INDEX_FILE", + "GIT_NO_REPLACE_OBJECTS", + "GIT_REPLACE_REF_BASE", + "GIT_PREFIX", + "GIT_INTERNAL_SUPER_PREFIX", + "GIT_SHALLOW_FILE", + "GIT_COMMON_DIR", + "GIT_CONFIG_COUNT", +]; + +/// A utility to spawn a helper process to actually transmit data, possibly over `ssh`. +/// +/// It can only be instantiated using the local [`connect()`] or [ssh connect][crate::client::ssh::connect()]. +pub struct SpawnProcessOnDemand { + desired_version: Protocol, + url: gix_url::Url, + path: BString, + ssh_cmd: Option<(OsString, ssh::ProgramKind)>, + /// The environment variables to set in the invoked command. + envs: Vec<(&'static str, String)>, + ssh_disallow_shell: bool, + connection: Option<git::Connection<Box<dyn std::io::Read + Send>, process::ChildStdin>>, + child: Option<process::Child>, +} + +impl SpawnProcessOnDemand { + pub(crate) fn new_ssh( + url: gix_url::Url, + program: impl Into<OsString>, + path: BString, + ssh_kind: ssh::ProgramKind, + ssh_disallow_shell: bool, + version: Protocol, + ) -> SpawnProcessOnDemand { + SpawnProcessOnDemand { + url, + path, + ssh_cmd: Some((program.into(), ssh_kind)), + envs: Default::default(), + ssh_disallow_shell, + child: None, + connection: None, + desired_version: version, + } + } + fn new_local(path: BString, version: Protocol) -> SpawnProcessOnDemand { + SpawnProcessOnDemand { + url: gix_url::Url::from_parts_as_alternative_form(gix_url::Scheme::File, None, None, None, path.clone()) + .expect("valid url"), + path, + ssh_cmd: None, + envs: (version != Protocol::V1) + .then(|| vec![("GIT_PROTOCOL", format!("version={}", version as usize))]) + .unwrap_or_default(), + ssh_disallow_shell: false, + child: None, + connection: None, + desired_version: version, + } + } +} + +impl client::TransportWithoutIO for SpawnProcessOnDemand { + fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), client::Error> { + if self.url.scheme == gix_url::Scheme::Ssh { + self.url + .set_user((!identity.username.is_empty()).then_some(identity.username)); + Ok(()) + } else { + Err(client::Error::AuthenticationUnsupported) + } + } + + fn request( + &mut self, + write_mode: WriteMode, + on_into_read: MessageKind, + ) -> Result<RequestWriter<'_>, client::Error> { + self.connection + .as_mut() + .expect("handshake() to have been called first") + .request(write_mode, on_into_read) + } + + fn to_url(&self) -> Cow<'_, BStr> { + Cow::Owned(self.url.to_bstring()) + } + + fn connection_persists_across_multiple_requests(&self) -> bool { + true + } + + fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { + Ok(()) + } +} + +struct ReadStdoutFailOnError { + recv: std::sync::mpsc::Receiver<std::io::Error>, + read: std::process::ChildStdout, +} + +fn supervise_stderr( + ssh_kind: ssh::ProgramKind, + stderr: std::process::ChildStderr, + stdout: std::process::ChildStdout, +) -> ReadStdoutFailOnError { + impl ReadStdoutFailOnError { + fn swap_err_if_present_in_stderr(&self, wanted: usize, res: std::io::Result<usize>) -> std::io::Result<usize> { + match self.recv.try_recv().ok() { + Some(err) => Err(err), + None => match res { + Ok(n) if n == wanted => Ok(n), + Ok(n) => { + // TODO: fix this + // When parsing refs this seems to happen legitimately + // (even though we read packet lines only and should always know exactly how much to read) + // Maybe this still happens in `read_exact()` as sometimes we just don't get enough bytes + // despite knowing how many. + // To prevent deadlock, we have to set a timeout which slows down legitimate parts of the protocol. + // This code was specifically written to make the `cargo` test-suite pass, and we can reduce + // the timeouts even more once there is a native ssh transport that is used by `cargo`, it will + // be able to handle these properly. + // Alternatively, one could implement something like `read2` to avoid blocking on stderr entirely. + self.recv + .recv_timeout(std::time::Duration::from_millis(5)) + .ok() + .map(Err) + .unwrap_or(Ok(n)) + } + Err(err) => Err(self.recv.recv().ok().unwrap_or(err)), + }, + } + } + } + impl std::io::Read for ReadStdoutFailOnError { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + let res = self.read.read(buf); + self.swap_err_if_present_in_stderr(buf.len(), res) + } + } + + let (send, recv) = std::sync::mpsc::sync_channel(1); + std::thread::Builder::new() + .name("supervise ssh stderr".into()) + .stack_size(128 * 1024) + .spawn(move || -> std::io::Result<()> { + let mut process_stderr = std::io::stderr(); + for line in std::io::BufReader::new(stderr).byte_lines() { + let line = line?; + match ssh_kind.line_to_err(line.into()) { + Ok(err) => { + send.send(err).ok(); + } + Err(line) => { + process_stderr.write_all(&line).ok(); + writeln!(&process_stderr).ok(); + } + } + } + Ok(()) + }) + .expect("named threads with small stack work on all platforms"); + ReadStdoutFailOnError { read: stdout, recv } +} + +impl client::Transport for SpawnProcessOnDemand { + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, client::Error> { + let (mut cmd, ssh_kind, cmd_name) = match &self.ssh_cmd { + Some((command, kind)) => ( + kind.prepare_invocation(command, &self.url, self.desired_version, self.ssh_disallow_shell) + .map_err(client::Error::SshInvocation)? + .stderr(Stdio::piped()), + Some(*kind), + Cow::Owned(command.to_owned()), + ), + None => ( + gix_command::prepare(service.as_str()).stderr(Stdio::null()), + None, + Cow::Borrowed(OsStr::new(service.as_str())), + ), + }; + cmd.stdin = Stdio::piped(); + cmd.stdout = Stdio::piped(); + let repo_path = if self.ssh_cmd.is_some() { + cmd.args.push(service.as_str().into()); + gix_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned() + } else { + self.path.to_os_str_lossy().into_owned() + }; + cmd.args.push(repo_path); + + let mut cmd = std::process::Command::from(cmd); + for env_to_remove in ENV_VARS_TO_REMOVE { + cmd.env_remove(env_to_remove); + } + cmd.envs(std::mem::take(&mut self.envs)); + + let mut child = cmd.spawn().map_err(|err| client::Error::InvokeProgram { + source: err, + command: cmd_name.into_owned(), + })?; + let stdout: Box<dyn std::io::Read + Send> = match ssh_kind { + Some(ssh_kind) => Box::new(supervise_stderr( + ssh_kind, + child.stderr.take().expect("configured beforehand"), + child.stdout.take().expect("configured"), + )), + None => Box::new(child.stdout.take().expect("stdout configured")), + }; + self.connection = Some(git::Connection::new_for_spawned_process( + stdout, + child.stdin.take().expect("stdin configured"), + self.desired_version, + self.path.clone(), + )); + self.child = Some(child); + self.connection + .as_mut() + .expect("connection to be there right after setting it") + .handshake(service, extra_parameters) + } +} + +/// Connect to a locally readable repository at `path` using the given `desired_version`. +/// +/// This will spawn a `git` process locally. +pub fn connect( + path: impl Into<BString>, + desired_version: Protocol, +) -> Result<SpawnProcessOnDemand, std::convert::Infallible> { + Ok(SpawnProcessOnDemand::new_local(path.into(), desired_version)) +} + +#[cfg(test)] +mod tests { + mod ssh { + mod connect { + use crate::{client::blocking_io::ssh::connect, Protocol}; + + #[test] + fn path() { + for (url, expected) in [ + ("ssh://host.xy/~/repo", "~/repo"), + ("ssh://host.xy/~username/repo", "~username/repo"), + ("user@host.xy:/username/repo", "/username/repo"), + ("user@host.xy:username/repo", "username/repo"), + ("user@host.xy:../username/repo", "../username/repo"), + ("user@host.xy:~/repo", "~/repo"), + ] { + let url = gix_url::parse((*url).into()).expect("valid url"); + let cmd = connect(url, Protocol::V1, Default::default()).expect("parse success"); + assert_eq!(cmd.path, expected, "the path will be substituted by the remote shell"); + } + } + } + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs new file mode 100644 index 000000000..ad980dd20 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -0,0 +1,169 @@ +use std::{ + sync::mpsc::{Receiver, SyncSender}, + thread, +}; + +use gix_features::io; + +use crate::client::{blocking_io::http, http::traits::PostBodyDataKind}; + +mod remote; + +/// Options to configure the `curl` HTTP handler. +#[derive(Default)] +pub struct Options { + /// If `true` and runtime configuration is possible for `curl` backends, certificates revocation will be checked. + /// + /// This only works on windows apparently. Ignored if `None`. + pub schannel_check_revoke: Option<bool>, +} + +/// The error returned by the 'remote' helper, a purely internal construct to perform http requests. +/// +/// It can be used for downcasting errors, which are boxed to hide the actual implementation. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Curl(#[from] curl::Error), + #[error(transparent)] + Redirect(#[from] http::redirect::Error), + #[error("Could not finish reading all data to post to the remote")] + ReadPostBody(#[from] std::io::Error), + #[error(transparent)] + Authenticate(#[from] gix_credentials::protocol::Error), +} + +impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Curl(err) => curl_is_spurious(err), + _ => false, + } + } +} + +pub(crate) fn curl_is_spurious(err: &curl::Error) -> bool { + err.is_couldnt_connect() + || err.is_couldnt_resolve_proxy() + || err.is_couldnt_resolve_host() + || err.is_operation_timedout() + || err.is_recv_error() + || err.is_send_error() + || err.is_http2_error() + || err.is_http2_stream_error() + || err.is_ssl_connect_error() + || err.is_partial_file() +} + +/// A utility to abstract interactions with curl handles. +pub struct Curl { + req: SyncSender<remote::Request>, + res: Receiver<remote::Response>, + handle: Option<thread::JoinHandle<Result<(), Error>>>, + config: http::Options, +} + +impl Curl { + fn restore_thread_after_failure(&mut self) -> http::Error { + let err_that_brought_thread_down = self + .handle + .take() + .expect("thread handle present") + .join() + .expect("handler thread should never panic") + .expect_err("something should have gone wrong with curl (we join on error only)"); + let (handle, req, res) = remote::new(); + self.handle = Some(handle); + self.req = req; + self.res = res; + err_that_brought_thread_down.into() + } + + fn make_request( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + upload_body_kind: Option<PostBodyDataKind>, + ) -> Result<http::PostResponse<io::pipe::Reader, io::pipe::Reader, io::pipe::Writer>, http::Error> { + let mut list = curl::easy::List::new(); + for header in headers { + list.append(header.as_ref())?; + } + if self + .req + .send(remote::Request { + url: url.to_owned(), + base_url: base_url.to_owned(), + headers: list, + upload_body_kind, + config: self.config.clone(), + }) + .is_err() + { + return Err(self.restore_thread_after_failure()); + } + let remote::Response { + headers, + body, + upload_body, + } = match self.res.recv() { + Ok(res) => res, + Err(_) => return Err(self.restore_thread_after_failure()), + }; + Ok(http::PostResponse { + post_body: upload_body, + headers, + body, + }) + } +} + +impl Default for Curl { + fn default() -> Self { + let (handle, req, res) = remote::new(); + Curl { + handle: Some(handle), + req, + res, + config: http::Options::default(), + } + } +} + +#[allow(clippy::type_complexity)] +impl http::Http for Curl { + type Headers = io::pipe::Reader; + type ResponseBody = io::pipe::Reader; + type PostBody = io::pipe::Writer; + + fn get( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + ) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> { + self.make_request(url, base_url, headers, None).map(Into::into) + } + + fn post( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + body: PostBodyDataKind, + ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> { + self.make_request(url, base_url, headers, Some(body)) + } + + fn configure( + &mut self, + config: &dyn std::any::Any, + ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { + if let Some(config) = config.downcast_ref::<http::Options>() { + self.config = config.clone(); + } + Ok(()) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs new file mode 100644 index 000000000..8f5867698 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -0,0 +1,392 @@ +use std::{ + io, + io::{Read, Write}, + sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError}, + thread, + time::Duration, +}; + +use curl::easy::{Auth, Easy2}; +use gix_features::io::pipe; + +use crate::client::{ + blocking_io::http::{self, curl::Error, redirect}, + http::{ + curl::curl_is_spurious, + options::{FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion}, + traits::PostBodyDataKind, + }, +}; + +enum StreamOrBuffer { + Stream(pipe::Reader), + Buffer(std::io::Cursor<Vec<u8>>), +} + +#[derive(Default)] +struct Handler { + send_header: Option<pipe::Writer>, + send_data: Option<pipe::Writer>, + receive_body: Option<StreamOrBuffer>, + checked_status: bool, + last_status: usize, + follow: FollowRedirects, +} + +impl Handler { + fn reset(&mut self) { + self.checked_status = false; + self.last_status = 0; + self.follow = FollowRedirects::default(); + } + fn parse_status_inner(data: &[u8]) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> { + let code = data + .split(|b| *b == b' ') + .nth(1) + .ok_or("Expected HTTP/<VERSION> STATUS")?; + let code = std::str::from_utf8(code)?; + code.parse().map_err(Into::into) + } + fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Box<dyn std::error::Error + Send + Sync>)> { + let valid_end = match follow { + FollowRedirects::Initial | FollowRedirects::All => 308, + FollowRedirects::None => 299, + }; + match Self::parse_status_inner(data) { + Ok(status) if !(200..=valid_end).contains(&status) => { + Some((status, format!("Received HTTP status {status}").into())) + } + Ok(_) => None, + Err(err) => Some((500, err)), + } + } +} + +impl curl::easy::Handler for Handler { + fn write(&mut self, data: &[u8]) -> Result<usize, curl::easy::WriteError> { + drop(self.send_header.take()); // signal header readers to stop trying + match self.send_data.as_mut() { + Some(writer) => writer.write_all(data).map(|_| data.len()).or(Ok(0)), + None => Ok(0), // nothing more to receive, reader is done + } + } + fn read(&mut self, data: &mut [u8]) -> Result<usize, curl::easy::ReadError> { + match self.receive_body.as_mut() { + Some(StreamOrBuffer::Stream(reader)) => reader.read(data).map_err(|_err| curl::easy::ReadError::Abort), + Some(StreamOrBuffer::Buffer(cursor)) => cursor.read(data).map_err(|_err| curl::easy::ReadError::Abort), + None => Ok(0), // nothing more to read/writer depleted + } + } + + fn header(&mut self, data: &[u8]) -> bool { + if let Some(writer) = self.send_header.as_mut() { + if self.checked_status { + writer.write_all(data).ok(); + } else { + self.checked_status = true; + self.last_status = 200; + if let Some((status, err)) = Handler::parse_status(data, self.follow) { + self.last_status = status; + writer + .channel + .send(Err(io::Error::new( + if status == 401 { + io::ErrorKind::PermissionDenied + } else if (500..600).contains(&status) { + io::ErrorKind::ConnectionAborted + } else { + io::ErrorKind::Other + }, + err, + ))) + .ok(); + } + } + }; + true + } +} + +pub struct Request { + pub url: String, + pub base_url: String, + pub headers: curl::easy::List, + pub upload_body_kind: Option<PostBodyDataKind>, + pub config: http::Options, +} + +pub struct Response { + pub headers: pipe::Reader, + pub body: pipe::Reader, + pub upload_body: pipe::Writer, +} + +pub fn new() -> ( + thread::JoinHandle<Result<(), Error>>, + SyncSender<Request>, + Receiver<Response>, +) { + let (req_send, req_recv) = sync_channel(0); + let (res_send, res_recv) = sync_channel(0); + let handle = std::thread::spawn(move || -> Result<(), Error> { + let mut handle = Easy2::new(Handler::default()); + // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway. + handle.pipewait(false)?; + handle.tcp_keepalive(true)?; + + let mut follow = None; + let mut redirected_base_url = None::<String>; + + for Request { + url, + base_url, + mut headers, + upload_body_kind, + config: + http::Options { + extra_headers, + follow_redirects, + low_speed_limit_bytes_per_second, + low_speed_time_seconds, + connect_timeout, + proxy, + no_proxy, + proxy_auth_method, + user_agent, + proxy_authenticate, + verbose, + ssl_ca_info, + ssl_version, + http_version, + backend, + }, + } in req_recv + { + let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); + handle.url(&effective_url)?; + + handle.post(upload_body_kind.is_some())?; + for header in extra_headers { + headers.append(&header)?; + } + // needed to avoid sending Expect: 100-continue, which adds another response and only CURL wants that + headers.append("Expect:")?; + handle.verbose(verbose)?; + + if let Some(ca_info) = ssl_ca_info { + handle.cainfo(ca_info)?; + } + + if let Some(ref mut curl_options) = backend.as_ref().and_then(|backend| backend.lock().ok()) { + if let Some(opts) = curl_options.downcast_mut::<super::Options>() { + if let Some(enabled) = opts.schannel_check_revoke { + handle.ssl_options(curl::easy::SslOpt::new().no_revoke(!enabled))?; + } + } + } + + if let Some(ssl_version) = ssl_version { + let (min, max) = ssl_version.min_max(); + if min == max { + handle.ssl_version(to_curl_ssl_version(min))?; + } else { + handle.ssl_min_max_version(to_curl_ssl_version(min), to_curl_ssl_version(max))?; + } + } + + if let Some(http_version) = http_version { + let version = match http_version { + HttpVersion::V1_1 => curl::easy::HttpVersion::V11, + HttpVersion::V2 => curl::easy::HttpVersion::V2, + }; + // Failing to set the version isn't critical, and may indeed fail depending on the version + // of libcurl we are built against. + // Furthermore, `git` itself doesn't actually check for errors when configuring curl at all, + // treating all or most flags as non-critical. + handle.http_version(version).ok(); + } + + let mut proxy_auth_action = None; + if let Some(proxy) = proxy { + handle.proxy(&proxy)?; + let proxy_type = if proxy.starts_with("socks5h") { + curl::easy::ProxyType::Socks5Hostname + } else if proxy.starts_with("socks5") { + curl::easy::ProxyType::Socks5 + } else if proxy.starts_with("socks4a") { + curl::easy::ProxyType::Socks4a + } else if proxy.starts_with("socks") { + curl::easy::ProxyType::Socks4 + } else { + curl::easy::ProxyType::Http + }; + handle.proxy_type(proxy_type)?; + + if let Some((obtain_creds_action, authenticate)) = proxy_authenticate { + let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action)? + .expect("action to fetch credentials"); + handle.proxy_username(&creds.identity.username)?; + handle.proxy_password(&creds.identity.password)?; + proxy_auth_action = Some((creds.next, authenticate)); + } + } + if let Some(no_proxy) = no_proxy { + handle.noproxy(&no_proxy)?; + } + if let Some(user_agent) = user_agent { + handle.useragent(&user_agent)?; + } + handle.transfer_encoding(false)?; + if let Some(timeout) = connect_timeout { + handle.connect_timeout(timeout)?; + } + { + let mut auth = Auth::new(); + match proxy_auth_method { + ProxyAuthMethod::AnyAuth => auth + .basic(true) + .digest(true) + .digest_ie(true) + .gssnegotiate(true) + .ntlm(true) + .aws_sigv4(true), + ProxyAuthMethod::Basic => auth.basic(true), + ProxyAuthMethod::Digest => auth.digest(true), + ProxyAuthMethod::Negotiate => auth.digest_ie(true), + ProxyAuthMethod::Ntlm => auth.ntlm(true), + }; + handle.proxy_auth(&auth)?; + } + handle.tcp_keepalive(true)?; + + if low_speed_time_seconds > 0 && low_speed_limit_bytes_per_second > 0 { + handle.low_speed_limit(low_speed_limit_bytes_per_second)?; + handle.low_speed_time(Duration::from_secs(low_speed_time_seconds))?; + } + let (receive_data, receive_headers, send_body, mut receive_body) = { + let handler = handle.get_mut(); + let (send, receive_data) = pipe::unidirectional(1); + handler.send_data = Some(send); + let (send, receive_headers) = pipe::unidirectional(1); + handler.send_header = Some(send); + let (send_body, receive_body) = pipe::unidirectional(None); + (receive_data, receive_headers, send_body, receive_body) + }; + + let follow = follow.get_or_insert(follow_redirects); + handle.get_mut().follow = *follow; + handle.follow_location(matches!(*follow, FollowRedirects::Initial | FollowRedirects::All))?; + + if *follow == FollowRedirects::Initial { + *follow = FollowRedirects::None; + } + + if res_send + .send(Response { + headers: receive_headers, + body: receive_data, + upload_body: send_body, + }) + .is_err() + { + break; + } + + handle.get_mut().receive_body = Some(match upload_body_kind { + Some(PostBodyDataKind::Unbounded) | None => StreamOrBuffer::Stream(receive_body), + Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { + let mut buf = Vec::<u8>::with_capacity(512); + receive_body.read_to_end(&mut buf)?; + handle.post_field_size(buf.len() as u64)?; + drop(receive_body); + StreamOrBuffer::Buffer(std::io::Cursor::new(buf)) + } + }); + handle.http_headers(headers)?; + + if let Err(err) = handle.perform() { + let handler = handle.get_mut(); + handler.reset(); + + if let Some((action, authenticate)) = proxy_auth_action { + authenticate.lock().expect("no panics in other threads")(action.erase()).ok(); + } + let err = Err(io::Error::new( + if curl_is_spurious(&err) { + std::io::ErrorKind::ConnectionReset + } else { + std::io::ErrorKind::Other + }, + err, + )); + handler.receive_body.take(); + match (handler.send_header.take(), handler.send_data.take()) { + (Some(header), mut data) => { + if let Err(TrySendError::Disconnected(err)) | Err(TrySendError::Full(err)) = + header.channel.try_send(err) + { + if let Some(body) = data.take() { + body.channel.try_send(err).ok(); + } + } + } + (None, Some(body)) => { + body.channel.try_send(err).ok(); + } + (None, None) => {} + }; + } else { + let handler = handle.get_mut(); + if let Some((action, authenticate)) = proxy_auth_action { + authenticate.lock().expect("no panics in other threads")(if handler.last_status == 200 { + action.store() + } else { + action.erase() + })?; + } + handler.reset(); + handler.receive_body.take(); + handler.send_header.take(); + handler.send_data.take(); + let actual_url = handle + .effective_url()? + .expect("effective url is present and valid UTF-8"); + if actual_url != effective_url { + redirected_base_url = redirect::base_url(actual_url, &base_url, url)?.into(); + } + } + } + Ok(()) + }); + (handle, req_send, res_recv) +} + +fn to_curl_ssl_version(vers: SslVersion) -> curl::easy::SslVersion { + use curl::easy::SslVersion::*; + match vers { + SslVersion::Default => Default, + SslVersion::TlsV1 => Tlsv1, + SslVersion::SslV2 => Sslv2, + SslVersion::SslV3 => Sslv3, + SslVersion::TlsV1_0 => Tlsv10, + SslVersion::TlsV1_1 => Tlsv11, + SslVersion::TlsV1_2 => Tlsv12, + SslVersion::TlsV1_3 => Tlsv13, + } +} + +impl From<Error> for http::Error { + fn from(err: Error) -> Self { + http::Error::Detail { + description: err.to_string(), + } + } +} + +impl From<curl::Error> for http::Error { + fn from(err: curl::Error) -> Self { + http::Error::Detail { + description: err.to_string(), + } + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/http/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/mod.rs new file mode 100644 index 000000000..d88d6bf26 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/mod.rs @@ -0,0 +1,512 @@ +use std::{ + any::Any, + borrow::Cow, + io::{BufRead, Read}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use base64::Engine; +use bstr::BStr; +use gix_packetline::PacketLineRef; +pub use traits::{Error, GetResponse, Http, PostBodyDataKind, PostResponse}; + +use crate::{ + client::{ + self, + blocking_io::bufread_ext::ReadlineBufRead, + capabilities, + http::options::{HttpVersion, SslVersionRangeInclusive}, + 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")] +/// +pub 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(gix_credentials::helper::Action) -> gix_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 + } + } + + /// Available SSL version numbers. + #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] + #[allow(missing_docs)] + pub enum SslVersion { + /// The implementation default, which is unknown to this layer of abstraction. + Default, + TlsV1, + SslV2, + SslV3, + TlsV1_0, + TlsV1_1, + TlsV1_2, + TlsV1_3, + } + + /// Available HTTP version numbers. + #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] + #[allow(missing_docs)] + pub enum HttpVersion { + /// Equivalent to HTTP/1.1 + V1_1, + /// Equivalent to HTTP/2 + V2, + } + + /// The desired range of acceptable SSL versions, or the single version to allow if both are set to the same value. + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub struct SslVersionRangeInclusive { + /// The smallest allowed ssl version to use. + pub min: SslVersion, + /// The highest allowed ssl version to use. + pub max: SslVersion, + } + + impl SslVersionRangeInclusive { + /// Return `min` and `max` fields in the right order so `min` is smaller or equal to `max`. + pub fn min_max(&self) -> (SslVersion, SslVersion) { + if self.min > self.max { + (self.max, self.min) + } else { + (self.min, self.max) + } + } + } +} + +/// 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]`. + /// + /// Note that an empty string means the proxy is disabled entirely. + /// 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<( + gix_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, + /// If set, use this path to point to a file with CA certificates to verify peers. + pub ssl_ca_info: Option<PathBuf>, + /// The SSL version or version range to use, or `None` to let the TLS backend determine which versions are acceptable. + pub ssl_version: Option<SslVersionRangeInclusive>, + /// The HTTP version to enforce. If unset, it is implementation defined. + pub http_version: Option<HttpVersion>, + /// 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<gix_packetline::StreamingPeekableIter<H::ResponseBody>>, + identity: Option<gix_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 '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported." + ), + })); + } + 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(gix_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::engine::general_purpose::STANDARD.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: gix_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(|| gix_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<'_>, gix_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; diff --git a/vendor/gix-transport/src/client/blocking_io/http/redirect.rs b/vendor/gix-transport/src/client/blocking_io/http/redirect.rs new file mode 100644 index 000000000..f429961e0 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/redirect.rs @@ -0,0 +1,66 @@ +/// The error provided when redirection went beyond what we deem acceptable. +#[derive(Debug, thiserror::Error)] +#[error("Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as they don't share the same suffix")] +pub struct Error { + redirect_url: String, + expected_url: String, +} + +pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> { + let tail = url + .strip_prefix(base_url) + .expect("BUG: caller assures `base_url` is subset of `url`"); + redirect_url + .strip_suffix(tail) + .ok_or_else(|| Error { + redirect_url: redirect_url.into(), + expected_url: url, + }) + .map(ToOwned::to_owned) +} + +pub(crate) fn swap_tails(effective_base_url: Option<&str>, base_url: &str, mut url: String) -> String { + match effective_base_url { + Some(effective_base) => { + url.replace_range(..base_url.len(), effective_base); + url + } + None => url, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn base_url_complete() { + assert_eq!( + base_url( + "https://redirected.org/b/info/refs?hi", + "https://original/a", + "https://original/a/info/refs?hi".into() + ) + .unwrap(), + "https://redirected.org/b" + ); + } + + #[test] + fn swap_tails_complete() { + assert_eq!( + swap_tails(None, "not interesting", "used".into()), + "used", + "without effective base url, it passes url, no redirect happened yet" + ); + assert_eq!( + swap_tails( + Some("https://redirected.org/b"), + "https://original/a", + "https://original/a/info/refs?something".into() + ), + "https://redirected.org/b/info/refs?something", + "the tail stays the same if redirection happened" + ) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs new file mode 100644 index 000000000..7c68b166e --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs @@ -0,0 +1,28 @@ +/// An implementation for HTTP requests via `reqwest`. +pub struct Remote { + /// A worker thread which performs the actual request. + handle: Option<std::thread::JoinHandle<Result<(), remote::Error>>>, + /// A channel to send requests (work) to the worker thread. + request: std::sync::mpsc::SyncSender<remote::Request>, + /// A channel to receive the result of the prior request. + response: std::sync::mpsc::Receiver<remote::Response>, + /// A mechanism for configuring the remote. + config: crate::client::http::Options, +} + +/// A function to configure a single request prior to sending it, support most complex configuration beyond what's possible with +/// basic `git` http configuration. +pub type ConfigureRequestFn = dyn FnMut(&mut reqwest::blocking::Request) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> + + Send + + Sync + + 'static; + +/// Options to configure the reqwest HTTP handler. +#[derive(Default)] +pub struct Options { + /// A function to configure the request that is about to be made. + pub configure_request: Option<Box<ConfigureRequestFn>>, +} + +/// +pub mod remote; diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs new file mode 100644 index 000000000..724528ab9 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -0,0 +1,263 @@ +use std::{ + any::Any, + convert::TryFrom, + io::{Read, Write}, + str::FromStr, +}; + +use gix_features::io::pipe; + +use crate::client::{ + http, + http::{reqwest::Remote, traits::PostBodyDataKind}, +}; + +/// The error returned by the 'remote' helper, a purely internal construct to perform http requests. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error("Could not finish reading all data to post to the remote")] + ReadPostBody(#[from] std::io::Error), + #[error("Request configuration failed")] + ConfigureRequest(#[from] Box<dyn std::error::Error + Send + Sync + 'static>), +} + +impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Reqwest(err) => { + err.is_timeout() || err.is_connect() || err.status().map_or(false, |status| status.is_server_error()) + } + _ => false, + } + } +} + +impl Default for Remote { + fn default() -> Self { + let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); + let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); + let handle = std::thread::spawn(move || -> Result<(), Error> { + // We may error while configuring, which is expected as part of the internal protocol. The error will be + // received and the sender of the request might restart us. + let client = reqwest::blocking::ClientBuilder::new() + .connect_timeout(std::time::Duration::from_secs(20)) + .http1_title_case_headers() + .build()?; + for Request { + url, + headers, + upload_body_kind, + config, + } in req_recv + { + let mut req_builder = if upload_body_kind.is_some() { + client.post(url) + } else { + client.get(url) + } + .headers(headers); + let (post_body_tx, mut post_body_rx) = pipe::unidirectional(0); + let (mut response_body_tx, response_body_rx) = pipe::unidirectional(0); + let (mut headers_tx, headers_rx) = pipe::unidirectional(0); + if res_send + .send(Response { + headers: headers_rx, + body: response_body_rx, + upload_body: post_body_tx, + }) + .is_err() + { + // This means our internal protocol is violated as the one who sent the request isn't listening anymore. + // Shut down as something is off. + break; + } + req_builder = match upload_body_kind { + Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { + let mut buf = Vec::<u8>::with_capacity(512); + post_body_rx.read_to_end(&mut buf)?; + req_builder.body(buf) + } + Some(PostBodyDataKind::Unbounded) => req_builder.body(reqwest::blocking::Body::new(post_body_rx)), + None => req_builder, + }; + let mut req = req_builder.build()?; + if let Some(ref mut request_options) = config.backend.as_ref().and_then(|backend| backend.lock().ok()) { + if let Some(options) = request_options.downcast_mut::<super::Options>() { + if let Some(configure_request) = &mut options.configure_request { + configure_request(&mut req)?; + } + } + } + let mut res = match client.execute(req).and_then(|res| res.error_for_status()) { + Ok(res) => res, + Err(err) => { + let (kind, err) = match err.status() { + Some(status) => { + let kind = if status == reqwest::StatusCode::UNAUTHORIZED { + std::io::ErrorKind::PermissionDenied + } else if status.is_server_error() { + std::io::ErrorKind::ConnectionAborted + } else { + std::io::ErrorKind::Other + }; + (kind, format!("Received HTTP status {}", status.as_str())) + } + None => (std::io::ErrorKind::Other, err.to_string()), + }; + let err = Err(std::io::Error::new(kind, err)); + headers_tx.channel.send(err).ok(); + continue; + } + }; + + let send_headers = { + let headers = res.headers(); + move || -> std::io::Result<()> { + for (name, value) in headers { + headers_tx.write_all(name.as_str().as_bytes())?; + headers_tx.write_all(b":")?; + headers_tx.write_all(value.as_bytes())?; + headers_tx.write_all(b"\n")?; + } + // Make sure this is an FnOnce closure to signal the remote reader we are done. + drop(headers_tx); + Ok(()) + } + }; + + // We don't have to care if anybody is receiving the header, as a matter of fact we cannot fail sending them. + // Thus an error means the receiver failed somehow, but might also have decided not to read headers at all. Fine with us. + send_headers().ok(); + + // reading the response body is streaming and may fail for many reasons. If so, we send the error over the response + // body channel and that's all we can do. + if let Err(err) = std::io::copy(&mut res, &mut response_body_tx) { + response_body_tx.channel.send(Err(err)).ok(); + } + } + Ok(()) + }); + + Remote { + handle: Some(handle), + request: req_send, + response: res_recv, + config: http::Options::default(), + } + } +} + +/// utilities +impl Remote { + fn make_request( + &mut self, + url: &str, + _base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + upload_body_kind: Option<PostBodyDataKind>, + ) -> Result<http::PostResponse<pipe::Reader, pipe::Reader, pipe::Writer>, http::Error> { + let mut header_map = reqwest::header::HeaderMap::new(); + for header_line in headers { + let header_line = header_line.as_ref(); + let colon_pos = header_line + .find(':') + .expect("header line must contain a colon to separate key and value"); + let header_name = &header_line[..colon_pos]; + let value = &header_line[colon_pos + 1..]; + + match reqwest::header::HeaderName::from_str(header_name) + .ok() + .zip(reqwest::header::HeaderValue::try_from(value.trim()).ok()) + { + Some((key, val)) => header_map.insert(key, val), + None => continue, + }; + } + self.request + .send(Request { + url: url.to_owned(), + headers: header_map, + upload_body_kind, + config: self.config.clone(), + }) + .expect("the remote cannot be down at this point"); + + let Response { + headers, + body, + upload_body, + } = match self.response.recv() { + Ok(res) => res, + Err(_) => { + let err = self + .handle + .take() + .expect("always present") + .join() + .expect("no panic") + .expect_err("no receiver means thread is down with init error"); + *self = Self::default(); + return Err(http::Error::InitHttpClient { source: Box::new(err) }); + } + }; + + Ok(http::PostResponse { + post_body: upload_body, + headers, + body, + }) + } +} + +impl http::Http for Remote { + type Headers = pipe::Reader; + type ResponseBody = pipe::Reader; + type PostBody = pipe::Writer; + + fn get( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + ) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> { + self.make_request(url, base_url, headers, None).map(Into::into) + } + + fn post( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + post_body_kind: PostBodyDataKind, + ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> { + self.make_request(url, base_url, headers, Some(post_body_kind)) + } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { + if let Some(config) = config.downcast_ref::<http::Options>() { + self.config = config.clone(); + } + Ok(()) + } +} + +pub(crate) struct Request { + pub url: String, + pub headers: reqwest::header::HeaderMap, + pub upload_body_kind: Option<PostBodyDataKind>, + pub config: http::Options, +} + +/// A link to a thread who provides data for the contained readers. +/// The expected order is: +/// - write `upload_body` +/// - read `headers` to end +/// - read `body` to hend +pub(crate) struct Response { + pub headers: pipe::Reader, + pub body: pipe::Reader, + pub upload_body: pipe::Writer, +} diff --git a/vendor/gix-transport/src/client/blocking_io/http/traits.rs b/vendor/gix-transport/src/client/blocking_io/http/traits.rs new file mode 100644 index 000000000..5b163f892 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/http/traits.rs @@ -0,0 +1,133 @@ +use crate::client::WriteMode; + +/// The error used by the [Http] trait. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could initialize the http client")] + InitHttpClient { + source: Box<dyn std::error::Error + Send + Sync + 'static>, + }, + #[error("{description}")] + Detail { description: String }, + #[error("An IO error occurred while uploading the body of a POST request")] + PostBody(#[from] std::io::Error), +} + +impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::PostBody(err) => err.is_spurious(), + #[cfg(any(feature = "http-client-reqwest", feature = "http-client-curl"))] + Error::InitHttpClient { source } => { + #[cfg(feature = "http-client-curl")] + if let Some(err) = source.downcast_ref::<crate::client::http::curl::Error>() { + return err.is_spurious(); + }; + #[cfg(feature = "http-client-reqwest")] + if let Some(err) = source.downcast_ref::<crate::client::http::reqwest::remote::Error>() { + return err.is_spurious(); + }; + false + } + _ => false, + } + } +} + +/// The return value of [Http::get()]. +pub struct GetResponse<H, B> { + /// The response headers. + pub headers: H, + /// The response body. + pub body: B, +} + +/// The return value of [Http::post()]. +pub struct PostResponse<H, B, PB> { + /// The body to post to the server as part of the request. + /// + /// **Note**: Implementations should drop the handle to avoid deadlocks. + pub post_body: PB, + /// The headers of the post response. + pub headers: H, + /// The body of the post response. + pub body: B, +} + +/// Whether or not the post body is expected to fit into memory or not. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +pub enum PostBodyDataKind { + /// We know how much data we are sending and think it will fit into memory. This allows to collect it into a buffer + /// and send it with `Content-Length: <body-len>`. + BoundedAndFitsIntoMemory, + /// We don't know how much data we will send and assume it won't fit into memory. This enables streaming mode. + Unbounded, +} + +impl From<WriteMode> for PostBodyDataKind { + fn from(m: WriteMode) -> Self { + match m { + WriteMode::Binary => PostBodyDataKind::Unbounded, + WriteMode::OneLfTerminatedLinePerWriteCall => PostBodyDataKind::BoundedAndFitsIntoMemory, + } + } +} + +impl<A, B, C> From<PostResponse<A, B, C>> for GetResponse<A, B> { + fn from(v: PostResponse<A, B, C>) -> Self { + GetResponse { + headers: v.headers, + body: v.body, + } + } +} + +/// A trait to abstract the HTTP operations needed to power all git interactions: read via GET and write via POST. +/// Note that 401 must be turned into `std::io::Error(PermissionDenied)`, and other non-success http stati must be transformed +/// into `std::io::Error(Other)` +#[allow(clippy::type_complexity)] +pub trait Http { + /// A type providing headers line by line. + type Headers: std::io::BufRead + Unpin; + /// A type providing the response. + type ResponseBody: std::io::BufRead; + /// A type allowing to write the content to post. + type PostBody: std::io::Write; + + /// Initiate a `GET` request to `url` provided the given `headers`, where `base_url` is so that `base_url + tail == url`. + /// + /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect. + /// + /// The `headers` are provided verbatim and include both the key as well as the value. + fn get( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + ) -> Result<GetResponse<Self::Headers, Self::ResponseBody>, Error>; + + /// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`. + /// + /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect. + /// + /// The `headers` are provided verbatim and include both the key as well as the value. + /// Note that the [`PostResponse`] contains the [`post_body`][PostResponse::post_body] field which implements [`std::io::Write`] + /// and is expected to receive the body to post to the server. **It must be dropped** before reading the response + /// to prevent deadlocks. + fn post( + &mut self, + url: &str, + base_url: &str, + headers: impl IntoIterator<Item = impl AsRef<str>>, + body: PostBodyDataKind, + ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>; + + /// Pass `config` which can deserialize in the implementation's configuration, as documented separately. + /// + /// The caller must know how that `config` data looks like for the intended implementation. + fn configure( + &mut self, + config: &dyn std::any::Any, + ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>; +} diff --git a/vendor/gix-transport/src/client/blocking_io/mod.rs b/vendor/gix-transport/src/client/blocking_io/mod.rs new file mode 100644 index 000000000..dfb3752af --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/mod.rs @@ -0,0 +1,20 @@ +/// +pub mod connect; + +/// +pub mod file; +/// +#[cfg(feature = "http-client")] +pub mod http; + +mod bufread_ext; +pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead}; + +mod request; +pub use request::RequestWriter; + +/// +pub mod ssh; + +mod traits; +pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; diff --git a/vendor/gix-transport/src/client/blocking_io/request.rs b/vendor/gix-transport/src/client/blocking_io/request.rs new file mode 100644 index 000000000..ae818eff3 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/request.rs @@ -0,0 +1,79 @@ +use std::io; + +use crate::client::{ExtendedBufRead, MessageKind, WriteMode}; + +/// A [`Write`][io::Write] implementation optimized for writing packet lines. +/// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for +/// obtaining the response. +pub struct RequestWriter<'a> { + on_into_read: MessageKind, + writer: gix_packetline::Writer<Box<dyn io::Write + 'a>>, + reader: Box<dyn ExtendedBufRead + Unpin + 'a>, +} + +impl<'a> io::Write for RequestWriter<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.writer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +/// methods with bonds to IO +impl<'a> RequestWriter<'a> { + /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the + /// writes are finished, along with configuration for the `write_mode` and information about which message to write + /// when this instance is converted into a `reader` to read the request's response. + pub fn new_from_bufread<W: io::Write + 'a>( + writer: W, + reader: Box<dyn ExtendedBufRead + Unpin + 'a>, + write_mode: WriteMode, + on_into_read: MessageKind, + ) -> Self { + let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box<dyn io::Write>); + match write_mode { + WriteMode::Binary => writer.enable_binary_mode(), + WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), + } + RequestWriter { + on_into_read, + writer, + reader, + } + } + + /// Write the given message as packet line. + pub fn write_message(&mut self, message: MessageKind) -> io::Result<()> { + match message { + MessageKind::Flush => gix_packetline::PacketLineRef::Flush.write_to(self.writer.inner_mut()), + MessageKind::Delimiter => gix_packetline::PacketLineRef::Delimiter.write_to(self.writer.inner_mut()), + MessageKind::ResponseEnd => gix_packetline::PacketLineRef::ResponseEnd.write_to(self.writer.inner_mut()), + MessageKind::Text(t) => gix_packetline::TextRef::from(t).write_to(self.writer.inner_mut()), + } + .map(|_| ()) + } + + /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response. + /// + /// Doing so will also write the message type this instance was initialized with. + pub fn into_read(mut self) -> std::io::Result<Box<dyn ExtendedBufRead + Unpin + 'a>> { + self.write_message(self.on_into_read)?; + Ok(self.reader) + } + + /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [RequestWriter::into_read()]. + /// + /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the + /// underlying channel. + /// + /// # Note + /// + /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on + /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism + /// is implemented. + pub fn into_parts(self) -> (Box<dyn io::Write + 'a>, Box<dyn ExtendedBufRead + Unpin + 'a>) { + (self.writer.into_inner(), self.reader) + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs b/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs new file mode 100644 index 000000000..63e4dc817 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -0,0 +1,131 @@ +use std::process::Stdio; + +use crate::{client::blocking_io, Protocol}; + +/// The error used in [`connect()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())] + UnsupportedScheme(gix_url::Url), +} + +impl crate::IsSpuriousError for Error {} + +/// The kind of SSH programs we have built-in support for. +/// +/// Various different programs exists with different capabilities, and we have a few built in. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ProgramKind { + /// The standard linux ssh program + Ssh, + /// The `(plink|putty).exe` binaries, typically only on windows. + Plink, + /// The `putty.exe` binary, typically only on windows. + Putty, + /// The `tortoiseplink.exe` binary, only on windows. + TortoisePlink, + /// A minimal ssh client that supports on options. + Simple, +} + +mod program_kind; + +/// +pub mod invocation { + use std::ffi::OsString; + + /// The error returned when producing ssh invocation arguments based on a selected invocation kind. + #[derive(Debug, thiserror::Error)] + #[error("The 'Simple' ssh variant doesn't support {function}")] + pub struct Error { + /// The simple command that should have been invoked. + pub command: OsString, + /// The function that was unsupported + pub function: &'static str, + } +} + +/// +pub mod connect { + use std::ffi::{OsStr, OsString}; + + use crate::client::ssh::ProgramKind; + + /// The options for use when [connecting][super::connect()] via the `ssh` protocol. + #[derive(Debug, Clone, Default)] + pub struct Options { + /// The program or script to use. + /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set. + pub command: Option<OsString>, + /// If `true`, a shell must not be used to execute `command`. + /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be + /// used unnecessarily. + pub disallow_shell: bool, + /// The ssh variant further identifying `program`. This determines which arguments will be used + /// when invoking the program. + /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself. + pub kind: Option<ProgramKind>, + } + + impl Options { + /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set. + pub fn ssh_command(&self) -> &OsStr { + self.command + .as_deref() + .or_else(|| self.kind.and_then(|kind| kind.exe())) + .unwrap_or_else(|| OsStr::new("ssh")) + } + } +} + +/// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote. +/// +/// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard +/// ssh ports. +/// +/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be +/// downgraded by servers not supporting it. +pub fn connect( + url: gix_url::Url, + desired_version: Protocol, + options: connect::Options, +) -> Result<blocking_io::file::SpawnProcessOnDemand, Error> { + if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() { + return Err(Error::UnsupportedScheme(url)); + } + let ssh_cmd = options.ssh_command(); + let mut kind = options.kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd)); + if options.kind.is_none() && kind == ProgramKind::Simple { + kind = if std::process::Command::from( + gix_command::prepare(ssh_cmd) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .with_shell() + .arg("-G") + .arg(url.host().expect("always set for ssh urls")), + ) + .status() + .ok() + .map_or(false, |status| status.success()) + { + ProgramKind::Ssh + } else { + ProgramKind::Simple + }; + } + + let path = gix_url::expand_path::for_shell(url.path.clone()); + Ok(blocking_io::file::SpawnProcessOnDemand::new_ssh( + url, + ssh_cmd, + path, + kind, + options.disallow_shell, + desired_version, + )) +} + +#[cfg(test)] +mod tests; diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs b/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs new file mode 100644 index 000000000..5e9d14a82 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs @@ -0,0 +1,127 @@ +use std::{ffi::OsStr, io::ErrorKind}; + +use bstr::{BString, ByteSlice, ByteVec}; + +use crate::{ + client::{ssh, ssh::ProgramKind}, + Protocol, +}; + +impl ProgramKind { + /// Provide the name of the executable that belongs to this kind, or `None` if the kind is `Simple`. + pub fn exe(&self) -> Option<&'static OsStr> { + Some(OsStr::new(match self { + ProgramKind::Ssh => "ssh", + ProgramKind::Plink => "plink", + ProgramKind::Putty => "putty", + ProgramKind::TortoisePlink => "tortoiseplink.exe", + ProgramKind::Simple => return None, + })) + } + + /// Prepare all information needed to invoke the ssh command + pub(crate) fn prepare_invocation( + &self, + ssh_cmd: &OsStr, + url: &gix_url::Url, + desired_version: Protocol, + disallow_shell: bool, + ) -> Result<gix_command::Prepare, ssh::invocation::Error> { + let mut prepare = gix_command::prepare(ssh_cmd).with_shell(); + if disallow_shell { + prepare.use_shell = false; + } + let host = url.host().expect("present in ssh urls"); + match self { + ProgramKind::Ssh => { + if desired_version != Protocol::V1 { + prepare = prepare + .args(["-o", "SendEnv=GIT_PROTOCOL"]) + .env("GIT_PROTOCOL", format!("version={}", desired_version as usize)) + } + if let Some(port) = url.port { + prepare = prepare.arg(format!("-p{port}")); + } + } + ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => { + if *self == ProgramKind::TortoisePlink { + prepare = prepare.arg("-batch"); + } + if let Some(port) = url.port { + prepare = prepare.arg("-P"); + prepare = prepare.arg(port.to_string()); + } + } + ProgramKind::Simple => { + if url.port.is_some() { + return Err(ssh::invocation::Error { + command: ssh_cmd.into(), + function: "setting the port", + }); + } + } + }; + let host_as_ssh_arg = match url.user() { + Some(user) => format!("{user}@{host}"), + None => host.into(), + }; + + // Try to force ssh to yield english messages (for parsing later) + Ok(prepare.arg(host_as_ssh_arg).env("LANG", "C").env("LC_ALL", "C")) + } + + /// Note that the caller has to assure that the ssh program is launched in English by setting the locale. + pub(crate) fn line_to_err(&self, line: BString) -> Result<std::io::Error, BString> { + let kind = match self { + ProgramKind::Ssh | ProgramKind::Simple => { + if line.contains_str(b"Permission denied") || line.contains_str(b"permission denied") { + Some(ErrorKind::PermissionDenied) + } else if line.contains_str(b"resolve hostname") { + Some(ErrorKind::ConnectionRefused) + } else if line.contains_str(b"connect to host") + || line.contains_str("Connection to ") + || line.contains_str("Connection closed by ") + { + // TODO: turn this into HostUnreachable when stable, or NetworkUnreachable in 'no route' example. + // It's important that it WON'T be considered spurious, but is considered a permanent failure. + Some(ErrorKind::NotFound) + } else { + None + } + } + ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => { + if line.contains_str(b"publickey") { + Some(ErrorKind::PermissionDenied) + } else { + None + } + } + }; + match kind { + Some(kind) => Ok(std::io::Error::new(kind, Vec::from(line).into_string_lossy())), + None => Err(line), + } + } +} + +impl<'a> From<&'a OsStr> for ProgramKind { + fn from(v: &'a OsStr) -> Self { + let p = std::path::Path::new(v); + match p.file_stem().and_then(|s| s.to_str()) { + None => ProgramKind::Simple, + Some(stem) => { + if stem.eq_ignore_ascii_case("ssh") { + ProgramKind::Ssh + } else if stem.eq_ignore_ascii_case("plink") { + ProgramKind::Plink + } else if stem.eq_ignore_ascii_case("putty") { + ProgramKind::Putty + } else if stem.eq_ignore_ascii_case("tortoiseplink") { + ProgramKind::TortoisePlink + } else { + ProgramKind::Simple + } + } + } + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs b/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs new file mode 100644 index 000000000..27c661bd8 --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs @@ -0,0 +1,285 @@ +mod options { + mod ssh_command { + use crate::client::ssh::{connect::Options, ProgramKind}; + + #[test] + fn no_field_means_ssh() { + assert_eq!(Options::default().ssh_command(), "ssh"); + } + + #[test] + fn command_field_determines_ssh_command() { + assert_eq!( + Options { + command: Some("field-value".into()), + ..Default::default() + } + .ssh_command(), + "field-value" + ); + assert_eq!( + Options { + command: Some("field-value".into()), + kind: Some(ProgramKind::TortoisePlink), + ..Default::default() + } + .ssh_command(), + "field-value" + ); + } + + #[test] + fn kind_serves_as_fallback() { + assert_eq!( + Options { + kind: Some(ProgramKind::TortoisePlink), + ..Default::default() + } + .ssh_command(), + "tortoiseplink.exe" + ); + } + } +} + +mod program_kind { + mod from_os_str { + use std::ffi::OsStr; + + use crate::client::ssh::ProgramKind; + + #[test] + fn known_variants_are_derived_from_basename() { + for name_or_path in [ + "ssh", + "ssh.exe", + "SSH", + "SSH.exe", + "/bin/ssh", + "/bin/SSH", + #[cfg(windows)] + "c:\\bin\\ssh.exe", + ] { + assert_eq!( + ProgramKind::from(OsStr::new(name_or_path)), + ProgramKind::Ssh, + "{name_or_path:?} could not be identified correctly" + ); + } + assert_eq!( + ProgramKind::from(OsStr::new("TortoisePlink.exe")), + ProgramKind::TortoisePlink + ); + assert_eq!(ProgramKind::from(OsStr::new("putty")), ProgramKind::Putty); + assert_eq!( + ProgramKind::from(OsStr::new("../relative/Plink.exe")), + ProgramKind::Plink + ); + } + + #[test] + fn unknown_variants_fallback_to_simple() { + assert_eq!( + ProgramKind::from(OsStr::new("something-unknown-that-does-not-exist-for-sure-foobar")), + ProgramKind::Simple, + "in theory, we could fail right here but we don't and leave non-existing programs to fail during handshake" + ); + } + + #[test] + fn ssh_disguised_within_a_script_cannot_be_detected_due_to_invocation_with_dash_g() { + assert_eq!( + ProgramKind::from(OsStr::new("ssh -VVV")), + ProgramKind::Simple, + "we don't execute the command here but assume simple, even though we could determine it's ssh if we would do what git does here" + ); + } + } + + mod prepare_invocation { + use std::ffi::OsStr; + + use crate::{ + client::{ssh, ssh::ProgramKind}, + Protocol, + }; + + #[test] + fn ssh() { + for (url, protocol, expected) in [ + ("ssh://user@host:42/p", Protocol::V1, &["ssh", "-p42", "user@host"][..]), + ("ssh://user@host/p", Protocol::V1, &["ssh", "user@host"][..]), + ("ssh://host/p", Protocol::V1, &["ssh", "host"][..]), + ( + "ssh://user@host:42/p", + Protocol::V2, + &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "-p42", "user@host"][..], + ), + ( + "ssh://user@host/p", + Protocol::V2, + &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@host"][..], + ), + ( + "ssh://host/p", + Protocol::V2, + &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "host"][..], + ), + ] { + assert_eq!(call_args(ProgramKind::Ssh, url, protocol), quoted(expected)); + } + } + + #[test] + fn tortoise_plink_has_batch_command() { + assert_eq!( + call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2), + quoted(&["tortoiseplink.exe", "-batch", "-P", "42", "user@host"]) + ); + } + + #[test] + fn port_for_all() { + for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { + assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with(r#""-P" "43" "user@host""#)); + } + } + + #[test] + fn simple_cannot_handle_any_arguments() { + match try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2) { + Err(ssh::invocation::Error { .. }) => {} + _ => panic!("BUG: unexpected outcome"), + } + assert_eq!( + call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), + quoted(&["simple", "user@host"]), + "simple can only do simple invocations" + ); + } + + #[test] + fn ssh_env_v2() { + let prepare = call(ProgramKind::Ssh, "ssh://host/p", Protocol::V2); + assert_eq!( + prepare.env, + &[ + ("GIT_PROTOCOL".into(), "version=2".into()), + ("LANG".into(), "C".into()), + ("LC_ALL".into(), "C".into()) + ] + ); + assert!(!prepare.use_shell); + } + + #[test] + fn disallow_shell_is_honored() -> Result { + let url = gix_url::parse("ssh://host/path".into()).expect("valid url"); + + let disallow_shell = false; + let prepare = + ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; + assert!(prepare.use_shell, "shells are used when needed"); + + let disallow_shell = true; + let prepare = + ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; + assert!( + !prepare.use_shell, + "but we can enforce it not to be used as well for historical reasons" + ); + Ok(()) + } + + fn quoted(input: &[&str]) -> String { + input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ") + } + fn try_call( + kind: ProgramKind, + url: &str, + version: Protocol, + ) -> std::result::Result<gix_command::Prepare, ssh::invocation::Error> { + let ssh_cmd = kind.exe().unwrap_or_else(|| OsStr::new("simple")); + let url = gix_url::parse(url.into()).expect("valid url"); + kind.prepare_invocation(ssh_cmd, &url, version, false) + } + fn call(kind: ProgramKind, url: &str, version: Protocol) -> gix_command::Prepare { + try_call(kind, url, version).expect("no error") + } + fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> String { + format!("{:?}", std::process::Command::from(call(kind, url, version))) + } + + type Result = std::result::Result<(), ssh::invocation::Error>; + } + + mod line_to_err { + use std::io::ErrorKind; + + use crate::client::ssh::ProgramKind; + + #[test] + fn all() { + for (kind, line, expected) in [ + ( + ProgramKind::Ssh, + "byron@github.com: Permission denied (publickey).", + ErrorKind::PermissionDenied, + ), + ( + ProgramKind::Ssh, + "ssh: Could not resolve hostname hostfoobar: nodename nor servname provided, or not known", + ErrorKind::ConnectionRefused, + ), + ( + ProgramKind::Ssh, + "ssh: connect to host example.org port 22: No route to host", + ErrorKind::NotFound, + ), + // connection closed by remote on windows + ( + ProgramKind::Ssh, + "banner exchange: Connection to 127.0.0.1 port 61024: Software caused connection abort", + ErrorKind::NotFound, + ), + // connection closed by remote on unix + ( + ProgramKind::Ssh, + "Connection closed by 127.0.0.1 port 8888", // + ErrorKind::NotFound, + ), + // this kind is basically unknown but we try our best, and simple equals ssh + ( + ProgramKind::Simple, + "something permission denied something", + ErrorKind::PermissionDenied, + ), + ( + ProgramKind::Simple, + "something resolve hostname hostfoobar: nodename nor servname something", + ErrorKind::ConnectionRefused, + ), + ( + ProgramKind::Simple, + "something connect to host something", + ErrorKind::NotFound, + ), + ] { + assert_eq!(kind.line_to_err(line.into()).map(|err| err.kind()), Ok(expected)); + } + } + + #[test] + fn tortoiseplink_putty_plink() { + for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { + assert_eq!( + kind + .line_to_err("publickey".into()) + .map(|err| err.kind()), + Ok(std::io::ErrorKind::PermissionDenied), + "this program pops up error messages in a window, no way to extract information from it. Maybe there is other ways to use it, 'publickey' they mention all" + ); + } + } + } +} diff --git a/vendor/gix-transport/src/client/blocking_io/traits.rs b/vendor/gix-transport/src/client/blocking_io/traits.rs new file mode 100644 index 000000000..7b2eaebbe --- /dev/null +++ b/vendor/gix-transport/src/client/blocking_io/traits.rs @@ -0,0 +1,101 @@ +use std::{io::Write, ops::DerefMut}; + +use bstr::BString; + +use crate::{ + client::{Capabilities, Error, ExtendedBufRead, MessageKind, TransportWithoutIO, WriteMode}, + Protocol, Service, +}; + +/// The response of the [`handshake()`][Transport::handshake()] method. +pub struct SetServiceResponse<'a> { + /// The protocol the service can provide. May be different from the requested one + pub actual_protocol: Protocol, + /// The capabilities parsed from the server response. + pub capabilities: Capabilities, + /// In protocol version one, this is set to a list of refs and their peeled counterparts. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + 'a>>, +} + +/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol] +/// used to connect to them. +/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible. +/// Thus the consumer of this trait will not have to deal with packet lines at all. +/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted. +pub trait Transport: TransportWithoutIO { + /// Initiate connection to the given service and send the given `extra_parameters` along with it. + /// + /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key` + /// if it is None. + /// + /// Returns the service capabilities according according to the actual [Protocol] it supports, + /// and possibly a list of refs to be obtained. + /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one + /// if [TransportWithoutIO::supported_protocol_versions()] includes it. + /// Exhaust the returned [BufReader][SetServiceResponse::refs] for a list of references in case of protocol V1 + /// before making another request. + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error>; +} + +// Would be nice if the box implementation could auto-forward to all implemented traits. +impl<T: Transport + ?Sized> Transport for Box<T> { + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error> { + self.deref_mut().handshake(service, extra_parameters) + } +} + +impl<T: Transport + ?Sized> Transport for &mut T { + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, Error> { + self.deref_mut().handshake(service, extra_parameters) + } +} + +/// An extension trait to add more methods to everything implementing [`Transport`]. +pub trait TransportV2Ext { + /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`. + /// The `capabilities` were communicated during the handshake. + /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. + fn invoke<'a>( + &mut self, + command: &str, + capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a, + arguments: Option<impl Iterator<Item = bstr::BString>>, + ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error>; +} + +impl<T: Transport> TransportV2Ext for T { + fn invoke<'a>( + &mut self, + command: &str, + capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a, + arguments: Option<impl Iterator<Item = BString>>, + ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error> { + let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush)?; + writer.write_all(format!("command={command}").as_bytes())?; + for (name, value) in capabilities { + match value { + Some(value) => writer.write_all(format!("{name}={}", value.as_ref()).as_bytes()), + None => writer.write_all(name.as_bytes()), + }?; + } + if let Some(arguments) = arguments { + writer.write_message(MessageKind::Delimiter)?; + for argument in arguments { + writer.write_all(argument.as_ref())?; + } + } + Ok(writer.into_read()?) + } +} diff --git a/vendor/gix-transport/src/client/capabilities.rs b/vendor/gix-transport/src/client/capabilities.rs new file mode 100644 index 000000000..4c10dc100 --- /dev/null +++ b/vendor/gix-transport/src/client/capabilities.rs @@ -0,0 +1,307 @@ +use bstr::{BStr, BString, ByteSlice}; + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +use crate::client; +use crate::Protocol; + +/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Capabilities were missing entirely as there was no 0 byte")] + MissingDelimitingNullByte, + #[error("there was not a single capability behind the delimiter")] + NoCapabilities, + #[error("a version line was expected, but none was retrieved")] + MissingVersionLine, + #[error("expected 'version X', got {0:?}")] + MalformattedVersionLine(BString), + #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)] + UnsupportedVersion { desired: Protocol, actual: BString }, + #[error("An IO error occurred while reading V2 lines")] + Io(#[from] std::io::Error), +} + +/// A structure to represent multiple [capabilities][Capability] or features supported by the server. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Capabilities { + data: BString, + value_sep: u8, +} + +/// The name of a single capability. +pub struct Capability<'a>(&'a BStr); + +impl<'a> Capability<'a> { + /// Returns the name of the capability. + /// + /// Most capabilities only consist of a name, making them appear like a feature toggle. + pub fn name(&self) -> &'a BStr { + self.0 + .splitn(2, |b| *b == b'=') + .next() + .expect("there is always a single item") + .as_bstr() + } + /// Returns the value associated with the capability. + /// + /// Note that the caller must know whether a single or multiple values are expected, in which + /// case [`values()`][Capability::values()] should be called. + pub fn value(&self) -> Option<&'a BStr> { + self.0.splitn(2, |b| *b == b'=').nth(1).map(|s| s.as_bstr()) + } + /// Returns the values of a capability if its [`value()`][Capability::value()] is space separated. + pub fn values(&self) -> Option<impl Iterator<Item = &'a BStr>> { + self.value().map(|v| v.split(|b| *b == b' ').map(|s| s.as_bstr())) + } + /// Returns true if its space-separated [`value()`][Capability::value()] contains the given `want`ed capability. + pub fn supports(&self, want: impl Into<&'a BStr>) -> Option<bool> { + let want = want.into(); + self.values().map(|mut iter| iter.any(|v| v == want)) + } +} + +impl Capabilities { + /// Parse capabilities from the given `bytes`. + /// + /// Useful in case they are encoded within a `ref` behind a null byte. + pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> { + let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?; + if delimiter_pos + 1 == bytes.len() { + return Err(Error::NoCapabilities); + } + let capabilities = &bytes[delimiter_pos + 1..]; + Ok(( + Capabilities { + data: capabilities.as_bstr().to_owned(), + value_sep: b' ', + }, + delimiter_pos, + )) + } + + /// Parse capabilities from the given a `lines_buf` which is expected to be all newline separated lines + /// from the server. + /// + /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with + /// blocking and async traits for as long as possible. There is no value in parsing a few bytes + /// in a non-blocking fashion. + pub fn from_lines(lines_buf: BString) -> Result<Capabilities, Error> { + let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); + let version_line = lines.next().ok_or(Error::MissingVersionLine)?; + let (name, value) = version_line.split_at( + version_line + .find(b" ") + .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?, + ); + if name != b"version" { + return Err(Error::MalformattedVersionLine(version_line.to_owned().into())); + } + if value != b" 2" { + return Err(Error::UnsupportedVersion { + desired: Protocol::V2, + actual: value.to_owned().into(), + }); + } + Ok(Capabilities { + value_sep: b'\n', + data: lines.as_bytes().into(), + }) + } + + /// Returns true of the given `feature` is mentioned in this list of capabilities. + pub fn contains(&self, feature: &str) -> bool { + self.capability(feature).is_some() + } + + /// Returns the capability with `name`. + pub fn capability(&self, name: &str) -> Option<Capability<'_>> { + self.iter().find(|c| c.name() == name.as_bytes().as_bstr()) + } + + /// Returns an iterator over all capabilities. + pub fn iter(&self) -> impl Iterator<Item = Capability<'_>> { + self.data + .split(move |b| *b == self.value_sep) + .map(|c| Capability(c.as_bstr())) + } +} + +/// internal use +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +impl Capabilities { + fn extract_protocol(capabilities_or_version: gix_packetline::TextRef<'_>) -> Result<Protocol, client::Error> { + let line = capabilities_or_version.as_bstr(); + let version = if line.starts_with_str("version ") { + if line.len() != "version X".len() { + return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())); + } + match line { + line if line.ends_with_str("1") => Protocol::V1, + line if line.ends_with_str("2") => Protocol::V2, + _ => return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())), + } + } else { + Protocol::V1 + }; + Ok(version) + } +} + +#[cfg(feature = "blocking-client")] +/// +pub mod recv { + use std::io; + + use bstr::ByteVec; + + use crate::{client, client::Capabilities, Protocol}; + + /// Success outcome of [`Capabilities::from_lines_with_version_detection`]. + pub struct Outcome<'a> { + /// The [`Capabilities`] the remote advertised. + pub capabilities: Capabilities, + /// The remote refs as a [`io::BufRead`]. + /// + /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by + /// the caller. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + 'a>>, + /// The [`Protocol`] the remote advertised. + pub protocol: Protocol, + } + + impl Capabilities { + /// Read the capabilities and version advertisement from the given packetline reader. + /// + /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs + /// advertisement will also be included in the [`Outcome`]. + pub fn from_lines_with_version_detection<T: io::Read>( + rd: &mut gix_packetline::StreamingPeekableIter<T>, + ) -> Result<Outcome<'_>, client::Error> { + // NOTE that this is vitally important - it is turned on and stays on for all following requests so + // we automatically abort if the server sends an ERR line anywhere. + // We are sure this can't clash with binary data when sent due to the way the PACK + // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. + rd.fail_on_err_lines(true); + + let line = rd + .peek_line() + .ok_or(client::Error::ExpectedLine("capabilities or version"))???; + let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?; + + let version = Capabilities::extract_protocol(line)?; + match version { + Protocol::V1 => { + let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; + rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); + Ok(Outcome { + capabilities, + refs: Some(Box::new(rd.as_read())), + protocol: Protocol::V1, + }) + } + Protocol::V2 => Ok(Outcome { + capabilities: { + let mut rd = rd.as_read(); + let mut buf = Vec::new(); + while let Some(line) = rd.read_data_line() { + let line = line??; + match line.as_bstr() { + Some(line) => { + buf.push_str(line); + if buf.last() != Some(&b'\n') { + buf.push(b'\n'); + } + } + None => break, + } + } + Capabilities::from_lines(buf.into())? + }, + refs: None, + protocol: Protocol::V2, + }), + } + } + } +} + +#[cfg(feature = "async-client")] +#[allow(missing_docs)] +/// +pub mod recv { + use bstr::ByteVec; + use futures_io::AsyncRead; + + use crate::{client, client::Capabilities, Protocol}; + + /// Success outcome of [`Capabilities::from_lines_with_version_detection`]. + pub struct Outcome<'a> { + /// The [`Capabilities`] the remote advertised. + pub capabilities: Capabilities, + /// The remote refs as an [`AsyncBufRead`]. + /// + /// This is `Some` only when protocol v1 is used. The [`AsyncBufRead`] must be exhausted by + /// the caller. + pub refs: Option<Box<dyn crate::client::ReadlineBufRead + Unpin + 'a>>, + /// The [`Protocol`] the remote advertised. + pub protocol: Protocol, + } + + impl Capabilities { + /// Read the capabilities and version advertisement from the given packetline reader. + /// + /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs + /// advertisement will also be included in the [`Outcome`]. + pub async fn from_lines_with_version_detection<T: AsyncRead + Unpin>( + rd: &mut gix_packetline::StreamingPeekableIter<T>, + ) -> Result<Outcome<'_>, client::Error> { + // NOTE that this is vitally important - it is turned on and stays on for all following requests so + // we automatically abort if the server sends an ERR line anywhere. + // We are sure this can't clash with binary data when sent due to the way the PACK + // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. + rd.fail_on_err_lines(true); + + let line = rd + .peek_line() + .await + .ok_or(client::Error::ExpectedLine("capabilities or version"))???; + let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?; + + let version = Capabilities::extract_protocol(line)?; + match version { + Protocol::V1 => { + let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; + rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); + Ok(Outcome { + capabilities, + refs: Some(Box::new(rd.as_read())), + protocol: Protocol::V1, + }) + } + Protocol::V2 => Ok(Outcome { + capabilities: { + let mut rd = rd.as_read(); + let mut buf = Vec::new(); + while let Some(line) = rd.read_data_line().await { + let line = line??; + match line.as_bstr() { + Some(line) => { + buf.push_str(line); + if buf.last() != Some(&b'\n') { + buf.push(b'\n'); + } + } + None => break, + } + } + Capabilities::from_lines(buf.into())? + }, + refs: None, + protocol: Protocol::V2, + }), + } + } + } +} diff --git a/vendor/gix-transport/src/client/git/async_io.rs b/vendor/gix-transport/src/client/git/async_io.rs new file mode 100644 index 000000000..00bea41a3 --- /dev/null +++ b/vendor/gix-transport/src/client/git/async_io.rs @@ -0,0 +1,151 @@ +use std::{borrow::Cow, error::Error}; + +use async_trait::async_trait; +use bstr::{BStr, BString, ByteVec}; +use futures_io::{AsyncRead, AsyncWrite}; +use futures_lite::AsyncWriteExt; +use gix_packetline::PacketLineRef; + +use crate::{ + client::{self, capabilities, git, Capabilities, SetServiceResponse}, + Protocol, Service, +}; + +impl<R, W> client::TransportWithoutIO for git::Connection<R, W> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + fn request( + &mut self, + write_mode: client::WriteMode, + on_into_read: client::MessageKind, + ) -> Result<client::RequestWriter<'_>, client::Error> { + Ok(client::RequestWriter::new_from_bufread( + &mut self.writer, + Box::new(self.line_provider.as_read_without_sidebands()), + write_mode, + on_into_read, + )) + } + fn to_url(&self) -> Cow<'_, BStr> { + self.custom_url.as_ref().map_or_else( + || { + let mut possibly_lossy_url = self.path.clone(); + possibly_lossy_url.insert_str(0, "file://"); + Cow::Owned(possibly_lossy_url) + }, + |url| Cow::Borrowed(url.as_ref()), + ) + } + + fn connection_persists_across_multiple_requests(&self) -> bool { + true + } + + fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { + Ok(()) + } +} + +#[async_trait(?Send)] +impl<R, W> client::Transport for git::Connection<R, W> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + async fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, client::Error> { + if self.mode == git::ConnectMode::Daemon { + let mut line_writer = gix_packetline::Writer::new(&mut self.writer).binary_mode(); + line_writer + .write_all(&git::message::connect( + service, + self.desired_version, + &self.path, + self.virtual_host.as_ref(), + extra_parameters, + )) + .await?; + line_writer.flush().await?; + } + + let capabilities::recv::Outcome { + capabilities, + refs, + protocol: actual_protocol, + } = Capabilities::from_lines_with_version_detection(&mut self.line_provider).await?; + Ok(SetServiceResponse { + actual_protocol, + capabilities, + refs, + }) + } +} + +impl<R, W> git::Connection<R, W> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol + /// and the transfer of the repository at `repository_path`. + /// + /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to. + pub fn new( + read: R, + write: W, + desired_version: Protocol, + repository_path: impl Into<BString>, + virtual_host: Option<(impl Into<String>, Option<u16>)>, + mode: git::ConnectMode, + ) -> Self { + git::Connection { + writer: write, + line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush]), + path: repository_path.into(), + virtual_host: virtual_host.map(|(h, p)| (h.into(), p)), + desired_version, + custom_url: None, + mode, + } + } +} + +#[cfg(feature = "async-std")] +mod async_net { + use std::time::Duration; + + use async_std::net::TcpStream; + + use crate::client::{git, Error}; + + impl git::Connection<TcpStream, TcpStream> { + /// Create a new TCP connection using the `git` protocol of `desired_version`, and make a connection to `host` + /// at `port` for accessing the repository at `path` on the server side. + pub async fn new_tcp( + host: &str, + port: Option<u16>, + path: bstr::BString, + desired_version: crate::Protocol, + ) -> Result<git::Connection<TcpStream, TcpStream>, Error> { + let read = async_std::io::timeout( + Duration::from_secs(5), + TcpStream::connect(&(host, port.unwrap_or(9418))), + ) + .await?; + let write = read.clone(); + Ok(git::Connection::new( + read, + write, + desired_version, + path, + None::<(String, _)>, + git::ConnectMode::Daemon, + )) + } + } +} diff --git a/vendor/gix-transport/src/client/git/blocking_io.rs b/vendor/gix-transport/src/client/git/blocking_io.rs new file mode 100644 index 000000000..8087b8aea --- /dev/null +++ b/vendor/gix-transport/src/client/git/blocking_io.rs @@ -0,0 +1,199 @@ +use std::{any::Any, borrow::Cow, error::Error, io::Write}; + +use bstr::{BStr, BString, ByteVec}; +use gix_packetline::PacketLineRef; + +use crate::{ + client::{self, capabilities, git, Capabilities, SetServiceResponse}, + Protocol, Service, +}; + +impl<R, W> client::TransportWithoutIO for git::Connection<R, W> +where + R: std::io::Read, + W: std::io::Write, +{ + fn request( + &mut self, + write_mode: client::WriteMode, + on_into_read: client::MessageKind, + ) -> Result<client::RequestWriter<'_>, client::Error> { + Ok(client::RequestWriter::new_from_bufread( + &mut self.writer, + Box::new(self.line_provider.as_read_without_sidebands()), + write_mode, + on_into_read, + )) + } + + fn to_url(&self) -> Cow<'_, BStr> { + self.custom_url.as_ref().map_or_else( + || { + let mut possibly_lossy_url = self.path.clone(); + possibly_lossy_url.insert_str(0, "file://"); + Cow::Owned(possibly_lossy_url) + }, + |url| Cow::Borrowed(url.as_ref()), + ) + } + + fn connection_persists_across_multiple_requests(&self) -> bool { + true + } + + fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { + Ok(()) + } +} + +impl<R, W> client::Transport for git::Connection<R, W> +where + R: std::io::Read, + W: std::io::Write, +{ + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result<SetServiceResponse<'_>, client::Error> { + if self.mode == git::ConnectMode::Daemon { + let mut line_writer = gix_packetline::Writer::new(&mut self.writer).binary_mode(); + line_writer.write_all(&git::message::connect( + service, + self.desired_version, + &self.path, + self.virtual_host.as_ref(), + extra_parameters, + ))?; + line_writer.flush()?; + } + + let capabilities::recv::Outcome { + capabilities, + refs, + protocol: actual_protocol, + } = Capabilities::from_lines_with_version_detection(&mut self.line_provider)?; + Ok(SetServiceResponse { + actual_protocol, + capabilities, + refs, + }) + } +} + +impl<R, W> git::Connection<R, W> +where + R: std::io::Read, + W: std::io::Write, +{ + /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol + /// and the transfer of the repository at `repository_path`. + /// + /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to. + pub fn new( + read: R, + write: W, + desired_version: Protocol, + repository_path: impl Into<BString>, + virtual_host: Option<(impl Into<String>, Option<u16>)>, + mode: git::ConnectMode, + ) -> Self { + git::Connection { + writer: write, + line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush]), + path: repository_path.into(), + virtual_host: virtual_host.map(|(h, p)| (h.into(), p)), + desired_version, + custom_url: None, + mode, + } + } + pub(crate) fn new_for_spawned_process( + reader: R, + writer: W, + desired_version: Protocol, + repository_path: impl Into<BString>, + ) -> Self { + Self::new( + reader, + writer, + desired_version, + repository_path, + None::<(&str, _)>, + git::ConnectMode::Process, + ) + } +} + +/// +pub mod connect { + use std::net::{TcpStream, ToSocketAddrs}; + + use bstr::BString; + + use crate::client::git; + /// The error used in [`connect()`]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("An IO error occurred when connecting to the server")] + Io(#[from] std::io::Error), + #[error("Could not parse {host:?} as virtual host with format <host>[:port]")] + VirtualHostInvalid { host: String }, + } + + impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Io(err) => err.is_spurious(), + _ => false, + } + } + } + + fn parse_host(input: String) -> Result<(String, Option<u16>), Error> { + let mut tokens = input.splitn(2, ':'); + Ok(match (tokens.next(), tokens.next()) { + (Some(host), None) => (host.to_owned(), None), + (Some(host), Some(port)) => ( + host.to_owned(), + Some(port.parse().map_err(|_| Error::VirtualHostInvalid { host: input })?), + ), + _ => unreachable!("we expect at least one token, the original string"), + }) + } + + /// Connect to a git daemon running on `host` and optionally `port` and a repository at `path`. + /// + /// Use `desired_version` to specify a preferred protocol to use, knowing that it can be downgraded by a server not supporting it. + pub fn connect( + host: &str, + path: BString, + desired_version: crate::Protocol, + port: Option<u16>, + ) -> Result<git::Connection<TcpStream, TcpStream>, Error> { + let read = TcpStream::connect_timeout( + &(host, port.unwrap_or(9418)) + .to_socket_addrs()? + .next() + .expect("after successful resolution there is an IP address"), + std::time::Duration::from_secs(5), + )?; + let write = read.try_clone()?; + let vhost = std::env::var("GIT_OVERRIDE_VIRTUAL_HOST") + .ok() + .map(parse_host) + .transpose()? + .unwrap_or_else(|| (host.to_owned(), port)); + Ok(git::Connection::new( + read, + write, + desired_version, + path, + Some(vhost), + git::ConnectMode::Daemon, + )) + } +} + +pub use connect::connect; diff --git a/vendor/gix-transport/src/client/git/mod.rs b/vendor/gix-transport/src/client/git/mod.rs new file mode 100644 index 000000000..2b950b44a --- /dev/null +++ b/vendor/gix-transport/src/client/git/mod.rs @@ -0,0 +1,177 @@ +use bstr::BString; + +use crate::Protocol; + +/// The way to connect to a process speaking the `git` protocol. +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ConnectMode { + /// A git daemon. + Daemon, + /// A spawned `git` process to upload a pack to the client. + Process, +} + +/// A TCP connection to either a `git` daemon or a spawned `git` process. +/// +/// When connecting to a daemon, additional context information is sent with the first line of the handshake. Otherwise that +/// context is passed using command line arguments to a [spawned `git` process][crate::client::file::SpawnProcessOnDemand]. +pub struct Connection<R, W> { + pub(in crate::client) writer: W, + pub(in crate::client) line_provider: gix_packetline::StreamingPeekableIter<R>, + pub(in crate::client) path: BString, + pub(in crate::client) virtual_host: Option<(String, Option<u16>)>, + pub(in crate::client) desired_version: Protocol, + custom_url: Option<BString>, + pub(in crate::client) mode: ConnectMode, +} + +impl<R, W> Connection<R, W> { + /// Return the inner reader and writer + pub fn into_inner(self) -> (R, W) { + (self.line_provider.into_inner(), self.writer) + } + + /// Optionally set the URL to be returned when asked for it if `Some` or calculate a default for `None`. + /// + /// The URL is required as parameter for authentication helpers which are called in transports + /// that support authentication. Even though plain git transports don't support that, this + /// may well be the case in custom transports. + pub fn custom_url(mut self, url: Option<BString>) -> Self { + self.custom_url = url; + self + } +} + +mod message { + use bstr::{BString, ByteVec}; + + use crate::{Protocol, Service}; + + pub fn connect( + service: Service, + desired_version: Protocol, + path: &[u8], + virtual_host: Option<&(String, Option<u16>)>, + extra_parameters: &[(&str, Option<&str>)], + ) -> BString { + let mut out = bstr::BString::from(service.as_str()); + out.push(b' '); + let path = gix_url::expand_path::for_shell(path.into()); + out.extend_from_slice(&path); + out.push(0); + if let Some((host, port)) = virtual_host { + out.push_str("host="); + out.extend_from_slice(host.as_bytes()); + if let Some(port) = port { + out.push_byte(b':'); + out.push_str(&format!("{port}")); + } + out.push(0); + } + // We only send the version when needed, as otherwise a V2 server who is asked for V1 will respond with 'version 1' + // as extra lines in the reply, which we don't want to handle. Especially since an old server will not respond with that + // line (is what I assume, at least), so it's an optional part in the response to understand and handle. There is no value + // in that, so let's help V2 servers to respond in a way that assumes V1. + let extra_params_need_null_prefix = if desired_version != Protocol::V1 { + out.push(0); + out.push_str(format!("version={}", desired_version as usize)); + out.push(0); + false + } else { + true + }; + + if !extra_parameters.is_empty() { + if extra_params_need_null_prefix { + out.push(0); + } + for (key, value) in extra_parameters { + match value { + Some(value) => out.push_str(format!("{key}={value}")), + None => out.push_str(key), + } + out.push(0); + } + } + out + } + #[cfg(test)] + mod tests { + use crate::{client::git, Protocol, Service}; + + #[test] + fn version_1_without_host_and_version() { + assert_eq!( + git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]), + "git-upload-pack hello/world\0" + ) + } + #[test] + fn version_2_without_host_and_version() { + assert_eq!( + git::message::connect(Service::UploadPack, Protocol::V2, b"hello\\world", None, &[]), + "git-upload-pack hello\\world\0\0version=2\0" + ) + } + #[test] + fn version_2_without_host_and_version_and_exta_parameters() { + assert_eq!( + git::message::connect( + Service::UploadPack, + Protocol::V2, + b"/path/project.git", + None, + &[("key", Some("value")), ("value-only", None)] + ), + "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0" + ) + } + #[test] + fn with_host_without_port() { + assert_eq!( + git::message::connect( + Service::UploadPack, + Protocol::V1, + b"hello\\world", + Some(&("host".into(), None)), + &[] + ), + "git-upload-pack hello\\world\0host=host\0" + ) + } + #[test] + fn with_host_without_port_and_extra_parameters() { + assert_eq!( + git::message::connect( + Service::UploadPack, + Protocol::V1, + b"hello\\world", + Some(&("host".into(), None)), + &[("key", Some("value")), ("value-only", None)] + ), + "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0" + ) + } + #[test] + fn with_host_with_port() { + assert_eq!( + git::message::connect( + Service::UploadPack, + Protocol::V1, + b"hello\\world", + Some(&("host".into(), Some(404))), + &[] + ), + "git-upload-pack hello\\world\0host=host:404\0" + ) + } + } +} + +#[cfg(feature = "async-client")] +mod async_io; + +#[cfg(feature = "blocking-client")] +mod blocking_io; +#[cfg(feature = "blocking-client")] +pub use blocking_io::connect; diff --git a/vendor/gix-transport/src/client/mod.rs b/vendor/gix-transport/src/client/mod.rs new file mode 100644 index 000000000..b5296543e --- /dev/null +++ b/vendor/gix-transport/src/client/mod.rs @@ -0,0 +1,36 @@ +#[cfg(feature = "async-client")] +mod async_io; +#[cfg(feature = "async-client")] +pub use async_io::{ + connect, ExtendedBufRead, HandleProgress, ReadlineBufRead, RequestWriter, SetServiceResponse, Transport, + TransportV2Ext, +}; + +mod traits; +pub use traits::TransportWithoutIO; + +#[cfg(feature = "blocking-client")] +mod blocking_io; +#[cfg(feature = "http-client")] +pub use blocking_io::http; +#[cfg(feature = "blocking-client")] +pub use blocking_io::{ + connect, file, ssh, ExtendedBufRead, HandleProgress, ReadlineBufRead, RequestWriter, SetServiceResponse, Transport, + TransportV2Ext, +}; +#[cfg(feature = "blocking-client")] +#[doc(inline)] +pub use connect::function::connect; + +/// +pub mod capabilities; +#[doc(inline)] +pub use capabilities::Capabilities; + +mod non_io_types; +pub use gix_sec::identity::Account; +pub use non_io_types::{Error, MessageKind, WriteMode}; + +/// +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +pub mod git; diff --git a/vendor/gix-transport/src/client/non_io_types.rs b/vendor/gix-transport/src/client/non_io_types.rs new file mode 100644 index 000000000..a207f7f0b --- /dev/null +++ b/vendor/gix-transport/src/client/non_io_types.rs @@ -0,0 +1,159 @@ +/// Configure how the [`RequestWriter`][crate::client::RequestWriter] behaves when writing bytes. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum WriteMode { + /// Each [write()][std::io::Write::write()] call writes the bytes verbatim as one or more packet lines. + /// + /// This mode also indicates to the transport that it should try to stream data as it is unbounded. This mode is typically used + /// for sending packs whose exact size is not necessarily known in advance. + Binary, + /// Each [write()][std::io::Write::write()] call assumes text in the input, assures a trailing newline and writes it as single packet line. + /// + /// This mode also indicates that the lines written fit into memory, hence the transport may chose to not stream it but to buffer it + /// instead. This is relevant for some transports, like the one for HTTP. + OneLfTerminatedLinePerWriteCall, +} + +impl Default for WriteMode { + fn default() -> Self { + WriteMode::OneLfTerminatedLinePerWriteCall + } +} + +/// The kind of packet line to write when transforming a [`RequestWriter`][crate::client::RequestWriter] into an +/// [`ExtendedBufRead`][crate::client::ExtendedBufRead]. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum MessageKind { + /// A `flush` packet. + Flush, + /// A V2 delimiter. + Delimiter, + /// The end of a response. + ResponseEnd, + /// The given text. + Text(&'static [u8]), +} + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +pub(crate) mod connect { + /// Options for connecting to a remote. + #[derive(Debug, Default, Clone)] + pub struct Options { + /// Use `version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. + pub version: crate::Protocol, + #[cfg(feature = "blocking-client")] + /// Options to use if the scheme of the URL is `ssh`. + pub ssh: crate::client::ssh::connect::Options, + } + + /// The error used in [`connect()`][crate::connect()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Url(#[from] gix_url::parse::Error), + #[error("The git repository path could not be converted to UTF8")] + PathConversion(#[from] bstr::Utf8Error), + #[error("connection failed")] + Connection(#[from] Box<dyn std::error::Error + Send + Sync>), + #[error("The url {url:?} contains information that would not be used by the {scheme} protocol")] + UnsupportedUrlTokens { + url: bstr::BString, + scheme: gix_url::Scheme, + }, + #[error("The '{0}' protocol is currently unsupported")] + UnsupportedScheme(gix_url::Scheme), + #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] + #[error( + "'{0}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature" + )] + CompiledWithoutHttp(gix_url::Scheme), + } + + // TODO: maybe fix this workaround: want `IsSpuriousError` in `Connection(…)` + impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Connection(err) => { + #[cfg(feature = "blocking-client")] + if let Some(err) = err.downcast_ref::<crate::client::git::connect::Error>() { + return err.is_spurious(); + }; + if let Some(err) = err.downcast_ref::<crate::client::Error>() { + return err.is_spurious(); + } + false + } + _ => false, + } + } + } +} + +mod error { + use std::ffi::OsString; + + use bstr::BString; + + use crate::client::capabilities; + #[cfg(feature = "http-client")] + use crate::client::http; + #[cfg(feature = "blocking-client")] + use crate::client::ssh; + + #[cfg(feature = "http-client")] + type HttpError = http::Error; + #[cfg(feature = "blocking-client")] + type SshInvocationError = ssh::invocation::Error; + #[cfg(not(feature = "http-client"))] + type HttpError = std::convert::Infallible; + #[cfg(not(feature = "blocking-client"))] + type SshInvocationError = std::convert::Infallible; + + /// The error used in most methods of the [`client`][crate::client] module + #[derive(thiserror::Error, Debug)] + #[allow(missing_docs)] + pub enum Error { + #[error("An IO error occurred when talking to the server")] + Io(#[from] std::io::Error), + #[error("Capabilities could not be parsed")] + Capabilities { + #[from] + err: capabilities::Error, + }, + #[error("A packet line could not be decoded")] + LineDecode { + #[from] + err: gix_packetline::decode::Error, + }, + #[error("A {0} line was expected, but there was none")] + ExpectedLine(&'static str), + #[error("Expected a data line, but got a delimiter")] + ExpectedDataLine, + #[error("The transport layer does not support authentication")] + AuthenticationUnsupported, + #[error("The transport layer refuses to use a given identity: {0}")] + AuthenticationRefused(&'static str), + #[error("The protocol version indicated by {:?} is unsupported", {0})] + UnsupportedProtocolVersion(BString), + #[error("Failed to invoke program {command:?}")] + InvokeProgram { source: std::io::Error, command: OsString }, + #[error(transparent)] + Http(#[from] HttpError), + #[error(transparent)] + SshInvocation(SshInvocationError), + } + + impl crate::IsSpuriousError for Error { + fn is_spurious(&self) -> bool { + match self { + Error::Io(err) => err.is_spurious(), + Error::Http(err) => err.is_spurious(), + _ => false, + } + } + } +} + +pub use error::Error; diff --git a/vendor/gix-transport/src/client/traits.rs b/vendor/gix-transport/src/client/traits.rs new file mode 100644 index 000000000..408935391 --- /dev/null +++ b/vendor/gix-transport/src/client/traits.rs @@ -0,0 +1,115 @@ +use std::{ + any::Any, + borrow::Cow, + ops::{Deref, DerefMut}, +}; + +use bstr::BStr; + +#[cfg(any(feature = "blocking-client", feature = "async-client"))] +use crate::client::{MessageKind, RequestWriter, WriteMode}; +use crate::{client::Error, Protocol}; + +/// This trait represents all transport related functions that don't require any input/output to be done which helps +/// implementation to share more code across blocking and async programs. +pub trait TransportWithoutIO { + /// If the handshake or subsequent reads failed with [std::io::ErrorKind::PermissionDenied], use this method to + /// inform the transport layer about the identity to use for subsequent calls. + /// If authentication continues to fail even with an identity set, consider communicating this to the provider + /// of the identity in order to mark it as invalid. Otherwise the user might have difficulty updating obsolete + /// credentials. + /// Please note that most transport layers are unauthenticated and thus return [an error][Error::AuthenticationUnsupported] here. + fn set_identity(&mut self, _identity: gix_sec::identity::Account) -> Result<(), Error> { + Err(Error::AuthenticationUnsupported) + } + /// Get a writer for sending data and obtaining the response. It can be configured in various ways + /// to support the task at hand. + /// `write_mode` determines how calls to the `write(…)` method are interpreted, and `on_into_read` determines + /// which message to write when the writer is turned into the response reader using [`into_read()`][RequestWriter::into_read()]. + #[cfg(any(feature = "blocking-client", feature = "async-client"))] + fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error>; + + /// Returns the canonical URL pointing to the destination of this transport. + fn to_url(&self) -> Cow<'_, BStr>; + + /// If the actually advertised server version is contained in the returned slice or it is empty, continue as normal, + /// assume the server's protocol version is desired or acceptable. + /// + /// Otherwise, abort the fetch operation with an error to avoid continuing any interaction with the transport. + /// + /// In V1 this means a potentially large list of advertised refs won't be read, instead the connection is ignored + /// leaving the server with a client who potentially unexpectedly terminated the connection. + /// + /// Note that `transport.close()` is not called explicitly. + /// + /// Custom transports can override this to prevent any use of older protocol versions. + fn supported_protocol_versions(&self) -> &[Protocol] { + &[] + } + + /// Returns true if the transport provides persistent connections across multiple requests, or false otherwise. + /// Not being persistent implies that certain information has to be resent on each 'turn' + /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated + /// to the server for most graceful termination of the connection. + fn connection_persists_across_multiple_requests(&self) -> bool; + + /// Pass `config` can be cast and interpreted by the implementation, as documented separately. + /// + /// The caller must know how that `config` data looks like for the intended implementation. + fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>; +} + +// Would be nice if the box implementation could auto-forward to all implemented traits. +impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for Box<T> { + fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), Error> { + self.deref_mut().set_identity(identity) + } + + #[cfg(any(feature = "blocking-client", feature = "async-client"))] + fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error> { + self.deref_mut().request(write_mode, on_into_read) + } + + fn to_url(&self) -> Cow<'_, BStr> { + self.deref().to_url() + } + + fn supported_protocol_versions(&self) -> &[Protocol] { + self.deref().supported_protocol_versions() + } + + fn connection_persists_across_multiple_requests(&self) -> bool { + self.deref().connection_persists_across_multiple_requests() + } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { + self.deref_mut().configure(config) + } +} + +impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T { + fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), Error> { + self.deref_mut().set_identity(identity) + } + + #[cfg(any(feature = "blocking-client", feature = "async-client"))] + fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error> { + self.deref_mut().request(write_mode, on_into_read) + } + + fn to_url(&self) -> Cow<'_, BStr> { + self.deref().to_url() + } + + fn supported_protocol_versions(&self) -> &[Protocol] { + self.deref().supported_protocol_versions() + } + + fn connection_persists_across_multiple_requests(&self) -> bool { + self.deref().connection_persists_across_multiple_requests() + } + + fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { + self.deref_mut().configure(config) + } +} diff --git a/vendor/gix-transport/src/lib.rs b/vendor/gix-transport/src/lib.rs new file mode 100644 index 000000000..ca02b3b59 --- /dev/null +++ b/vendor/gix-transport/src/lib.rs @@ -0,0 +1,99 @@ +//! An implementation of the `git` transport layer, abstracting over all of its [versions][Protocol], providing +//! [`connect()`] to establish a connection given a repository URL. +//! +//! All git transports are supported, including `ssh`, `git`, `http` and `https`, as well as local repository paths. +//! ## Feature Flags +#![cfg_attr( + feature = "document-features", + cfg_attr(doc, doc = ::document_features::document_features!()) +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![deny(missing_docs, rust_2018_idioms)] +#![forbid(unsafe_code)] + +#[cfg(feature = "async-trait")] +pub use async_trait; +pub use bstr; +#[cfg(feature = "futures-io")] +pub use futures_io; +pub use gix_packetline as packetline; + +/// The version of the way client and server communicate. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +#[allow(missing_docs)] +pub enum Protocol { + /// Version 1 was the first one conceived, is stateful, and our implementation was seen to cause deadlocks. Prefer V2 + V1 = 1, + /// A command-based and stateless protocol with clear semantics, and the one to use assuming the server isn't very old. + /// This is the default. + V2 = 2, +} + +impl Default for Protocol { + fn default() -> Self { + Protocol::V2 + } +} + +/// The kind of service to invoke on the client or the server side. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub enum Service { + /// The service sending packs from a server to the client. Used for fetching pack data. + UploadPack, + /// The service receiving packs produced by the client, who sends a pack to the server. + ReceivePack, +} + +impl Service { + /// Render this instance as string recognized by the git transport layer. + pub fn as_str(&self) -> &'static str { + match self { + Service::ReceivePack => "git-receive-pack", + Service::UploadPack => "git-upload-pack", + } + } +} + +mod traits { + use std::convert::Infallible; + + /// An error which can tell whether it's worth retrying to maybe succeed next time. + pub trait IsSpuriousError: std::error::Error { + /// Return `true` if retrying might result in a different outcome due to IO working out differently. + fn is_spurious(&self) -> bool { + false + } + } + + impl IsSpuriousError for Infallible {} + + impl IsSpuriousError for std::io::Error { + fn is_spurious(&self) -> bool { + // TODO: also include the new special Kinds (currently unstable) + use std::io::ErrorKind::*; + match self.kind() { + Unsupported | WriteZero | InvalidInput | InvalidData | WouldBlock | AlreadyExists + | AddrNotAvailable | NotConnected | Other | PermissionDenied | NotFound => false, + Interrupted | UnexpectedEof | OutOfMemory | TimedOut | BrokenPipe | AddrInUse | ConnectionAborted + | ConnectionReset | ConnectionRefused => true, + _ => false, + } + } + } +} +pub use traits::IsSpuriousError; + +/// +pub mod client; + +#[doc(inline)] +#[cfg(any( + feature = "blocking-client", + all(feature = "async-client", any(feature = "async-std")) +))] +pub use client::connect; + +#[cfg(all(feature = "async-client", feature = "blocking-client"))] +compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive"); |