1#[cfg(feature = "controller")]
4use std::collections::BTreeMap;
5#[cfg(feature = "controller")]
6use std::fs::{File, OpenOptions, TryLockError};
7use std::future::Future;
8#[cfg(feature = "controller")]
9use std::io::Write as _;
10use std::path::Path;
11#[cfg(feature = "controller")]
12use std::path::PathBuf;
13#[cfg(feature = "controller")]
14use std::process::Stdio;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::time::{Duration, Instant};
17
18use anyhow::{Context, Result, anyhow, bail, ensure};
19use serde::{Deserialize, Serialize};
20use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
21#[cfg(feature = "controller")]
22use tokio::process::Command;
23use tokio_util::sync::CancellationToken;
24
25#[cfg(feature = "controller")]
26use crate::hel_local_git::canonical_repository;
27#[cfg(feature = "controller")]
28use crate::hel_targets::CommandSpec;
29
30const BRIDGE_MAGIC: &[u8] = b"HEL-GIT-BRIDGE-2\n";
31const MAX_FRAME: usize = 1024 * 1024;
32const MAX_OPEN: usize = 16 * 1024;
33#[cfg(feature = "controller")]
36const BRIDGE_EXIT_GRACE: Duration = Duration::from_secs(5);
37#[cfg(unix)]
44const HANDSHAKE_DEADLINE: Duration = Duration::from_secs(30);
45const EXCHANGE_IDLE_DEADLINE: Duration = Duration::from_secs(300);
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59#[cfg(feature = "controller")]
60pub struct GitBrokerSpec {
61 pub session_id: String,
62 pub bridge: CommandSpec,
63 pub repositories: BTreeMap<String, PathBuf>,
64 pub ready_path: PathBuf,
65 pub pid_path: PathBuf,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70struct GitOpen {
71 repository: String,
72 service: String,
73}
74
75#[cfg(feature = "controller")]
76impl GitBrokerSpec {
77 pub fn write(&self, path: &Path) -> Result<()> {
78 let parent = path.parent().unwrap_or_else(|| Path::new("."));
79 std::fs::create_dir_all(parent)?;
80 crate::hel_config::atomic_write(path, &serde_json::to_vec_pretty(self)?)
81 }
82
83 pub fn read(path: &Path) -> Result<Self> {
84 let bytes = std::fs::read(path)
85 .with_context(|| format!("read Git broker spec {}", path.display()))?;
86 serde_json::from_slice(&bytes)
87 .with_context(|| format!("parse Git broker spec {}", path.display()))
88 }
89}
90
91#[cfg(feature = "controller")]
93enum BrokerLock {
94 Held(Option<i32>),
97 Free,
99}
100
101#[cfg(feature = "controller")]
110fn broker_lock(pid_path: &Path) -> BrokerLock {
111 let Ok(file) = OpenOptions::new().read(true).write(true).open(pid_path) else {
112 return BrokerLock::Free;
113 };
114 match file.try_lock() {
115 Ok(()) => {
117 let _ = file.unlock();
118 BrokerLock::Free
119 }
120 Err(TryLockError::WouldBlock) => BrokerLock::Held(
121 std::fs::read_to_string(pid_path)
122 .ok()
123 .and_then(|pid| pid.trim().parse().ok()),
124 ),
125 Err(TryLockError::Error(error)) => {
126 tracing::warn!(
127 path = %pid_path.display(),
128 error = %error,
129 "could not test the Git broker lock; treating the broker as gone"
130 );
131 BrokerLock::Free
132 }
133 }
134}
135
136#[cfg(feature = "controller")]
138pub fn broker_is_alive(pid_path: &Path) -> bool {
139 matches!(broker_lock(pid_path), BrokerLock::Held(_))
140}
141
142#[cfg(feature = "controller")]
147pub fn running_broker_pid(pid_path: &Path) -> Option<i32> {
148 match broker_lock(pid_path) {
149 BrokerLock::Held(pid) => pid,
150 BrokerLock::Free => None,
151 }
152}
153
154#[doc(hidden)]
159#[cfg(feature = "controller")]
160pub fn claim_broker_pid_file(pid_path: &Path) -> Result<File> {
161 let mut options = OpenOptions::new();
162 options.create(true).read(true).write(true);
163 #[cfg(unix)]
164 {
165 use std::os::unix::fs::OpenOptionsExt;
166 options.mode(0o600);
167 }
168 let mut file = options
169 .open(pid_path)
170 .with_context(|| format!("open Git broker lock {}", pid_path.display()))?;
171 match file.try_lock() {
172 Ok(()) => {}
173 Err(TryLockError::WouldBlock) => bail!(
174 "another local Git broker already owns {}",
175 pid_path.display()
176 ),
177 Err(TryLockError::Error(error)) => {
178 return Err(error)
179 .with_context(|| format!("lock Git broker file {}", pid_path.display()));
180 }
181 }
182 file.set_len(0)?;
183 file.write_all(std::process::id().to_string().as_bytes())?;
184 file.flush()?;
185 Ok(file)
186}
187
188#[cfg(feature = "controller")]
189pub async fn run_broker(spec_path: &Path) -> Result<()> {
190 let spec = GitBrokerSpec::read(spec_path)?;
191 let repositories = spec
192 .repositories
193 .iter()
194 .map(|(id, path)| Ok((id.clone(), canonical_repository(path)?)))
195 .collect::<Result<BTreeMap<_, _>>>()?;
196 if let Some(parent) = spec.ready_path.parent() {
197 std::fs::create_dir_all(parent)?;
198 }
199 let pid_file = claim_broker_pid_file(&spec.pid_path)?;
201 let result = run_bridge_process(&spec, repositories).await;
202 for path in [&spec.ready_path, &spec.pid_path] {
203 if let Err(error) = std::fs::remove_file(path)
204 && error.kind() != std::io::ErrorKind::NotFound
205 {
206 tracing::warn!(
207 path = %path.display(),
208 %error,
209 "could not remove Git broker lifecycle marker"
210 );
211 }
212 }
213 drop(pid_file);
214 result
215}
216
217#[cfg(feature = "controller")]
218async fn run_bridge_process(
219 spec: &GitBrokerSpec,
220 repositories: BTreeMap<String, PathBuf>,
221) -> Result<()> {
222 let mut child = Command::new(&spec.bridge.program)
223 .args(&spec.bridge.args)
224 .envs(&spec.bridge.env)
225 .stdin(Stdio::piped())
226 .stdout(Stdio::piped())
227 .stderr(Stdio::inherit())
228 .spawn()
229 .with_context(|| format!("start {}", spec.bridge.purpose))?;
230 let mut input = child.stdin.take().context("Git bridge stdin is missing")?;
231 let mut output = child
232 .stdout
233 .take()
234 .context("Git bridge stdout is missing")?;
235 let mut magic = vec![0; BRIDGE_MAGIC.len()];
236 output
237 .read_exact(&mut magic)
238 .await
239 .context("read Git bridge greeting")?;
240 ensure!(magic == BRIDGE_MAGIC, "target Git bridge version mismatch");
241 crate::hel_config::atomic_write(&spec.ready_path, b"ready\n")?;
242
243 let outcome = serve_bridge(
244 &mut input,
245 &mut output,
246 &repositories,
247 &spec.session_id,
248 EXCHANGE_IDLE_DEADLINE,
249 )
250 .await;
251 drop(input);
254 drop(output);
255 let status = match tokio::time::timeout(BRIDGE_EXIT_GRACE, child.wait()).await {
256 Ok(status) => Some(status.context("wait for target Git bridge")?),
257 Err(_) => {
258 if let Err(error) = child.kill().await
259 && error.kind() != std::io::ErrorKind::NotFound
260 {
261 tracing::warn!(
262 session_id = %spec.session_id,
263 %error,
264 "could not terminate a timed-out target Git bridge"
265 );
266 }
267 None
268 }
269 };
270 outcome?;
271 let status = status.context("target Git bridge did not exit after its stream closed")?;
272 ensure!(status.success(), "target Git bridge exited with {status}");
273 Ok(())
274}
275
276#[must_use]
283enum Exchange {
284 Completed,
285 Failed(anyhow::Error),
286}
287
288struct ExchangeWatch {
295 idle: Duration,
296 start: Instant,
297 progress: AtomicU64,
299 abort: CancellationToken,
300}
301
302impl ExchangeWatch {
303 fn new(idle: Duration) -> Self {
304 Self {
305 idle,
306 start: Instant::now(),
307 progress: AtomicU64::new(0),
308 abort: CancellationToken::new(),
309 }
310 }
311
312 fn mark(&self) {
314 let elapsed = self.start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
315 self.progress.store(elapsed, Ordering::Relaxed);
316 }
317
318 async fn stalled(&self) {
320 loop {
321 let progress = self.progress.load(Ordering::Relaxed);
322 let deadline = self.start + Duration::from_millis(progress) + self.idle;
323 tokio::time::sleep_until(deadline.into()).await;
324 if self.progress.load(Ordering::Relaxed) == progress {
325 return;
326 }
327 }
328 }
329
330 fn abort(&self) {
334 self.mark();
335 self.abort.cancel();
336 }
337
338 fn is_aborted(&self) -> bool {
339 self.abort.is_cancelled()
340 }
341
342 async fn aborted(&self) {
344 self.abort.cancelled().await;
345 }
346
347 fn stall(&self) -> anyhow::Error {
348 anyhow!(
349 "a bridged Git exchange moved nothing for {} seconds",
350 self.idle.as_secs_f64()
351 )
352 }
353}
354
355async fn watch_exchange<T>(
368 watch: &ExchangeWatch,
369 transfer: impl Future<Output = Result<T>>,
370) -> Result<(T, Option<anyhow::Error>)> {
371 tokio::pin!(transfer);
372 tokio::select! {
373 result = &mut transfer => Ok((result?, None)),
374 () = watch.stalled() => {
375 watch.abort();
376 tokio::select! {
377 result = &mut transfer => Ok((result?, Some(watch.stall()))),
378 () = watch.stalled() => Err(watch
379 .stall()
380 .context("a stalled Git bridge exchange was never ended by its peer")),
381 }
382 }
383 }
384}
385
386#[cfg(feature = "controller")]
388async fn serve_bridge(
389 input: &mut (impl AsyncWrite + Unpin),
390 output: &mut (impl AsyncRead + Unpin),
391 repositories: &BTreeMap<String, PathBuf>,
392 session_id: &str,
393 idle: Duration,
394) -> Result<()> {
395 loop {
396 let open = match read_frame(output, MAX_OPEN).await? {
397 Frame::Data(open) => open,
398 Frame::Closed => return Ok(()),
400 Frame::End => {
401 tracing::warn!(
402 session_id,
403 "target Git bridge ended an exchange that was not open"
404 );
405 continue;
406 }
407 };
408 match serve_exchange(input, output, repositories, &open, idle).await? {
409 Exchange::Completed => {}
410 Exchange::Failed(error) => tracing::warn!(
411 session_id,
412 error = format!("{error:#}"),
413 "bridged Git exchange failed"
414 ),
415 }
416 }
417}
418
419#[cfg(feature = "controller")]
420async fn serve_exchange(
421 input: &mut (impl AsyncWrite + Unpin),
422 output: &mut (impl AsyncRead + Unpin),
423 repositories: &BTreeMap<String, PathBuf>,
424 open: &[u8],
425 idle: Duration,
426) -> Result<Exchange> {
427 let request: GitOpen = match serde_json::from_slice(open) {
428 Ok(request) => request,
429 Err(error) => {
430 let error = anyhow!(error).context("decode Git bridge request");
431 return refuse_exchange(input, output, error).await;
432 }
433 };
434 let Some(repository) = repositories.get(&request.repository) else {
435 let error = anyhow!(
436 "Git bridge requested unknown repository {:?}",
437 request.repository
438 );
439 return refuse_exchange(input, output, error).await;
440 };
441 let command = match git_service(&request.service) {
442 Ok(command) => command,
443 Err(error) => return refuse_exchange(input, output, error).await,
444 };
445 serve_git(input, output, repository, command, idle).await
446}
447
448#[cfg(feature = "controller")]
449fn git_service(service: &str) -> Result<&'static str> {
450 match service {
451 "git-upload-pack" => Ok("upload-pack"),
452 "git-receive-pack" => Ok("receive-pack"),
453 _ => bail!("unsupported Git service {service:?}"),
454 }
455}
456
457#[cfg(feature = "controller")]
460async fn refuse_exchange(
461 input: &mut (impl AsyncWrite + Unpin),
462 output: &mut (impl AsyncRead + Unpin),
463 error: anyhow::Error,
464) -> Result<Exchange> {
465 write_frame(input, &[]).await?;
466 drain_exchange(output).await?;
467 Ok(Exchange::Failed(error))
468}
469
470#[cfg(feature = "controller")]
472async fn drain_exchange(output: &mut (impl AsyncRead + Unpin)) -> Result<()> {
473 loop {
474 match read_frame(output, MAX_FRAME).await? {
475 Frame::Data(_) => {}
476 Frame::End => return Ok(()),
477 Frame::Closed => bail!("the target Git bridge closed during an exchange"),
478 }
479 }
480}
481
482#[cfg(feature = "controller")]
483async fn serve_git<W, R>(
484 bridge_input: &mut W,
485 bridge_output: &mut R,
486 repository: &Path,
487 command: &str,
488 idle: Duration,
489) -> Result<Exchange>
490where
491 W: AsyncWrite + Unpin,
492 R: AsyncRead + Unpin,
493{
494 let mut git = Command::new("git");
495 git.args([
496 "-c",
497 "core.hooksPath=/dev/null",
498 "-c",
499 "receive.denyCurrentBranch=updateInstead",
500 "-c",
501 "receive.denyNonFastForwards=true",
502 "-c",
503 "receive.denyDeletes=true",
504 command,
505 ]);
506 git.arg(repository)
507 .stdin(Stdio::piped())
508 .stdout(Stdio::piped())
509 .stderr(Stdio::inherit());
510 let mut git = match git.spawn().with_context(|| format!("start git {command}")) {
511 Ok(git) => git,
512 Err(error) => return refuse_exchange(bridge_input, bridge_output, error).await,
513 };
514 let (Some(mut git_input), Some(mut git_output)) = (git.stdin.take(), git.stdout.take()) else {
515 if let Err(kill_error) = git.kill().await {
516 tracing::warn!(%kill_error, "could not stop Git service with missing pipes");
517 }
518 let error = anyhow!("git {command} was started without both pipes");
519 return refuse_exchange(bridge_input, bridge_output, error).await;
520 };
521
522 let watch = ExchangeWatch::new(idle);
523 let watch = &watch;
524 let to_target = async {
525 let mut buffer = vec![0; 64 * 1024];
526 let mut failure = None;
527 loop {
528 let count = tokio::select! {
529 biased;
530 () = watch.aborted() => break,
533 result = git_output.read(&mut buffer) => match result {
536 Ok(0) => break,
537 Ok(count) => count,
538 Err(error) => {
539 failure = Some(anyhow!(error).context(format!("read git {command} output")));
540 break;
541 }
542 },
543 };
544 watch.mark();
545 write_frame(bridge_input, &buffer[..count]).await?;
546 }
547 write_frame(bridge_input, &[]).await?;
550 Ok::<_, anyhow::Error>(failure)
551 };
552 let from_target = async move {
553 loop {
554 match read_frame(bridge_output, MAX_FRAME).await? {
555 Frame::Data(frame) => {
561 watch.mark();
562 if !watch.is_aborted() {
563 tokio::select! {
564 biased;
565 () = watch.aborted() => {}
566 result = git_input.write_all(&frame) => {
567 let _ = result;
568 }
569 }
570 }
571 }
572 Frame::End => break,
573 Frame::Closed => bail!("the target Git bridge closed mid-exchange"),
574 }
575 }
576 drop(git_input);
580 Ok::<_, anyhow::Error>(())
581 };
582 let ((service_failure, ()), stalled) =
585 watch_exchange(watch, async { tokio::try_join!(to_target, from_target) }).await?;
586 if let Some(stalled) = stalled {
587 if let Err(kill_error) = git.kill().await {
590 tracing::warn!(
591 service = command,
592 %kill_error,
593 "could not stop stalled Git service"
594 );
595 }
596 return Ok(Exchange::Failed(
597 stalled.context(format!("git {command} stalled")),
598 ));
599 }
600 if let Some(failure) = service_failure {
601 if let Err(kill_error) = git.kill().await {
602 tracing::warn!(
603 service = command,
604 %kill_error,
605 "could not stop failed Git service"
606 );
607 }
608 return Ok(Exchange::Failed(failure));
609 }
610 match git.wait().await {
611 Ok(status) if status.success() => Ok(Exchange::Completed),
612 Ok(status) => Ok(Exchange::Failed(anyhow!(
613 "git {command} exited with {status}"
614 ))),
615 Err(error) => Ok(Exchange::Failed(
616 anyhow!(error).context("wait for Git service"),
617 )),
618 }
619}
620
621async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), data: &[u8]) -> Result<()> {
622 ensure!(data.len() <= MAX_FRAME, "Git bridge frame is too large");
623 writer.write_u32(data.len() as u32).await?;
624 writer.write_all(data).await?;
625 writer.flush().await?;
626 Ok(())
627}
628
629enum Frame {
631 Data(Vec<u8>),
633 End,
635 Closed,
637}
638
639async fn read_frame(reader: &mut (impl AsyncRead + Unpin), maximum: usize) -> Result<Frame> {
640 let length = match reader.read_u32().await {
641 Ok(length) => length as usize,
642 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
643 return Ok(Frame::Closed);
644 }
645 Err(error) => return Err(error.into()),
646 };
647 if length == 0 {
648 return Ok(Frame::End);
649 }
650 ensure!(length <= maximum, "Git bridge frame is too large");
651 let mut data = vec![0; length];
652 reader.read_exact(&mut data).await?;
653 Ok(Frame::Data(data))
654}
655
656#[cfg(unix)]
657pub async fn run_worker_bridge(root: &Path) -> Result<()> {
658 run_worker_bridge_over(
659 root,
660 tokio::io::stdin(),
661 tokio::io::stdout(),
662 HANDSHAKE_DEADLINE,
663 EXCHANGE_IDLE_DEADLINE,
664 )
665 .await
666}
667
668#[cfg(unix)]
669async fn run_worker_bridge_over(
670 root: &Path,
671 mut broker_input: impl AsyncRead + Unpin,
672 mut broker_output: impl AsyncWrite + Unpin,
673 handshake: Duration,
674 idle: Duration,
675) -> Result<()> {
676 use tokio::net::UnixListener;
677
678 std::fs::create_dir_all(root)?;
679 let socket = root.join("git.sock");
680 if socket.exists() {
681 std::fs::remove_file(&socket)
682 .with_context(|| format!("remove stale Git proxy socket {}", socket.display()))?;
683 }
684 let listener = UnixListener::bind(&socket)
685 .with_context(|| format!("bind Git proxy socket {}", socket.display()))?;
686 use std::os::unix::fs::PermissionsExt;
687 std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o600))?;
688 broker_output.write_all(BRIDGE_MAGIC).await?;
689 broker_output.flush().await?;
690 loop {
691 let stream = tokio::select! {
692 accepted = listener.accept() => accepted.context("accept Git proxy client")?.0,
693 frame = read_frame(&mut broker_input, MAX_FRAME) => match frame? {
697 Frame::Closed => return Ok(()),
698 _ => bail!("Git broker sent a frame between exchanges"),
699 },
700 };
701 match serve_client(
702 stream,
703 &mut broker_input,
704 &mut broker_output,
705 handshake,
706 idle,
707 )
708 .await?
709 {
710 Exchange::Completed => {}
711 Exchange::Failed(error) => {
712 tracing::warn!(error = format!("{error:#}"), "bridged Git client failed")
713 }
714 }
715 }
716}
717
718#[cfg(unix)]
720async fn serve_client(
721 stream: tokio::net::UnixStream,
722 broker_input: &mut (impl AsyncRead + Unpin),
723 broker_output: &mut (impl AsyncWrite + Unpin),
724 handshake: Duration,
725 idle: Duration,
726) -> Result<Exchange> {
727 let (mut read, mut write) = stream.into_split();
728 let open = match tokio::time::timeout(handshake, read_handshake(&mut read)).await {
731 Ok(Ok(open)) => open,
732 Ok(Err(error)) => return Ok(Exchange::Failed(error)),
733 Err(_) => {
734 return Ok(Exchange::Failed(anyhow!(
735 "a Git proxy client sent no handshake within {} seconds",
736 handshake.as_secs_f64()
737 )));
738 }
739 };
740 write_frame(broker_output, &open).await?;
741
742 let watch = ExchangeWatch::new(idle);
743 let watch = &watch;
744 let served = tokio::sync::Notify::new();
745 let to_broker = async {
746 let mut buffer = vec![0; 64 * 1024];
747 let mut failure = None;
748 loop {
749 let count = tokio::select! {
750 biased;
751 () = served.notified() => break,
754 () = watch.aborted() => break,
757 result = read.read(&mut buffer) => match result {
760 Ok(0) => break,
761 Ok(count) => count,
762 Err(error) => {
763 failure = Some(anyhow!(error).context("read Git proxy client"));
764 break;
765 }
766 },
767 };
768 watch.mark();
769 write_frame(broker_output, &buffer[..count]).await?;
770 }
771 write_frame(broker_output, &[]).await?;
773 Ok::<_, anyhow::Error>(failure)
774 };
775 let from_broker = async {
776 loop {
777 match read_frame(broker_input, MAX_FRAME).await? {
778 Frame::Data(frame) => {
784 watch.mark();
785 if !watch.is_aborted() {
786 tokio::select! {
787 biased;
788 () = watch.aborted() => {}
789 result = write.write_all(&frame) => {
790 let _ = result;
791 }
792 }
793 }
794 }
795 Frame::End => break,
796 Frame::Closed => bail!("the Git broker closed the bridge"),
797 }
798 }
799 if let Err(error) = write.shutdown().await {
800 tracing::debug!(%error, "Git proxy client closed before its response was flushed");
801 }
802 served.notify_one();
803 Ok::<_, anyhow::Error>(())
804 };
805 let (client_failure, stalled) = watch_exchange(watch, async {
806 let (failure, ()) = tokio::try_join!(to_broker, from_broker)?;
807 Ok(failure)
808 })
809 .await?;
810 Ok(match stalled.or(client_failure) {
811 Some(error) => Exchange::Failed(error),
812 None => Exchange::Completed,
813 })
814}
815
816#[cfg(unix)]
817pub async fn run_worker_proxy(root: &Path, repository: &str, service: &str) -> Result<()> {
818 use tokio::net::UnixStream;
819
820 crate::hel_config::validate_id("repository", repository)?;
821 ensure!(
822 matches!(service, "git-upload-pack" | "git-receive-pack"),
823 "unsupported Git service"
824 );
825 let mut socket = UnixStream::connect(root.join("git.sock"))
826 .await
827 .with_context(|| format!("connect Git bridge at {}", root.display()))?;
828 let open = serde_json::to_vec(&GitOpen {
829 repository: repository.into(),
830 service: service.into(),
831 })?;
832 socket.write_all(&open).await?;
833 socket.write_all(b"\n").await?;
834 let (mut socket_read, mut socket_write) = socket.into_split();
835 let mut stdin = tokio::io::stdin();
836 let mut stdout = tokio::io::stdout();
837 let from_git = async {
838 tokio::io::copy(&mut stdin, &mut socket_write).await?;
839 socket_write.shutdown().await
840 };
841 let to_git = async {
842 tokio::io::copy(&mut socket_read, &mut stdout).await?;
843 stdout.shutdown().await
844 };
845 tokio::pin!(from_git);
846 tokio::pin!(to_git);
847 tokio::select! {
848 result = &mut to_git => {
849 result?;
850 }
851 result = &mut from_git => {
852 result?;
853 to_git.await?;
854 }
855 }
856 Ok(())
857}
858
859#[cfg(unix)]
860async fn read_handshake(reader: &mut (impl AsyncRead + Unpin)) -> Result<Vec<u8>> {
861 let mut data = Vec::new();
862 loop {
863 ensure!(data.len() < MAX_OPEN, "Git proxy handshake is too large");
864 let byte = reader.read_u8().await?;
865 if byte == b'\n' {
866 break;
867 }
868 data.push(byte);
869 }
870 Ok(data)
871}
872
873#[cfg(not(unix))]
874pub async fn run_worker_bridge(_root: &Path) -> Result<()> {
875 bail!("Git proxy workers require Unix")
876}
877
878#[cfg(not(unix))]
879pub async fn run_worker_proxy(_root: &Path, _repository: &str, _service: &str) -> Result<()> {
880 bail!("Git proxy workers require Unix")
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 async fn read_exchange(reader: &mut (impl AsyncRead + Unpin)) -> Vec<u8> {
889 let mut received = Vec::new();
890 loop {
891 match read_frame(reader, MAX_FRAME).await.unwrap() {
892 Frame::Data(frame) => received.extend_from_slice(&frame),
893 Frame::End => return received,
894 Frame::Closed => panic!("the bridge stream closed mid-exchange"),
895 }
896 }
897 }
898
899 async fn read_data_frame(reader: &mut (impl AsyncRead + Unpin)) -> Vec<u8> {
900 match read_frame(reader, MAX_FRAME).await.unwrap() {
901 Frame::Data(frame) => frame,
902 Frame::End => panic!("expected a data frame, not the end of an exchange"),
903 Frame::Closed => panic!("expected a data frame, not a closed stream"),
904 }
905 }
906
907 #[cfg(unix)]
908 async fn write_all_framed(writer: &mut (impl AsyncWrite + Unpin), data: &[u8]) {
909 for chunk in data.chunks(64 * 1024) {
910 write_frame(writer, chunk).await.unwrap();
911 }
912 write_frame(writer, &[]).await.unwrap();
913 }
914
915 #[cfg(unix)]
917 fn handshake_line(repository: &str) -> Vec<u8> {
918 let mut line = open_frame(repository);
919 line.push(b'\n');
920 line
921 }
922
923 fn open_frame(repository: &str) -> Vec<u8> {
924 serde_json::to_vec(&GitOpen {
925 repository: repository.into(),
926 service: "git-upload-pack".into(),
927 })
928 .unwrap()
929 }
930
931 fn git(directory: &Path, args: &[&str]) {
932 let status = std::process::Command::new("git")
933 .args(args)
934 .current_dir(directory)
935 .status()
936 .unwrap();
937 assert!(status.success(), "git {args:?} failed");
938 }
939
940 fn repository_with_large_advertisement(root: &Path) -> PathBuf {
943 let repository = root.join("main");
944 std::fs::create_dir_all(&repository).unwrap();
945 git(&repository, &["init", "-q", "-b", "main"]);
946 git(&repository, &["config", "user.name", "Hel Test"]);
947 git(&repository, &["config", "user.email", "hel@example.test"]);
948 std::fs::write(repository.join("tracked"), "content").unwrap();
949 git(&repository, &["add", "."]);
950 git(&repository, &["commit", "-qm", "base"]);
951 let head = std::process::Command::new("git")
952 .args(["rev-parse", "HEAD"])
953 .current_dir(&repository)
954 .output()
955 .unwrap();
956 let head = String::from_utf8(head.stdout).unwrap().trim().to_owned();
957 let mut packed = String::from("# pack-refs with: peeled fully-peeled sorted \n");
958 packed.push_str(&format!("{head} refs/heads/main\n"));
959 for index in 0..3000 {
960 packed.push_str(&format!("{head} refs/tags/advertised-{index:04}\n"));
961 }
962 std::fs::write(repository.join(".git/packed-refs"), packed).unwrap();
963 repository
964 }
965
966 #[tokio::test]
970 async fn the_broker_serves_later_exchanges_after_one_fails_mid_transfer() {
971 let directory = tempfile::tempdir().unwrap();
972 let repository = repository_with_large_advertisement(directory.path());
973 let not_a_repository = directory.path().join("not-a-repository");
974 std::fs::create_dir_all(¬_a_repository).unwrap();
975 let repositories = BTreeMap::from([
976 ("main".to_owned(), repository),
977 ("broken".to_owned(), not_a_repository),
978 ]);
979
980 let (broker_end, target_end) = tokio::io::duplex(16 * 1024);
981 let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
982 let (mut target_read, mut target_write) = tokio::io::split(target_end);
983 let serving = tokio::spawn(async move {
984 serve_bridge(
985 &mut broker_write,
986 &mut broker_read,
987 &repositories,
988 "test",
989 EXCHANGE_IDLE_DEADLINE,
990 )
991 .await
992 });
993
994 write_frame(&mut target_write, &open_frame("absent"))
996 .await
997 .unwrap();
998 write_frame(&mut target_write, &[]).await.unwrap();
999 assert!(read_exchange(&mut target_read).await.is_empty());
1000
1001 write_frame(&mut target_write, &open_frame("broken"))
1003 .await
1004 .unwrap();
1005 write_frame(&mut target_write, &[]).await.unwrap();
1006 assert!(read_exchange(&mut target_read).await.is_empty());
1007
1008 write_frame(&mut target_write, &open_frame("main"))
1011 .await
1012 .unwrap();
1013 let mut abandoned = read_data_frame(&mut target_read).await.len();
1014 while abandoned <= 64 * 1024 {
1015 abandoned += read_data_frame(&mut target_read).await.len();
1016 }
1017 write_frame(&mut target_write, &[]).await.unwrap();
1018 let remainder = read_exchange(&mut target_read).await;
1019 assert!(abandoned + remainder.len() > 64 * 1024);
1020
1021 write_frame(&mut target_write, &open_frame("main"))
1023 .await
1024 .unwrap();
1025 write_frame(&mut target_write, b"0000").await.unwrap();
1027 write_frame(&mut target_write, &[]).await.unwrap();
1028 let advertisement = read_exchange(&mut target_read).await;
1029 assert!(
1030 advertisement.len() > 64 * 1024,
1031 "advertisement was {} bytes",
1032 advertisement.len()
1033 );
1034 assert!(String::from_utf8_lossy(&advertisement).contains("refs/heads/main"));
1035
1036 drop((target_read, target_write));
1038 serving.await.unwrap().unwrap();
1039 }
1040
1041 #[cfg(unix)]
1043 #[tokio::test]
1044 async fn the_worker_bridge_serves_the_next_client_after_one_dies_mid_transfer() {
1045 let directory = tempfile::tempdir().unwrap();
1046 let root = directory.path().to_path_buf();
1047 let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1048 let (worker_read, worker_write) = tokio::io::split(worker_end);
1049 let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1050 let serving = tokio::spawn(async move {
1051 run_worker_bridge_over(
1052 &root,
1053 worker_read,
1054 worker_write,
1055 HANDSHAKE_DEADLINE,
1056 EXCHANGE_IDLE_DEADLINE,
1057 )
1058 .await
1059 });
1060
1061 let mut magic = vec![0; BRIDGE_MAGIC.len()];
1062 broker_read.read_exact(&mut magic).await.unwrap();
1063 assert_eq!(magic, BRIDGE_MAGIC);
1064 let socket = directory.path().join("git.sock");
1065 let request = vec![b'q'; 256 * 1024];
1066 let reply = vec![b'r'; 256 * 1024];
1067
1068 let mut client = tokio::net::UnixStream::connect(&socket).await.unwrap();
1071 client
1072 .write_all(b"{\"repository\":\"main\",\"service\":\"git-upload-pack\"}\n")
1073 .await
1074 .unwrap();
1075 let open = read_data_frame(&mut broker_read).await;
1076 assert!(String::from_utf8_lossy(&open).contains("git-upload-pack"));
1077 let abandoning = tokio::spawn({
1078 let request = request.clone();
1079 async move {
1080 let _ = client.write_all(&request).await;
1081 drop(client);
1082 }
1083 });
1084 let abandoned = read_exchange(&mut broker_read).await;
1085 abandoning.await.unwrap();
1086 assert!(!abandoned.is_empty());
1087 write_all_framed(&mut broker_write, &reply).await;
1088
1089 let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1091 .await
1092 .unwrap()
1093 .into_split();
1094 client_write
1095 .write_all(b"{\"repository\":\"main\",\"service\":\"git-upload-pack\"}\n")
1096 .await
1097 .unwrap();
1098 let open = read_data_frame(&mut broker_read).await;
1099 assert!(String::from_utf8_lossy(&open).contains("main"));
1100 let sending = tokio::spawn(async move {
1101 client_write.write_all(&request).await.unwrap();
1102 client_write.shutdown().await.unwrap();
1103 });
1104 let receiving = tokio::spawn(async move {
1105 let mut received = Vec::new();
1106 client_read.read_to_end(&mut received).await.unwrap();
1107 received
1108 });
1109 let received_request = read_exchange(&mut broker_read).await;
1110 sending.await.unwrap();
1111 write_all_framed(&mut broker_write, &reply).await;
1112 let received_reply = receiving.await.unwrap();
1113
1114 assert_eq!(received_request.len(), 256 * 1024);
1115 assert_eq!(received_reply.len(), 256 * 1024);
1116
1117 drop((broker_read, broker_write));
1120 serving.await.unwrap().unwrap();
1121 }
1122
1123 #[cfg(unix)]
1126 #[tokio::test]
1127 async fn a_client_that_never_finishes_its_handshake_is_timed_out() {
1128 let directory = tempfile::tempdir().unwrap();
1129 let root = directory.path().to_path_buf();
1130 let handshake = Duration::from_millis(500);
1131 let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1132 let (worker_read, worker_write) = tokio::io::split(worker_end);
1133 let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1134 let serving = tokio::spawn(async move {
1135 run_worker_bridge_over(
1136 &root,
1137 worker_read,
1138 worker_write,
1139 handshake,
1140 EXCHANGE_IDLE_DEADLINE,
1141 )
1142 .await
1143 });
1144 let mut magic = vec![0; BRIDGE_MAGIC.len()];
1145 broker_read.read_exact(&mut magic).await.unwrap();
1146 let socket = directory.path().join("git.sock");
1147
1148 let mut silent = tokio::net::UnixStream::connect(&socket).await.unwrap();
1150 let line = handshake_line("silent");
1151 silent.write_all(&line[..line.len() - 1]).await.unwrap();
1152
1153 let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1156 .await
1157 .unwrap()
1158 .into_split();
1159 client_write
1160 .write_all(&handshake_line("served"))
1161 .await
1162 .unwrap();
1163 let open = tokio::time::timeout(Duration::from_secs(30), read_data_frame(&mut broker_read))
1164 .await
1165 .expect("a silent client held the bridge");
1166 assert!(
1167 String::from_utf8_lossy(&open).contains("served"),
1168 "first framed request was {}",
1169 String::from_utf8_lossy(&open)
1170 );
1171
1172 let mut byte = [0u8; 1];
1174 assert_eq!(
1175 tokio::time::timeout(Duration::from_secs(30), silent.read(&mut byte))
1176 .await
1177 .expect("a timed-out client was left connected")
1178 .unwrap(),
1179 0
1180 );
1181
1182 let reply = vec![b'r'; 256 * 1024];
1186 let receiving = tokio::spawn(async move {
1187 let mut received = Vec::new();
1188 client_read.read_to_end(&mut received).await.unwrap();
1189 received
1190 });
1191 write_all_framed(&mut broker_write, &reply).await;
1192 assert!(read_exchange(&mut broker_read).await.is_empty());
1193 assert_eq!(receiving.await.unwrap().len(), 256 * 1024);
1194 drop(client_write);
1195
1196 drop((broker_read, broker_write));
1197 serving.await.unwrap().unwrap();
1198 }
1199
1200 #[cfg(unix)]
1203 #[tokio::test]
1204 async fn a_client_that_stalls_mid_transfer_loses_only_its_own_exchange() {
1205 let directory = tempfile::tempdir().unwrap();
1206 let root = directory.path().to_path_buf();
1207 let idle = Duration::from_millis(500);
1208 let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1209 let (worker_read, worker_write) = tokio::io::split(worker_end);
1210 let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1211 let serving = tokio::spawn(async move {
1212 run_worker_bridge_over(&root, worker_read, worker_write, HANDSHAKE_DEADLINE, idle).await
1213 });
1214 let mut magic = vec![0; BRIDGE_MAGIC.len()];
1215 broker_read.read_exact(&mut magic).await.unwrap();
1216 let socket = directory.path().join("git.sock");
1217 let request = vec![b'q'; 256 * 1024];
1218 let reply = vec![b'r'; 256 * 1024];
1219
1220 let mut wedged = tokio::net::UnixStream::connect(&socket).await.unwrap();
1223 wedged.write_all(&handshake_line("wedged")).await.unwrap();
1224 let open = read_data_frame(&mut broker_read).await;
1225 assert!(String::from_utf8_lossy(&open).contains("wedged"));
1226 let started = Instant::now();
1227 let draining = tokio::spawn(async move {
1228 let received =
1229 tokio::time::timeout(Duration::from_secs(30), read_exchange(&mut broker_read))
1230 .await
1231 .expect("a wedged client held the bridge");
1232 (broker_read, received)
1233 });
1234 wedged.write_all(&request).await.unwrap();
1235 let (mut broker_read, received) = draining.await.unwrap();
1236 assert_eq!(received.len(), 256 * 1024);
1237 assert!(
1238 started.elapsed() >= idle,
1239 "the exchange ended after {:?}, before its idle window",
1240 started.elapsed()
1241 );
1242
1243 write_all_framed(&mut broker_write, &reply).await;
1246 let mut byte = [0u8; 1];
1247 assert_eq!(
1248 tokio::time::timeout(Duration::from_secs(30), wedged.read(&mut byte))
1249 .await
1250 .expect("an abandoned client was left connected")
1251 .unwrap(),
1252 0
1253 );
1254
1255 let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1257 .await
1258 .unwrap()
1259 .into_split();
1260 client_write
1261 .write_all(&handshake_line("main"))
1262 .await
1263 .unwrap();
1264 let open = read_data_frame(&mut broker_read).await;
1265 assert!(String::from_utf8_lossy(&open).contains("main"));
1266 let sending = tokio::spawn(async move {
1267 client_write.write_all(&request).await.unwrap();
1268 client_write.shutdown().await.unwrap();
1269 });
1270 let receiving = tokio::spawn(async move {
1271 let mut received = Vec::new();
1272 client_read.read_to_end(&mut received).await.unwrap();
1273 received
1274 });
1275 let received_request = read_exchange(&mut broker_read).await;
1276 sending.await.unwrap();
1277 write_all_framed(&mut broker_write, &reply).await;
1278 assert_eq!(received_request.len(), 256 * 1024);
1279 assert_eq!(receiving.await.unwrap().len(), 256 * 1024);
1280
1281 drop((broker_read, broker_write));
1282 serving.await.unwrap().unwrap();
1283 }
1284
1285 #[tokio::test]
1287 async fn the_broker_gives_up_on_a_service_that_stops_moving() {
1288 let directory = tempfile::tempdir().unwrap();
1289 let repository = repository_with_large_advertisement(directory.path());
1290 let repositories = BTreeMap::from([("main".to_owned(), repository)]);
1291 let idle = Duration::from_secs(1);
1292 let (broker_end, target_end) = tokio::io::duplex(16 * 1024);
1293 let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1294 let (mut target_read, mut target_write) = tokio::io::split(target_end);
1295 let serving = tokio::spawn(async move {
1296 serve_bridge(
1297 &mut broker_write,
1298 &mut broker_read,
1299 &repositories,
1300 "test",
1301 idle,
1302 )
1303 .await
1304 });
1305
1306 write_frame(&mut target_write, &open_frame("main"))
1309 .await
1310 .unwrap();
1311 let started = Instant::now();
1312 let advertisement =
1313 tokio::time::timeout(Duration::from_secs(60), read_exchange(&mut target_read))
1314 .await
1315 .expect("a stalled Git service held the broker");
1316 assert!(
1317 advertisement.len() > 64 * 1024,
1318 "advertisement was {} bytes",
1319 advertisement.len()
1320 );
1321 assert!(
1322 started.elapsed() >= idle,
1323 "the exchange ended after {:?}, before its idle window",
1324 started.elapsed()
1325 );
1326 write_frame(&mut target_write, &[]).await.unwrap();
1328
1329 write_frame(&mut target_write, &open_frame("main"))
1331 .await
1332 .unwrap();
1333 write_frame(&mut target_write, b"0000").await.unwrap();
1334 write_frame(&mut target_write, &[]).await.unwrap();
1335 let advertisement = read_exchange(&mut target_read).await;
1336 assert!(advertisement.len() > 64 * 1024);
1337 assert!(String::from_utf8_lossy(&advertisement).contains("refs/heads/main"));
1338
1339 drop((target_read, target_write));
1340 serving.await.unwrap().unwrap();
1341 }
1342
1343 #[test]
1346 fn broker_liveness_follows_the_lock_and_not_the_written_pid() {
1347 let directory = tempfile::tempdir().unwrap();
1348 let pid_path = directory.path().join("session.pid");
1349
1350 assert!(!broker_is_alive(&pid_path));
1351 assert_eq!(running_broker_pid(&pid_path), None);
1352
1353 std::fs::write(&pid_path, std::process::id().to_string()).unwrap();
1356 assert!(!broker_is_alive(&pid_path));
1357 assert_eq!(running_broker_pid(&pid_path), None);
1358
1359 let claimed = claim_broker_pid_file(&pid_path).unwrap();
1360 assert!(broker_is_alive(&pid_path));
1361 assert_eq!(
1362 running_broker_pid(&pid_path),
1363 Some(std::process::id() as i32)
1364 );
1365 assert!(claim_broker_pid_file(&pid_path).is_err());
1366 let written = std::fs::read_to_string(&pid_path).unwrap();
1367 assert_eq!(written.trim(), std::process::id().to_string());
1368
1369 drop(claimed);
1370 let released = std::time::Instant::now();
1377 while broker_is_alive(&pid_path) {
1378 assert!(
1379 released.elapsed() < Duration::from_secs(5),
1380 "a released broker lock was never observed free"
1381 );
1382 std::thread::sleep(Duration::from_millis(1));
1383 }
1384 assert_eq!(running_broker_pid(&pid_path), None);
1385 }
1386}