OXYGENHUB

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

304 of 364 lines run, 66 of 126 functions · run #403 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
pub mod witness;
15
16
use axum::body::Bytes;
17
use axum::extract::{Path, State};
18
use axum::http::{header, HeaderMap, Method, StatusCode};
19
use axum::response::{IntoResponse, Response};
20
use axum::routing::any;
21
use axum::Router;
22
use oxy_core::protocol::{wire, CONTENT_TYPE};
23
use oxy_core::repo::Repo;
24
use std::collections::HashMap;
25
use std::path::PathBuf;
26
use std::sync::{Arc, Mutex};
27
28
#[derive(Clone, Debug)]
29
pub struct ServerConfig {
30
    /// Directory containing one repository per subdirectory.
31
    pub root: PathBuf,
32
    /// Bearer tokens allowed to push; empty means no authentication.
33
    pub tokens: Vec<String>,
34
    /// Serve HTTPS with this certificate chain and private key (PEM files).
35
    pub tls: Option<TlsFiles>,
36
}
37
38
#[derive(Clone, Debug)]
39
pub struct TlsFiles {
40
    pub cert: PathBuf,
41
    pub key: PathBuf,
42
}
43
44
/// Which kind of access a request needs (spec §15.1: reads may be open;
45
/// a push must be authenticated when any credential is configured).
46
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47
pub enum Access {
48
    Read,
49
    Push,
50
}
51
52
/// What a host provides to the protocol server: how a repository name maps
53
/// to a repository on disk and who may do what. `oxy serve` uses
54
/// [`DirBackend`]; a hub supplies its own with accounts and roles.
55
pub trait Backend: Send + Sync + 'static {
56
    /// Open the repository called `name`, or fail with the status to
57
    /// answer. An unknown name is `404`; a name the caller may not see
58
    /// SHOULD also be `404`, so private repositories are not enumerable.
59
    fn open(&self, name: &str) -> Result<Repo, Failure>;
60
61
    /// Decide whether this request may perform `access` on `name`.
