1use 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
23pub(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 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 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 #[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 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 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 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 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 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 let placeholders = std::iter::repeat_n("?", profiles.len())
254 .collect::<Vec<_>>()
255 .join(", ");
256 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 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 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 #[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 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 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 #[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 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 #[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}