1use sqlx::Row;
16use sqlx::sqlite::SqliteRow;
17use tracing::debug;
18
19use crate::sqlite::db::Database;
20use crate::sqlite::nonce::now_secs;
21
22pub(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 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 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 #[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 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 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 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 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 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 let placeholders = std::iter::repeat_n("?", profiles.len())
238 .collect::<Vec<_>>()
239 .join(", ");
240 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 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 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 #[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 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 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 #[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 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 #[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}