62
    fn authorize(&self, name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure>;
63
64
    /// Called after a push was processed, with its per-reference results
65
    /// (accepted or refused), the states each accepted reference moved
66
    /// between, and the changes stored, for logs and cache invalidation.
6733×
    fn after_push(&self, _name: &str, _report: &oxy_core::repo::PushReport, _headers: &HeaderMap) {}
68
69
    /// Notes for the pusher on references this push moved: each
70
    /// `(reference, note)` is added to that accepted reference's result,
71
    /// which a client shows beside it (`Pushed origin/x - note`). For a
72
    /// push that did what the pusher may not expect, say moving the
73
    /// bookmark of a proposal that has landed (oxygenhub#329, #148). Called
74
    /// before [`Backend::after_push`], which sees the notes.
7534×
    fn push_notes(
7634×
        &self,
7734×
        _name: &str,
7834×
        _report: &oxy_core::repo::PushReport,
7934×
        _headers: &HeaderMap,
8034×
    ) -> Vec<(String, String)> {
8134×
        Vec::new()
8234×
    }
83
84
    /// Vet a push by what it adds before anything of it is stored or any
85
    /// reference moves (spec §16.1): `bundle` is the push as decoded, its
86
    /// changes with their lines, `updates` the references it would move.
87
    /// A host refuses the whole push by returning a [`Failure`], whose
88
    /// text reaches the client as the push's error, as it is, several
89
    /// lines included; it should answer neither 401, 403 nor 404, which a
90
    /// client takes for a credential it should change. Called for every
91
    /// part of a push sent in parts (each part's changes are its own),
92
    /// outside the repository's push lock, so a slow vet holds up no
93
    /// other push. The default accepts everything at no cost.
9435×
    fn vet_push(
9535×
        &self,
9635×
        _name: &str,
9735×
        _repo: &Repo,
9835×
        _bundle: &oxy_core::protocol::Bundle,
9935×
        _updates: &[oxy_core::protocol::RefUpdate],
10035×
        _headers: &HeaderMap,
10135×
    ) -> Result<(), Failure> {
10235×
        Ok(())
10335×
    }
104
105
    /// Add the host's transparency log to a refs answer (spec §14.8): its
106
    /// checkpoint, and each attestation's entry and inclusion proof. A host
107
    /// without a log leaves it as it is.
108148×
    fn add_log(&self, _name: &str, _refs: &mut oxy_core::protocol::RefsResponse) {}
109
110
    /// Tailor a refs answer to the client asking, by its request headers
111
    /// (its `User-Agent` and the features it declares in `Oxy-Features`),
112
    /// after [`Backend::add_log`]. The default leaves out the tags a client
113
    /// that does not declare `certified-tags` would refuse
114
    /// ([`Repo::refs_for_client`], oxygen#131); a host that overrides it
115
    /// should apply that too ([`tailor_refs`]).
116148×
    fn adjust_refs(
117148×
        &self,
118148×
        _name: &str,
119148×
        repo: &Repo,
120148×
        refs: &mut oxy_core::protocol::RefsResponse,
121148×
        headers: &HeaderMap,
122148×
    ) {
123148×
        tailor_refs(repo, refs, headers);
124148×
    }
125
126
    /// Whether tag `tag` of `name` may be withdrawn (oxygenhub#301): called
127
    /// for each withdrawal a push carries, before the repository sees it.
128
    /// A host refuses one it depends on (a forge's release names the tag)
129
    /// with the reason, which is that withdrawal's result. The default
130
    /// allows every one; the policy is checked after, as for any push.
1310×
    fn vet_tag_withdrawal(&self, _name: &str, _tag: &str) -> Result<(), String> {
1320×
        Ok(())
1330×
    }
134
135
    /// The bookmark of `name` that pushes may never delete (its default),
136
    /// when the host keeps one apart from the repository's own config.
13732×
    fn default_bookmark(&self, _name: &str) -> Option<String> {
13832×
        None
13932×
    }
140
}
141
142
/// A protocol server over any [`Backend`], serialising the pushes to each
143
/// repository. Hosts route requests to [`Server::handle`].
144
pub struct Server {
145
    backend: Box<dyn Backend>,
146
    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
147
}
148
149
/// An error response: a status and a plain-text body.
150
#[derive(Debug)]
151
pub struct Failure(pub StatusCode, pub String);
152
153
impl IntoResponse for Failure {
1549×
    fn into_response(self) -> Response {
1559×
        (self.0, self.1).into_response()
1569×
    }
157
}
158
159
impl From<oxy_core::repo::RepoError> for Failure {
1600×
    fn from(e: oxy_core::repo::RepoError) -> Failure {
161
        use oxy_core::repo::RepoError;
162
        // The server's own storage failing is not the client's fault.
1630×
        let status = match &e {
1640×
            RepoError::Io(_) | RepoError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
1650×
            _ => StatusCode::BAD_REQUEST,
166
        };
1670×
        Failure(status, e.to_string())
1680×
    }
169
}
170
171
/// The wire protocol's endpoints (spec §16.1), named as in the URL.
172
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173
pub enum Endpoint {
174
    Capabilities,
175
    Refs,
176
    Negotiate,
177
    Fetch,
178
    Objects,
179
    Checkpoint,
180
    CheckpointIndex,
181
    Shallow,
182
    Push,
183
}
184
185
impl Endpoint {
186453×
    pub fn parse(name: &str) -> Option<Endpoint> {
187453×
        Some(match name {
188453×
            "capabilities" => Endpoint::Capabilities,
189303×
            "refs" => Endpoint::Refs,
190155×
            "negotiate" => Endpoint::Negotiate,
19199×
            "fetch" => Endpoint::Fetch,
19243×
            "objects" => Endpoint::Objects,
19342×
            "checkpoint" => Endpoint::Checkpoint,
19442×
            "checkpoint-index" => Endpoint::CheckpointIndex,
19542×
            "shallow" => Endpoint::Shallow,
19642×
            "push" => Endpoint::Push,
1970×
            _ => return None,
198
        })
199453×
    }
200
2013×
    pub fn name(self) -> &'static str {
2023×
        match self {
2032×
            Endpoint::Capabilities => "capabilities",
2040×
            Endpoint::Refs => "refs",
2050×
            Endpoint::Negotiate => "negotiate",
2060×
            Endpoint::Fetch => "fetch",
2070×
            Endpoint::Objects => "objects",
2080×
            Endpoint::Checkpoint => "checkpoint",
2090×
            Endpoint::CheckpointIndex => "checkpoint-index",
2100×
            Endpoint::Shallow => "shallow",
2111×
            Endpoint::Push => "push",
212
        }
2133×
    }
