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