git_lfs_api/ssh.rs
1//! SSH-based endpoint resolution hook.
2//!
3//! When the LFS endpoint is reached via SSH (e.g. `lfs.url =
4//! ssh://...`, or a `git@host:repo` remote without a separate
5//! `lfs.url`), upstream Git LFS shells out to `git-lfs-authenticate` to
6//! obtain a replacement HTTPS URL plus auth headers. This crate is
7//! transport-agnostic, so it expresses the hook as a [`SshResolver`]
8//! trait — the actual `ssh` invocation lives in `git-lfs-creds`.
9//!
10//! The [`Client`](crate::Client) consults the resolver once per
11//! request: a non-empty [`SshAuth::href`] overrides the endpoint URL
12//! prefix for that call, and [`SshAuth::headers`] are merged into the
13//! outgoing request. Caching is the resolver's responsibility — see
14//! `git_lfs_creds::SshAuthClient` for the production implementation.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use crate::error::ApiError;
20
21/// `git-lfs-authenticate <path> <op>` operation argument. Wire form is
22/// lowercase `upload` / `download`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum SshOperation {
25 Upload,
26 Download,
27}
28
29impl SshOperation {
30 /// Default mirrors upstream's `endpointOperation`: GET/HEAD →
31 /// download, anything else → upload. Used as the fallback when a
32 /// caller doesn't pass an explicit operation.
33 pub fn from_http_method(method: &reqwest::Method) -> Self {
34 if matches!(*method, reqwest::Method::GET | reqwest::Method::HEAD) {
35 Self::Download
36 } else {
37 Self::Upload
38 }
39 }
40}
41
42/// Resolved auth from a `git-lfs-authenticate` call.
43#[derive(Debug, Clone, Default)]
44pub struct SshAuth {
45 /// Replacement endpoint URL prefix. Empty (`""`) when the server
46 /// expects the original URL to be used as-is.
47 pub href: String,
48 /// Headers to merge into the outgoing request — typically a single
49 /// `Authorization` entry, but the schema lets servers set arbitrary
50 /// keys (e.g. `X-RemoteAuth-Provider` for vendor extensions).
51 pub headers: HashMap<String, String>,
52}
53
54/// Hook called once per LFS API request to resolve SSH-mediated auth.
55///
56/// Implementations are typically backed by a `git-lfs-authenticate`
57/// invocation with a small in-memory cache keyed on `(host, path,
58/// operation)` so the SSH command runs at most once per cache TTL.
59pub trait SshResolver: Send + Sync {
60 /// Return the auth response for `operation`. `Ok(default)` (empty
61 /// `href`, empty `headers`) means "no SSH override — use the
62 /// configured endpoint with whatever auth is already on the
63 /// request".
64 fn resolve(&self, operation: SshOperation) -> Result<SshAuth, ApiError>;
65}
66
67/// Type alias for the boxed-resolver field on [`Client`](crate::Client).
68pub type SharedSshResolver = Arc<dyn SshResolver>;