Skip to content
This repository was archived by the owner on Apr 16, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,046 changes: 1,425 additions & 621 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
[workspace]
members = [ "librad", "rad2", "radicle-keystore" ]

[patch.crates-io]
# FIXME: we need this obscure patch to get ed25519-signed certs.
# See https://github.com/ctz/rustls/pull/292
#rustls = { path = "../../vendor/rustls/rustls" }
rustls = { git = "https://github.com/kim/rustls", rev = "259f9732fb59dd57a641b6dd00834bac0ca8afd3" }
19 changes: 17 additions & 2 deletions librad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ authors = ["Monadic <team@monadic.xyz>"]
edition = "2018"

[dependencies]
bit-vec = "0.6"
bs58 = { version = "0.3", features = ["check"] }
bytes = "0.5"
directories = "2.0"
failure = { version = "0.1", features = ["backtrace"] }
failure_derive = "0.1"
futures = "0.3"
git2 = "0.10"
hex = "0.4"
http = "0.2"
lazy_static = "1.4"
log = "0.4"
nettle = { version = "5.0", features = ["vendored"] }
nonempty = "0.2"
olpc-cjson = "0.1"
quinn = { git = "https://github.com/djc/quinn", rev = "fc1035ff3c0690e49ecad415641e711215de4c9f" }
quinn-h3 = { git = "https://github.com/djc/quinn", rev = "fc1035ff3c0690e49ecad415641e711215de4c9f" }
radicle-keystore = { path = "../radicle-keystore" }
radicle-surf = { git = "https://github.com/radicle-dev/radicle-surf", rev = "58a0e6680d61cc312eee3255abe5d0d494f631d2" }
rcgen = "0.7"
regex = "1.3"
rustls = { version = "0.16.1", features = ["logging", "dangerous_configuration"] }
secstr = "0.3"
sequoia-openpgp = "0.12"
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -27,9 +37,14 @@ sodiumoxide = "0.2"
tempfile = "3.1"
url = { version = "2.1", features = ["serde"] }
urltemplate = "0.1"
hex = "0.4.0"
webpki = "0.21"
yasna = { version = "0.3", features = ["bit-vec"] }

[dev-dependencies]
anyhow = "1.0"
futures = "0.3"
matches = "0.1"
proptest = "0.9"
proptest-derive = "0.1"
matches = "0.1.8"
tokio = { version = "0.2", features = ["macros", "rt-threaded", "blocking"] }

256 changes: 256 additions & 0 deletions librad/src/git/h3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
use std::{
fmt::{self, Display},
io::{self, prelude::*},
mem,
net::ToSocketAddrs,
sync::Arc,
};

use bytes::Bytes;
use futures::{executor::block_on, io::AsyncReadExt};
use git2::transport::{Service, SmartSubtransport, SmartSubtransportStream, Transport};
use http::{request::Builder as RequestBuilder, Method, Request, Response, Uri};
use log::debug;
use quinn_h3::{
body::BodyReader,
client::{Client, Connection},
};

/// A custom HTTP/3 transport for libgit.
///
/// This is currently for demonstration purposes only. Specifically, we expect
/// the call to [`SmartSubtransport::action`] to pass a network address in the
/// `url` parameter -- the consequence being that we need to establish a fresh
/// connection every time.
///
/// The real implementation would instead ask some peer-to-peer membership state
/// to hand us back only a QUIC _stream_ on an already existing connection,
/// which the algorithm has determined to provide the repo we're interested in.
struct Http3Transport {
client: Arc<Client>,
}

struct Http3Subtransport {
connection: Connection,
request: Req,
response: Option<BodyReader>,
}

// FIXME(kim): clippy laments that the variants differ in size too much. Perhaps
// we should also support streaming the request body.
enum Req {
BodyExpected(RequestBuilder),
Ready(Request<Bytes>),
Sent,
}

impl Req {
fn is_ready(&self) -> bool {
match self {
Self::Ready(_) => true,
_ => false,
}
}
}

#[derive(Debug)]
enum RequestError {
BodyExpected,
AlreadySent,
ErrorResponse(Response<()>),
H3(quinn_h3::Error),
}

impl std::error::Error for RequestError {}

impl Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::BodyExpected => f.write_str("Request error: body expected"),
Self::AlreadySent => f.write_str("Request error: already sent"),
Self::ErrorResponse(resp) => write!(f, "Error response received: {:?}", resp),
Self::H3(e) => write!(f, "HTTP error: {}", e),
}
}
}

