This repository has no description
1use std::net::IpAddr;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use futures::StreamExt;
8use knot_acl::{KnotAcl, can_push};
9use knot_index::Resolved;
10use knot_lfs::TransferOp;
11use knot_pack::{PackError, PackLimits, RepoLookup};
12use knot_resource::SubjectKey;
13use knot_runtime::{Clock, HttpTransport};
14use knot_types::{AccountDid, ClonePath, ObjectFormat, OwnerDid, RepoDid};
15use russh::Channel;
16use russh::server::Msg;
17use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
18use tokio::runtime::Handle;
19use tokio::sync::mpsc;
20
21use crate::SshState;
22use crate::identity::Credential;
23
24const READ_CHUNK: usize = 64 * 1024;
25const MAX_UPLOAD_REQUEST: usize = 16 * 1024 * 1024;
26const RECEIVE_BODY_DEADLINE: Duration = Duration::from_secs(1800);
27const ARCHIVE_REQUEST_DEADLINE: Duration = Duration::from_secs(60);
28const LFS_PROGRESS_GRACE: Duration = Duration::from_secs(60);
29const LFS_PROGRESS_FLOOR_BYTES_PER_SEC: u64 = 1024;
30const LFS_STALL_TIMEOUT: Duration = Duration::from_secs(120);
31const CANDIDATE_FANOUT: usize = 4;
32const AUTHORIZED_NAMES_SHOWN: usize = 4;
33
34fn lfs_within_progress_budget(waited: Duration, moved_bytes: u64) -> bool {
35 waited
36 <= LFS_PROGRESS_GRACE + Duration::from_secs(moved_bytes / LFS_PROGRESS_FLOOR_BYTES_PER_SEC)
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40enum Service {
41 Upload,
42 UploadArchive,
43 Receive,
44 Lfs(TransferOp),
45}
46
47enum ReadError {
48 Io(std::io::Error),
49 Pack(PackError),
50 TooLarge,
51 Truncated,
52 Deadline,
53}
54
55enum RepoRef {
56 Did(RepoDid),
57 OwnerPath(OwnerDid, ClonePath),
58 HandlePath(knot_types::Handle, ClonePath),
59}
60
61enum ResolvedRef {
62 Did(RepoDid),
63 OwnerPath(OwnerDid, ClonePath),
64}
65
66fn parse_exec(command: &[u8]) -> Option<(Service, RepoRef)> {
67 let text = std::str::from_utf8(command).ok()?.trim();
68 if let Some(rest) = text.strip_prefix("git-lfs-transfer ") {
69 let (path, op_token) = rest.trim().rsplit_once(' ')?;
70 let op = TransferOp::parse(op_token.trim())?;
71 return Some((Service::Lfs(op), parse_repo_path(path)?));
72 }
73 let (service, rest) = [
74 ("git-upload-pack ", Service::Upload),
75 ("git upload-pack ", Service::Upload),
76 ("git-upload-archive ", Service::UploadArchive),
77 ("git upload-archive ", Service::UploadArchive),
78 ("git-receive-pack ", Service::Receive),
79 ("git receive-pack ", Service::Receive),
80 ]
81 .into_iter()
82 .find_map(|(prefix, service)| text.strip_prefix(prefix).map(|rest| (service, rest)))?;
83 Some((service, parse_repo_path(rest)?))
84}
85
86fn parse_repo_path(raw: &str) -> Option<RepoRef> {
87 let path = raw
88 .trim()
89 .trim_matches('\'')
90 .trim_matches('"')
91 .trim_start_matches('/');
92 match path.split_once('/') {
93 Some((owner, name)) => {
94 let candidates = ClonePath::parse(name)?;
95 match knot_types::OwnerRef::parse(owner)? {
96 knot_types::OwnerRef::Did(owner) => Some(RepoRef::OwnerPath(owner, candidates)),
97 knot_types::OwnerRef::Handle(handle) => {
98 Some(RepoRef::HandlePath(handle, candidates))
99 }
100 }
101 }
102 None => Some(RepoRef::Did(RepoDid::new(path).ok()?)),
103 }
104}
105
106fn resolve_repo_ref<H: HttpTransport, C: Clock>(
107 state: &Arc<SshState<H, C>>,
108 repo_ref: ResolvedRef,
109) -> RepoLookup {
110 let candidate = match repo_ref {
111 ResolvedRef::Did(did) => RepoLookup::Hosted(did),
112 ResolvedRef::OwnerPath(owner, candidates) => RepoLookup::from_resolved(
113 state.index.resolve_clone_path(&owner, &candidates),
114 |found| found,
115 ),
116 };
117 match candidate {
118 RepoLookup::Hosted(did) => {
119 RepoLookup::from_resolved(state.index.owner_of(&did), |_| did.clone())
120 }
121 undecided => undecided,
122 }
123}
124
125pub(crate) async fn run_exec<H: HttpTransport, C: Clock>(
126 state: Arc<SshState<H, C>>,
127 credential: Credential,
128 channel: Channel<Msg>,
129 command: &[u8],
130 protocol_v2: bool,
131 peer: Option<IpAddr>,
132) {
133 let Some((service, repo_ref)) = parse_exec(command) else {
134 fail(channel, &state.catalog.ssh.unsupported_command.text()).await;
135 return;
136 };
137 let peer_limiter = match &service {
138 Service::Lfs(_) => state
139 .lfs
140 .as_ref()
141 .map_or(&state.peer_slots, |lfs| &lfs.peer_slots),
142 _ => &state.peer_slots,
143 };
144 let _peer_guard = match peer_limiter.admit(peer, state.atproto.now()) {
145 Ok(guard) => guard,
146 Err(refusal) => {
147 let reason = match refusal {
148 knot_resource::Refusal::RateLimited => "peer request rate exceeded",
149 knot_resource::Refusal::Saturated => "peer concurrency limit reached",
150 };
151 tracing::warn!(?peer, reason, "ssh exec rejected");
152 return fail(channel, &state.catalog.ssh.too_many_operations.text()).await;
153 }
154 };
155 let resolved_ref = match repo_ref {
156 RepoRef::Did(did) => ResolvedRef::Did(did),
157 RepoRef::OwnerPath(owner, candidates) => ResolvedRef::OwnerPath(owner, candidates),
158 RepoRef::HandlePath(owner_handle, candidates) => {
159 let Some(_lookup_permit) = state.lookup_slots.try_acquire() else {
160 tracing::warn!(
161 ?peer,
162 "ssh exec rejected, the lookup budget can't resolve another handle"
163 );
164 return fail(channel, &state.catalog.ssh.too_many_operations.text()).await;
165 };
166 match state
167 .atproto
168 .resolve_handle_to_did(&owner_handle)
169 .await
170 .ok()
171 {
172 Some(did) => ResolvedRef::OwnerPath(did.into(), candidates),
173 None => {
174 fail(channel, &state.catalog.ssh.repo_not_found.text()).await;
175 return;
176 }
177 }
178 }
179 };
180 let repo_did = match resolve_repo_ref(&state, resolved_ref) {
181 RepoLookup::Hosted(did) => did,
182 RepoLookup::Unhosted => {
183 fail(channel, &state.catalog.ssh.repo_not_found.text()).await;
184 return;
185 }
186 RepoLookup::Unavailable => {
187 fail(channel, &state.catalog.ssh.index_warming.text()).await;
188 return;
189 }
190 };
191 let layout = state.layout.clone();
192 let did = repo_did.clone();
193 let opened = tokio::task::spawn_blocking(move || layout.open(&did).is_ok())
194 .await
195 .unwrap_or(false);
196 if !opened {
197 fail(channel, &state.catalog.ssh.repo_not_found.text()).await;
198 return;
199 }
200 match service {
201 Service::Upload => serve_upload(state, channel, repo_did, protocol_v2).await,
202 Service::UploadArchive => serve_upload_archive(state, channel, repo_did).await,
203 Service::Receive => serve_receive(state, credential, channel, repo_did, peer).await,
204 Service::Lfs(op) => serve_lfs(state, credential, channel, repo_did, op, peer).await,
205 }
206}
207
208async fn serve_lfs<H: HttpTransport, C: Clock>(
209 state: Arc<SshState<H, C>>,
210 credential: Credential,
211 mut channel: Channel<Msg>,
212 repo_did: RepoDid,
213 op: TransferOp,
214 peer: Option<IpAddr>,
215) {
216 let Some(lfs) = state.lfs.clone() else {
217 return fail(channel, &state.catalog.ssh.lfs_disabled.text()).await;
218 };
219 if op == TransferOp::Upload
220 && let PushAuth::Refused { reason, message } =
221 authorize_push(&state, &credential, &repo_did, peer).await
222 {
223 tracing::warn!(repo = repo_did.as_str(), reason, "ssh lfs upload denied");
224 return fail(channel, &message).await;
225 }
226 let permit = match Arc::clone(&lfs.slots).acquire_owned().await {
227 Ok(permit) => permit,
228 Err(_) => return fail(channel, &state.catalog.ssh.shutting_down.text()).await,
229 };
230 let started = std::time::Instant::now();
231
232 let (tx, rx) = mpsc::channel::<Vec<u8>>(8);
233 let writer = Box::pin(channel.make_writer());
234 let handle = lfs.handle.clone();
235 let runtime = Handle::current();
236 let did = repo_did.clone();
237 let catalog = Arc::clone(&state.catalog);
238 let mut engine = tokio::task::spawn_blocking(move || {
239 let _permit = permit;
240 let output = std::io::BufWriter::new(MeteredWrite::new(runtime.clone(), writer));
241 knot_lfs::serve_transfer(
242 handle.store.as_ref(),
243 handle.admission.as_ref(),
244 &did,
245 op,
246 &catalog.lfs,
247 MpscRead::new(runtime, rx),
248 output,
249 )
250 });
251 let joined = {
252 let reader = channel.make_reader();
253 tokio::select! {
254 joined = &mut engine => joined,
255 () = pump_input(reader, tx) => engine.await,
256 }
257 };
258 let status = match joined {
259 Ok(Ok(())) => {
260 tracing::info!(
261 repo = repo_did.as_str(),
262 op = match op {
263 TransferOp::Upload => "upload",
264 TransferOp::Download => "download",
265 },
266 duration_ms = started.elapsed().as_millis() as u64,
267 "ssh lfs transfer finished"
268 );
269 0
270 }
271 Ok(Err(fault)) => {
272 tracing::warn!(repo = repo_did.as_str(), %fault, "ssh lfs transfer failed");
273 1
274 }
275 Err(join) => {
276 tracing::error!(repo = repo_did.as_str(), %join, "ssh lfs transfer task panicked");
277 1
278 }
279 };
280 finish(channel, status).await;
281}
282
283async fn pump_input<R: AsyncRead + Unpin>(reader: R, tx: mpsc::Sender<Vec<u8>>) {
284 use futures::TryStreamExt;
285 let _ = tokio_util::io::ReaderStream::with_capacity(reader, READ_CHUNK)
286 .map_err(|_| ())
287 .try_for_each(|chunk| {
288 let tx = &tx;
289 async move {
290 match chunk.is_empty() {
291 true => Ok(()),
292 false => tx.send(chunk.to_vec()).await.map_err(|_| ()),
293 }
294 }
295 })
296 .await;
297}
298
299fn stalled(direction: &'static str) -> std::io::Error {
300 std::io::Error::other(format!("lfs {direction} stalled past the idle timeout"))
301}
302
303struct MpscRead {
304 runtime: Handle,
305 rx: mpsc::Receiver<Vec<u8>>,
306 buffer: Vec<u8>,
307 offset: usize,
308 waited: Duration,
309 received: u64,
310}
311
312impl MpscRead {
313 fn new(runtime: Handle, rx: mpsc::Receiver<Vec<u8>>) -> Self {
314 Self {
315 runtime,
316 rx,
317 buffer: Vec::new(),
318 offset: 0,
319 waited: Duration::ZERO,
320 received: 0,
321 }
322 }
323}
324
325impl std::io::Read for MpscRead {
326 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
327 if self.offset >= self.buffer.len() {
328 let started = std::time::Instant::now();
329 let rx = &mut self.rx;
330 let received = self
331 .runtime
332 // I know I know, but these aren't runtime workers here
333 .block_on(async { tokio::time::timeout(LFS_STALL_TIMEOUT, rx.recv()).await });
334 match received {
335 Ok(Some(chunk)) => {
336 self.waited += started.elapsed();
337 self.received += chunk.len() as u64;
338 if !lfs_within_progress_budget(self.waited, self.received) {
339 return Err(std::io::Error::other(
340 "lfs input trickles below the progress floor",
341 ));
342 }
343 self.buffer = chunk;
344 self.offset = 0;
345 }
346 Ok(None) => return Ok(0),
347 Err(_) => return Err(stalled("input")),
348 }
349 }
350 let take = out.len().min(self.buffer.len() - self.offset);
351 out[..take].copy_from_slice(&self.buffer[self.offset..self.offset + take]);
352 self.offset += take;
353 Ok(take)
354 }
355}
356
357struct MeteredWrite<W> {
358 runtime: Handle,
359 inner: W,
360 waited: Duration,
361 written: u64,
362}
363
364impl<W: AsyncWrite + Unpin> MeteredWrite<W> {
365 fn new(runtime: Handle, inner: W) -> Self {
366 Self {
367 runtime,
368 inner,
369 waited: Duration::ZERO,
370 written: 0,
371 }
372 }
373
374 fn charge(&mut self, started: std::time::Instant) -> std::io::Result<()> {
375 self.waited += started.elapsed();
376 match lfs_within_progress_budget(self.waited, self.written) {
377 true => Ok(()),
378 false => Err(std::io::Error::other(
379 "lfs output trickles below the progress floor",
380 )),
381 }
382 }
383}
384
385impl<W: AsyncWrite + Unpin> std::io::Write for MeteredWrite<W> {
386 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
387 let started = std::time::Instant::now();
388 let inner = &mut self.inner;
389 let wrote = self
390 .runtime
391 .block_on(async { tokio::time::timeout(LFS_STALL_TIMEOUT, inner.write(buf)).await })
392 .map_err(|_| stalled("output"))??;
393 self.written += wrote as u64;
394 self.charge(started).map(|()| wrote)
395 }
396
397 fn flush(&mut self) -> std::io::Result<()> {
398 let started = std::time::Instant::now();
399 let inner = &mut self.inner;
400 self.runtime
401 .block_on(async { tokio::time::timeout(LFS_STALL_TIMEOUT, inner.flush()).await })
402 .map_err(|_| stalled("output"))??;
403 self.charge(started)
404 }
405}
406
407async fn serve_upload_archive<H: HttpTransport, C: Clock>(
408 state: Arc<SshState<H, C>>,
409 mut channel: Channel<Msg>,
410 repo_did: RepoDid,
411) {
412 let request = {
413 let mut reader = channel.make_reader();
414 tokio::time::timeout(ARCHIVE_REQUEST_DEADLINE, read_archive_request(&mut reader)).await
415 };
416 let request = match request {
417 Ok(Ok(request)) => request,
418 Ok(Err(())) => return fail(channel, &state.catalog.ssh.archive_malformed.text()).await,
419 Err(_) => return fail(channel, &state.catalog.ssh.archive_timeout.text()).await,
420 };
421
422 let permit = state.slots.pack.acquire().await;
423 let (tx, mut rx) = mpsc::channel::<Vec<u8>>(16);
424 let layout = state.layout.clone();
425 let did = repo_did.clone();
426 let archive_limit = state.archive_limit;
427 let handle = tokio::task::spawn_blocking(move || -> Result<(), PackError> {
428 let _permit = permit;
429 let repo = layout.open(&did)?;
430 let mut sink = |chunk: &[u8]| -> std::io::Result<()> {
431 tx.blocking_send(chunk.to_vec())
432 .map_err(|_| std::io::Error::other("client disconnected"))
433 };
434 knot_pack::upload_archive_streamed(&repo, &request, archive_limit, &mut sink)
435 });
436
437 let mut writer = channel.make_writer();
438 let mut forward = Ok(());
439 while let Some(chunk) = rx.recv().await {
440 if writer.write_all(&chunk).await.is_err() {
441 forward = Err(());
442 break;
443 }
444 }
445 drop(rx);
446 let produced = handle.await;
447 match &produced {
448 Ok(Err(error)) => {
449 tracing::warn!(repo = repo_did.as_str(), %error, "upload-archive failed")
450 }
451 Err(join) => {
452 tracing::error!(repo = repo_did.as_str(), %join, "upload-archive task panicked")
453 }
454 Ok(Ok(())) => {}
455 }
456 match (forward, produced) {
457 (Ok(()), Ok(Ok(()))) if writer.flush().await.is_ok() => finish(channel, 0).await,
458 _ => fail(channel, &state.catalog.ssh.archive_failed.text()).await,
459 }
460}
461
462async fn read_archive_request<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Vec<u8>, ()> {
463 let mut buf = Vec::new();
464 loop {
465 if knot_pack::archive_request_complete(&buf).is_some() {
466 return Ok(buf);
467 }
468 match read_chunk(reader, &mut buf, MAX_UPLOAD_REQUEST).await {
469 Ok(true) => {}
470 Ok(false) | Err(_) => return Err(()),
471 }
472 }
473}
474
475async fn serve_upload<H: HttpTransport, C: Clock>(
476 state: Arc<SshState<H, C>>,
477 mut channel: Channel<Msg>,
478 repo_did: RepoDid,
479 protocol_v2: bool,
480) {
481 let advert = {
482 let layout = state.layout.clone();
483 let did = repo_did.clone();
484 tokio::task::spawn_blocking(move || -> Result<Vec<u8>, PackError> {
485 let repo = layout.open(&did)?;
486 if protocol_v2 {
487 knot_pack::advertise_upload_ssh(&repo)
488 } else {
489 knot_pack::advertise_upload_v0_ssh(&repo)
490 }
491 })
492 .await
493 };
494 let advert = match advert {
495 Ok(Ok(bytes)) => bytes,
496 _ => return fail(channel, &state.catalog.ssh.advertise_failed.text()).await,
497 };
498
499 let mut writer = channel.make_writer();
500 if writer.write_all(&advert).await.is_err() || writer.flush().await.is_err() {
501 return;
502 }
503
504 let outcome = {
505 let mut reader = channel.make_reader();
506 if protocol_v2 {
507 upload_loop_v2(&state, &repo_did, &mut reader, &mut writer).await
508 } else {
509 upload_loop_v0(&state, &repo_did, &mut reader, &mut writer).await
510 }
511 };
512 let status = match outcome {
513 Ok(()) => 0,
514 Err(()) => 1,
515 };
516 finish(channel, status).await;
517}
518
519async fn upload_loop_v2<H, C, R, W>(
520 state: &Arc<SshState<H, C>>,
521 repo_did: &RepoDid,
522 reader: &mut R,
523 writer: &mut W,
524) -> Result<(), ()>
525where
526 H: HttpTransport,
527 C: Clock,
528 R: AsyncRead + Unpin,
529 W: AsyncWriteExt + Unpin,
530{
531 let mut buf = Vec::new();
532 let mut framer = knot_pack::UploadFramer::new();
533 loop {
534 if let Some(len) = framer.advance(&buf) {
535 let request: Vec<u8> = buf.drain(..len).collect();
536 stream_upload(state, repo_did, request, writer).await?;
537 framer = knot_pack::UploadFramer::new();
538 continue;
539 }
540 match read_chunk(reader, &mut buf, MAX_UPLOAD_REQUEST).await {
541 Ok(true) => {}
542 Ok(false) => return Ok(()),
543 Err(_) => return Err(()),
544 }
545 }
546}
547
548async fn upload_loop_v0<H, C, R, W>(
549 state: &Arc<SshState<H, C>>,
550 repo_did: &RepoDid,
551 reader: &mut R,
552 writer: &mut W,
553) -> Result<(), ()>
554where
555 H: HttpTransport,
556 C: Clock,
557 R: AsyncRead + Unpin,
558 W: AsyncWriteExt + Unpin,
559{
560 let mut buf = Vec::new();
561 let mut framer = knot_pack::UploadFramer::new();
562 let mut naks_sent = 0usize;
563 loop {
564 if let Some(len) = framer.advance(&buf) {
565 let request: Vec<u8> = buf.drain(..len).collect();
566 return stream_upload(state, repo_did, request, writer).await;
567 }
568 let needed = framer.unanswered_flushes();
569 if naks_sent < needed {
570 let nak = knot_pack::upload_v0_nak();
571 if writer.write_all(&nak).await.is_err() || writer.flush().await.is_err() {
572 return Err(());
573 }
574 naks_sent += 1;
575 continue;
576 }
577 match read_chunk(reader, &mut buf, MAX_UPLOAD_REQUEST).await {
578 Ok(true) => {}
579 Ok(false) => return Ok(()),
580 Err(_) => return Err(()),
581 }
582 }
583}
584
585async fn stream_upload<H, C, W>(
586 state: &Arc<SshState<H, C>>,
587 repo_did: &RepoDid,
588 request: Vec<u8>,
589 writer: &mut W,
590) -> Result<(), ()>
591where
592 H: HttpTransport,
593 C: Clock,
594 W: AsyncWriteExt + Unpin,
595{
596 let permit = state.slots.pack.acquire().await;
597 let (tx, mut rx) = mpsc::channel::<Vec<u8>>(16);
598 let layout = state.layout.clone();
599 let did = repo_did.clone();
600 let catalog = Arc::clone(&state.catalog);
601 let knot = state.hostname.clone();
602 let handle = tokio::task::spawn_blocking(move || -> Result<(), PackError> {
603 let _permit = permit;
604 let repo = layout.open(&did)?;
605 let mut sink = |chunk: &[u8]| -> std::io::Result<()> {
606 tx.blocking_send(chunk.to_vec())
607 .map_err(|_| std::io::Error::other("client disconnected"))
608 };
609 knot_pack::upload_pack_streamed(&repo, &request, &catalog.fetch, &knot, &mut sink)
610 });
611
612 let mut forward = Ok(());
613 while let Some(chunk) = rx.recv().await {
614 if writer.write_all(&chunk).await.is_err() {
615 forward = Err(());
616 break;
617 }
618 }
619 drop(rx);
620 match (forward, handle.await) {
621 (Ok(()), Ok(Ok(()))) => writer.flush().await.map_err(|_| ()),
622 _ => Err(()),
623 }
624}
625
626async fn serve_receive<H: HttpTransport, C: Clock>(
627 state: Arc<SshState<H, C>>,
628 credential: Credential,
629 mut channel: Channel<Msg>,
630 repo_did: RepoDid,
631 peer: Option<IpAddr>,
632) {
633 let advert = {
634 let layout = state.layout.clone();
635 let did = repo_did.clone();
636 tokio::task::spawn_blocking(move || -> Result<(Vec<u8>, ObjectFormat), PackError> {
637 let repo = layout.open(&did)?;
638 let bytes = knot_pack::advertise_receive_ssh(&repo)?;
639 Ok((bytes, repo.object_format()))
640 })
641 .await
642 };
643 let (advert, object_format) = match advert {
644 Ok(Ok(pair)) => pair,
645 _ => return fail(channel, &state.catalog.ssh.advertise_failed.text()).await,
646 };
647
648 let mut writer = channel.make_writer();
649 if writer.write_all(&advert).await.is_err() || writer.flush().await.is_err() {
650 return;
651 }
652
653 let committer = match authorize_push(&state, &credential, &repo_did, peer).await {
654 PushAuth::Allowed(did) => did,
655 PushAuth::Refused { reason, message } => {
656 tracing::warn!(repo = repo_did.as_str(), reason, "ssh push denied");
657 return fail(channel, &message).await;
658 }
659 };
660
661 let _receive_permit = state.slots.receive.acquire().await;
662
663 let limits = state.limits;
664 let body = {
665 let mut reader = channel.make_reader();
666 let dir = state.layout.scratch_dir().to_path_buf();
667 match tokio::time::timeout(
668 RECEIVE_BODY_DEADLINE,
669 read_receive(
670 &mut reader,
671 dir,
672 state.max_pack_bytes,
673 limits,
674 object_format,
675 ),
676 )
677 .await
678 {
679 Ok(result) => result,
680 Err(_) => Err(ReadError::Deadline),
681 }
682 };
683 let body = match body {
684 Ok(body) => body,
685 Err(ReadError::TooLarge) => {
686 return fail(channel, &state.catalog.ssh.push_too_large.text()).await;
687 }
688 Err(ReadError::Deadline) => {
689 return fail(channel, &state.catalog.ssh.receive_deadline.text()).await;
690 }
691 Err(ReadError::Pack(error)) => {
692 tracing::warn!(repo = repo_did.as_str(), %error, "receive framing failed");
693 return fail(channel, &state.catalog.ssh.malformed_pack.text()).await;
694 }
695 Err(ReadError::Io(error)) => {
696 tracing::warn!(repo = repo_did.as_str(), %error, "receive read error");
697 return fail(channel, &state.catalog.ssh.receive_read_error.text()).await;
698 }
699 Err(ReadError::Truncated) => {
700 return fail(channel, &state.catalog.ssh.receive_ended_early.text()).await;
701 }
702 };
703 if body.is_empty() {
704 return finish(channel, 0).await;
705 }
706
707 let _pack_permit = state.slots.pack.acquire().await;
708 let landed = knot_receive::land(knot_receive::Push {
709 layout: &state.layout,
710 repo_did: &repo_did,
711 received: body,
712 limits: state.limits,
713 knot_actor: state.knot_actor.clone(),
714 committer,
715 events: Arc::clone(&state.events),
716 index: &state.index,
717 atproto: &state.atproto,
718 resolve_slots: &state.slots.resolve,
719 appview: &state.appview,
720 maintenance: &state.maintenance,
721 hostname: &state.hostname,
722 languages_push_budget: state.languages_push_budget,
723 catalog: Arc::clone(&state.catalog),
724 ci_logs: state.ci_logs.clone(),
725 })
726 .await;
727 match landed {
728 Ok(framed) => {
729 let _ = writer.write_all(&framed).await;
730 let _ = writer.flush().await;
731 finish(channel, 0).await;
732 }
733 Err(error) => {
734 tracing::warn!(repo = repo_did.as_str(), %error, "receive-pack failed");
735 fail(channel, &state.catalog.ssh.receive_failed.text()).await;
736 }
737 }
738}
739
740pub(crate) async fn run_greeting<H: HttpTransport, C: Clock>(
741 state: Arc<SshState<H, C>>,
742 credential: Credential,
743 channel: Channel<Msg>,
744) {
745 let knot = state.hostname.as_str().to_string();
746 let greeting = match greeting_visitor(&state, &credential).await {
747 Visitor::Named(who) => state.catalog.ssh.greeting.lines(|key| match key {
748 knot_messages::GreetingKey::User => who.clone(),
749 knot_messages::GreetingKey::Knot => knot.clone(),
750 }),
751 Visitor::Unknown => state
752 .catalog
753 .ssh
754 .greeting_unknown
755 .lines(|knot_messages::KnotKey::Knot| knot.clone()),
756 };
757 if greeting.is_empty() {
758 return finish(channel, 0).await;
759 }
760 let body = greeting.join("\r\n");
761 let _ = channel
762 .extended_data_bytes(1, format!("{body}\r\n").into_bytes())
763 .await;
764 finish(channel, 0).await;
765}
766
767async fn greeting_visitor<H: HttpTransport, C: Clock>(
768 state: &Arc<SshState<H, C>>,
769 credential: &Credential,
770) -> Visitor {
771 let did = match credential {
772 Credential::Identified(did) => did.clone(),
773 Credential::Offered(key) => {
774 match state.index.owner_of_key(key, state.atproto.now().seconds()) {
775 Resolved::Ready(Some(did)) => did,
776 _ => return Visitor::Unknown,
777 }
778 }
779 };
780 match knot_receive::resolve_handle(&state.atproto, &state.slots.resolve, &did).await {
781 Some(handle) => Visitor::Named(format!("@{}", handle.as_str())),
782 None => Visitor::Named(did.as_str().to_string()),
783 }
784}
785
786enum PusherLookup {
787 Matched(AccountDid),
788 Unmatched(Vec<AccountDid>),
789 Unavailable,
790}
791
792enum PushAuth {
793 Allowed(AccountDid),
794 Refused {
795 reason: &'static str,
796 message: String,
797 },
798}
799
800enum Visitor {
801 Named(String),
802 Unknown,
803}
804
805async fn authorize_push<H: HttpTransport, C: Clock>(
806 state: &Arc<SshState<H, C>>,
807 credential: &Credential,
808 repo: &RepoDid,
809 peer: Option<IpAddr>,
810) -> PushAuth {
811 match resolve_pusher(state, credential, repo, peer).await {
812 PusherLookup::Matched(did) => {
813 let acl = KnotAcl::new(&state.admins, state.admission, &state.index);
814 match can_push(&acl, &did, repo).is_allowed() {
815 true => PushAuth::Allowed(did),
816 false => PushAuth::Refused {
817 reason: "unauthorized",
818 message: state.catalog.ssh.push_denied.text(),
819 },
820 }
821 }
822 PusherLookup::Unavailable => PushAuth::Refused {
823 reason: "identity_unavailable",
824 message: state.catalog.ssh.identity_unavailable.text(),
825 },
826 PusherLookup::Unmatched(candidates) => {
827 let authorized = describe_authorized(state, &candidates).await;
828 PushAuth::Refused {
829 reason: "unregistered_key",
830 message: state
831 .catalog
832 .ssh
833 .key_not_registered
834 .line(|knot_messages::AuthorizedKey::Authorized| authorized.clone()),
835 }
836 }
837 }
838}
839
840async fn describe_authorized<H: HttpTransport, C: Clock>(
841 state: &Arc<SshState<H, C>>,
842 candidates: &[AccountDid],
843) -> String {
844 let names: Vec<String> = futures::stream::iter(
845 candidates
846 .iter()
847 .take(AUTHORIZED_NAMES_SHOWN)
848 .cloned()
849 .collect::<Vec<_>>(),
850 )
851 .map(|did| {
852 let state = Arc::clone(state);
853 async move {
854 match knot_receive::resolve_handle(&state.atproto, &state.slots.resolve, &did).await {
855 Some(handle) => format!("@{}", handle.as_str()),
856 None => did.as_str().to_string(),
857 }
858 }
859 })
860 .buffered(CANDIDATE_FANOUT)
861 .collect()
862 .await;
863 match (
864 names.as_slice(),
865 candidates.len().saturating_sub(AUTHORIZED_NAMES_SHOWN),
866 ) {
867 ([], _) => "nobody".to_string(),
868 (shown, 0) => shown.join(", "),
869 (shown, hidden) => format!("{}, and {hidden} more", shown.join(", ")),
870 }
871}
872
873fn probe_due<H: HttpTransport, C: Clock>(state: &Arc<SshState<H, C>>, did: &AccountDid) -> bool {
874 state
875 .probe_pace
876 .reserve_now(&SubjectKey::new(did.as_str()), state.atproto.now())
877}
878
879async fn resolve_pusher<H: HttpTransport, C: Clock>(
880 state: &Arc<SshState<H, C>>,
881 credential: &Credential,
882 repo: &RepoDid,
883 peer: Option<IpAddr>,
884) -> PusherLookup {
885 let key = match credential {
886 Credential::Identified(did) => return PusherLookup::Matched(did.clone()),
887 Credential::Offered(key) => key,
888 };
889 let owner = match state.index.owner_of(repo) {
890 Resolved::Ready(Some(owner)) => Some(AccountDid::from(owner)),
891 _ => None,
892 };
893 {
894 let index = Arc::clone(&state.index);
895 let target = repo.clone();
896 let _ = tokio::task::spawn_blocking(move || index.ensure_collaborators(&target)).await;
897 }
898 let collaborators = match state.index.collaborators_of(repo) {
899 Resolved::Ready(collaborators) => collaborators,
900 _ => Vec::new(),
901 };
902 let candidates: Vec<AccountDid> = owner.into_iter().chain(collaborators).collect();
903 let now = state.atproto.now().seconds();
904 if let Some(publisher) = state.index.keys().publisher_among(&candidates, key, now) {
905 return PusherLookup::Matched(publisher);
906 }
907 let unread: Vec<AccountDid> = candidates
908 .iter()
909 .filter(|did| !state.index.keys().is_fresh(did, now) || probe_due(state, did))
910 .cloned()
911 .collect();
912 if unread.is_empty() {
913 tracing::debug!(
914 ?peer,
915 repo = repo.as_str(),
916 candidates = candidates.len(),
917 "push check has every candidate's keys on file, and the candidates don't publish \
918 the offered key"
919 );
920 return PusherLookup::Unmatched(candidates);
921 }
922 let lease = state.key_ttl.lease_from(now);
923 let unresolved = Arc::new(AtomicBool::new(false));
924 let read: Vec<Option<AccountDid>> = futures::stream::iter(unread)
925 .map(|did| {
926 let state = Arc::clone(state);
927 let key = key.clone();
928 let unresolved = Arc::clone(&unresolved);
929 async move {
930 let _permit = state.slots.resolve.acquire().await;
931 match state.atproto.resolve_pubkeys(&did).await {
932 Ok(keys) => {
933 let matches = keys.contains(&key);
934 state.index.keys().record(&did, keys, lease);
935 matches.then_some(did)
936 }
937 Err(error) if error.is_gone() => {
938 tracing::debug!(
939 did = did.as_str(),
940 %error,
941 "push check records an empty key set for a candidate whose DID document is gone"
942 );
943 state.index.keys().record(&did, Vec::new(), lease);
944 None
945 }
946 Err(error) => {
947 tracing::debug!(
948 did = did.as_str(),
949 %error,
950 "push check couldn't read a candidate's records"
951 );
952 unresolved.store(true, Ordering::Relaxed);
953 None
954 }
955 }
956 }
957 })
958 .buffered(CANDIDATE_FANOUT)
959 .collect()
960 .await;
961 match read.into_iter().flatten().next() {
962 Some(did) => PusherLookup::Matched(did),
963 None if unresolved.load(Ordering::Relaxed) => PusherLookup::Unavailable,
964 None => PusherLookup::Unmatched(candidates),
965 }
966}
967
968async fn read_chunk<R: AsyncRead + Unpin>(
969 reader: &mut R,
970 buf: &mut Vec<u8>,
971 limit: usize,
972) -> Result<bool, ReadError> {
973 let mut chunk = [0u8; READ_CHUNK];
974 let read = reader.read(&mut chunk).await.map_err(ReadError::Io)?;
975 if read == 0 {
976 return Ok(false);
977 }
978 buf.extend_from_slice(&chunk[..read]);
979 if buf.len() > limit {
980 return Err(ReadError::TooLarge);
981 }
982 Ok(true)
983}
984
985async fn read_receive<R: AsyncRead + Unpin>(
986 reader: &mut R,
987 dir: PathBuf,
988 limit: knot_pack::MaxWireBytes,
989 limits: PackLimits,
990 format: ObjectFormat,
991) -> Result<knot_pack::ReceivedPack, ReadError> {
992 let (tx, rx) = mpsc::channel::<Vec<u8>>(8);
993 let mut framer =
994 tokio::task::spawn_blocking(move || frame_receive(rx, dir, limit, limits, format));
995 let mut chunk = [0u8; READ_CHUNK];
996 let mut io_error = None;
997 loop {
998 tokio::select! {
999 biased;
1000 framed = &mut framer => return join_framed(framed, io_error),
1001 read = reader.read(&mut chunk) => match read {
1002 Ok(0) => break,
1003 Ok(read) => {
1004 if tx.send(chunk[..read].to_vec()).await.is_err() {
1005 break;
1006 }
1007 }
1008 Err(error) => {
1009 io_error = Some(error);
1010 break;
1011 }
1012 },
1013 }
1014 }
1015 drop(tx);
1016 join_framed(framer.await, io_error)
1017}
1018
1019fn join_framed(
1020 framed: Result<Result<knot_pack::ReceivedPack, ReadError>, tokio::task::JoinError>,
1021 io_error: Option<std::io::Error>,
1022) -> Result<knot_pack::ReceivedPack, ReadError> {
1023 match framed {
1024 Ok(Ok(body)) => Ok(body),
1025 Ok(Err(ReadError::Truncated)) => {
1026 Err(io_error.map(ReadError::Io).unwrap_or(ReadError::Truncated))
1027 }
1028 Ok(Err(other)) => Err(other),
1029 Err(_) => Err(ReadError::Truncated),
1030 }
1031}
1032
1033fn read_error(error: knot_pack::ReceiveReadError) -> ReadError {
1034 match error {
1035 knot_pack::ReceiveReadError::Io(error) => ReadError::Io(error),
1036 knot_pack::ReceiveReadError::Pack(error) => ReadError::Pack(error),
1037 knot_pack::ReceiveReadError::TooLarge => ReadError::TooLarge,
1038 knot_pack::ReceiveReadError::Truncated => ReadError::Truncated,
1039 }
1040}
1041
1042fn frame_receive(
1043 mut rx: mpsc::Receiver<Vec<u8>>,
1044 dir: PathBuf,
1045 limit: knot_pack::MaxWireBytes,
1046 limits: PackLimits,
1047 format: ObjectFormat,
1048) -> Result<knot_pack::ReceivedPack, ReadError> {
1049 let mut receiver =
1050 knot_pack::PackReceiver::new(&dir, limit, limits, format.kind()).map_err(ReadError::Io)?;
1051 loop {
1052 match rx.blocking_recv() {
1053 Some(chunk) => {
1054 if receiver.write(&chunk).map_err(read_error)? {
1055 return receiver.finish().map_err(read_error);
1056 }
1057 }
1058 None => return receiver.finish().map_err(read_error),
1059 }
1060 }
1061}
1062
1063async fn fail(channel: Channel<Msg>, message: &str) {
1064 let _ = channel
1065 .extended_data_bytes(1, format!("{message}\n").into_bytes())
1066 .await;
1067 finish(channel, 1).await;
1068}
1069
1070async fn finish(channel: Channel<Msg>, status: u32) {
1071 let _ = channel.exit_status(status).await;
1072 let _ = channel.eof().await;
1073 let _ = channel.close().await;
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078 use super::*;
1079
1080 #[test]
1081 fn the_lfs_progress_budget_spares_slow_links_and_cuts_trickles() {
1082 assert!(lfs_within_progress_budget(Duration::from_secs(59), 0));
1083 assert!(!lfs_within_progress_budget(Duration::from_secs(61), 0));
1084 assert!(lfs_within_progress_budget(
1085 Duration::from_secs(50_000),
1086 5 * 1024 * 1024 * 1024
1087 ));
1088 assert!(!lfs_within_progress_budget(Duration::from_secs(1_000), 10));
1089 }
1090
1091 #[test]
1092 fn the_repo_path_parser_separates_dids_from_handles() {
1093 assert!(matches!(
1094 parse_repo_path("did:plc:nel/squid"),
1095 Some(RepoRef::OwnerPath(..))
1096 ));
1097 assert!(matches!(
1098 parse_repo_path("nel.pet/squid"),
1099 Some(RepoRef::HandlePath(..))
1100 ));
1101 assert!(matches!(
1102 parse_repo_path("did:plc:barnacle"),
1103 Some(RepoRef::Did(_))
1104 ));
1105 assert!(parse_repo_path("did:nonsense/squid").is_none());
1106 assert!(parse_repo_path("nel.pet").is_none());
1107 }
1108}