gix-transport-0.42.1/.cargo_vcs_info.json0000644000000001530000000000100137400ustar { "git": { "sha1": "4f98e94e0e8b79ed2899b35bef40f3c30b3025b0" }, "path_in_vcs": "gix-transport" }gix-transport-0.42.1/Cargo.toml0000644000000065720000000000100117510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "gix-transport" version = "0.42.1" authors = ["Sebastian Thiel "] build = false include = [ "src/**/*", "LICENSE-*", ] autobins = false autoexamples = false autotests = false autobenches = false description = "A crate of the gitoxide project dedicated to implementing the git transport layer" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/Byron/gitoxide" [package.metadata.docs.rs] features = [ "http-client-curl", "document-features", "serde", ] [lib] name = "gix_transport" path = "src/lib.rs" doctest = false [dependencies.async-std] version = "1.12.0" optional = true [dependencies.async-trait] version = "0.1.51" optional = true [dependencies.base64] version = "0.22.1" optional = true [dependencies.bstr] version = "1.3.0" features = [ "std", "unicode", ] default-features = false [dependencies.curl] version = "0.4" optional = true [dependencies.document-features] version = "0.2.0" optional = true [dependencies.futures-io] version = "0.3.16" optional = true [dependencies.futures-lite] version = "2.1.0" features = ["std"] optional = true default-features = false [dependencies.gix-command] version = "^0.3.7" [dependencies.gix-credentials] version = "^0.24.2" optional = true [dependencies.gix-features] version = "^0.38.2" [dependencies.gix-packetline] version = "^0.17.5" [dependencies.gix-quote] version = "^0.4.12" [dependencies.gix-sec] version = "^0.10.6" [dependencies.gix-url] version = "^0.27.3" [dependencies.pin-project-lite] version = "0.2.6" optional = true [dependencies.reqwest] version = "0.12.0" features = [ "charset", "http2", "macos-system-configuration", "blocking", ] optional = true default-features = false [dependencies.serde] version = "1.0.114" features = [ "std", "derive", ] optional = true default-features = false [dependencies.thiserror] version = "1.0.26" [dev-dependencies.async-std] version = "1.9.0" features = ["attributes"] [dev-dependencies.blocking] version = "1.0.2" [dev-dependencies.maybe-async] version = "0.2.6" [features] async-client = [ "gix-packetline/async-io", "async-trait", "futures-lite", "futures-io", "pin-project-lite", ] blocking-client = ["gix-packetline/blocking-io"] default = [] http-client = [ "base64", "gix-features/io-pipe", "blocking-client", "gix-credentials", ] http-client-curl = [ "curl", "http-client", ] http-client-curl-rust-tls = [ "http-client-curl", "curl/rustls", ] http-client-reqwest = [ "reqwest", "http-client", ] http-client-reqwest-native-tls = [ "http-client-reqwest", "reqwest/default-tls", ] http-client-reqwest-rust-tls = [ "http-client-reqwest", "reqwest/rustls-tls", ] http-client-reqwest-rust-tls-trust-dns = [ "http-client-reqwest", "reqwest/rustls-tls", "reqwest/trust-dns", ] serde = ["dep:serde"] gix-transport-0.42.1/Cargo.toml.orig000064400000000000000000000121261046102023000154220ustar 00000000000000[package] name = "gix-transport" version = "0.42.1" repository = "https://github.com/Byron/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project dedicated to implementing the git transport layer" authors = ["Sebastian Thiel "] edition = "2021" include = ["src/**/*", "LICENSE-*"] rust-version = "1.65" [lib] doctest = false [features] default = [] #! ### _Mutually Exclusive Client_ #! The _client_ portion of transport can be blocking or async. If none is selected, it will be missing entirely. #! Specifying both causes a compile error, preventing the use of `--all-features`. ## If set, blocking implementations of the typical git transports become available in `crate::client` blocking-client = ["gix-packetline/blocking-io"] ## Implies `blocking-client`, and adds support for the http and https transports. http-client = [ "base64", "gix-features/io-pipe", "blocking-client", "gix-credentials", ] ## Implies `http-client`, and adds support for the http and https transports using the Rust bindings for `libcurl`. http-client-curl = ["curl", "http-client"] ## Implies `http-client-curl` and enables `rustls` for creating `https://` connections. http-client-curl-rust-tls = ["http-client-curl", "curl/rustls"] ### Implies `http-client` and adds support for http and https transports using the blocking version of `reqwest`. http-client-reqwest = ["reqwest", "http-client"] ## Stacks with `blocking-http-transport-reqwest` and enables `https://` via the `rustls` crate. http-client-reqwest-rust-tls = ["http-client-reqwest", "reqwest/rustls-tls"] ## Stacks with `blocking-http-transport-reqwest` and enables `https://` via the `rustls` crate. ## This also makes use of `trust-dns` to avoid `getaddrinfo`, but note it comes with its own problems. http-client-reqwest-rust-tls-trust-dns = [ "http-client-reqwest", "reqwest/rustls-tls", "reqwest/trust-dns", ] ## Stacks with `blocking-http-transport-reqwest` and enables `https://` via the `native-tls` crate. http-client-reqwest-native-tls = ["http-client-reqwest", "reqwest/default-tls"] ## If set, an async implementations of the git transports becomes available in `crate::client`. ## Suitable for implementing your own transports while using git's way of communication, typically in conjunction with a custom server. ## **Note** that the _blocking_ client has a wide range of available transports, with the _async_ version of it supporting only the TCP based `git` transport leaving you ## with the responsibility to providing such an implementation of `futures-io::AsyncRead/AsyncWrite` yourself. async-client = [ "gix-packetline/async-io", "async-trait", "futures-lite", "futures-io", "pin-project-lite", ] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde"] [[test]] name = "blocking-transport" path = "tests/blocking-transport.rs" required-features = ["blocking-client", "maybe-async/is_sync"] [[test]] name = "blocking-transport-http-only" path = "tests/blocking-transport-http.rs" required-features = ["http-client-curl", "maybe-async/is_sync"] [[test]] name = "async-transport" path = "tests/async-transport.rs" required-features = ["async-client"] [dependencies] gix-command = { version = "^0.3.7", path = "../gix-command" } gix-features = { version = "^0.38.2", path = "../gix-features" } gix-url = { version = "^0.27.3", path = "../gix-url" } gix-sec = { version = "^0.10.6", path = "../gix-sec" } gix-packetline = { version = "^0.17.5", path = "../gix-packetline" } gix-credentials = { version = "^0.24.2", path = "../gix-credentials", optional = true } gix-quote = { version = "^0.4.12", path = "../gix-quote" } serde = { version = "1.0.114", optional = true, default-features = false, features = [ "std", "derive", ] } bstr = { version = "1.3.0", default-features = false, features = [ "std", "unicode", ] } thiserror = "1.0.26" # for async-client async-trait = { version = "0.1.51", optional = true } futures-io = { version = "0.3.16", optional = true } futures-lite = { workspace = true, optional = true } pin-project-lite = { version = "0.2.6", optional = true } # for http-client base64 = { version = "0.22.1", optional = true } # for http-client-curl. Additional configuration should be performed on higher levels of the dependency tree. curl = { workspace = true, optional = true } # for http-client-reqwest reqwest = { workspace = true, optional = true, features = ["blocking"] } ## If used in conjunction with `async-client`, the `connect()` method will become available along with supporting the git protocol over TCP, ## where the TCP stream is created using this crate. async-std = { version = "1.12.0", optional = true } document-features = { version = "0.2.0", optional = true } [dev-dependencies] gix-pack = { path = "../gix-pack", default-features = false, features = [ "streaming-input", ] } gix-hash = { path = "../gix-hash" } async-std = { version = "1.9.0", features = ["attributes"] } maybe-async = "0.2.6" blocking = "1.0.2" [package.metadata.docs.rs] features = ["http-client-curl", "document-features", "serde"] gix-transport-0.42.1/LICENSE-APACHE000064400000000000000000000247461046102023000144720ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gix-transport-0.42.1/LICENSE-MIT000064400000000000000000000017771046102023000142010ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-transport-0.42.1/src/client/async_io/bufread_ext.rs000064400000000000000000000135121046102023000212420ustar 00000000000000use std::{ io, ops::{Deref, DerefMut}, }; use async_trait::async_trait; use futures_io::{AsyncBufRead, AsyncRead}; use gix_packetline::{read::ProgressAction, 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<'a> = Box ProgressAction + 'a>; /// 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, gix_packetline::decode::Error>>>; /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines /// which might concatenate multiple distinct packet lines. /// /// Making this a trait method allows to handle differences between async and blocking. async fn readline_str(&mut self, line: &mut String) -> io::Result; } /// Provide even more access to the underlying packet reader. #[async_trait(?Send)] pub trait ExtendedBufRead<'a>: 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>); /// 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>>; /// 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; } #[async_trait(?Send)] impl<'a, T: ReadlineBufRead + ?Sized + 'a + Unpin> ReadlineBufRead for Box { async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.deref_mut().readline().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { self.deref_mut().readline_str(line).await } } #[async_trait(?Send)] impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a + Unpin> ExtendedBufRead<'a> for Box { fn set_progress_handler(&mut self, handle_progress: Option>) { self.deref_mut().set_progress_handler(handle_progress) } async fn peek_data_line(&mut self) -> Option>> { self.deref_mut().peek_data_line().await } fn reset(&mut self, version: Protocol) { self.deref_mut().reset(version) } fn stopped_at(&self) -> Option { self.deref().stopped_at() } } #[async_trait(?Send)] impl ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, for<'b> fn(bool, &'b [u8]) -> ProgressAction> { async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.read_data_line().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { self.read_line_to_string(line).await } } #[async_trait(?Send)] impl<'a, T: AsyncRead + Unpin> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress<'a>> { async fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.read_data_line().await } async fn readline_str(&mut self, line: &mut String) -> io::Result { self.read_line_to_string(line).await } } #[async_trait(?Send)] impl<'a, T: AsyncRead + Unpin> ExtendedBufRead<'a> for gix_packetline::read::WithSidebands<'a, T, HandleProgress<'a>> { fn set_progress_handler(&mut self, handle_progress: Option>) { self.set_progress_handler(handle_progress) } async fn peek_data_line(&mut self) -> Option>> { 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::V0 | 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 { 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"), }) } } gix-transport-0.42.1/src/client/async_io/connect.rs000064400000000000000000000033111046102023000203770ustar 00000000000000pub use crate::client::non_io_types::connect::{Error, Options}; #[cfg(feature = "async-std")] pub(crate) mod function { 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: Url, options: super::Options, ) -> Result, Error> where Url: TryInto, gix_url::parse::Error: From, { 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, options.trace, ) .await .map_err(|e| Box::new(e) as Box)?, ) } scheme => return Err(Error::UnsupportedScheme(scheme)), }) } } gix-transport-0.42.1/src/client/async_io/mod.rs000064400000000000000000000005131046102023000175260ustar 00000000000000mod bufread_ext; pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead}; mod request; pub use request::RequestWriter; mod traits; pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; /// #[allow(clippy::empty_docs)] pub mod connect; #[cfg(feature = "async-std")] pub use connect::function::connect; gix-transport-0.42.1/src/client/async_io/request.rs000064400000000000000000000117571046102023000204530ustar 00000000000000use 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>, reader: Box + Unpin + 'a>, trace: bool, } } impl<'a> futures_io::AsyncWrite for RequestWriter<'a> { fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { self.project().writer.poll_write(cx, buf) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().writer.poll_flush(cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 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. /// If `trace` is true, `gix_trace` will be used on every written message or data. pub fn new_from_bufread( writer: W, reader: Box + Unpin + 'a>, write_mode: WriteMode, on_into_read: MessageKind, trace: bool, ) -> Self { let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box); match write_mode { WriteMode::Binary => writer.enable_binary_mode(), WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), } RequestWriter { on_into_read, writer, reader, trace, } } /// Write the given message as packet line. pub async fn write_message(&mut self, message: MessageKind) -> io::Result<()> { match message { MessageKind::Flush => { if self.trace { gix_features::trace::trace!(">> FLUSH"); } gix_packetline::PacketLineRef::Flush .write_to(self.writer.inner_mut()) .await } MessageKind::Delimiter => { if self.trace { gix_features::trace::trace!(">> DELIM"); } gix_packetline::PacketLineRef::Delimiter .write_to(self.writer.inner_mut()) .await } MessageKind::ResponseEnd => { if self.trace { gix_features::trace::trace!(">> RESPONSE_END"); } gix_packetline::PacketLineRef::ResponseEnd .write_to(self.writer.inner_mut()) .await } MessageKind::Text(t) => { #[allow(unused_variables, unused_imports)] if self.trace { use bstr::ByteSlice; gix_features::trace::trace!(">> {}", t.as_bstr()); } 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 + Unpin + 'a>> { use futures_lite::AsyncWriteExt; self.write_message(self.on_into_read).await?; self.writer.inner_mut().flush().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, Box + Unpin + 'a>, ) { (self.writer.into_inner(), self.reader) } } gix-transport-0.42.1/src/client/async_io/traits.rs000064400000000000000000000117431046102023000202640ustar 00000000000000use 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>, } /// 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, Error>; } // Would be nice if the box implementation could auto-forward to all implemented traits. #[async_trait(?Send)] impl Transport for Box { async fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, 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 Transport for &mut T { async fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, 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. /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. /// /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. async fn invoke<'a>( &mut self, command: &str, capabilities: impl Iterator>)> + 'a, arguments: Option + 'a>, trace: bool, ) -> Result + Unpin + '_>, Error>; } #[async_trait(?Send)] impl TransportV2Ext for T { async fn invoke<'a>( &mut self, command: &str, capabilities: impl Iterator>)> + 'a, arguments: Option + 'a>, trace: bool, ) -> Result + Unpin + '_>, Error> { let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush, trace)?; 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?) } } gix-transport-0.42.1/src/client/blocking_io/bufread_ext.rs000064400000000000000000000125261046102023000217210ustar 00000000000000use std::{ io, ops::{Deref, DerefMut}, }; use gix_packetline::{read::ProgressAction, PacketLineRef}; use crate::{ client::{Error, MessageKind}, Protocol, }; /// A function `f(is_error, text)` receiving progress or error information. pub type HandleProgress<'a> = Box ProgressAction + 'a>; /// 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, gix_packetline::decode::Error>>>; /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines /// which might concatenate multiple distinct packet lines. /// /// Making this a trait method allows to handle differences between async and blocking. fn readline_str(&mut self, line: &mut String) -> io::Result; } /// Provide even more access to the underlying packet reader. pub trait ExtendedBufRead<'a>: 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>); /// 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>>; /// 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; } impl<'a, T: ReadlineBufRead + ?Sized + 'a> ReadlineBufRead for Box { fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { ReadlineBufRead::readline(self.deref_mut()) } fn readline_str(&mut self, line: &mut String) -> io::Result { ReadlineBufRead::readline_str(self.deref_mut(), line) } } impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a> ExtendedBufRead<'a> for Box { fn set_progress_handler(&mut self, handle_progress: Option>) { self.deref_mut().set_progress_handler(handle_progress) } fn peek_data_line(&mut self) -> Option>> { self.deref_mut().peek_data_line() } fn reset(&mut self, version: Protocol) { self.deref_mut().reset(version) } fn stopped_at(&self) -> Option { self.deref().stopped_at() } } impl ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, fn(bool, &[u8]) -> ProgressAction> { fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.read_data_line() } fn readline_str(&mut self, line: &mut String) -> io::Result { self.read_line_to_string(line) } } impl<'a, T: io::Read> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress<'a>> { fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { self.read_data_line() } fn readline_str(&mut self, line: &mut String) -> io::Result { self.read_line_to_string(line) } } impl<'a, T: io::Read> ExtendedBufRead<'a> for gix_packetline::read::WithSidebands<'a, T, HandleProgress<'a>> { fn set_progress_handler(&mut self, handle_progress: Option>) { self.set_progress_handler(handle_progress) } fn peek_data_line(&mut self) -> Option>> { 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::V0 | 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 { 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"), }) } } gix-transport-0.42.1/src/client/blocking_io/connect.rs000064400000000000000000000062161046102023000210610ustar 00000000000000pub use crate::client::non_io_types::connect::{Error, Options}; pub(crate) mod function { 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: Url, options: super::Options) -> Result, Error> where Url: TryInto, gix_url::parse::Error: From, { 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.password().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, options.trace) .map_err(|e| Box::new(e) as Box)?, ) } gix_url::Scheme::Ssh => Box::new({ crate::client::blocking_io::ssh::connect(url, options.version, options.ssh, options.trace) .map_err(|e| Box::new(e) as Box)? }), 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, options.trace, ) .map_err(|e| Box::new(e) as Box)? }) } #[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, options.version, options.trace)) } }) } } gix-transport-0.42.1/src/client/blocking_io/file.rs000064400000000000000000000300571046102023000203470ustar 00000000000000use 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, process::ChildStdin>>, child: Option, trace: bool, } impl SpawnProcessOnDemand { pub(crate) fn new_ssh( url: gix_url::Url, program: impl Into, path: BString, ssh_kind: ssh::ProgramKind, ssh_disallow_shell: bool, version: Protocol, trace: bool, ) -> SpawnProcessOnDemand { SpawnProcessOnDemand { url, path, ssh_cmd: Some((program.into(), ssh_kind)), envs: Default::default(), ssh_disallow_shell, child: None, connection: None, desired_version: version, trace, } } fn new_local(path: BString, version: Protocol, trace: bool) -> SpawnProcessOnDemand { SpawnProcessOnDemand { url: gix_url::Url::from_parts(gix_url::Scheme::File, None, None, None, None, path.clone(), true) .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, trace, } } } 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, trace: bool, ) -> Result, client::Error> { self.connection .as_mut() .expect("handshake() to have been called first") .request(write_mode, on_into_read, trace) } 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> { Ok(()) } } impl Drop for SpawnProcessOnDemand { fn drop(&mut self) { if let Some(mut child) = self.child.take() { // The child process (e.g. `ssh`) may still be running at this point, so kill it before joining/waiting. // In the happy-path case, it should have already exited gracefully, but in error cases or if the user // interrupted the operation, it will likely still be running. child.kill().ok(); child.wait().ok(); } } } struct ReadStdoutFailOnError { recv: std::sync::mpsc::Receiver, read: std::process::ChildStdout, } impl std::io::Read for ReadStdoutFailOnError { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let res = self.read.read(buf); self.swap_err_if_present_in_stderr(buf.len(), res) } } impl ReadStdoutFailOnError { fn swap_err_if_present_in_stderr(&self, wanted: usize, res: std::io::Result) -> std::io::Result { 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_or(Ok(n), Err) } Err(err) => Err(self.recv.recv().ok().unwrap_or(err)), }, } } } fn supervise_stderr( ssh_kind: ssh::ProgramKind, stderr: std::process::ChildStderr, stdout: std::process::ChildStdout, ) -> ReadStdoutFailOnError { 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, 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(); if self.path.trim().first() == Some(&b'-') { return Err(client::Error::AmbiguousPath { path: self.path.clone(), }); } 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)); gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand"); let mut child = cmd.spawn().map_err(|err| client::Error::InvokeProgram { source: err, command: cmd_name.into_owned(), })?; let stdout: Box = 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.trace, )); 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`. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. /// /// This will spawn a `git` process locally. pub fn connect( path: impl Into, desired_version: Protocol, trace: bool, ) -> Result { Ok(SpawnProcessOnDemand::new_local(path.into(), desired_version, trace)) } #[cfg(test)] mod tests { mod ssh { mod connect { use crate::{client::blocking_io::ssh, 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 = ssh::connect(url, Protocol::V1, Default::default(), false).expect("parse success"); assert_eq!(cmd.path, expected, "the path will be substituted by the remote shell"); } } #[test] fn ambiguous_host_disallowed() { for url in [ "ssh://-oProxyCommand=open$IFS-aCalculator/foo", "user@-oProxyCommand=open$IFS-aCalculator:username/repo", ] { let url = gix_url::parse((*url).into()).expect("valid url"); let options = ssh::connect::Options { command: Some("unrecognized".into()), disallow_shell: false, kind: None, }; assert!(matches!( ssh::connect(url, Protocol::V1, options, false), Err(ssh::Error::AmbiguousHostName { host }) if host == "-oProxyCommand=open$IFS-aCalculator", )); } } } } } gix-transport-0.42.1/src/client/blocking_io/http/curl/mod.rs000064400000000000000000000117111046102023000221270ustar 00000000000000use 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, } /// 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, res: Receiver, handle: Option>>, 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>, upload_body_kind: Option, ) -> Result, 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>, ) -> Result, 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>, body: PostBodyDataKind, ) -> Result, http::Error> { self.make_request(url, base_url, headers, Some(body)) } fn configure( &mut self, config: &dyn std::any::Any, ) -> Result<(), Box> { if let Some(config) = config.downcast_ref::() { self.config = config.clone(); } Ok(()) } } gix-transport-0.42.1/src/client/blocking_io/http/curl/remote.rs000064400000000000000000000353641046102023000226550ustar 00000000000000use 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>), } #[derive(Default)] struct Handler { send_header: Option, send_data: Option, receive_body: Option, 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> { let code = data .split(|b| *b == b' ') .nth(1) .ok_or("Expected HTTP/ STATUS")?; let code = std::str::from_utf8(code)?; code.parse().map_err(Into::into) } fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Box)> { 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 { 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 { 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, 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>, SyncSender, Receiver, ) { 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::; 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, ssl_verify, 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::() { 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))?; } } handle.ssl_verify_peer(ssl_verify)?; handle.ssl_verify_host(ssl_verify)?; 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(0); (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::::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) | 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 for http::Error { fn from(err: Error) -> Self { http::Error::Detail { description: err.to_string(), } } } impl From for http::Error { fn from(err: curl::Error) -> Self { http::Error::Detail { description: err.to_string(), } } } gix-transport-0.42.1/src/client/blocking_io/http/mod.rs000064400000000000000000000520121046102023000211610ustar 00000000000000use 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")] /// #[allow(clippy::empty_docs)] 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; /// #[allow(clippy::empty_docs)] 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(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum FollowRedirects { /// Follow only the first redirect request, most suitable for typical git requests. #[default] Initial, /// Follow all redirect requests from the server unconditionally All, /// Follow no redirect request. None, } /// The way to configure a proxy for authentication if a username is present in the configured proxy. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum ProxyAuthMethod { /// Automatically pick a suitable authentication method. #[default] 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, } /// 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(Clone)] pub struct Options { /// Headers to be added to every request. /// They are applied unconditionally and are expected to be valid as they occur in an HTTP request, like `header: value`, without newlines. /// /// Refers to `http.extraHeader` multi-var. pub extra_headers: Vec, /// 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, /// The comma-separated list of hosts to not send through the `proxy`, or `*` to entirely disable all proxying. pub no_proxy: Option, /// 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>, )>, /// 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, /// 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, /// 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, /// The SSL version or version range to use, or `None` to let the TLS backend determine which versions are acceptable. pub ssl_version: Option, /// Controls whether to perform SSL identity verification or not. Turning this off is not recommended and can lead to /// various security risks. An example where this may be needed is when an internal git server uses a self-signed /// certificate and the user accepts the associated security risks. pub ssl_verify: bool, /// The HTTP version to enforce. If unset, it is implementation defined. pub http_version: Option, /// Backend specific options, if available. pub backend: Option>>, } impl Default for Options { fn default() -> Self { Options { extra_headers: vec![], follow_redirects: Default::default(), low_speed_limit_bytes_per_second: 0, low_speed_time_seconds: 0, proxy: None, no_proxy: None, proxy_auth_method: Default::default(), proxy_authenticate: None, user_agent: None, connect_timeout: None, verbose: false, ssl_ca_info: None, ssl_version: None, ssl_verify: true, http_version: None, backend: None, } } } /// 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 { url: String, user_agent_header: &'static str, desired_version: Protocol, actual_version: Protocol, http: H, service: Option, line_provider: Option>, identity: Option, trace: bool, } impl Transport { /// 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub fn new_http(http: H, url: gix_url::Url, desired_version: Protocol, trace: bool) -> Self { let identity = url .user() .zip(url.password()) .map(|(user, pass)| gix_sec::identity::Account { username: user.to_string(), password: pass.to_string(), }); Transport { url: url.to_bstring().to_string(), 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, trace, } } } impl Transport { /// Returns the identity that the transport uses when connecting to the remote. pub fn identity(&self) -> Option<&gix_sec::identity::Account> { self.identity.as_ref() } } #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] impl Transport { /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. /// /// Note that the actual implementation depends on feature toggles. pub fn new(url: gix_url::Url, desired_version: Protocol, trace: bool) -> Self { Self::new_http(Impl::default(), url, desired_version, trace) } } impl Transport { fn check_content_type(service: Service, kind: &str, headers: ::Headers) -> Result<(), client::Error> { let wanted_content_type = format!("application/x-{}-{}", service.as_str(), kind); if !headers.lines().collect::, _>>()?.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>) -> 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 client::TransportWithoutIO for Transport { 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, trace: bool, ) -> Result, 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:: { service, headers: Some(headers), body: line_provider.as_read_without_sidebands(), }), write_mode, on_into_read, trace, )) } 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> { self.http.configure(config) } } impl client::Transport for Transport { fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, 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::>::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::>() .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))?; >::check_content_type(service, "advertisement", headers)?; let line_reader = self.line_provider.get_or_insert_with(|| { gix_packetline::StreamingPeekableIter::new(body, &[PacketLineRef::Flush], self.trace) }); // 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 { service: Service, headers: Option, body: B, } impl HeadersThenBody { fn handle_headers(&mut self) -> std::io::Result<()> { if let Some(headers) = self.headers.take() { >::check_content_type(self.service, "result", headers) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))? } Ok(()) } } impl Read for HeadersThenBody { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.handle_headers()?; self.body.read(buf) } } impl BufRead for HeadersThenBody { 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 ReadlineBufRead for HeadersThenBody { fn readline(&mut self) -> Option, gix_packetline::decode::Error>>> { if let Err(err) = self.handle_headers() { return Some(Err(err)); } self.body.readline() } fn readline_str(&mut self, line: &mut String) -> std::io::Result { self.handle_headers()?; self.body.readline_str(line) } } impl<'a, H: Http, B: ExtendedBufRead<'a> + Unpin> ExtendedBufRead<'a> for HeadersThenBody { fn set_progress_handler(&mut self, handle_progress: Option>) { self.body.set_progress_handler(handle_progress) } fn peek_data_line(&mut self) -> Option>> { 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 { self.body.stopped_at() } } /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[cfg(all(feature = "http-client", not(feature = "http-client-curl")))] pub fn connect_http(http: H, url: gix_url::Url, desired_version: Protocol, trace: bool) -> Transport { Transport::new_http(http, url, desired_version, trace) } /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] pub fn connect(url: gix_url::Url, desired_version: Protocol, trace: bool) -> Transport { Transport::new(url, desired_version, trace) } /// #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] pub mod redirect; gix-transport-0.42.1/src/client/blocking_io/http/redirect.rs000064400000000000000000000037131046102023000222070ustar 00000000000000/// 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 { 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" ) } } gix-transport-0.42.1/src/client/blocking_io/http/reqwest/mod.rs000064400000000000000000000022061046102023000226530ustar 00000000000000/// An implementation for HTTP requests via `reqwest`. pub struct Remote { /// A worker thread which performs the actual request. handle: Option>>, /// A channel to send requests (work) to the worker thread. request: std::sync::mpsc::SyncSender, /// A channel to receive the result of the prior request. response: std::sync::mpsc::Receiver, /// 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> + 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>, } /// #[allow(clippy::empty_docs)] pub mod remote; gix-transport-0.42.1/src/client/blocking_io/http/reqwest/remote.rs000064400000000000000000000313021046102023000233660ustar 00000000000000use std::{ any::Any, io::{Read, Write}, str::FromStr, sync::{atomic, Arc}, }; use gix_features::io::pipe; use crate::client::http::{self, options::FollowRedirects, redirect, 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), #[error(transparent)] Redirect(#[from] redirect::Error), } 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> { let mut follow = None; let mut redirected_base_url = None::; let allow_redirects = Arc::new(atomic::AtomicBool::new(false)); // 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() .redirect(reqwest::redirect::Policy::custom({ let allow_redirects = allow_redirects.clone(); move |attempt| { if allow_redirects.load(atomic::Ordering::Relaxed) { let curr_url = attempt.url(); let prev_urls = attempt.previous(); match prev_urls.first() { Some(prev_url) if prev_url.host_str() != curr_url.host_str() => { // git does not want to be redirected to a different host. attempt.stop() } _ => { // emulate default git behaviour which relies on curl default behaviour apparently. const CURL_DEFAULT_REDIRS: usize = 50; if prev_urls.len() >= CURL_DEFAULT_REDIRS { attempt.error("too many redirects") } else { attempt.follow() } } } } else { attempt.stop() } } })) .build()?; for Request { url, base_url, headers, upload_body_kind, config, } in req_recv { let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); let mut req_builder = if upload_body_kind.is_some() { client.post(&effective_url) } else { client.get(&effective_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::::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::() { if let Some(configure_request) = &mut options.configure_request { configure_request(&mut req)?; } } } let follow = follow.get_or_insert(config.follow_redirects); allow_redirects.store( matches!(follow, FollowRedirects::Initial | FollowRedirects::All), atomic::Ordering::Relaxed, ); if *follow == FollowRedirects::Initial { *follow = FollowRedirects::None; } let mut res = match client .execute(req) .and_then(reqwest::blocking::Response::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 actual_url = res.url().as_str(); if actual_url != effective_url.as_str() { redirected_base_url = redirect::base_url(actual_url, &base_url, url)?.into(); } 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 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)"); *self = Remote::default(); http::Error::InitHttpClient { source: Box::new(err_that_brought_thread_down), } } fn make_request( &mut self, url: &str, base_url: &str, headers: impl IntoIterator>, upload_body_kind: Option, ) -> Result, 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, }; } if self .request .send(Request { url: url.to_owned(), base_url: base_url.to_owned(), headers: header_map, upload_body_kind, config: self.config.clone(), }) .is_err() { return Err(self.restore_thread_after_failure()); } let Response { headers, body, upload_body, } = match self.response.recv() { Ok(res) => res, Err(_) => { return Err(self.restore_thread_after_failure()); } }; 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>, ) -> Result, 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>, post_body_kind: PostBodyDataKind, ) -> Result, http::Error> { self.make_request(url, base_url, headers, Some(post_body_kind)) } fn configure(&mut self, config: &dyn Any) -> Result<(), Box> { if let Some(config) = config.downcast_ref::() { self.config = config.clone(); } Ok(()) } } pub(crate) struct Request { pub url: String, pub base_url: String, pub headers: reqwest::header::HeaderMap, pub upload_body_kind: Option, 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, } gix-transport-0.42.1/src/client/blocking_io/http/traits.rs000064400000000000000000000121361046102023000217130ustar 00000000000000use crate::client::WriteMode; /// The error used by the [Http] trait. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Could not initialize the http client")] InitHttpClient { source: Box, }, #[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::() { return err.is_spurious(); }; #[cfg(feature = "http-client-reqwest")] if let Some(err) = source.downcast_ref::() { return err.is_spurious(); }; false } _ => false, } } } /// The return value of [`Http::get()`]. pub struct GetResponse { /// The response headers. pub headers: H, /// The response body. pub body: B, } /// The return value of [`Http::post()`]. pub struct PostResponse { /// 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: `. 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 for PostBodyDataKind { fn from(m: WriteMode) -> Self { match m { WriteMode::Binary => PostBodyDataKind::Unbounded, WriteMode::OneLfTerminatedLinePerWriteCall => PostBodyDataKind::BoundedAndFitsIntoMemory, } } } impl From> for GetResponse { fn from(v: PostResponse) -> 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 statuses 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>, ) -> Result, 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>, body: PostBodyDataKind, ) -> Result, 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>; } gix-transport-0.42.1/src/client/blocking_io/mod.rs000064400000000000000000000006321046102023000202030ustar 00000000000000/// #[allow(clippy::empty_docs)] pub mod connect; /// #[allow(clippy::empty_docs)] 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; /// #[allow(clippy::empty_docs)] pub mod ssh; mod traits; pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; gix-transport-0.42.1/src/client/blocking_io/request.rs000064400000000000000000000106311046102023000211140ustar 00000000000000use std::{io, io::Write}; 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>, reader: Box + Unpin + 'a>, trace: bool, } impl<'a> io::Write for RequestWriter<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { #[allow(unused_imports)] if self.trace { use bstr::ByteSlice; gix_features::trace::trace!(">> {}", buf.as_bstr()); } 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. /// If `trace` is true, `gix_trace` will be used on every written message or data. pub fn new_from_bufread( writer: W, reader: Box + Unpin + 'a>, write_mode: WriteMode, on_into_read: MessageKind, trace: bool, ) -> Self { let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box); match write_mode { WriteMode::Binary => writer.enable_binary_mode(), WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), } RequestWriter { on_into_read, writer, reader, trace, } } /// Write the given message as packet line. pub fn write_message(&mut self, message: MessageKind) -> io::Result<()> { match message { MessageKind::Flush => { if self.trace { gix_features::trace::trace!(">> FLUSH"); } gix_packetline::PacketLineRef::Flush.write_to(self.writer.inner_mut()) } MessageKind::Delimiter => { if self.trace { gix_features::trace::trace!(">> DELIM"); } gix_packetline::PacketLineRef::Delimiter.write_to(self.writer.inner_mut()) } MessageKind::ResponseEnd => { if self.trace { gix_features::trace::trace!(">> RESPONSE_END"); } gix_packetline::PacketLineRef::ResponseEnd.write_to(self.writer.inner_mut()) } MessageKind::Text(t) => { #[allow(unused_variables, unused_imports)] if self.trace { use bstr::ByteSlice; gix_features::trace::trace!(">> {}", t.as_bstr()); } 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 + Unpin + 'a>> { self.write_message(self.on_into_read)?; self.writer.inner_mut().flush()?; 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, Box + Unpin + 'a>) { (self.writer.into_inner(), self.reader) } } gix-transport-0.42.1/src/client/blocking_io/ssh/mod.rs000064400000000000000000000125721046102023000210060ustar 00000000000000use std::process::Stdio; use gix_url::ArgumentSafety::*; 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), #[error("Host name '{host}' could be mistaken for a command-line argument")] AmbiguousHostName { host: String }, } 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; /// #[allow(clippy::empty_docs)] 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)] #[allow(missing_docs)] pub enum Error { #[error("Username '{user}' could be mistaken for a command-line argument")] AmbiguousUserName { user: String }, #[error("Host name '{host}' could be mistaken for a command-line argument")] AmbiguousHostName { host: String }, #[error("The 'Simple' ssh variant doesn't support {function}")] Unsupported { /// The simple command that should have been invoked. command: OsString, /// The function that was unsupported function: &'static str, }, } } /// #[allow(clippy::empty_docs)] 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, /// 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, } 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[allow(clippy::result_large_err)] pub fn connect( url: gix_url::Url, desired_version: Protocol, options: connect::Options, trace: bool, ) -> Result { 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 { let mut cmd = std::process::Command::from( gix_command::prepare(ssh_cmd) .stderr(Stdio::null()) .stdout(Stdio::null()) .stdin(Stdio::null()) .with_shell() .arg("-G") .arg(match url.host_as_argument() { Usable(host) => host, Dangerous(host) => Err(Error::AmbiguousHostName { host: host.into() })?, Absent => panic!("BUG: host should always be present in SSH URLs"), }), ); gix_features::trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check"); kind = if cmd.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, trace, )) } #[cfg(test)] mod tests; gix-transport-0.42.1/src/client/blocking_io/ssh/program_kind.rs000064400000000000000000000125711046102023000227020ustar 00000000000000use std::{ffi::OsStr, io::ErrorKind}; use bstr::{BString, ByteSlice, ByteVec}; use gix_url::ArgumentSafety::*; 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 { let mut prepare = gix_command::prepare(ssh_cmd).with_shell(); if disallow_shell { prepare.use_shell = false; } 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::Unsupported { command: ssh_cmd.into(), function: "setting the port", }); } } }; let host_maybe_with_user_as_ssh_arg = match (url.user_as_argument(), url.host_as_argument()) { (Usable(user), Usable(host)) => format!("{user}@{host}"), (Usable(user), Dangerous(host)) => format!("{user}@{host}"), // The `user@` makes it safe. (Absent, Usable(host)) => host.into(), (Dangerous(user), _) => Err(ssh::invocation::Error::AmbiguousUserName { user: user.into() })?, (_, Dangerous(host)) => Err(ssh::invocation::Error::AmbiguousHostName { host: host.into() })?, (_, Absent) => panic!("BUG: host should always be present in SSH URLs"), }; Ok(prepare .arg(host_maybe_with_user_as_ssh_arg) // Try to force ssh to yield English messages (for parsing later). .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 { 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(OsStr::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 } } } } } gix-transport-0.42.1/src/client/blocking_io/ssh/tests.rs000064400000000000000000000304231046102023000213640ustar 00000000000000mod 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), joined(expected)); } } #[test] fn tortoise_plink_has_batch_command() { assert_eq!( call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2), joined(&["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("-P 43 user@host")); } } #[test] fn ambiguous_user_is_disallowed_explicit_ssh() { assert!(matches!( try_call(ProgramKind::Ssh, "ssh://-arg@host/p", Protocol::V2), Err(ssh::invocation::Error::AmbiguousUserName { user }) if user == "-arg" )) } #[test] fn ambiguous_user_is_disallowed_implicit_ssh() { assert!(matches!( try_call(ProgramKind::Ssh, "-arg@host:p/q", Protocol::V2), Err(ssh::invocation::Error::AmbiguousUserName { user }) if user == "-arg" )) } #[test] fn ambiguous_host_is_allowed_with_user_explicit_ssh() { assert_eq!( call_args(ProgramKind::Ssh, "ssh://user@-arg/p", Protocol::V2), joined(&["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"]) ); } #[test] fn ambiguous_host_is_allowed_with_user_implicit_ssh() { assert_eq!( call_args(ProgramKind::Ssh, "user@-arg:p/q", Protocol::V2), joined(&["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"]) ); } #[test] fn ambiguous_host_is_disallowed_without_user() { assert!(matches!( try_call(ProgramKind::Ssh, "ssh://-arg/p", Protocol::V2), Err(ssh::invocation::Error::AmbiguousHostName { host }) if host == "-arg" )); } #[test] fn ambiguous_user_and_host_remain_disallowed_together_explicit_ssh() { assert!(matches!( try_call(ProgramKind::Ssh, "ssh://-arg@host/p", Protocol::V2), Err(ssh::invocation::Error::AmbiguousUserName { user }) if user == "-arg" )); } #[test] fn ambiguous_user_and_host_remain_disallowed_together_implicit_ssh() { assert!(matches!( try_call(ProgramKind::Ssh, "-userarg@-hostarg:p/q", Protocol::V2), Err(ssh::invocation::Error::AmbiguousUserName { user }) if user == "-userarg" )); } #[test] fn simple_cannot_handle_any_arguments() { assert!(matches!( try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2), Err(ssh::invocation::Error::Unsupported { .. }) )); assert_eq!( call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), joined(&["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 joined(input: &[&str]) -> String { input.to_vec().join(" ") } fn try_call( kind: ProgramKind, url: &str, version: Protocol, ) -> std::result::Result { 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 { let cmd = std::process::Command::from(call(kind, url, version)); format!( "{} {}", cmd.get_program().to_string_lossy(), cmd.get_args() .map(|arg| arg.to_string_lossy().into_owned()) .collect::>() .join(" ") ) } 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" ); } } } } gix-transport-0.42.1/src/client/blocking_io/traits.rs000064400000000000000000000110431046102023000207300ustar 00000000000000use 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>, } /// 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, Error>; } // Would be nice if the box implementation could auto-forward to all implemented traits. impl Transport for Box { fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, Error> { self.deref_mut().handshake(service, extra_parameters) } } impl Transport for &mut T { fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, 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. /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. /// /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. fn invoke<'a>( &mut self, command: &str, capabilities: impl Iterator>)> + 'a, arguments: Option>, trace: bool, ) -> Result + Unpin + '_>, Error>; } impl TransportV2Ext for T { fn invoke<'a>( &mut self, command: &str, capabilities: impl Iterator>)> + 'a, arguments: Option>, trace: bool, ) -> Result + Unpin + '_>, Error> { let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush, trace)?; 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()?) } } gix-transport-0.42.1/src/client/capabilities.rs000064400000000000000000000344741046102023000176110ustar 00000000000000use 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. /// /// ### Deviation /// /// As a *shortcoming*, we are unable to parse `V1` as emitted from `git-upload-pack` without a `git-daemon` or server, /// as it will not emit any capabilities for some reason. Only `V2` and `V0` work in that context. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Capabilities { data: BString, value_sep: u8, } /// This implementation yields exactly those minimal capabilities that are required for `gix` to work, nothing more and nothing less. /// /// This is a bit of a hack just get tests with Protocol V0 to work, which is a good way to enforce stateful transports. /// Of course, V1 would also do that but when calling `git-upload-pack` directly, it advertises so badly that this is easier to implement. impl Default for Capabilities { fn default() -> Self { Capabilities::from_lines("version 2\nmulti_ack_detailed\nside-band-64k\n".into()) .expect("valid format, known at compile time") } } /// 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(ByteSlice::as_bstr) } /// Returns the values of a capability if its [`value()`][Capability::value()] is space separated. pub fn values(&self) -> Option> { self.value().map(|v| v.split(|b| *b == b' ').map(ByteSlice::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 { 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 { 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> { self.iter().find(|c| c.name() == name.as_bytes().as_bstr()) } /// Returns an iterator over all capabilities. pub fn iter(&self) -> impl Iterator> { 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 { 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")] /// #[allow(clippy::empty_docs)] 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>, /// 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( rd: &mut gix_packetline::StreamingPeekableIter, ) -> Result, 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); Ok(match rd.peek_line() { Some(line) => { let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; let version = Capabilities::extract_protocol(line)?; match version { Protocol::V0 => unreachable!("already handled in `None` case"), Protocol::V1 => { let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); Outcome { capabilities, refs: Some(Box::new(rd.as_read())), protocol: Protocol::V1, } } Protocol::V2 => 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, }, } } None => Outcome { capabilities: Capabilities::default(), refs: Some(Box::new(rd.as_read())), protocol: Protocol::V0, }, }) } } } #[cfg(feature = "async-client")] #[allow(missing_docs)] /// #[allow(clippy::empty_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>, /// 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( rd: &mut gix_packetline::StreamingPeekableIter, ) -> Result, 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); Ok(match rd.peek_line().await { Some(line) => { let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; let version = Capabilities::extract_protocol(line)?; match version { Protocol::V0 => unreachable!("already handled in `None` case"), Protocol::V1 => { let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); Outcome { capabilities, refs: Some(Box::new(rd.as_read())), protocol: Protocol::V1, } } Protocol::V2 => 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, }, } } None => Outcome { capabilities: Capabilities::default(), refs: Some(Box::new(rd.as_read())), protocol: Protocol::V0, }, }) } } } gix-transport-0.42.1/src/client/git/async_io.rs000064400000000000000000000117361046102023000175430ustar 00000000000000use 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 client::TransportWithoutIO for git::Connection where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, { fn request( &mut self, write_mode: client::WriteMode, on_into_read: client::MessageKind, trace: bool, ) -> Result, 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, trace, )) } 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> { Ok(()) } } #[async_trait(?Send)] impl client::Transport for git::Connection where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, { async fn handshake<'a>( &mut self, service: Service, extra_parameters: &'a [(&'a str, Option<&'a str>)], ) -> Result, 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 git::Connection 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub fn new( read: R, write: W, desired_version: Protocol, repository_path: impl Into, virtual_host: Option<(impl Into, Option)>, mode: git::ConnectMode, trace: bool, ) -> Self { git::Connection { writer: write, line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush], trace), 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 { /// 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub async fn new_tcp( host: &str, port: Option, path: bstr::BString, desired_version: crate::Protocol, trace: bool, ) -> Result, 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, trace, )) } } } gix-transport-0.42.1/src/client/git/blocking_io.rs000064400000000000000000000147231046102023000202150ustar 00000000000000use 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 client::TransportWithoutIO for git::Connection where R: std::io::Read, W: std::io::Write, { fn request( &mut self, write_mode: client::WriteMode, on_into_read: client::MessageKind, trace: bool, ) -> Result, 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, trace, )) } 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> { Ok(()) } } impl client::Transport for git::Connection 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, 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 git::Connection 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub fn new( read: R, write: W, desired_version: Protocol, repository_path: impl Into, virtual_host: Option<(impl Into, Option)>, mode: git::ConnectMode, trace: bool, ) -> Self { git::Connection { writer: write, line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush], trace), 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, trace: bool, ) -> Self { Self::new( reader, writer, desired_version, repository_path, None::<(&str, _)>, git::ConnectMode::Process, trace, ) } } /// #[allow(clippy::empty_docs)] 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 [: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), 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub fn connect( host: &str, path: BString, desired_version: crate::Protocol, port: Option, trace: bool, ) -> Result, 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, trace, )) } } pub use connect::connect; gix-transport-0.42.1/src/client/git/mod.rs000064400000000000000000000154061046102023000165140ustar 00000000000000use 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 { pub(in crate::client) writer: W, pub(in crate::client) line_provider: gix_packetline::StreamingPeekableIter, pub(in crate::client) path: BString, pub(in crate::client) virtual_host: Option<(String, Option)>, pub(in crate::client) desired_version: Protocol, custom_url: Option, pub(in crate::client) mode: ConnectMode, } impl Connection { /// 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) -> 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)>, 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" ) } #[test] fn with_strange_host_and_port() { assert_eq!( git::message::connect( Service::UploadPack, Protocol::V1, b"--upload-pack=attack", Some(&("--proxy=other-attack".into(), Some(404))), &[] ), "git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0", "we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases" ) } } } #[cfg(feature = "async-client")] mod async_io; #[cfg(feature = "blocking-client")] mod blocking_io; #[cfg(feature = "blocking-client")] pub use blocking_io::connect; gix-transport-0.42.1/src/client/mod.rs000064400000000000000000000017311046102023000157250ustar 00000000000000#[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; /// #[allow(clippy::empty_docs)] 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; gix-transport-0.42.1/src/client/non_io_types.rs000064400000000000000000000144161046102023000176570ustar 00000000000000/// Configure how the [`RequestWriter`][crate::client::RequestWriter] behaves when writing bytes. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", 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. #[default] 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 = "serde", 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, /// If `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. pub trace: bool, } /// 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), #[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::() { return err.is_spurious(); }; if let Some(err) = err.downcast_ref::() { 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), #[error("The repository path '{path}' could be mistaken for a command-line argument")] AmbiguousPath { path: BString }, } 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; gix-transport-0.42.1/src/client/traits.rs000064400000000000000000000132771046102023000164640ustar 00000000000000use 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()]. /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. #[cfg(any(feature = "blocking-client", feature = "async-client"))] fn request( &mut self, write_mode: WriteMode, on_into_read: MessageKind, trace: bool, ) -> Result, 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>; } // Would be nice if the box implementation could auto-forward to all implemented traits. impl TransportWithoutIO for Box { 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, trace: bool, ) -> Result, Error> { self.deref_mut().request(write_mode, on_into_read, trace) } 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> { self.deref_mut().configure(config) } } impl 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, trace: bool, ) -> Result, Error> { self.deref_mut().request(write_mode, on_into_read, trace) } 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> { self.deref_mut().configure(config) } } gix-transport-0.42.1/src/lib.rs000064400000000000000000000070631046102023000144420ustar 00000000000000//! 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( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), 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(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Protocol { /// Version 0 is like V1, but doesn't show capabilities at all, at least when hosted without `git-daemon`. V0 = 0, /// 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. #[default] V2 = 2, } /// 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 = "serde", 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; /// #[allow(clippy::empty_docs)] pub mod client; #[doc(inline)] #[cfg(any(feature = "blocking-client", all(feature = "async-client", 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");