impl Http3Subtransport {
fn send_request(&mut self, data: &[u8]) -> Result<(), RequestError> {
debug!("subtransport send_request");
let old = mem::replace(&mut self.request, Req::Sent);
let req = match old {
Req::Sent => {
debug!("request already sent!");
Err(RequestError::AlreadySent)
},
Req::BodyExpected(builder) => Ok(builder.body(Bytes::copy_from_slice(data)).unwrap()),
Req::Ready(req) => Ok(req),
}?;

debug!("sending request");
let recv_body = block_on(async {
let (recv_response, _) = self
.connection
.send_request(req)
.await
.map_err(RequestError::H3)?;

let (resp, recv_body) = recv_response.await.map_err(RequestError::H3)?;
debug!("received response {:?}", resp);

if resp.status().is_success() {
Ok(recv_body)
} else {
Err(RequestError::ErrorResponse(resp))
}
})?;

self.response = Some(recv_body);

Ok(())
}
}

/// Register the HTTP/3 transport with libgit.
///
/// # Safety
///
/// This is unsafe for the same reasons `git::transport::register` is unsafe:
/// the call must be externally synchronised with all calls to `libgit`.
/// Repeatedly calling this function will leak the `Client`, as the rust
/// bindings don't expose a way to unregister a previously-registered
/// transport with the same scheme.
pub unsafe fn register_h3_transport(client: Client) {
let client = Arc::new(client);
git2::transport::register("rad", move |remote| {
Transport::smart(
&remote,
true,
Http3Transport {
client: client.clone(),
},
)
})
.unwrap()
}

impl SmartSubtransport for Http3Transport {
fn action(
&self,
url: &str,
action: Service,
) -> Result<Box<dyn SmartSubtransportStream>, git2::Error> {
let uri = url.parse::<Uri>().map_err(as_git2_error)?;

let request = match action {
Service::UploadPackLs => Req::Ready(
Request::builder()
.method(Method::GET)
.uri(format!("{}/info/refs?service=git-upload-pack", url))
.body(Default::default())
.unwrap(),
),
Service::UploadPack => Req::BodyExpected(
Request::builder()
.method(Method::POST)
.uri(format!("{}/git-upload-pack", url)),
),
Service::ReceivePackLs => Req::Ready(
Request::builder()
.method(Method::GET)
.uri(format!("{}/info/refs?service=git-receive-pack", url))
.body(Default::default())
.unwrap(),
),
Service::ReceivePack => Req::BodyExpected(
Request::builder()
.method(Method::POST)
.uri(format!("{}/git-receive-pack", url)),
),
};

let addr = (
uri.host().unwrap_or("localhost"),
uri.port_u16().unwrap_or(4433),
)
.to_socket_addrs()
.map_err(as_git2_error)?
.next()
.ok_or_else(|| git2::Error::from_str("Couldn't resolve address"))?;

debug!("connecting to {:?}", addr);

// Fake the SNI by using the userinfo
let server_name: String = uri
.authority()
.map(|auth| auth.to_string().chars().take_while(|c| *c != '@').collect())
.expect("no userinfo in uri");

let connection = block_on(async {
self.client
.connect(&addr, &server_name)
.map_err(|e| {
debug!("connect: {}", e);
as_git2_error(e)
})?
.await
.map_err(|e| {
debug!("await connect: {}", e);
as_git2_error(e)
})
})?;

let should_send = request.is_ready();
let mut subtrans = Http3Subtransport {
connection,
request,
response: None,
};
debug!("http3 transport created");

if should_send {
debug!("sending request immediately");
subtrans.send_request(&[]).map_err(as_git2_error)?;
}
Ok(Box::new(subtrans))
}

fn close(&self) -> Result<(), git2::Error> {
Ok(()) // ...
}
}

impl Read for Http3Subtransport {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if let Some(reader) = self.response.as_mut() {
block_on(reader.read(buf))
} else {
debug!("transport read: no data available");
Ok(0)
}
}
}

impl Write for Http3Subtransport {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
debug!("transport write");
self.send_request(data)
.map(|()| data.len())
.map_err(as_io_error)
}

fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

fn as_io_error<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::Other, err)
}

fn as_git2_error<E: Display>(err: E) -> git2::Error {
git2::Error::from_str(&err.to_string())
}
3 changes: 3 additions & 0 deletions librad/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use crate::{
peer::PeerId,
};

pub mod h3;
pub mod server;

const PROJECT_METADATA_BRANCH: &str = "rad/project";
const PROJECT_METADATA_FILE: &str = "project.json";

Expand Down
Loading