Skip to main content

acme_proxy_core/
routes.rs

1//! The ACME resource paths, profile-relative, and the namespace every profile
2//! is mounted under.
3//!
4//! One definition each, because they are written in three places that must
5//! agree and previously agreed only by inspection: the router that *mounts*
6//! them (`build_router`), the directory that *advertises* them
7//! (`handlers::get_directory`), and `middlewares::nonce`, which singles out
8//! `newNonce`. A directory advertising a path nothing serves is a client that
9//! fails on its very first request, and nothing structural caught it.
10//!
11//! Only the resources with a fixed path are here; the id-bearing ones
12//! (`/acct/{id}`, `/order/{id}/finalize`, …) are never advertised, so they have
13//! exactly one call site and gain nothing from a constant.
14
15/// The directory (RFC 8555 §7.1.1), the one URL a client is configured with.
16pub const DIRECTORY: &str = "/directory";
17/// `newNonce` (RFC 8555 §7.2).
18pub const NEW_NONCE: &str = "/newNonce";
19/// `newAccount` (RFC 8555 §7.3).
20pub const NEW_ACCOUNT: &str = "/newAccount";
21/// `newOrder` (RFC 8555 §7.4).
22pub const NEW_ORDER: &str = "/newOrder";
23/// `revokeCert` (RFC 8555 §7.6).
24pub const REVOKE_CERT: &str = "/revokeCert";
25/// `keyChange` (RFC 8555 §7.3.5).
26pub const KEY_CHANGE: &str = "/keyChange";
27/// RFC 9773 §4.1 has the client append the certID, so the directory
28/// advertises this bare while the router mounts `{id}` under it.
29pub const RENEWAL_INFO: &str = "/renewalInfo";
30/// The local CA's CRL, DER encoded. Not advertised in the directory.
31pub const CRL: &str = "/crl";
32/// The trust anchor a client installs to accept this profile's leaves.
33/// Routed beside [`CRL`] and, like it, deliberately not advertised in the
34/// directory — both are CA infrastructure rather than ACME resources.
35pub const CA_CHAIN: &str = "/ca.pem";
36
37/// The URL namespace every ACME endpoint is mounted under: a profile named
38/// `le` serves `/profile/le/directory`.
39///
40/// Reserved and fixed, which is the point — server-level routes live at the
41/// root and a profile can never collide with one, now or when the next one is
42/// added.
43pub const PROFILE_PREFIX: &str = "/profile";
44
45/// The path profile `name` is mounted at: `/profile/<name>`.
46#[must_use]
47pub fn profile_path(name: &str) -> String {
48    format!("{PROFILE_PREFIX}/{name}")
49}
50
51/// The public base URL of profile `name` under the process's `base_url` —
52/// what every URL a client is handed starts with. One derivation, shared by
53/// the router that serves the profile and the admin output that links to it,
54/// so the two cannot disagree on a trailing slash.
55#[must_use]
56pub fn profile_base_url(base_url: &str, name: &str) -> String {
57    format!("{}{}", base_url.trim_end_matches('/'), profile_path(name))
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn a_profile_base_url_has_one_slash_whatever_the_base_ends_with() {
66        for base in ["https://acme.example", "https://acme.example/"] {
67            assert_eq!(
68                profile_base_url(base, "le"),
69                "https://acme.example/profile/le"
70            );
71        }
72        assert_eq!(profile_path("le"), "/profile/le");
73    }
74}