OXYGENHUB
◆ v0.6.0 oxygen / crates / oxy-server / src / lib.rs
19.9 KiB · Rust · last changed Depth 0 takes no change, and a checkpoint is gathered in parallel (refs #137) 11 days ago source blame history raw
750251b2 Andy McCall ↶ 1
//! Reference HTTP server for the Oxygen wire protocol (spec §15.1, §16).
2
//!
2b5ee371 Andy McCall ↶ 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).
750251b2 Andy McCall ↶ 6
//!
7
//! Authentication: when bearer tokens are configured, `push` requires one;
8
//! reads are open. With no tokens configured everything is open (development).
e02bf0cd Andy McCall ↶ 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`].
750251b2 Andy McCall ↶ 13
51d4862a Andy McCall ↶ 14
pub mod witness;
15
750251b2 Andy McCall ↶ 16
use axum::body::Bytes;
17
use axum::extract::{Path, State};
e02bf0cd Andy McCall ↶ 18
use axum::http::{header, HeaderMap, Method, StatusCode};
750251b2 Andy McCall ↶ 19
use axum::response::{IntoResponse, Response};
e02bf0cd Andy McCall ↶ 20
use axum::routing::any;
750251b2 Andy McCall ↶ 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>,
2b5ee371 Andy McCall ↶ 34
    /// Serve HTTPS with this certificate chain and private key (PEM files).
35
    pub tls: Option<TlsFiles>,
750251b2 Andy McCall ↶ 36
}
37
2b5ee371 Andy McCall ↶ 38
#[derive(Clone, Debug)]
39
pub struct TlsFiles {
40
    pub cert: PathBuf,
41
    pub key: PathBuf,
42
}
43
e02bf0cd Andy McCall ↶ 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
7aa2dc93 Andy McCall ↶ 65
    /// (accepted or refused), the states each accepted reference moved
66
    /// between, and the changes stored, for logs and cache invalidation.
76f75250 Andy McCall ↶ 67
    fn after_push(&self, _name: &str, _report: &oxy_core::repo::PushReport, _headers: &HeaderMap) {}
f8c69576 Andy McCall ↶ 68
67a4fe1a Andy McCall ↶ 69
    /// Vet a push by what it adds before anything of it is stored or any
70
    /// reference moves (spec §16.1): `bundle` is the push as decoded, its
71
    /// changes with their lines, `updates` the references it would move.
72
    /// A host refuses the whole push by returning a [`Failure`], whose
73
    /// text reaches the client as the push's error, as it is, several
74
    /// lines included; it should answer neither 401, 403 nor 404, which a
75
    /// client takes for a credential it should change. Called for every
76
    /// part of a push sent in parts (each part's changes are its own),
77
    /// outside the repository's push lock, so a slow vet holds up no
78
    /// other push. The default accepts everything at no cost.
79
    fn vet_push(
80
        &self,
81
        _name: &str,
82
        _repo: &Repo,
83
        _bundle: &oxy_core::protocol::Bundle,
84
        _updates: &[oxy_core::protocol::RefUpdate],
85
        _headers: &HeaderMap,
86
    ) -> Result<(), Failure> {
87
        Ok(())
88
    }
89
4b9d0677 Andy McCall ↶ 90
    /// Add the host's transparency log to a refs answer (spec §14.8): its
91
    /// checkpoint, and each attestation's entry and inclusion proof. A host
92
    /// without a log leaves it as it is.
93
    fn add_log(&self, _name: &str, _refs: &mut oxy_core::protocol::RefsResponse) {}
94
ee130dd9 Andy McCall ↶ 95
    /// Tailor a refs answer to the client asking, by its request headers
96
    /// (its `User-Agent` and the features it declares in `Oxy-Features`),
97
    /// after [`Backend::add_log`]. The default leaves out the tags a client
98
    /// that does not declare `certified-tags` would refuse
99
    /// ([`Repo::refs_for_client`], oxygen#131); a host that overrides it
100
    /// should apply that too ([`tailor_refs`]).
101
    fn adjust_refs(
102
        &self,
103
        _name: &str,
104
        repo: &Repo,
105
        refs: &mut oxy_core::protocol::RefsResponse,
106
        headers: &HeaderMap,
107
    ) {
108
        tailor_refs(repo, refs, headers);
109
    }
110
b63fc7a7 Andy McCall ↶ 111
    /// Whether tag `tag` of `name` may be withdrawn (oxygenhub#301): called
112
    /// for each withdrawal a push carries, before the repository sees it.
113
    /// A host refuses one it depends on (a forge's release names the tag)
114
    /// with the reason, which is that withdrawal's result. The default
115
    /// allows every one; the policy is checked after, as for any push.
116
    fn vet_tag_withdrawal(&self, _name: &str, _tag: &str) -> Result<(), String> {
117
        Ok(())
118
    }
119
f8c69576 Andy McCall ↶ 120
    /// The bookmark of `name` that pushes may never delete (its default),
121
    /// when the host keeps one apart from the repository's own config.
122
    fn default_bookmark(&self, _name: &str) -> Option<String> {
123
        None
124
    }
e02bf0cd Andy McCall ↶ 125
}
126
127
/// A protocol server over any [`Backend`], serialising the pushes to each
128
/// repository. Hosts route requests to [`Server::handle`].
129
pub struct Server {
130
    backend: Box<dyn Backend>,
750251b2 Andy McCall ↶ 131
    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
132
}
133
e02bf0cd Andy McCall ↶ 134
/// An error response: a status and a plain-text body.
135
#[derive(Debug)]
136
pub struct Failure(pub StatusCode, pub String);
750251b2 Andy McCall ↶ 137
138
impl IntoResponse for Failure {
139
    fn into_response(self) -> Response {
140
        (self.0, self.1).into_response()
141
    }
142
}
143
144
impl From<oxy_core::repo::RepoError> for Failure {
145
    fn from(e: oxy_core::repo::RepoError) -> Failure {
be451356 Andy McCall ↶ 146
        use oxy_core::repo::RepoError;
147
        // The server's own storage failing is not the client's fault.
148
        let status = match &e {
149
            RepoError::Io(_) | RepoError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
150
            _ => StatusCode::BAD_REQUEST,
151
        };
152
        Failure(status, e.to_string())
750251b2 Andy McCall ↶ 153
    }
154
}
155
e02bf0cd Andy McCall ↶ 156
/// The wire protocol's endpoints (spec §16.1), named as in the URL.
157
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158
pub enum Endpoint {
159
    Capabilities,
160
    Refs,
161
    Negotiate,
162
    Fetch,
163
    Objects,
164
    Checkpoint,
165
    Shallow,
166
    Push,
167
}
168
169
impl Endpoint {
170
    pub fn parse(name: &str) -> Option<Endpoint> {
171
        Some(match name {
172
            "capabilities" => Endpoint::Capabilities,
173
            "refs" => Endpoint::Refs,
174
            "negotiate" => Endpoint::Negotiate,
175
            "fetch" => Endpoint::Fetch,
176
            "objects" => Endpoint::Objects,
177
            "checkpoint" => Endpoint::Checkpoint,
178
            "shallow" => Endpoint::Shallow,
179
            "push" => Endpoint::Push,
180
            _ => return None,
181
        })
182
    }
183
184
    pub fn name(self) -> &'static str {
185
        match self {
186
            Endpoint::Capabilities => "capabilities",
187
            Endpoint::Refs => "refs",
188
            Endpoint::Negotiate => "negotiate",
189
            Endpoint::Fetch => "fetch",
190
            Endpoint::Objects => "objects",
191
            Endpoint::Checkpoint => "checkpoint",
192
            Endpoint::Shallow => "shallow",
193
            Endpoint::Push => "push",
194
        }
195
    }
196
197
    /// The HTTP method the endpoint answers to.
198
    pub fn method(self) -> Method {
199
        match self {
200
            Endpoint::Capabilities | Endpoint::Refs => Method::GET,
201
            _ => Method::POST,
202
        }
203
    }
204
205
    fn access(self) -> Access {
206
        match self {
207
            Endpoint::Push => Access::Push,
208
            _ => Access::Read,
209
        }
210
    }
211
}
212
750251b2 Andy McCall ↶ 213
fn ok(body: Vec<u8>) -> Response {
214
    ([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response()
215
}
216
e02bf0cd Andy McCall ↶ 217
pub fn valid_repo_name(name: &str) -> bool {
750251b2 Andy McCall ↶ 218
    !name.is_empty()
219
        && !name.starts_with('.')
75167672 Andy McCall 220
        && name
221
            .chars()
222
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
750251b2 Andy McCall ↶ 223
}
224
e02bf0cd Andy McCall ↶ 225
impl Server {
226
    pub fn new(backend: impl Backend) -> Arc<Server> {
75167672 Andy McCall 227
        Arc::new(Server {
228
            backend: Box::new(backend),
229
            locks: Mutex::new(HashMap::new()),
230
        })
e02bf0cd Andy McCall ↶ 231
    }
232
233
    pub fn backend(&self) -> &dyn Backend {
234
        self.backend.as_ref()
235
    }
236
237
    fn lock_for(&self, name: &str) -> Arc<Mutex<()>> {
75167672 Andy McCall 238
        self.locks
239
            .lock()
240
            .unwrap()
241
            .entry(name.to_string())
242
            .or_default()
243
            .clone()
e02bf0cd Andy McCall ↶ 244
    }
245
246
    /// Answer one protocol request for repository `name`. `method` is
247
    /// checked against the endpoint; the body is the raw CBOR request.
75167672 Andy McCall 248
    pub async fn handle(
249
        self: &Arc<Self>,
250
        name: String,
251
        endpoint: Endpoint,
252
        method: &Method,
253
        headers: &HeaderMap,
254
        body: Bytes,
255
    ) -> Response {
e02bf0cd Andy McCall ↶ 256
        if *method != endpoint.method() {
75167672 Andy McCall 257
            return Failure(
258
                StatusCode::METHOD_NOT_ALLOWED,
259
                format!("{} is {}", endpoint.name(), endpoint.method()),
260
            )
261
            .into_response();
e02bf0cd Andy McCall ↶ 262
        }
263
        if let Err(f) = self.backend.authorize(&name, endpoint.access(), headers) {
264
            return f.into_response();
265
        }
75167672 Andy McCall 266
        let claimed = headers
267
            .get(oxy_core::protocol::REPO_ID_HEADER)
268
            .and_then(|v| v.to_str().ok())
269
            .and_then(hex_id);
e02bf0cd Andy McCall ↶ 270
        let server = Arc::clone(self);
76f75250 Andy McCall ↶ 271
        let request_headers = headers.clone();
be451356 Andy McCall ↶ 272
        let repo = name.clone();
be0161a8 Andy McCall ↶ 273
        let result = tokio::task::spawn_blocking(move || {
be451356 Andy McCall ↶ 274
            server.respond(&repo, endpoint, body, claimed, &request_headers)
be0161a8 Andy McCall ↶ 275
        })
276
        .await;
be451356 Andy McCall ↶ 277
        let failure = match result {
278
            Ok(Ok(r)) => return r,
279
            Ok(Err(f)) => f,
280
            Err(e) => Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
281
        };
282
        // Name the operation and repository, in the log and for the client.
283
        let f = Failure(
284
            failure.0,
285
            format!("{} of {name}: {}", endpoint.name(), failure.1),
286
        );
287
        eprintln!("oxy-server: {}: {}", f.0.as_u16(), f.1);
288
        f.into_response()
e02bf0cd Andy McCall ↶ 289
    }
290
05e71ed6 Andy McCall ↶ 291
    fn respond(
292
        &self,
293
        name: &str,
294
        endpoint: Endpoint,
295
        body: Bytes,
296
        claimed: Option<[u8; 16]>,
76f75250 Andy McCall ↶ 297
        headers: &HeaderMap,
05e71ed6 Andy McCall ↶ 298
    ) -> Result<Response, Failure> {
e02bf0cd Andy McCall ↶ 299
        let r = self.backend.open(name)?;
300
        Ok(match endpoint {
301
            Endpoint::Capabilities => ok(wire::encode_capabilities(r.id()?)),
14edbaaf Andy McCall ↶ 302
            Endpoint::Refs => {
303
                let mut refs = r.remote_refs()?;
304
                if let Some(d) = self.backend.default_bookmark(name) {
305
                    refs.default_bookmark = Some(d);
306
                }
4b9d0677 Andy McCall ↶ 307
                self.backend.add_log(name, &mut refs);
ee130dd9 Andy McCall ↶ 308
                self.backend.adjust_refs(name, &r, &mut refs, headers);
14edbaaf Andy McCall ↶ 309
                ok(wire::encode_refs(r.id()?, &refs))
310
            }
e02bf0cd Andy McCall ↶ 311
            Endpoint::Negotiate => {
312
                let (have, want) = wire::decode_negotiate(&body)?;
313
                ok(wire::encode_hashes(&r.negotiate(&have, &want)?))
314
            }
315
            Endpoint::Fetch => {
316
                let (changes, max_blob) = wire::decode_fetch(&body)?;
75167672 Andy McCall 317
                ok(wire::encode_bundle(
318
                    &r.bundle_for_filtered(&changes, max_blob)?,
319
                ))
e02bf0cd Andy McCall ↶ 320
            }
321
            Endpoint::Objects => {
322
                let refs = wire::decode_hashes(&body)?;
75167672 Andy McCall 323
                let bundle = oxy_core::protocol::Bundle {
324
                    objects: r.objects_for(&refs)?,
325
                    ..Default::default()
326
                };
e02bf0cd Andy McCall ↶ 327
                ok(wire::encode_bundle(&bundle))
328
            }
329
            Endpoint::Checkpoint => {
330
                let heads = wire::decode_hashes(&body)?;
331
                let lock = self.lock_for(name);
332
                let _guard = lock.lock().unwrap();
f3811eb2 Andy McCall ↶ 333
                let bundle = r.checkpoint_bundle_for(&heads)?;
334
                let _t = oxy_core::timing::phase("serve: encode the checkpoint");
335
                ok(wire::encode_bundle(&bundle))
e02bf0cd Andy McCall ↶ 336
            }
337
            Endpoint::Shallow => {
338
                let (heads, depth) = wire::decode_shallow(&body)?;
339
                ok(wire::encode_hashes(&r.shallow_changes(&heads, depth)?))
340
            }
341
            Endpoint::Push => {
342
                let (bundle, updates, policy, tags) = wire::decode_push(&body)?;
67a4fe1a Andy McCall ↶ 343
                // The host vets what the push adds before anything is stored
344
                // or bound: a refused push leaves the repository as it was.
345
                self.backend
346
                    .vet_push(name, &r, &bundle, &updates, headers)?;
e02bf0cd Andy McCall ↶ 347
                let lock = self.lock_for(name);
348
                let _guard = lock.lock().unwrap();
75167672 Andy McCall 349
                if r.is_unbound() {
350
                    // The first push binds the repository to the pusher's id (spec §16.1).
351
                    match claimed {
352
                        Some(id) => r.bind_id(id)?,
353
                        None => {
354
                            return Err(Failure(
355
                                StatusCode::BAD_REQUEST,
05e71ed6 Andy McCall ↶ 356
                                format!(
357
                                    "this repository is unbound; push with a client that sends {}",
358
                                    oxy_core::protocol::REPO_ID_HEADER
359
                                ),
75167672 Andy McCall 360
                            ))
361
                        }
362
                    }
f8c69576 Andy McCall ↶ 363
                }
364
                let protected = self.backend.default_bookmark(name);
365
                let (refused, updates): (Vec<_>, Vec<_>) = updates.into_iter().partition(|u| {
366
                    u.new_state.is_zero() && protected.as_deref() == Some(u.name.as_str())
367
                });
b63fc7a7 Andy McCall ↶ 368
                let mut kept_tags = Vec::new();
369
                let mut withdrawals_refused = Vec::new();
370
                for t in tags {
371
                    match t
372
                        .is_withdrawal()
373
                        .then(|| self.backend.vet_tag_withdrawal(name, &t.name))
374
                    {
375
                        Some(Err(reason)) => withdrawals_refused.push((t.name, reason)),
376
                        _ => kept_tags.push(t),
377
                    }
378
                }
379
                let mut report = r.receive_push_report(bundle, updates, policy, kept_tags)?;
cb8b8082 Andy McCall ↶ 380
                // The request named references, all refused here: it was no
381
                // push of objects alone, whatever the repository saw.
b63fc7a7 Andy McCall ↶ 382
                if !refused.is_empty() || !withdrawals_refused.is_empty() {
cb8b8082 Andy McCall ↶ 383
                    report
384
                        .results
385
                        .retain(|r| r.name != oxy_core::repo::OBJECTS_RESULT);
386
                }
f8c69576 Andy McCall ↶ 387
                for u in refused {
388
                    report.results.push(oxy_core::protocol::RefResult {
389
                        name: u.name.clone(),
390
                        ok: false,
391
                        reason: format!(
392
                            "{:?} is this repository's default bookmark and cannot be deleted",
393
                            u.name
394
                        ),
b63fc7a7 Andy McCall ↶ 395
                    });
396
                }
397
                for (tag, reason) in withdrawals_refused {
398
                    report.results.push(oxy_core::protocol::RefResult {
399
                        name: format!("tags/{tag}"),
400
                        ok: false,
401
                        reason,
f8c69576 Andy McCall ↶ 402
                    });
be451356 Andy McCall ↶ 403
                }
404
                for r in report.results.iter().filter(|r| !r.ok) {
405
                    eprintln!(
406
                        "oxy-server: push to {name}: {} refused: {}",
407
                        r.name, r.reason
408
                    );
75167672 Andy McCall 409
                }
76f75250 Andy McCall ↶ 410
                self.backend.after_push(name, &report, headers);
7aa2dc93 Andy McCall ↶ 411
                ok(wire::encode_push_response(&report.results))
e02bf0cd Andy McCall ↶ 412
            }
413
        })
414
    }
415
}
416
417
/// The backend of `oxy serve`: one repository per subdirectory of a root,
418
/// open reads, and pushes gated by a list of bearer tokens.
419
pub struct DirBackend {
420
    pub root: PathBuf,
421
    pub tokens: Vec<String>,
422
}
423
424
impl Backend for DirBackend {
750251b2 Andy McCall ↶ 425
    fn open(&self, name: &str) -> Result<Repo, Failure> {
426
        if !valid_repo_name(name) {
427
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
428
        }
e02bf0cd Andy McCall ↶ 429
        let dir = self.root.join(name);
750251b2 Andy McCall ↶ 430
        if !dir.join(".oxy").is_dir() {
431
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
432
        }
433
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
434
    }
435
8c30d814 Andy McCall ↶ 436
    fn default_bookmark(&self, name: &str) -> Option<String> {
437
        self.open(name).ok().map(|r| r.config.bookmark.clone())
438
    }
439
e02bf0cd Andy McCall ↶ 440
    fn authorize(&self, _name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> {
441
        if access == Access::Read || self.tokens.is_empty() {
750251b2 Andy McCall ↶ 442
            return Ok(());
443
        }
e02bf0cd Andy McCall ↶ 444
        match bearer_token(headers) {
445
            Some(t) if self.tokens.iter().any(|x| x == t) => Ok(()),
75167672 Andy McCall 446
            _ => Err(Failure(
447
                StatusCode::UNAUTHORIZED,
448
                "a valid bearer token is required to push".into(),
449
            )),
750251b2 Andy McCall ↶ 450
        }
451
    }
452
}
453
75167672 Andy McCall 454
fn hex_id(h: &str) -> Option<[u8; 16]> {
455
    let h = h.trim();
456
    if h.len() != 32 {
457
        return None;
458
    }
459
    let mut out = [0u8; 16];
460
    for (i, byte) in out.iter_mut().enumerate() {
461
        *byte = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?;
462
    }
463
    Some(out)
ee130dd9 Andy McCall ↶ 464
}
465
466
/// The features a request declares in its `Oxy-Features` header, as the
467
/// header's text; `None` when it sent none (a client of the 0.5.0 era).
468
pub fn client_features(headers: &HeaderMap) -> Option<&str> {
469
    headers
470
        .get(oxy_core::protocol::FEATURES_HEADER)
471
        .and_then(|v| v.to_str().ok())
472
}
473
474
/// Tailor `refs` of `repo` to the client whose request carried `headers`:
475
/// the shared rule of [`Repo::refs_for_client`], which every host applies.
476
pub fn tailor_refs(repo: &Repo, refs: &mut oxy_core::protocol::RefsResponse, headers: &HeaderMap) {
477
    repo.refs_for_client(refs, client_features(headers));
75167672 Andy McCall 478
}
479
e02bf0cd Andy McCall ↶ 480
/// The token of an `Authorization: Bearer` header, if any.
481
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
75167672 Andy McCall 482
    headers
483
        .get(header::AUTHORIZATION)
484
        .and_then(|v| v.to_str().ok())
485
        .and_then(|v| v.strip_prefix("Bearer "))
486
        .map(str::trim)
b3c5a59a Andy McCall ↶ 487
}
488
75167672 Andy McCall 489
async fn dispatch(
490
    State(server): State<Arc<Server>>,
491
    Path((repo, endpoint)): Path<(String, String)>,
492
    method: Method,
493
    headers: HeaderMap,
494
    body: Bytes,
495
) -> Response {
e02bf0cd Andy McCall ↶ 496
    match Endpoint::parse(&endpoint) {
497
        Some(e) => server.handle(repo, e, &method, &headers, body).await,
498
        None => Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response(),
499
    }
750251b2 Andy McCall ↶ 500
}
501
e02bf0cd Andy McCall ↶ 502
/// The router of `oxy serve`: `/{repo}/oxy/v1/{endpoint}` over a [`DirBackend`].
750251b2 Andy McCall ↶ 503
pub fn router(cfg: ServerConfig) -> Router {
75167672 Andy McCall 504
    let server = Server::new(DirBackend {
505
        root: cfg.root,
506
        tokens: cfg.tokens,
507
    });
508
    Router::new()
509
        .route("/{repo}/oxy/v1/{endpoint}", any(dispatch))
510
        .with_state(server)
750251b2 Andy McCall ↶ 511
}
512
2b5ee371 Andy McCall ↶ 513
/// Serve on an already-bound listener until the process ends; HTTPS when
514
/// `cfg.tls` is set.
750251b2 Andy McCall ↶ 515
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
516
    listener.set_nonblocking(true)?;
2b5ee371 Andy McCall ↶ 517
    match cfg.tls.clone() {
518
        Some(tls) => {
519
            let _ = rustls::crypto::ring::default_provider().install_default();
75167672 Andy McCall 520
            let rustls_cfg =
521
                axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key)
522
                    .await
523
                    .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?;
524
            axum_server::from_tcp_rustls(listener, rustls_cfg)?
525
                .serve(router(cfg).into_make_service())
2b5ee371 Andy McCall ↶ 526
                .await
527
        }
528
        None => {
529
            let listener = tokio::net::TcpListener::from_std(listener)?;
530
            axum::serve(listener, router(cfg)).await
531
        }
532
    }
750251b2 Andy McCall ↶ 533
}
534
535
/// Build a runtime and serve; for the CLI and for tests on a background thread.
536
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
75167672 Andy McCall 537
    let rt = tokio::runtime::Builder::new_multi_thread()
538
        .enable_all()
539
        .build()?;
750251b2 Andy McCall ↶ 540
    rt.block_on(serve(listener, cfg))
541
}