use std::io;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use futures::executor::block_on;
use futures::io::AsyncRead as FuturesAsyncRead;
use futures::io::AsyncWriteExt as FuturesAsyncWriteExt;
use gix_packetline::async_io::{encode, StreamingPeekableIter};
use gix_packetline::{Channel, PacketLineRef};
use tokio::io::{AsyncRead as TokioAsyncRead, AsyncWrite as TokioAsyncWrite, ReadHalf, WriteHalf};
use tokio::sync::{mpsc, oneshot};
use tokio_util::compat::{Compat, TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::authorize::AuthorizedRepo;
use crate::backend::{GitPackGen, GitPackIngest, GitRefs, GitRegistry};
use crate::fetch::{serve_fetch_round, storage_to_session_error, FetchRounds};
use crate::limits::{Limits, SIDEBAND_CHUNK_SIZE};
use crate::push::{serve_receive_pack, PackTailDiscipline};
use crate::registry::AccessAction;
use crate::substrate::{Service, SubstrateError};
use crate::v0::advertise_v0;
use crate::v2::{advertise_v2, parse_ls_refs_args, serve_ls_refs};
use alkcall::core::Identity;
use tokio::sync::Semaphore;
pub(crate) const STOP_AT_FLUSH: &[PacketLineRef<'static>] = &[PacketLineRef::Flush];
pub(crate) const UNKNOWN_COMMAND_DECLINE: &str =
" — declined, only the served commands dispatch (ADR-003's honest surface)";
pub const SIDEBAND_CHANNEL_CAPACITY: usize = 4;
type SessionReader<T> = StreamingPeekableIter<CountingRead<Compat<ReadHalf<T>>>>;
type SessionWriter<T> = Compat<WriteHalf<T>>;
#[derive(Debug)]
pub(crate) struct CountingRead<T> {
pub(crate) inner: T,
pub(crate) consumed: Arc<AtomicU64>,
pub(crate) reads: Arc<AtomicU64>,
}
impl<T> futures::io::AsyncRead for CountingRead<T>
where
T: futures::io::AsyncRead + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
this.reads.fetch_add(1, Ordering::Relaxed);
match Pin::new(&mut this.inner).poll_read(cx, buf) {
Poll::Ready(Ok(n)) => {
this.consumed.fetch_add(n as u64, Ordering::Relaxed);
Poll::Ready(Ok(n))
}
other => other,
}
}
}
pub struct RawTail<R> {
read: R,
consumed: Arc<AtomicU64>,
}
impl<R> RawTail<R> {
pub fn new(read: R, consumed: Arc<AtomicU64>) -> Self {
RawTail { read, consumed }
}
pub fn consumed_bytes(&self) -> u64 {
self.consumed.load(Ordering::Relaxed)
}
}
impl<R> std::fmt::Debug for RawTail<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawTail")
.field("consumed_bytes", &self.consumed_bytes())
.finish_non_exhaustive()
}
}
impl<R> FuturesAsyncRead for RawTail<R>
where
R: FuturesAsyncRead + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
match Pin::new(&mut this.read).poll_read(cx, buf) {
Poll::Ready(Ok(n)) => {
this.consumed.fetch_add(n as u64, Ordering::Relaxed);
Poll::Ready(Ok(n))
}
other => other,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub command: String,
pub pre_lines: Vec<String>,
pub args: Vec<String>,
}
pub struct Session<T> {
limits: Limits,
service: Service,
reader: Option<SessionReader<T>>,
writer: Option<SessionWriter<T>>,
consumed: Arc<AtomicU64>,
reads: Arc<AtomicU64>,
stopped: bool,
terminal: Option<SubstrateError>,
}
impl<T> Session<T>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
pub fn new(stream: T, limits: Limits, service: Service) -> Self {
let (read_half, write_half) = tokio::io::split(stream);
let consumed = Arc::new(AtomicU64::new(0));
let reads = Arc::new(AtomicU64::new(0));
Session {
limits,
service,
reader: Some(StreamingPeekableIter::new(
CountingRead {
inner: read_half.compat(),
consumed: Arc::clone(&consumed),
reads: Arc::clone(&reads),
},
STOP_AT_FLUSH,
false,
)),
writer: Some(write_half.compat_write()),
consumed,
reads,
stopped: false,
terminal: None,
}
}
pub fn service(&self) -> Service {
self.service
}
pub fn limits(&self) -> &Limits {
&self.limits
}
pub fn consumed_bytes(&self) -> u64 {
self.consumed.load(Ordering::Relaxed)
}
pub fn transport_reads(&self) -> u64 {
self.reads.load(Ordering::Relaxed)
}
pub async fn read_request(&mut self) -> Result<Option<Request>, SubstrateError> {
let budget = self.limits.max_request_body;
let Self {
reader,
consumed,
stopped,
terminal,
..
} = self;
if let Some(cached) = terminal.clone() {
return Err(cached);
}
let Some(reader) = reader else {
return Err(SubstrateError::RawTailUnavailable(
"the pkt-line reader is gone — the raw tail was surrendered; push reads no more commands",
));
};
if *stopped {
return Err(SubstrateError::ResetRequired);
}
let consumed_before = consumed.load(Ordering::Relaxed);
let mut command: Option<String> = None;
let mut pre_lines: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let mut delim_seen = false;
loop {
enum Payload {
Text(String),
Delimiter,
StopPacket(String),
ErrLine(String),
}
let payload: Option<Payload> = match reader.read_line().await {
None => {
None
}
Some(Ok(Ok(line))) => Some(match line {
PacketLineRef::Data(d) => {
if let Some(message) = line
.check_error()
.map(|e| String::from_utf8_lossy(e.0).into_owned())
{
Payload::ErrLine(message)
} else {
let text = String::from_utf8_lossy(d)
.trim_end_matches(['\n', '\r'])
.to_owned();
Payload::Text(text)
}
}
PacketLineRef::Delimiter => Payload::Delimiter,
PacketLineRef::Flush | PacketLineRef::ResponseEnd => {
Payload::StopPacket("unexpected stop packet mid-request".to_owned())
}
}),
Some(Ok(Err(decode_error))) => {
let error = SubstrateError::Protocol(format!(
"malformed pkt-line from peer: {decode_error}"
));
*terminal = Some(error.clone());
return Err(error);
}
Some(Err(io_error)) => {
let consumed_total = consumed.load(Ordering::Relaxed);
let clean_close = io_error.kind() == io::ErrorKind::UnexpectedEof
&& consumed_total == consumed_before
&& command.is_none()
&& pre_lines.is_empty()
&& args.is_empty()
&& !delim_seen;
if clean_close {
*terminal = Some(SubstrateError::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the peer closed the stream",
)));
return Ok(None);
}
let error = SubstrateError::Io(io_error);
*terminal = Some(error.clone());
return Err(error);
}
};
match payload {
None => break,
Some(Payload::ErrLine(message)) => {
let error =
SubstrateError::Protocol(format!("peer sent an ERR line: {message}"));
*terminal = Some(error.clone());
return Err(error);
}
Some(Payload::StopPacket(message)) => {
let error = SubstrateError::Protocol(message);
*terminal = Some(error.clone());
return Err(error);
}
Some(Payload::Delimiter) => {
if delim_seen {
let error = SubstrateError::Protocol("repeated 0001 delimiter".to_owned());
*terminal = Some(error.clone());
return Err(error);
}
delim_seen = true;
}
Some(Payload::Text(text)) => {
if delim_seen {
args.push(text);
} else if let Some(value) = text.strip_prefix("command=") {
if command.is_some() {
let error = SubstrateError::Protocol(
"request carries a second command line".to_owned(),
);
*terminal = Some(error.clone());
return Err(error);
}
if value.is_empty() {
let error = SubstrateError::Protocol("empty command= line".to_owned());
*terminal = Some(error.clone());
return Err(error);
}
command = Some(value.to_owned());
} else {
pre_lines.push(text);
}
}
}
let consumed_total = consumed.load(Ordering::Relaxed);
if consumed_total.saturating_sub(consumed_before) > budget {
let error = SubstrateError::Protocol(format!(
"the request exceeds the max_request_body budget ({budget}) — fail closed (ADR-009)"
));
*terminal = Some(error.clone());
return Err(error);
}
}
let Some(command) = command else {
if pre_lines.is_empty() && args.is_empty() && !delim_seen {
*terminal = Some(SubstrateError::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the peer closed the stream",
)));
return Ok(None);
}
let error = SubstrateError::Protocol("request contained no command= line".to_owned());
*terminal = Some(error.clone());
return Err(error);
};
*stopped = true;
Ok(Some(Request {
command,
pre_lines,
args,
}))
}
pub fn reset_request(&mut self) {
if let Some(reader) = &mut self.reader {
reader.reset();
}
self.stopped = false;
}
pub fn take_raw_tail(&mut self) -> Result<RawTail<Compat<ReadHalf<T>>>, SubstrateError> {
if let Some(cached) = self.terminal.clone() {
return Err(cached);
}
let Some(reader) = self.reader.take() else {
return Err(SubstrateError::RawTailUnavailable(
"the raw tail was already surrendered — the escape is one-shot",
));
};
if !self.stopped {
self.reader = Some(reader);
return Err(SubstrateError::RawTailUnavailable(
"the request parse has not completed — the tail escapes only after the terminating flush",
));
}
let counting = reader.into_inner();
Ok(RawTail::new(counting.inner, counting.consumed))
}
pub async fn read_push_request(&mut self) -> Result<Option<Request>, SubstrateError> {
let budget = self.limits.max_request_body;
let Self {
reader,
consumed,
stopped,
terminal,
..
} = self;
if let Some(cached) = terminal.clone() {
return Err(cached);
}
let Some(reader) = reader else {
return Err(SubstrateError::RawTailUnavailable(
"the pkt-line reader is gone — the raw tail was surrendered; push reads no more commands",
));
};
if *stopped {
return Err(SubstrateError::ResetRequired);
}
let consumed_before = consumed.load(Ordering::Relaxed);
enum Payload {
Text(String),
StopPacket(String),
ErrLine(String),
}
let mut commands: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let mut in_options = false;
loop {
let payload: Option<Payload> = match reader.read_line().await {
None => {
None
}
Some(Ok(Ok(line))) => Some(match line {
PacketLineRef::Data(d) => {
if let Some(message) = line
.check_error()
.map(|e| String::from_utf8_lossy(e.0).into_owned())
{
Payload::ErrLine(message)
} else {
let text = String::from_utf8_lossy(d)
.trim_end_matches(['\n', '\r'])
.to_owned();
Payload::Text(text)
}
}
PacketLineRef::Delimiter => Payload::StopPacket(
"a 0001 delimiter in a push request is not a push shape".to_owned(),
),
PacketLineRef::Flush | PacketLineRef::ResponseEnd => {
Payload::StopPacket("unexpected stop packet mid-request".to_owned())
}
}),
Some(Ok(Err(decode_error))) => {
let error = SubstrateError::Protocol(format!(
"malformed pkt-line in the push request: {decode_error}"
));
*terminal = Some(error.clone());
return Err(error);
}
Some(Err(io_error)) => {
let clean_close = io_error.kind() == io::ErrorKind::UnexpectedEof
&& consumed.load(Ordering::Relaxed) == consumed_before
&& commands.is_empty()
&& args.is_empty();
if clean_close {
*terminal = Some(SubstrateError::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the peer closed the stream",
)));
return Ok(None);
}
let error = SubstrateError::Io(io_error);
*terminal = Some(error.clone());
return Err(error);
}
};
match payload {
None => {
reader.reset();
if commands.is_empty() && !in_options {
*stopped = true;
return Ok(Some(Request {
command: "git-receive-pack".to_owned(),
pre_lines: Vec::new(),
args: Vec::new(),
}));
}
if !in_options
&& commands
.first()
.and_then(|line| line.split_once('\0'))
.map(|(_, caps)| caps.split_whitespace().any(|t| t == "push-options"))
.unwrap_or(false)
{
in_options = true;
continue;
}
*stopped = true;
return Ok(Some(Request {
command: "git-receive-pack".to_owned(),
pre_lines: commands,
args,
}));
}
Some(Payload::ErrLine(message)) => {
let error =
SubstrateError::Protocol(format!("peer sent an ERR line: {message}"));
*terminal = Some(error.clone());
return Err(error);
}
Some(Payload::StopPacket(message)) => {
let error = SubstrateError::Protocol(message);
*terminal = Some(error.clone());
return Err(error);
}
Some(Payload::Text(text)) => {
if in_options {
args.push(text);
} else {
commands.push(text);
}
}
}
let consumed_total = consumed.load(Ordering::Relaxed);
if consumed_total.saturating_sub(consumed_before) > budget {
let error = SubstrateError::Protocol(format!(
"the request exceeds the max_request_body budget ({budget}) — fail closed (ADR-009)"
));
*terminal = Some(error.clone());
return Err(error);
}
}
}
pub async fn write_text_line(&mut self, line: &[u8]) -> Result<(), SubstrateError> {
let writer = self.writer.as_mut().ok_or(SubstrateError::WriterTaken)?;
encode::text_to_write(line, writer).await?;
Ok(())
}
pub async fn write_flush(&mut self) -> Result<(), SubstrateError> {
let writer = self.writer.as_mut().ok_or(SubstrateError::WriterTaken)?;
encode::flush_to_write(writer).await?;
Ok(())
}
pub async fn write_data_band(&mut self, data: &[u8]) -> Result<(), SubstrateError> {
let writer = self.writer.as_mut().ok_or(SubstrateError::WriterTaken)?;
for chunk in data.chunks(SIDEBAND_CHUNK_SIZE) {
encode::band_to_write(Channel::Data, chunk, &mut *writer).await?;
}
Ok(())
}
pub async fn write_error_band(&mut self, message: &str) -> Result<(), SubstrateError> {
let writer = self.writer.as_mut().ok_or(SubstrateError::WriterTaken)?;
encode::error_to_write(message.as_bytes(), writer).await?;
Ok(())
}
}
impl<T> Session<T>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send + 'static,
{
pub fn sideband(&mut self) -> Result<Sideband<T>, SubstrateError> {
let Some(writer) = self.writer.take() else {
return Err(SubstrateError::WriterTaken);
};
let (tx, rx) = mpsc::channel(SIDEBAND_CHANNEL_CAPACITY);
let (restore, restore_rx) = oneshot::channel();
tokio::spawn(drive_sideband(rx, writer, restore));
Ok(Sideband {
buf: Vec::with_capacity(SIDEBAND_CHUNK_SIZE),
tx,
restore_rx,
flushed: false,
bytes_pushed: 0,
})
}
pub async fn finish_sideband(&mut self, sink: Sideband<T>) -> Result<(), SubstrateError> {
let writer = sink.finish().await?;
self.writer = Some(writer);
Ok(())
}
}
enum SidebandMsg {
Chunk(Vec<u8>),
Flush,
}
async fn drive_sideband<T>(
mut rx: mpsc::Receiver<SidebandMsg>,
mut writer: SessionWriter<T>,
restore: oneshot::Sender<Result<SessionWriter<T>, SubstrateError>>,
) where
T: TokioAsyncWrite + Unpin,
{
let mut flushed = false;
let mut failure: Option<SubstrateError> = None;
while let Some(message) = rx.recv().await {
match message {
SidebandMsg::Chunk(chunk) => {
if flushed {
failure = Some(SubstrateError::Protocol(
"sideband data written after the section flush".to_owned(),
));
break;
}
if let Err(e) = encode::band_to_write(Channel::Data, &chunk, &mut writer).await {
failure = Some(SubstrateError::Io(e));
break;
}
}
SidebandMsg::Flush => {
if !flushed {
if let Err(e) = encode::flush_to_write(&mut writer).await {
failure = Some(SubstrateError::Io(e));
break;
}
flushed = true;
}
}
}
}
let _ = FuturesAsyncWriteExt::flush(&mut writer).await;
let _ = restore.send(match failure {
Some(e) => Err(e),
None => Ok(writer),
});
}
pub struct Sideband<T> {
buf: Vec<u8>,
tx: mpsc::Sender<SidebandMsg>,
restore_rx: oneshot::Receiver<Result<SessionWriter<T>, SubstrateError>>,
flushed: bool,
bytes_pushed: u64,
}
impl<T> std::fmt::Debug for Sideband<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sideband")
.field("flushed", &self.flushed)
.field("bytes_pushed", &self.bytes_pushed)
.finish_non_exhaustive()
}
}
impl<T> Sideband<T> {
pub fn bytes_pushed(&self) -> u64 {
self.bytes_pushed
}
fn send(&mut self, message: SidebandMsg) -> io::Result<()> {
block_on(self.tx.send(message)).map_err(|_| {
io::Error::new(io::ErrorKind::BrokenPipe, "the sideband drain task is gone")
})
}
fn push_buffered_chunk(&mut self) -> io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let chunk = std::mem::take(&mut self.buf);
self.send(SidebandMsg::Chunk(chunk))
}
async fn finish(mut self) -> Result<SessionWriter<T>, SubstrateError> {
std::io::Write::flush(&mut self).map_err(SubstrateError::Io)?;
let Sideband {
buf,
tx,
restore_rx,
..
} = self;
drop(buf);
drop(tx);
match restore_rx.await {
Ok(Ok(writer)) => Ok(writer),
Ok(Err(e)) => Err(e),
Err(_) => Err(SubstrateError::Io(io::Error::other(
"the sideband drain task ended without restoring the response writer",
))),
}
}
}
impl<T> io::Write for Sideband<T> {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
if self.flushed {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"the sideband section was already flushed",
));
}
let mut consumed = 0;
while consumed < data.len() {
let space = SIDEBAND_CHUNK_SIZE - self.buf.len();
let take = space.min(data.len() - consumed);
self.buf.extend_from_slice(&data[consumed..consumed + take]);
consumed += take;
if self.buf.len() == SIDEBAND_CHUNK_SIZE {
self.push_buffered_chunk()?;
}
}
self.bytes_pushed += consumed as u64;
Ok(consumed)
}
fn flush(&mut self) -> io::Result<()> {
if self.flushed {
return Ok(());
}
self.push_buffered_chunk()?;
self.send(SidebandMsg::Flush)?;
self.flushed = true;
Ok(())
}
}
fn wall_clock_breach(limits: &Limits) -> SubstrateError {
SubstrateError::Protocol(format!(
"the session exceeded the session_wall_clock budget ({:?}) — the session closes, server/session budget class (ADR-009)",
limits.session_wall_clock
))
}
async fn write_protocol_band<T>(
session: &mut Session<T>,
error: &SubstrateError,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
match error {
SubstrateError::Protocol(message) => session.write_error_band(message).await,
_ => Ok(()),
}
}
async fn drive_ls_refs<T>(
repo: &AuthorizedRepo,
session: &mut Session<T>,
request: &Request,
refs: Arc<dyn GitRefs>,
limits: &Limits,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send,
{
let args = parse_ls_refs_args(&request.args)?;
let listed = refs
.list_refs(repo.record())
.await
.map_err(storage_to_session_error)?;
serve_ls_refs(session, listed, &args, limits).await
}
async fn drive_fetch<T>(
repo: &AuthorizedRepo,
session: &mut Session<T>,
request: &Request,
rounds: &mut FetchRounds,
packgen: Arc<dyn GitPackGen>,
limits: &Limits,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send,
{
serve_fetch_round(
session,
request,
rounds,
packgen,
repo.record(),
limits,
pipeline,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn drive_command<T>(
repo: &AuthorizedRepo,
session: &mut Session<T>,
request: &Request,
rounds: &mut FetchRounds,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
limits: &Limits,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send,
{
match request.command.as_str() {
"ls-refs" => drive_ls_refs(repo, session, request, refs, limits).await,
"fetch" => drive_fetch(repo, session, request, rounds, packgen, limits, pipeline).await,
other => Err(SubstrateError::Protocol(format!(
"unknown command {other:?}{UNKNOWN_COMMAND_DECLINE}"
))),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn serve_duplex<T>(
repo: &AuthorizedRepo,
stream: T,
limits: Limits,
service: Service,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send + 'static,
{
let mut session = Session::new(stream, limits, service);
let deadline = tokio::time::Instant::now() + limits.session_wall_clock;
match service {
Service::ReceivePack => {
let listed =
match tokio::time::timeout_at(deadline, refs.list_refs(repo.record())).await {
Ok(Ok(listed)) => listed,
Ok(Err(error)) => {
let error = storage_to_session_error(error);
let _ = write_protocol_band(&mut session, &error).await;
return Err(error);
}
Err(_elapsed) => return Err(end_wall_clock_breach(&mut session, &limits).await),
};
match tokio::time::timeout_at(
deadline,
advertise_v0(&mut session, listed, push_options_negotiated, &limits),
)
.await
{
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(_elapsed) => return Err(end_wall_clock_breach(&mut session, &limits).await),
}
let read = tokio::time::timeout_at(deadline, session.read_push_request()).await;
let request = match read {
Ok(Ok(Some(request))) => request,
Ok(Ok(None)) => return Ok(()),
Ok(Err(e)) => {
let _ = write_protocol_band(&mut session, &e).await;
return Err(e);
}
Err(_elapsed) => return Err(end_wall_clock_breach(&mut session, &limits).await),
};
let tail = session.take_raw_tail()?;
let outcome = serve_receive_pack(
&mut session,
&request,
tail,
push_options_negotiated,
Arc::clone(&refs),
Arc::clone(&ingest),
repo.record(),
&limits,
PackTailDiscipline::Duplex,
Arc::clone(&pipeline),
);
match tokio::time::timeout_at(deadline, outcome).await {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => {
let _ = write_protocol_band(&mut session, &e).await;
Err(e)
}
Err(_elapsed) => Err(end_wall_clock_breach(&mut session, &limits).await),
}
}
Service::UploadPack => {
match tokio::time::timeout_at(deadline, advertise_v2(&mut session)).await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(_elapsed) => return Err(wall_clock_breach(&limits)),
}
let mut rounds = FetchRounds::default();
loop {
let read = tokio::time::timeout_at(deadline, session.read_request()).await;
let request = match read {
Ok(Ok(Some(request))) => request,
Ok(Ok(None)) => return Ok(()),
Ok(Err(e)) => {
let _ = write_protocol_band(&mut session, &e).await;
return Err(e);
}
Err(_elapsed) => {
return Err(end_wall_clock_breach(&mut session, &limits).await);
}
};
let outcome = drive_command(
repo,
&mut session,
&request,
&mut rounds,
Arc::clone(&refs),
Arc::clone(&packgen),
&limits,
Arc::clone(&pipeline),
);
let served = tokio::time::timeout_at(deadline, outcome).await;
match served {
Ok(Ok(())) => session.reset_request(),
Ok(Err(e)) => {
let _ = write_protocol_band(&mut session, &e).await;
return Err(e);
}
Err(_elapsed) => {
return Err(end_wall_clock_breach(&mut session, &limits).await);
}
}
}
}
}
}
async fn end_wall_clock_breach<T>(session: &mut Session<T>, limits: &Limits) -> SubstrateError
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
let breach = wall_clock_breach(limits);
if let SubstrateError::Protocol(message) = &breach {
let _ = session.write_error_band(message).await;
}
breach
}
#[allow(clippy::too_many_arguments)]
pub async fn serve_duplex_resolved<T>(
repo_id: &str,
identity: Option<&Identity>,
service: Service,
stream: T,
limits: Limits,
registry: Arc<dyn GitRegistry>,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
push_options_negotiated: bool,
pipeline: Arc<Semaphore>,
) -> Result<(), SubstrateError>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin + Send + 'static,
{
let action = if service.is_write() {
AccessAction::Write
} else {
AccessAction::Read
};
let marker = match registry.resolve(repo_id).await {
Ok(record) => AuthorizedRepo::authorize(record, identity, action),
Err(_) => None,
};
match marker {
Some(marker) => {
serve_duplex(
&marker,
stream,
limits,
service,
refs,
packgen,
ingest,
push_options_negotiated,
pipeline,
)
.await
}
None => Err(deny_generic(repo_id, stream).await),
}
}
async fn write_session_error<T>(stream: T, message: &str) -> Result<(), io::Error>
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
use tokio::io::AsyncWriteExt;
let mut compat = stream.compat_write();
encode::error_to_write(message.as_bytes(), &mut compat).await?;
FuturesAsyncWriteExt::flush(&mut compat).await?;
let shutdown = compat.into_inner().shutdown().await;
shutdown
}
pub(crate) async fn deny_generic<T>(repo_id: &str, stream: T) -> SubstrateError
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
let _ = write_session_error(stream, &format!("access denied: {repo_id}")).await;
SubstrateError::AccessDenied
}
pub(crate) async fn deny_decline<T>(stream: T, message: &str)
where
T: TokioAsyncRead + TokioAsyncWrite + Unpin,
{
let _ = write_session_error(stream, message).await;
}
#[cfg(test)]
mod tests {
use super::*;
use futures::io::AsyncReadExt as FuturesAsyncReadExt;
use std::io::Write as _;
use std::time::Duration;
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
use tokio::time::timeout;
const FLUSH: &[u8] = b"0000";
const DELIMITER: &[u8] = b"0001";
fn pkt(payload: &[u8]) -> Vec<u8> {
let total = payload.len() + 4;
let mut out = Vec::with_capacity(total);
out.extend_from_slice(format!("{total:04x}").as_bytes());
out.extend_from_slice(payload);
out
}
async fn read_pkts(reader: &mut tokio::io::DuplexStream, count: usize) -> Vec<Vec<u8>> {
let mut packets = Vec::with_capacity(count);
for _ in 0..count {
let mut header = [0u8; 4];
reader
.read_exact(&mut header)
.await
.expect("pkt-line header");
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
let mut payload = vec![0u8; len.saturating_sub(4)];
reader
.read_exact(&mut payload)
.await
.expect("pkt-line payload");
packets.push(payload);
}
packets
}
fn session() -> (tokio::io::DuplexStream, Session<tokio::io::DuplexStream>) {
let (peer, stream) = duplex(1 << 16);
(
peer,
Session::new(stream, Limits::default(), Service::UploadPack),
)
}
#[tokio::test]
async fn session_constructs_from_stream_limits_service() {
let (_peer, session) = session();
assert_eq!(session.service(), Service::UploadPack);
assert_eq!(*session.limits(), Limits::default());
assert_eq!(session.consumed_bytes(), 0);
assert_eq!(session.transport_reads(), 0);
}
#[tokio::test]
async fn request_reader_parses_command_pre_lines_args_and_flush() {
let (mut peer, mut session) = session();
let request = [
pkt(b"command=ls-refs\n"),
pkt(b"agent=git/2.43\n"),
DELIMITER.to_vec(),
pkt(b"peel\n"),
pkt(b"ref-prefix refs/heads/\n"),
FLUSH.to_vec(),
]
.concat();
peer.write_all(&request).await.unwrap();
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "ls-refs");
assert_eq!(parsed.pre_lines, vec!["agent=git/2.43"]);
assert_eq!(parsed.args, vec!["peel", "ref-prefix refs/heads/"]);
assert!(session.consumed_bytes() > 0);
}
#[tokio::test]
async fn raw_tail_escapes_after_the_flush_without_byte_loss() {
let (mut peer, mut session) = session();
let framed = [
pkt(b"command=git-receive-pack\n"),
pkt(b"agent=git/2.43\n"),
DELIMITER.to_vec(),
pkt(b"push-options"),
]
.concat();
let tail: Vec<u8> = (0..9000u32).map(|i| (i % 251) as u8).collect();
peer.write_all(&framed[..14]).await.unwrap();
peer.write_all(&framed[14..]).await.unwrap();
peer.write_all(&FLUSH[..2]).await.unwrap();
peer.write_all(&[&FLUSH[2..], &tail[..5]].concat())
.await
.unwrap();
peer.write_all(&tail[5..4000]).await.unwrap();
peer.flush().await.unwrap();
peer.write_all(&tail[4000..]).await.unwrap();
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "git-receive-pack");
assert_eq!(
session.consumed_bytes(),
(framed.len() + FLUSH.len()) as u64,
"the parse consumed exactly the framed request (flush included)"
);
let mut tail_reader = session.take_raw_tail().unwrap();
drop(peer);
let mut got = Vec::new();
tail_reader.read_to_end(&mut got).await.unwrap();
assert_eq!(got, tail, "the tail continues byte-exactly");
assert_eq!(
tail_reader.consumed_bytes(),
(framed.len() + FLUSH.len()) as u64 + tail.len() as u64,
"the tail rides the substrate counter: framed + tail"
);
assert_eq!(
tail_reader.consumed_bytes(),
session.consumed_bytes(),
"one shared counter"
);
}
#[tokio::test]
async fn the_raw_tail_escape_refuses_without_a_completed_parse() {
let (mut peer, mut session) = session();
peer.write_all(&pkt(b"command=ls-refs\n")).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
let error = session.take_raw_tail().unwrap_err();
match error {
SubstrateError::RawTailUnavailable(message) => {
assert!(message.contains("parse"), "{message}");
}
other => panic!("the escape before a parse is misuse: {other:?}"),
}
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "ls-refs");
}
#[tokio::test]
async fn the_raw_tail_escape_is_one_shot_and_the_reader_is_gone() {
let (mut peer, mut session) = session();
peer.write_all(&pkt(b"command=ls-refs\n")).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
session.read_request().await.unwrap().expect("a request");
let _tail = session.take_raw_tail().unwrap();
let error = session.take_raw_tail().unwrap_err();
match error {
SubstrateError::RawTailUnavailable(message) => {
assert!(message.contains("one-shot"), "{message}");
}
other => panic!("a second escape is misuse: {other:?}"),
}
let error = session.read_request().await.unwrap_err();
match error {
SubstrateError::RawTailUnavailable(message) => {
assert!(message.contains("gone"), "{message}");
assert!(!error.is_terminal(), "misuse, not a wire shape");
}
other => panic!("post-escape reads are misuse: {other:?}"),
}
}
#[tokio::test]
async fn the_raw_tail_escape_refuses_after_a_terminal_error() {
let (mut peer, mut session) = session();
peer.write_all(b"zzzz").await.unwrap();
let terminal = session.read_request().await.unwrap_err();
assert!(terminal.is_terminal());
let error = session.take_raw_tail().unwrap_err();
match (terminal, error) {
(SubstrateError::Protocol(first), SubstrateError::Protocol(message)) => {
assert!(first.contains("malformed pkt-line"), "{first}");
assert!(message.contains("malformed pkt-line"), "{message}");
}
other => panic!("the terminal error stays terminal: {other:?}"),
}
}
#[tokio::test]
async fn reset_is_required_after_flush_and_the_next_command_is_not_swallowed() {
let (mut peer, mut session) = session();
let requests = [
pkt(b"command=ls-refs\n"),
FLUSH.to_vec(),
pkt(b"command=fetch\n"),
FLUSH.to_vec(),
pkt(b"command=ls-refs\n"),
FLUSH.to_vec(),
]
.concat();
peer.write_all(&requests).await.unwrap();
for expected in ["ls-refs", "fetch", "ls-refs"] {
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, expected);
match session.read_request().await.unwrap_err() {
SubstrateError::ResetRequired => {}
other => panic!("expected the reset discipline: {other:?}"),
}
session.reset_request();
}
}
#[tokio::test]
async fn error_shaped_reads_break_unconditionally() {
let (peer, mut session) = session();
let driver = tokio::spawn(async move {
let mut peer = peer;
peer.write_all(b"000").await.ok();
drop(peer);
});
driver.await.unwrap();
let error = session.read_request().await.unwrap_err();
let SubstrateError::Io(e) = &error else {
panic!("a mid-header EOF is terminal io: {error:?}");
};
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
assert!(error.is_terminal());
assert_eq!(session.consumed_bytes(), 3);
let reads_frozen = session.transport_reads();
for _ in 0..10 {
let retry = session.read_request().await.unwrap_err();
let SubstrateError::Io(e) = &retry else {
panic!("terminal errors stay terminal: {retry:?}");
};
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
}
assert_eq!(
session.transport_reads(),
reads_frozen,
"no retry may re-enter the transport"
);
session.reset_request();
assert!(session.read_request().await.is_err());
}
#[tokio::test]
async fn clean_peer_close_reads_as_none_and_then_is_terminal() {
let (peer, mut session) = session();
let driver = tokio::spawn(async move {
let mut peer = peer;
peer.write_all(&pkt(b"command=ls-refs\n")).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
drop(peer);
});
driver.await.unwrap();
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "ls-refs");
session.reset_request();
let closed = session.read_request().await.unwrap();
assert!(closed.is_none(), "a clean close is not an error");
let reads_frozen = session.transport_reads();
assert!(session.read_request().await.is_err());
assert_eq!(
session.transport_reads(),
reads_frozen,
"after the clean close the transport is never re-entered"
);
}
#[tokio::test]
async fn a_flush_only_request_at_the_command_position_ends_the_session_quietly() {
let (peer, mut session) = session();
let driver = tokio::spawn(async move {
let mut peer = peer;
peer.write_all(&pkt(b"command=ls-refs\n")).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
peer.shutdown().await.unwrap();
});
driver.await.unwrap();
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "ls-refs");
session.reset_request();
let closed = session.read_request().await.unwrap();
assert!(closed.is_none(), "the trailing flush ends the session");
assert!(session.read_request().await.is_err(), "terminal afterward");
}
#[tokio::test]
async fn malformed_pkt_lines_are_protocol_errors() {
let (mut peer, mut session) = session();
peer.write_all(b"zzzz").await.unwrap();
let error = session.read_request().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("malformed pkt-line"), "{message}");
}
other => panic!("decode errors are protocol-shaped: {other:?}"),
}
assert!(
session.read_request().await.is_err(),
"terminal protocol errors stay terminal"
);
}
#[tokio::test]
async fn client_err_lines_map_to_protocol_errors() {
let (mut peer, mut session) = session();
peer.write_all(&pkt(b"ERR unauthorized client"))
.await
.unwrap();
peer.write_all(FLUSH).await.unwrap();
let error = session.read_request().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("ERR"), "{message}");
assert!(message.contains("unauthorized client"), "{message}");
}
other => panic!("client ERR lines are band-mappable: {other:?}"),
}
}
#[tokio::test]
async fn a_request_without_a_command_line_is_a_protocol_error() {
let (mut peer, mut session) = session();
peer.write_all(&pkt(b"peel\n")).await.unwrap();
peer.write_all(FLUSH).await.unwrap();
let error = session.read_request().await.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("no command"), "{message}");
}
other => panic!("a command-less request is out of grammar: {other:?}"),
}
}
#[tokio::test]
async fn a_request_over_the_max_request_body_budget_fails_closed() {
let (peer, stream) = duplex(1 << 16);
let mut session = Session::new(
stream,
Limits {
max_request_body: 200,
..Limits::default()
},
Service::UploadPack,
);
let driver = tokio::spawn(async move {
let mut peer = peer;
for _ in 0..100 {
peer.write_all(&pkt(b"peel\n")).await.unwrap();
}
peer.write_all(FLUSH).await.unwrap();
});
let error = session.read_request().await.unwrap_err();
driver.await.unwrap();
match &error {
SubstrateError::Protocol(message) => {
assert!(message.contains("max_request_body"), "{message}");
}
other => panic!("a budget breach is protocol-shaped: {other:?}"),
}
assert!(error.is_terminal());
let (peer, stream) = duplex(1 << 16);
let mut session = Session::new(
stream,
Limits {
max_request_body: 200,
..Limits::default()
},
Service::ReceivePack,
);
let driver = tokio::spawn(async move {
let mut peer = peer;
for _ in 0..100 {
peer.write_all(&pkt(format!(
"{} {} refs/heads/x",
"0".repeat(40),
"0a".repeat(20)
)
.as_bytes()))
.await
.unwrap();
}
peer.write_all(FLUSH).await.unwrap();
});
let error = session.read_push_request().await.unwrap_err();
driver.await.unwrap();
match &error {
SubstrateError::Protocol(message) => {
assert!(message.contains("max_request_body"), "{message}");
}
other => panic!("a budget breach is protocol-shaped: {other:?}"),
}
assert!(error.is_terminal());
}
#[tokio::test]
async fn a_request_under_the_budget_still_parses() {
let (mut peer, stream) = duplex(1 << 16);
let mut session = Session::new(
stream,
Limits {
max_request_body: 4000,
..Limits::default()
},
Service::UploadPack,
);
let mut request = Vec::new();
request.extend_from_slice(&pkt(b"command=ls-refs\n"));
for i in 0..20u32 {
request.extend_from_slice(&pkt(format!("peel-{i}\n").as_bytes()));
}
request.extend_from_slice(FLUSH);
peer.write_all(&request).await.unwrap();
let parsed = session.read_request().await.unwrap().expect("a request");
assert_eq!(parsed.command, "ls-refs");
assert_eq!(parsed.args.len(), 0);
assert_eq!(
parsed.pre_lines.len(),
20,
"command line separate, 20 pre-lines"
);
}
#[tokio::test]
async fn writes_frame_text_error_and_flush_lines() {
let (mut peer, mut session) = session();
session.write_text_line(b"version 2").await.unwrap();
session
.write_text_line(b"object-format=sha1")
.await
.unwrap();
session
.write_error_band("repository not found")
.await
.unwrap();
session.write_flush().await.unwrap();
let packets = read_pkts(&mut peer, 4).await;
assert_eq!(packets[0], b"version 2\n".to_vec());
assert_eq!(packets[1], b"object-format=sha1\n".to_vec());
assert_eq!(packets[2], b"ERR repository not found".to_vec());
assert_eq!(packets[3], Vec::<u8>::new(), "the flush pkt-line");
}
#[tokio::test]
async fn sideband_chunks_at_the_cap_and_flush_is_one_shot() {
let (mut peer, mut session) = session();
let sink = session.sideband().unwrap();
let payload = vec![b'x'; 150_000];
let driver = tokio::task::spawn_blocking(move || {
let mut sink = sink;
sink.write_all(&payload)
.expect("the sink accepts any byte count");
sink.flush().unwrap();
sink.flush().unwrap();
let refused = sink
.write(b"after-flush")
.expect_err("no data after the flush");
assert_eq!(refused.kind(), io::ErrorKind::BrokenPipe);
(sink.bytes_pushed(), sink)
});
let packets = read_pkts(&mut peer, 4).await;
for (i, size) in [65000usize, 65000, 20000].into_iter().enumerate() {
assert_eq!(packets[i][0], 1, "band 1");
assert_eq!(packets[i].len() - 1, size, "chunk {i}");
}
assert!(packets[3].is_empty(), "the one-shot section flush");
let (pushed, sink) = driver.await.unwrap();
assert_eq!(pushed, 150_000);
session.finish_sideband(sink).await.unwrap();
let mut extra = [0u8; 1];
let nothing = timeout(Duration::from_millis(150), peer.read_exact(&mut extra)).await;
assert!(
nothing.is_err(),
"no bytes may follow the one-shot section flush"
);
}
#[tokio::test]
async fn the_sideband_sink_exclusively_holds_the_writer() {
let (mut peer, mut session) = session();
let sink = session.sideband().unwrap();
match session.sideband().unwrap_err() {
SubstrateError::WriterTaken => {}
other => panic!("no second sink while one lives: {other:?}"),
}
match session.write_text_line(b"blocked").await.unwrap_err() {
SubstrateError::WriterTaken => {}
other => panic!("the writer is exclusively the sink's: {other:?}"),
}
session.finish_sideband(sink).await.unwrap();
session.write_text_line(b"restored").await.unwrap();
session.write_flush().await.unwrap();
let packets = read_pkts(&mut peer, 3).await;
assert!(packets[0].is_empty(), "the response flush from the sink");
assert_eq!(packets[1], b"restored\n".to_vec());
assert!(packets[2].is_empty(), "the session flush");
}
#[tokio::test]
async fn sideband_drain_failures_surface_through_finish() {
let (peer, mut session) = session();
let sink = session.sideband().unwrap();
drop(peer);
let driver = tokio::task::spawn_blocking(move || {
let mut sink = sink;
let chunk = vec![b'y'; SIDEBAND_CHUNK_SIZE];
let mut errored = false;
for _ in 0..32 {
match sink.write(&chunk) {
Err(_) => {
errored = true;
break;
}
Ok(written) => {
assert!(written > 0, "a successful write accepts bytes");
std::thread::sleep(Duration::from_millis(5));
}
}
}
assert!(errored, "the sink must surface the drain failure");
sink
});
let sink = driver.await.unwrap();
let error = session.finish_sideband(sink).await.unwrap_err();
assert!(
error.is_terminal(),
"the drain failure is terminal: {error:?}"
);
}
}