OXYGENHUB

Coverage / crates/oxy-server/src/lib.rs 85.7%

246 of 287 lines run, 38 of 83 functions · run #324 on main · run missed

1
//! Reference HTTP server for the Oxygen wire protocol (spec §15.1, §16).
2
//!
3
//! Serves every repository directory under a root at `/{repo}/oxy/v1/...`,
4
//! over TLS when a certificate and key are given, else plain HTTP for use
5
//! behind a TLS-terminating proxy or on a loopback interface (spec §15.1).
6
//!
7
//! Authentication: when bearer tokens are configured, `push` requires one;
8
//! reads are open. With no tokens configured everything is open (development).
9
//!
10
//! Hosts embed the protocol by implementing [`Backend`] (name resolution
11
//! and authorization) and routing to [`Server::handle`]; `router` is the
12
//! standalone server built on [`DirBackend`].
13
14
use axum::body::Bytes;
15
use axum::extract::{Path, State};
16
use axum::http::{header, HeaderMap, Method, StatusCode};
17
use axum::response::{IntoResponse, Response};
18
use axum::routing::any;
19
use axum::Router;
20
use oxy_core::protocol::{wire, CONTENT_TYPE};
21
use oxy_core::repo::Repo;
22
use std::collections::HashMap;
23
use std::path::PathBuf;
24
use std::sync::{Arc, Mutex};
25
26
#[derive(Clone, Debug)]
27
pub struct ServerConfig {
28
    /// Directory containing one repository per subdirectory.
29
    pub root: PathBuf,
30
    /// Bearer tokens allowed to push; empty means no authentication.
31
    pub tokens: Vec<String>,
32
    /// Serve HTTPS with this certificate chain and private key (PEM files).
33
    pub tls: Option<TlsFiles>,
34
}
35
36
#[derive(Clone, Debug)]
37
pub struct TlsFiles {
38
    pub cert: PathBuf,
39
    pub key: PathBuf,
40
}
41
42
/// Which kind of access a request needs (spec §15.1: reads may be open;
43
/// a push must be authenticated when any credential is configured).
44
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45
pub enum Access {
46
    Read,
47
    Push,
48
}
49
50
/// What a host provides to the protocol server: how a repository name maps
51
/// to a repository on disk and who may do what. `oxy serve` uses
52
/// [`DirBackend`]; a hub supplies its own with accounts and roles.
53
pub trait Backend: Send + Sync + 'static {
54
    /// Open the repository called `name`, or fail with the status to
55
    /// answer. An unknown name is `404`; a name the caller may not see
56
    /// SHOULD also be `404`, so private repositories are not enumerable.
57
    fn open(&self, name: &str) -> Result<Repo, Failure>;
58
59
    /// Decide whether this request may perform `access` on `name`.
60
    fn authorize(&self, name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure>;
61
62
    /// Called after a push was processed, with its per-reference results
63
    /// (accepted or refused), the states each accepted reference moved
64
    /// between, and the changes stored, for logs and cache invalidation.
6532×
    fn after_push(&self, _name: &str, _report: &oxy_core::repo::PushReport, _headers: &HeaderMap) {}
66
67
    /// Add the host's transparency log to a refs answer (spec §14.8): its
68
    /// checkpoint, and each attestation's entry and inclusion proof. A host
69
    /// without a log leaves it as it is.
70121×
    fn add_log(&self, _name: &str, _refs: &mut oxy_core::protocol::RefsResponse) {}
71
72
    /// The bookmark of `name` that pushes may never delete (its default),
73
    /// when the host keeps one apart from the repository's own config.
745×
    fn default_bookmark(&self, _name: &str) -> Option<String> {
755×
        None
765×
    }
77
}
78
79
/// A protocol server over any [`Backend`], serialising the pushes to each
80
/// repository. Hosts route requests to [`Server::handle`].
81
pub struct Server {
82
    backend: Box<dyn Backend>,
83
    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
84
}
85
86
/// An error response: a status and a plain-text body.
87
#[derive(Debug)]
88
pub struct Failure(pub StatusCode, pub String);
89
90
impl IntoResponse for Failure {
918×
    fn into_response(self) -> Response {
928×
        (self.0, self.1).into_response()
938×
    }
94
}
95
96
impl From<oxy_core::repo::RepoError> for Failure {
970×
    fn from(e: oxy_core::repo::RepoError) -> Failure {
98
        use oxy_core::repo::RepoError;
99
        // The server's own storage failing is not the client's fault.
1000×
        let status = match &e {
1010×
            RepoError::Io(_) | RepoError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
1020×
            _ => StatusCode::BAD_REQUEST,
103
        };
1040×
        Failure(status, e.to_string())
1050×
    }
106
}
107
108
/// The wire protocol's endpoints (spec §16.1), named as in the URL.
109
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110
pub enum Endpoint {
111
    Capabilities,
112
    Refs,
113
    Negotiate,
114
    Fetch,
115
    Objects,
116
    Checkpoint,
117
    Shallow,
118
    Push,
119
}
120
121
impl Endpoint {
122383×
    pub fn parse(name: &str) -> Option<Endpoint> {
123383×
        Some(match name {
124383×
            "capabilities" => Endpoint::Capabilities,
125256×
            "refs" => Endpoint::Refs,
126135×
            "negotiate" => Endpoint::Negotiate,
12787×
            "fetch" => Endpoint::Fetch,
12839×
            "objects" => Endpoint::Objects,
12938×
            "checkpoint" => Endpoint::Checkpoint,
13038×
            "shallow" => Endpoint::Shallow,
13138×
            "push" => Endpoint::Push,
1320×
            _ => return None,
133
        })
134383×
    }
135
1362×
    pub fn name(self) -> &'static str {
1372×
        match self {
1382×
            Endpoint::Capabilities => "capabilities",
1390×
            Endpoint::Refs => "refs",
1400×
            Endpoint::Negotiate => "negotiate",
1410×
            Endpoint::Fetch => "fetch",
1420×
            Endpoint::Objects => "objects",
1430×
            Endpoint::Checkpoint => "checkpoint",
1440×
            Endpoint::Shallow => "shallow",
1450×
            Endpoint::Push => "push",
146
        }
1472×
    }
