Skip to main content

acme_proxy/sqlite/
upstream_order.rs

1//! The mapping between a local order and the order the `relay` signer
2//! backend opened for it at the upstream CA.
3//!
4//! Kept in SQLite alongside the rest of the data model rather than in a
5//! sidecar file the way [`crate::signer::local_ca`]'s revocation ledger is:
6//! this state *is* per-order, so it belongs with the orders.
7//!
8//! ## Methods
9//!
10//! - `create`: record a newly opened upstream order, `processing`
11//! - `find_by_order_id`: the mapping for one local order
12//! - `mark_valid` / `mark_invalid`: terminal outcomes
13//! - `list_processing`: rows whose background task was lost to a restart
14
15use sqlx::Row;
16use sqlx::sqlite::SqliteRow;
17use tracing::debug;
18
19use crate::sqlite::db::Database;
20use crate::sqlite::nonce::now_secs;
21
22/// The most rows [`UpstreamOrder::list_processing`] returns in one call.
23///
24/// A restart after a long upstream outage can leave a very large number of
25/// orders `processing`; loading them all at once would spike memory before the
26/// relay semaphore ever got a say in how fast they are worked through.
27pub(crate) const MAX_PROCESSING_BATCH: usize = 500;
28
29#[derive(Debug, Clone)]
30pub struct UpstreamOrder {
31    pub order_id: String,
32    pub upstream_order_url: String,
33    pub upstream_finalize_url: Option<String>,
34    pub upstream_certificate_url: Option<String>,
35    /// The client's CSR, kept so a relay interrupted by a restart can still
36    /// finalize upstream. See the migration for why it cannot be recovered
37    /// any other way.
38    pub csr_der: Vec<u8>,
39    pub status: String,
40    pub error: Option<String>,
41    pub created_at: i64,
42    pub updated_at: i64,
43    /// The finalize request's context, stored by [`UpstreamOrder::set_client`]
44    /// so [`UpstreamOrder::client`] can hand it back to the audit row written
45    /// when the relay settles — long after that request returned. See the
46    /// migration for why it cannot come from anywhere else.
47    pub client_ip: Option<String>,
48    pub client_ptr: Option<String>,
49    pub user_agent: Option<String>,
50    pub request_id: Option<String>,
51}
52
53impl UpstreamOrder {
54    fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
55        Ok(UpstreamOrder {
56            order_id: row.try_get("order_id")?,
57            upstream_order_url: row.try_get("upstream_order_url")?,
58            upstream_finalize_url: row.try_get("upstream_finalize_url")?,
59            upstream_certificate_url: row.try_get("upstream_certificate_url")?,
60            csr_der: row.try_get("csr_der")?,
61            status: row.try_get("status")?,
62            error: row.try_get("error")?,
63            created_at: row.try_get("created_at")?,
64            updated_at: row.try_get("updated_at")?,
65            client_ip: row.try_get("client_ip")?,
66            client_ptr: row.try_get("client_ptr")?,
67            user_agent: row.try_get("user_agent")?,
68            request_id: row.try_get("request_id")?,
69        })
70    }
71
72    /// The stored finalize context, in the shape an audit row takes.
73    #[must_use]
74    pub fn client(&self) -> crate::audit::ClientContext {
75        crate::audit::ClientContext {
76            ip: self.client_ip.clone(),
77            ptr: self.client_ptr.clone(),
78            user_agent: self.user_agent.clone(),
79            request_id: self.request_id.clone(),
80        }
81    }
82
83    /// Records the finalize request's context on an already-created mapping row.
84    ///
85    /// A separate `UPDATE` rather than a parameter of [`UpstreamOrder::create`]
86    /// because that runs inside `SignerBackend::issue`, which is handed a CSR
87    /// and an order id and deliberately knows nothing about HTTP. `post_finalize`
88    /// calls this the moment the backend answers `Processing`.
89    ///
90    /// That leaves a window — between the backend's own `create` and this
91    /// `UPDATE` — in which a relay could theoretically settle and find no
92    /// context. It cannot happen in practice: settling takes at least one
93    /// round trip to the upstream CA, and this statement is a few microseconds
94    /// of local work on the same task. If it ever did, the audit row would
95    /// simply carry no address, which is the same degradation as a resumed
96    /// order and not a wrong answer.
97    pub async fn set_client(
98        order_id: &str,
99        client: &crate::audit::ClientContext,
100        database: &Database,
101    ) -> Result<(), sqlx::Error> {
102        sqlx::query(
103            "UPDATE upstream_orders \
104             SET client_ip = ?, client_ptr = ?, user_agent = ?, request_id = ? \
105             WHERE order_id = ?;",
106        )
107        .bind(&client.ip)
108        .bind(&client.ptr)
109        .bind(&client.user_agent)
110        .bind(&client.request_id)
111        .bind(order_id)
112        .execute(&database.pool)
113        .await?;
114        Ok(())
115    }
116
117    /// Records a newly opened upstream order as `processing`.
118    ///
119    /// Returns `Ok(None)` when a row for `order_id` already exists — the
120    /// primary key rejecting a duplicate is how two concurrent finalize
121    /// requests are stopped from opening two upstream orders for one local
122    /// order. The caller treats that as "a relay is already running".
123    pub async fn create(
124        order_id: &str,
125        upstream_order_url: &str,
126        upstream_finalize_url: Option<&str>,
127        csr_der: &[u8],
128        database: &Database,
129    ) -> Result<Option<UpstreamOrder>, sqlx::Error> {
130        let now = now_secs();
131        let record = UpstreamOrder {
132            order_id: order_id.to_string(),
133            upstream_order_url: upstream_order_url.to_string(),
134            upstream_finalize_url: upstream_finalize_url.map(str::to_string),
135            upstream_certificate_url: None,
136            csr_der: csr_der.to_vec(),
137            status: "processing".to_string(),
138            error: None,
139            created_at: now,
140            updated_at: now,
141            client_ip: None,
142            client_ptr: None,
143            user_agent: None,
144            request_id: None,
145        };
146
147        debug!(event = "db_upstream_order_create_started", outcome = "progress", order_id = ?order_id);
148        let result = sqlx::query(
149            "INSERT OR IGNORE INTO upstream_orders \
150             (order_id, upstream_order_url, upstream_finalize_url, csr_der, status, \
151              created_at, updated_at) \
152             VALUES (?, ?, ?, ?, ?, ?, ?);",
153        )
154        .bind(&record.order_id)
155        .bind(&record.upstream_order_url)
156        .bind(&record.upstream_finalize_url)
157        .bind(&record.csr_der)
158        .bind(&record.status)
159        .bind(record.created_at)
160        .bind(record.updated_at)
161        .execute(&database.pool)
162        .await?;
163
164        if result.rows_affected() == 0 {
165            return Ok(None);
166        }
167        Ok(Some(record))
168    }
169
170    pub async fn find_by_order_id(
171        order_id: &str,
172        database: &Database,
173    ) -> Result<Option<UpstreamOrder>, sqlx::Error> {
174        let row = sqlx::query("SELECT * FROM upstream_orders WHERE order_id = ?;")
175            .bind(order_id)
176            .fetch_optional(&database.pool)
177            .await?;
178        row.map(UpstreamOrder::from_row).transpose()
179    }
180
181    /// Marks the relay finished, recording where the certificate came from.
182    pub async fn mark_valid(
183        order_id: &str,
184        certificate_url: Option<&str>,
185        database: &Database,
186    ) -> Result<(), sqlx::Error> {
187        sqlx::query(
188            "UPDATE upstream_orders \
189             SET status = 'valid', upstream_certificate_url = ?, updated_at = ? \
190             WHERE order_id = ?;",
191        )
192        .bind(certificate_url)
193        .bind(now_secs())
194        .bind(order_id)
195        .execute(&database.pool)
196        .await?;
197        Ok(())
198    }
199
200    /// Marks the relay failed, recording why for an operator to read later —
201    /// the client only ever sees the problem document on the order itself.
202    pub async fn mark_invalid(
203        order_id: &str,
204        error: &str,
205        database: &Database,
206    ) -> Result<(), sqlx::Error> {
207        sqlx::query(
208            "UPDATE upstream_orders SET status = 'invalid', error = ?, updated_at = ? \
209             WHERE order_id = ?;",
210        )
211        .bind(error)
212        .bind(now_secs())
213        .bind(order_id)
214        .execute(&database.pool)
215        .await?;
216        Ok(())
217    }
218
219    /// Rows still `processing`, oldest first — relays whose in-memory task
220    /// died with the process and need one respawned at startup.
221    ///
222    /// Restricted to the `profiles` the calling backend actually serves, via
223    /// the owning order's own `profile`: several endpoints may relay to
224    /// several different upstreams, and resuming another one's order would
225    /// drive it against the wrong CA. An empty `profiles` therefore matches
226    /// nothing, which is the safe reading of "this backend serves nothing".
227    pub async fn list_processing(
228        profiles: &[String],
229        database: &Database,
230    ) -> Result<Vec<UpstreamOrder>, sqlx::Error> {
231        if profiles.is_empty() {
232            return Ok(Vec::new());
233        }
234
235        // sqlx has no array binding for SQLite, so the IN list is built from
236        // one placeholder per profile — never from the values themselves.
237        let placeholders = std::iter::repeat_n("?", profiles.len())
238            .collect::<Vec<_>>()
239            .join(", ");
240        // Bounded so a pathological backlog is not loaded into memory whole;
241        // the oldest are taken first, and a later restart picks up the rest.
242        let limit = MAX_PROCESSING_BATCH;
243        let sql = format!(
244            "SELECT u.* FROM upstream_orders u \
245             JOIN orders o ON o.id = u.order_id \
246             WHERE u.status = 'processing' AND o.profile IN ({placeholders}) \
247             ORDER BY u.created_at ASC LIMIT {limit};"
248        );
249
250        // `AssertSqlSafe` because sqlx refuses a non-`'static` query string
251        // outright: the only runtime part of this one is the count of `?`
252        // placeholders, never a value.
253        let mut query = sqlx::query(sqlx::AssertSqlSafe(sql));
254        for profile in profiles {
255            query = query.bind(profile);
256        }
257        let rows = query.fetch_all(&database.pool).await?;
258        rows.into_iter().map(UpstreamOrder::from_row).collect()
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::audit::ClientContext;
266    use crate::sqlite::account::Account;
267    use crate::sqlite::order::{Identifier, Order};
268    use std::sync::Arc;
269
270    /// `upstream_orders.order_id` is a foreign key, so a real order has to
271    /// exist behind every row these tests create.
272    async fn order(database: &Database) -> Order {
273        let (account, _) = Account::find_or_create(
274            "default",
275            uuid::Uuid::new_v4().as_bytes(),
276            Vec::new(),
277            &ClientContext::default(),
278            database,
279        )
280        .await
281        .unwrap();
282        Order::create(
283            "default",
284            &account.id,
285            vec![Identifier::dns("example.com")],
286            now_secs() + 3600,
287            None,
288            None,
289            database,
290        )
291        .await
292        .unwrap()
293    }
294
295    async fn database() -> Arc<Database> {
296        Arc::new(Database::connect_in_memory().await.unwrap())
297    }
298
299    /// The relay settles minutes after the finalize request returned, from a
300    /// task with no request in scope — so the context it audits has to come
301    /// back off this row. A row that never had one hands back an empty context
302    /// rather than a half-filled one.
303    #[tokio::test]
304    async fn the_finalize_context_is_stored_and_handed_back_for_the_audit_row() {
305        let database = database().await;
306        let order = order(&database).await;
307
308        UpstreamOrder::create(
309            &order.id,
310            "https://up.example/order/1",
311            None,
312            b"csr",
313            &database,
314        )
315        .await
316        .unwrap()
317        .unwrap();
318
319        // Before `post_finalize` stores anything: empty, not partial.
320        let mapping = UpstreamOrder::find_by_order_id(&order.id, &database)
321            .await
322            .unwrap()
323            .unwrap();
324        assert_eq!(mapping.client(), ClientContext::default());
325
326        let client = ClientContext {
327            ip: Some("203.0.113.7".to_string()),
328            ptr: Some("host.example.com".to_string()),
329            user_agent: Some("lego".to_string()),
330            request_id: Some("req-1".to_string()),
331        };
332        UpstreamOrder::set_client(&order.id, &client, &database)
333            .await
334            .unwrap();
335
336        let mapping = UpstreamOrder::find_by_order_id(&order.id, &database)
337            .await
338            .unwrap()
339            .unwrap();
340        assert_eq!(mapping.client(), client);
341
342        // Setting it for an order with no mapping row is a no-op, not an error:
343        // `post_finalize` calls this straight after the backend answered, and a
344        // backend that answered `Processing` without creating a row would be a
345        // different bug that must not surface here as a 500.
346        UpstreamOrder::set_client("no-such-order", &client, &database)
347            .await
348            .unwrap();
349    }
350
351    #[tokio::test]
352    async fn create_then_find_round_trips() {
353        let db = database().await;
354        let order = order(&db).await;
355
356        let created = UpstreamOrder::create(
357            &order.id,
358            "https://up.example/order/1",
359            Some("https://up.example/order/1/finalize"),
360            b"csr-bytes",
361            &db,
362        )
363        .await
364        .unwrap()
365        .expect("a first insert must succeed");
366        assert_eq!(created.status, "processing");
367
368        let found = UpstreamOrder::find_by_order_id(&order.id, &db)
369            .await
370            .unwrap()
371            .expect("the row just written must be found");
372        assert_eq!(found.upstream_order_url, "https://up.example/order/1");
373        assert_eq!(
374            found.upstream_finalize_url.as_deref(),
375            Some("https://up.example/order/1/finalize")
376        );
377        assert!(found.upstream_certificate_url.is_none());
378    }
379
380    /// The concurrency guard: the primary key must refuse a second relay for
381    /// the same order rather than opening a duplicate upstream order.
382    #[tokio::test]
383    async fn a_second_create_for_one_order_is_refused() {
384        let db = database().await;
385        let order = order(&db).await;
386
387        assert!(
388            UpstreamOrder::create(&order.id, "https://up.example/order/1", None, b"csr", &db)
389                .await
390                .unwrap()
391                .is_some()
392        );
393        assert!(
394            UpstreamOrder::create(&order.id, "https://up.example/order/2", None, b"csr", &db)
395                .await
396                .unwrap()
397                .is_none(),
398            "a duplicate must be reported as already-in-flight, not inserted"
399        );
400
401        // And the original row is untouched by the refused attempt.
402        let found = UpstreamOrder::find_by_order_id(&order.id, &db)
403            .await
404            .unwrap()
405            .unwrap();
406        assert_eq!(found.upstream_order_url, "https://up.example/order/1");
407    }
408
409    #[tokio::test]
410    async fn mark_valid_records_the_certificate_url() {
411        let db = database().await;
412        let order = order(&db).await;
413        UpstreamOrder::create(&order.id, "https://up.example/order/1", None, b"csr", &db)
414            .await
415            .unwrap();
416
417        UpstreamOrder::mark_valid(&order.id, Some("https://up.example/cert/1"), &db)
418            .await
419            .unwrap();
420
421        let found = UpstreamOrder::find_by_order_id(&order.id, &db)
422            .await
423            .unwrap()
424            .unwrap();
425        assert_eq!(found.status, "valid");
426        assert_eq!(
427            found.upstream_certificate_url.as_deref(),
428            Some("https://up.example/cert/1")
429        );
430    }
431
432    #[tokio::test]
433    async fn mark_invalid_records_the_reason() {
434        let db = database().await;
435        let order = order(&db).await;
436        UpstreamOrder::create(&order.id, "https://up.example/order/1", None, b"csr", &db)
437            .await
438            .unwrap();
439
440        UpstreamOrder::mark_invalid(&order.id, "upstream said no", &db)
441            .await
442            .unwrap();
443
444        let found = UpstreamOrder::find_by_order_id(&order.id, &db)
445            .await
446            .unwrap()
447            .unwrap();
448        assert_eq!(found.status, "invalid");
449        assert_eq!(found.error.as_deref(), Some("upstream said no"));
450    }
451
452    /// Only `processing` rows come back — a finished relay must not be
453    /// respawned at the next startup.
454    #[tokio::test]
455    async fn list_processing_skips_settled_rows() {
456        let db = database().await;
457        let still_running = order(&db).await;
458        let finished = order(&db).await;
459
460        UpstreamOrder::create(&still_running.id, "https://up.example/a", None, b"csr", &db)
461            .await
462            .unwrap();
463        UpstreamOrder::create(&finished.id, "https://up.example/b", None, b"csr", &db)
464            .await
465            .unwrap();
466        UpstreamOrder::mark_valid(&finished.id, None, &db)
467            .await
468            .unwrap();
469
470        let processing = UpstreamOrder::list_processing(&["default".to_string()], &db)
471            .await
472            .unwrap();
473        assert_eq!(processing.len(), 1);
474        assert_eq!(processing[0].order_id, still_running.id);
475    }
476
477    #[tokio::test]
478    async fn find_by_order_id_is_none_for_an_unknown_order() {
479        let db = database().await;
480        assert!(
481            UpstreamOrder::find_by_order_id("nope", &db)
482                .await
483                .unwrap()
484                .is_none()
485        );
486    }
487}