214
215
    /// The HTTP method the endpoint answers to.
216453×
    pub fn method(self) -> Method {
217453×
        match self {
218298×
            Endpoint::Capabilities | Endpoint::Refs => Method::GET,
219155×
            _ => Method::POST,
220
        }
221453×
    }
222
223453×
    fn access(self) -> Access {
224453×
        match self {
22542×
            Endpoint::Push => Access::Push,
226411×
            _ => Access::Read,
227
        }
228453×
    }
229
}
230
231444×
fn ok(body: Vec<u8>) -> Response {
232444×
    ([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response()
233444×
}
234
235542×
pub fn valid_repo_name(name: &str) -> bool {
236542×
    !name.is_empty()
237542×
        && !name.starts_with('.')
238541×
        && name
239541×
            .chars()
2402967×
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
241542×
}
242
243
impl Server {
24417×
    pub fn new(backend: impl Backend) -> Arc<Server> {
24517×
        Arc::new(Server {
24617×
            backend: Box::new(backend),
24717×
            locks: Mutex::new(HashMap::new()),
24817×
        })
24917×
    }
250
2510×
    pub fn backend(&self) -> &dyn Backend {
2520×
        self.backend.as_ref()
2530×
    }
254
25536×
    fn lock_for(&self, name: &str) -> Arc<Mutex<()>> {
25636×
        self.locks
25736×
            .lock()
25836×
            .unwrap()
25936×
            .entry(name.to_string())
26036×
            .or_default()
26136×
            .clone()
26236×
    }
263
264
    /// Answer one protocol request for repository `name`. `method` is
265
    /// checked against the endpoint; the body is the raw CBOR request.
266453×
    pub async fn handle(
267453×
        self: &Arc<Self>,
268453×
        name: String,
269453×
        endpoint: Endpoint,
270453×
        method: &Method,
271453×
        headers: &HeaderMap,
272453×
        body: Bytes,
273453×
    ) -> Response {
274453×
        if *method != endpoint.method() {
2750×
            return Failure(
2760×
                StatusCode::METHOD_NOT_ALLOWED,
2770×
                format!("{} is {}", endpoint.name(), endpoint.method()),
2780×
            )
2790×
            .into_response();
280453×
        }
281453×
        if let Err(f) = self.backend.authorize(&name, endpoint.access(), headers) {
2826×
            return f.into_response();
283447×
        }
284447×
        let claimed = headers
285447×
            .get(oxy_core::protocol::REPO_ID_HEADER)
286447×
            .and_then(|v| v.to_str().ok())
287447×
            .and_then(hex_id);
288447×
        let server = Arc::clone(self);
289447×
        let request_headers = headers.clone();
290447×
        let repo = name.clone();
291447×
        let result = tokio::task::spawn_blocking(move || {
292447×
            server.respond(&repo, endpoint, body, claimed, &request_headers)
293447×
        })
294447×
        .await;
295447×
        let failure = match result {
296444×
            Ok(Ok(r)) => return r,
2973×
            Ok(Err(f)) => f,
2980×
            Err(e) => Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
299
        };
300
        // Name the operation and repository, in the log and for the client.
3013×
        let f = Failure(
3023×
            failure.0,
3033×
            format!("{} of {name}: {}", endpoint.name(), failure.1),
3043×
        );
3053×
        eprintln!("oxy-server: {}: {}", f.0.as_u16(), f.1);
3063×
        f.into_response()
307453×
    }
308
309447×
    fn respond(
310447×
        &self,
311447×
        name: &str,
312447×
        endpoint: Endpoint,
313447×
        body: Bytes,
314447×
        claimed: Option<[u8; 16]>,
315447×
        headers: &HeaderMap,
316447×
    ) -> Result<Response, Failure> {
317447×
        let r = self.backend.open(name)?;
318445×
        Ok(match endpoint {
319147×
            Endpoint::Capabilities => ok(wire::encode_capabilities(r.id()?)),
320
            Endpoint::Refs => {
321148×
                let mut refs = r.remote_refs()?;
322148×
                if let Some(d) = self.backend.default_bookmark(name) {
323124×
                    refs.default_bookmark = Some(d);
324124×
                }
325148×
                self.backend.add_log(name, &mut refs);
326148×
                self.backend.adjust_refs(name, &r, &mut refs, headers);
327148×
                ok(wire::encode_refs(r.id()?, &refs))
328
            }
329
            Endpoint::Negotiate => {
33056×
                let (have, want) = wire::decode_negotiate(&body)?;
33156×
                ok(wire::encode_hashes(&r.negotiate(&have, &want)?))
332
            }
333
            Endpoint::Fetch => {
33456×
                let (changes, max_blob) = wire::decode_fetch(&body)?;
33556×
                ok(wire::encode_bundle(
33656×
                    &r.bundle_for_filtered(&changes, max_blob)?,
337
                ))
338
            }
339
            Endpoint::Objects => {
3401×
                let refs = wire::decode_hashes(&body)?;
3411×
                let bundle = oxy_core::protocol::Bundle {
3421×
                    objects: r.objects_for(&refs)?,
3431×
                    ..Default::default()
344
                };
3451×
                ok(wire::encode_bundle(&bundle))
346
            }
347
            Endpoint::Checkpoint => {
3480×
                let heads = wire::decode_hashes(&body)?;
3490×
                let lock = self.lock_for(name);
3500×
                let _guard = lock.lock().unwrap();
3510×
                let bundle = r.checkpoint_bundle_for(&heads)?;
3520×
                let _t = oxy_core::timing::phase("serve: encode the checkpoint");
3530×
                ok(wire::encode_bundle(&bundle))
354
            }
355
            Endpoint::CheckpointIndex => {
3560×
                let heads = wire::decode_hashes(&body)?;
3570×
                let lock = self.lock_for(name);
3580×
                let _guard = lock.lock().unwrap();
3590×
                ok(wire::encode_bundle(&r.checkpoint_index_for(&heads)?))
360
            }
361
            Endpoint::Shallow => {
3620×
                let (heads, depth) = wire::decode_shallow(&body)?;
3630×
                ok(wire::encode_hashes(&r.shallow_changes(&heads, depth)?))
364
            }
365
            Endpoint::Push => {
36637×
                let (bundle, updates, policy, tags) = wire::decode_push(&body)?;
367
                // The host vets what the push adds before anything is stored
368
                // or bound: a refused push leaves the repository as it was.
36937×
                self.backend
37037×
                    .vet_push(name, &r, &bundle, &updates, headers)?;
37136×
                let lock = self.lock_for(name);
37236×
                let _guard = lock.lock().unwrap();
37336×
                if r.is_unbound() {
374
                    // The first push binds the repository to the pusher's id (spec §16.1).
3754×
                    match claimed {
3764×
                        Some(id) => r.bind_id(id)?,
377
                        None => {
3780×
                            return Err(Failure(
3790×
                                StatusCode::BAD_REQUEST,
3800×
                                format!(
3810×
                                    "this repository is unbound; push with a client that sends {}",
3820×
                                    oxy_core::protocol::REPO_ID_HEADER
3830×
                                ),
3840×
                            ))
385
                        }
386
                    }
38732×
                }
38836×
                let protected = self.backend.default_bookmark(name);
38936×
                let (refused, updates): (Vec<_>, Vec<_>) = updates.into_iter().partition(|u| {
39036×
                    u.new_state.is_zero() && protected.as_deref() == Some(u.name.as_str())
39136×
                });
39236×
                let mut kept_tags = Vec::new();
39336×
                let mut withdrawals_refused = Vec::new();
39436×
                for t in tags {
3953×
                    match t
3963×
                        .is_withdrawal()
3973×
                        .then(|| self.backend.vet_tag_withdrawal(name, &t.name))
398
                    {
3990×
                        Some(Err(reason)) => withdrawals_refused.push((t.name, reason)),
4003×
                        _ => kept_tags.push(t),
401
                    }
402
                }
40336×
                let mut report = r.receive_push_report(bundle, updates, policy, kept_tags)?;
404
                // The request named references, all refused here: it was no
405
                // push of objects alone, whatever the repository saw.
40636×
                if !refused.is_empty() || !withdrawals_refused.is_empty() {
4071×
                    report
4081×
                        .results
4091×
                        .retain(|r| r.name != oxy_core::repo::OBJECTS_RESULT);
41035×
                }
41136×
                for u in refused {
4121×
                    report.results.push(oxy_core::protocol::RefResult {
4131×
                        name: u.name.clone(),
4141×
                        ok: false,
4151×
                        reason: format!(
4161×
                            "{:?} is this repository's default bookmark and cannot be deleted",
4171×
                            u.name
4181×
                        ),
4191×
                    });
4201×
                }
42136×
                for (tag, reason) in withdrawals_refused {
4220×
                    report.results.push(oxy_core::protocol::RefResult {
4230×
                        name: format!("tags/{tag}"),
4240×
                        ok: false,
4250×
                        reason,
4260×
                    });
4270×
                }
42842×
                for r in report.results.iter().filter(|r| !r.ok) {
4298×
                    eprintln!(
4308×
                        "oxy-server: push to {name}: {} refused: {}",
4318×
                        r.name, r.reason
4328×
                    );
4338×
                }
43436×
                for (reference, note) in self.backend.push_notes(name, &report, headers) {
4351×
                    if let Some(r) = report
4361×
                        .results
4371×
                        .iter_mut()
4381×
                        .find(|r| r.ok && r.name == reference)
439
                    {
4401×
                        r.reason = if r.reason.is_empty() {
4411×
                            note
442
                        } else {
4430×
                            format!("{}; {note}", r.reason)
444
                        };
4450×
                    }
446
                }
44736×
                self.backend.after_push(name, &report, headers);
44836×
                ok(wire::encode_push_response(&report.results))
449
            }
450
        })
451447×
    }
452
}
453
454
/// The backend of `oxy serve`: one repository per subdirectory of a root,
455
/// open reads, and pushes gated by a list of bearer tokens.
456
pub struct DirBackend {
457
    pub root: PathBuf,
458
    pub tokens: Vec<String>,
459
}
460
461
impl Backend for DirBackend {
462542×
    fn open(&self, name: &str) -> Result<Repo, Failure> {
463542×
        if !valid_repo_name(name) {
4641×
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
465541×
        }
466541×
        let dir = self.root.join(name);
467541×
        if !dir.join(".oxy").is_dir() {
4681×
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
469540×
        }
470540×
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
471542×
    }
472
473152×
    fn default_bookmark(&self, name: &str) -> Option<String> {
474152×
        self.open(name).ok().map(|r| r.config.bookmark.clone())
475152×
    }
476
477394×
    fn authorize(&self, _name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> {
478394×
        if access == Access::Read || self.tokens.is_empty() {
479388×
            return Ok(());
4806×
        }
4816×
        match bearer_token(headers) {
4825×
            Some(t) if self.tokens.iter().any(|x| x == t) => Ok(()),
4834×
            _ => Err(Failure(
4844×
                StatusCode::UNAUTHORIZED,
4854×
                "a valid bearer token is required to push".into(),
4864×
            )),
487
        }
488394×
    }
489
}
490
491414×
fn hex_id(h: &str) -> Option<[u8; 16]> {
492414×
    let h = h.trim();
493414×
    if h.len() != 32 {
4940×
        return None;
495414×
    }
496414×
    let mut out = [0u8; 16];
4976624×
    for (i, byte) in out.iter_mut().enumerate() {
4986624×
        *byte = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?;
499
    }
500414×
    Some(out)
501414×
}
502
503
/// The features a request declares in its `Oxy-Features` header, as the
504
/// header's text; `None` when it sent none (a client of the 0.5.0 era).
505148×
pub fn client_features(headers: &HeaderMap) -> Option<&str> {
506148×
    headers
507148×
        .get(oxy_core::protocol::FEATURES_HEADER)
508148×
        .and_then(|v| v.to_str().ok())
509148×
}
510
511
/// Tailor `refs` of `repo` to the client whose request carried `headers`:
512
/// the shared rule of [`Repo::refs_for_client`], which every host applies.
513148×
pub fn tailor_refs(repo: &Repo, refs: &mut oxy_core::protocol::RefsResponse, headers: &HeaderMap) {
514148×
    repo.refs_for_client(refs, client_features(headers));
515148×
}
516
517
/// The token of an `Authorization: Bearer` header, if any.
51819×
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
51919×
    headers
52019×
        .get(header::AUTHORIZATION)
52119×
        .and_then(|v| v.to_str().ok())
52219×
        .and_then(|v| v.strip_prefix("Bearer "))
52319×
        .map(str::trim)
52419×
}
525
526382×
async fn dispatch(
527382×
    State(server): State<Arc<Server>>,
528382×
    Path((repo, endpoint)): Path<(String, String)>,
529382×
    method: Method,
530382×
    headers: HeaderMap,
531382×
    body: Bytes,
532382×
) -> Response {
533382×
    match Endpoint::parse(&endpoint) {
534382×
        Some(e) => server.handle(repo, e, &method, &headers, body).await,
5350×
        None => Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(),
536
    }
537382×
}
538
539
/// The largest request `oxy serve` takes: a push part is about
540
/// `push.batch-bytes` (64 MiB), but one change can weigh more; OxygenHub's
541
/// default `push_body_limit` is the same.
542
pub const BODY_LIMIT: usize = if usize::BITS > 32 {
543
    4 << 30
544
} else {
545
    usize::MAX
546
};
547
548
/// The router of `oxy serve`: `/{repo}/oxy/v1/{endpoint}` over a [`DirBackend`].
54913×
pub fn router(cfg: ServerConfig) -> Router {
55013×
    let server = Server::new(DirBackend {
55113×
        root: cfg.root,
55213×
        tokens: cfg.tokens,
55313×
    });
554
    // Without a limit of its own a request takes axum's 2 MB, and a larger
555
    // push was cut off with no word in the log ("Broken pipe" at the client).
55613×
    Router::new()
55713×
        .route("/{repo}/oxy/v1/{endpoint}", any(dispatch))
55813×
        .layer(axum::extract::DefaultBodyLimit::max(BODY_LIMIT))
55913×
        .with_state(server)
56013×
}
561
562
/// Serve on an already-bound listener until the process ends; HTTPS when
563
/// `cfg.tls` is set.
56413×
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
56513×
    listener.set_nonblocking(true)?;
56613×
    match cfg.tls.clone() {
5671×
        Some(tls) => {
5681×
            let _ = rustls::crypto::ring::default_provider().install_default();
5691×
            let rustls_cfg =
5701×
                axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key)
5711×
                    .await
5721×
                    .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?;
5731×
            axum_server::from_tcp_rustls(listener, rustls_cfg)?
5741×
                .serve(router(cfg).into_make_service())
5751×
                .await
576
        }
577
        None => {
57812×
            let listener = tokio::net::TcpListener::from_std(listener)?;
57912×
            axum::serve(listener, router(cfg)).await
580
        }
581
    }
5820×
}
583
584
/// Build a runtime and serve; for the CLI and for tests on a background thread.
58513×
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
58613×
    let rt = tokio::runtime::Builder::new_multi_thread()
58713×
        .enable_all()
58813×
        .build()?;
58913×
    rt.block_on(serve(listener, cfg))
59013×
}