148
149
    /// The HTTP method the endpoint answers to.
150383×
    pub fn method(self) -> Method {
151383×
        match self {
152248×
            Endpoint::Capabilities | Endpoint::Refs => Method::GET,
153135×
            _ => Method::POST,
154
        }
155383×
    }
156
157383×
    fn access(self) -> Access {
158383×
        match self {
15938×
            Endpoint::Push => Access::Push,
160345×
            _ => Access::Read,
161
        }
162383×
    }
163
}
164
165375×
fn ok(body: Vec<u8>) -> Response {
166375×
    ([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response()
167375×
}
168
169515×
pub fn valid_repo_name(name: &str) -> bool {
170515×
    !name.is_empty()
171515×
        && !name.starts_with('.')
172514×
        && name
173514×
            .chars()
1742841×
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
175515×
}
176
177
impl Server {
17812×
    pub fn new(backend: impl Backend) -> Arc<Server> {
17912×
        Arc::new(Server {
18012×
            backend: Box::new(backend),
18112×
            locks: Mutex::new(HashMap::new()),
18212×
        })
18312×
    }
184
1850×
    pub fn backend(&self) -> &dyn Backend {
1860×
        self.backend.as_ref()
1870×
    }
188
18933×
    fn lock_for(&self, name: &str) -> Arc<Mutex<()>> {
19033×
        self.locks
19133×
            .lock()
19233×
            .unwrap()
19333×
            .entry(name.to_string())
19433×
            .or_default()
19533×
            .clone()
19633×
    }
197
198
    /// Answer one protocol request for repository `name`. `method` is
199
    /// checked against the endpoint; the body is the raw CBOR request.
200383×
    pub async fn handle(
201383×
        self: &Arc<Self>,
202383×
        name: String,
203383×
        endpoint: Endpoint,
204383×
        method: &Method,
205383×
        headers: &HeaderMap,
206383×
        body: Bytes,
207383×
    ) -> Response {
208383×
        if *method != endpoint.method() {
2090×
            return Failure(
2100×
                StatusCode::METHOD_NOT_ALLOWED,
2110×
                format!("{} is {}", endpoint.name(), endpoint.method()),
2120×
            )
2130×
            .into_response();
214383×
        }
215383×
        if let Err(f) = self.backend.authorize(&name, endpoint.access(), headers) {
2166×
            return f.into_response();
217377×
        }
218377×
        let claimed = headers
219377×
            .get(oxy_core::protocol::REPO_ID_HEADER)
220377×
            .and_then(|v| v.to_str().ok())
221377×
            .and_then(hex_id);
222377×
        let server = Arc::clone(self);
223377×
        let request_headers = headers.clone();
224377×
        let repo = name.clone();
225377×
        let result = tokio::task::spawn_blocking(move || {
226377×
            server.respond(&repo, endpoint, body, claimed, &request_headers)
227377×
        })
228377×
        .await;
229377×
        let failure = match result {
230375×
            Ok(Ok(r)) => return r,
2312×
            Ok(Err(f)) => f,
2320×
            Err(e) => Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
233
        };
234
        // Name the operation and repository, in the log and for the client.
2352×
        let f = Failure(
2362×
            failure.0,
2372×
            format!("{} of {name}: {}", endpoint.name(), failure.1),
2382×
        );
2392×
        eprintln!("oxy-server: {}: {}", f.0.as_u16(), f.1);
2402×
        f.into_response()
241383×
    }
242
243377×
    fn respond(
244377×
        &self,
245377×
        name: &str,
246377×
        endpoint: Endpoint,
247377×
        body: Bytes,
248377×
        claimed: Option<[u8; 16]>,
249377×
        headers: &HeaderMap,
250377×
    ) -> Result<Response, Failure> {
251377×
        let r = self.backend.open(name)?;
252375×
        Ok(match endpoint {
253124×
            Endpoint::Capabilities => ok(wire::encode_capabilities(r.id()?)),
254
            Endpoint::Refs => {
255121×
                let mut refs = r.remote_refs()?;
256121×
                if let Some(d) = self.backend.default_bookmark(name) {
257117×
                    refs.default_bookmark = Some(d);
258117×
                }
259121×
                self.backend.add_log(name, &mut refs);
260121×
                ok(wire::encode_refs(r.id()?, &refs))
261
            }
262
            Endpoint::Negotiate => {
26348×
                let (have, want) = wire::decode_negotiate(&body)?;
26448×
                ok(wire::encode_hashes(&r.negotiate(&have, &want)?))
265
            }
266
            Endpoint::Fetch => {
26748×
                let (changes, max_blob) = wire::decode_fetch(&body)?;
26848×
                ok(wire::encode_bundle(
26948×
                    &r.bundle_for_filtered(&changes, max_blob)?,
270
                ))
271
            }
272
            Endpoint::Objects => {
2731×
                let refs = wire::decode_hashes(&body)?;
2741×
                let bundle = oxy_core::protocol::Bundle {
2751×
                    objects: r.objects_for(&refs)?,
2761×
                    ..Default::default()
277
                };
2781×
                ok(wire::encode_bundle(&bundle))
279
            }
280
            Endpoint::Checkpoint => {
2810×
                let heads = wire::decode_hashes(&body)?;
2820×
                let lock = self.lock_for(name);
2830×
                let _guard = lock.lock().unwrap();
2840×
                ok(wire::encode_bundle(&r.checkpoint_bundle_for(&heads)?))
285
            }
286
            Endpoint::Shallow => {
2870×
                let (heads, depth) = wire::decode_shallow(&body)?;
2880×
                ok(wire::encode_hashes(&r.shallow_changes(&heads, depth)?))
289
            }
290
            Endpoint::Push => {
29133×
                let (bundle, updates, policy, tags) = wire::decode_push(&body)?;
29233×
                let lock = self.lock_for(name);
29333×
                let _guard = lock.lock().unwrap();
29433×
                if r.is_unbound() {
295
                    // The first push binds the repository to the pusher's id (spec §16.1).
2962×
                    match claimed {
2972×
                        Some(id) => r.bind_id(id)?,
298
                        None => {
2990×
                            return Err(Failure(
3000×
                                StatusCode::BAD_REQUEST,
3010×
                                format!(
3020×
                                    "this repository is unbound; push with a client that sends {}",
3030×
                                    oxy_core::protocol::REPO_ID_HEADER
3040×
                                ),
3050×
                            ))
306
                        }
307
                    }
30831×
                }
30933×
                let protected = self.backend.default_bookmark(name);
31033×
                let (refused, updates): (Vec<_>, Vec<_>) = updates.into_iter().partition(|u| {
31133×
                    u.new_state.is_zero() && protected.as_deref() == Some(u.name.as_str())
31233×
                });
31333×
                let mut report = r.receive_push_report(bundle, updates, policy, tags)?;
314
                // The request named references, all refused here: it was no
315
                // push of objects alone, whatever the repository saw.
31633×
                if !refused.is_empty() {
3171×
                    report
3181×
                        .results
3191×
                        .retain(|r| r.name != oxy_core::repo::OBJECTS_RESULT);
32032×
                }
32133×
                for u in refused {
3221×
                    report.results.push(oxy_core::protocol::RefResult {
3231×
                        name: u.name.clone(),
3241×
                        ok: false,
3251×
                        reason: format!(
3261×
                            "{:?} is this repository's default bookmark and cannot be deleted",
3271×
                            u.name
3281×
                        ),
3291×
                    });
3301×
                }
33134×
                for r in report.results.iter().filter(|r| !r.ok) {
33213×
                    eprintln!(
33313×
                        "oxy-server: push to {name}: {} refused: {}",
33413×
                        r.name, r.reason
33513×
                    );
33613×
                }
33733×
                self.backend.after_push(name, &report, headers);
33833×
                ok(wire::encode_push_response(&report.results))
339
            }
340
        })
341377×
    }
342
}
343
344
/// The backend of `oxy serve`: one repository per subdirectory of a root,
345
/// open reads, and pushes gated by a list of bearer tokens.
346
pub struct DirBackend {
347
    pub root: PathBuf,
348
    pub tokens: Vec<String>,
349
}
350
351
impl Backend for DirBackend {
352515×
    fn open(&self, name: &str) -> Result<Repo, Failure> {
353515×
        if !valid_repo_name(name) {
3541×
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
355514×
        }
356514×
        let dir = self.root.join(name);
357514×
        if !dir.join(".oxy").is_dir() {
3581×
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
359513×
        }
360513×
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
361515×
    }
362
363149×
    fn default_bookmark(&self, name: &str) -> Option<String> {
364149×
        self.open(name).ok().map(|r| r.config.bookmark.clone())
365149×
    }
366
367370×
    fn authorize(&self, _name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> {
368370×
        if access == Access::Read || self.tokens.is_empty() {
369364×
            return Ok(());
3706×
        }
3716×
        match bearer_token(headers) {
3725×
            Some(t) if self.tokens.iter().any(|x| x == t) => Ok(()),
3734×
            _ => Err(Failure(
3744×
                StatusCode::UNAUTHORIZED,
3754×
                "a valid bearer token is required to push".into(),
3764×
            )),
377
        }
378370×
    }
379
}
380
381354×
fn hex_id(h: &str) -> Option<[u8; 16]> {
382354×
    let h = h.trim();
383354×
    if h.len() != 32 {
3840×
        return None;
385354×
    }
386354×
    let mut out = [0u8; 16];
3875664×
    for (i, byte) in out.iter_mut().enumerate() {
3885664×
        *byte = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?;
389
    }
390354×
    Some(out)
391354×
}
392
393
/// The token of an `Authorization: Bearer` header, if any.
39419×
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
39519×
    headers
39619×
        .get(header::AUTHORIZATION)
39719×
        .and_then(|v| v.to_str().ok())
39819×
        .and_then(|v| v.strip_prefix("Bearer "))
39919×
        .map(str::trim)
40019×
}
401
402370×
async fn dispatch(
403370×
    State(server): State<Arc<Server>>,
404370×
    Path((repo, endpoint)): Path<(String, String)>,
405370×
    method: Method,
406370×
    headers: HeaderMap,
407370×
    body: Bytes,
408370×
) -> Response {
409370×
    match Endpoint::parse(&endpoint) {
410370×
        Some(e) => server.handle(repo, e, &method, &headers, body).await,
4110×
        None => Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(),
412
    }
413370×
}
414
415
/// The router of `oxy serve`: `/{repo}/oxy/v1/{endpoint}` over a [`DirBackend`].
41611×
pub fn router(cfg: ServerConfig) -> Router {
41711×
    let server = Server::new(DirBackend {
41811×
        root: cfg.root,
41911×
        tokens: cfg.tokens,
42011×
    });
42111×
    Router::new()
42211×
        .route("/{repo}/oxy/v1/{endpoint}", any(dispatch))
42311×
        .with_state(server)
42411×
}
425
426
/// Serve on an already-bound listener until the process ends; HTTPS when
427
/// `cfg.tls` is set.
42811×
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
42911×
    listener.set_nonblocking(true)?;
43011×
    match cfg.tls.clone() {
4311×
        Some(tls) => {
4321×
            let _ = rustls::crypto::ring::default_provider().install_default();
4331×
            let rustls_cfg =
4341×
                axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key)
4351×
                    .await
4361×
                    .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?;
4371×
            axum_server::from_tcp_rustls(listener, rustls_cfg)?
4381×
                .serve(router(cfg).into_make_service())
4391×
                .await
440
        }
441
        None => {
44210×
            let listener = tokio::net::TcpListener::from_std(listener)?;
44310×
            axum::serve(listener, router(cfg)).await
444
        }
445
    }
4460×
}
447
448
/// Build a runtime and serve; for the CLI and for tests on a background thread.
44911×
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
45011×
    let rt = tokio::runtime::Builder::new_multi_thread()
45111×
        .enable_all()
45211×
        .build()?;
45311×
    rt.block_on(serve(listener, cfg))
45411×
}