1use crate::sql::Row;
16use tracing::debug;
17use uuid::Uuid;
18
19use crate::db::Database;
20use crate::nonce::now_secs;
21use crate::status::{self, OrderStatus, UpstreamOrderStatus};
22use acme_proxy_core::identifier::Identifier;
23
24pub const MAX_PROCESSING_BATCH: usize = 500;
30
31#[derive(Debug, Clone)]
35pub struct UpstreamOrder {
36 pub order_id: Uuid,
37 pub upstream_order_url: String,
38 pub upstream_finalize_url: Option<String>,
39 pub upstream_certificate_url: Option<String>,
40 pub csr_der: Vec<u8>,
44 pub status: String,
45 pub error: Option<String>,
46 pub created_at: i64,
47 pub updated_at: i64,
48 pub client_ip: Option<String>,
53 pub client_ptr: Option<String>,
54 pub user_agent: Option<String>,
55 pub request_id: Option<String>,
56}
57
58impl UpstreamOrder {
59 fn from_row(row: Row) -> Result<Self, sqlx::Error> {
60 Ok(UpstreamOrder {
61 order_id: row.try_get("order_id")?,
62 upstream_order_url: row.try_get("upstream_order_url")?,
63 upstream_finalize_url: row.try_get("upstream_finalize_url")?,
64 upstream_certificate_url: row.try_get("upstream_certificate_url")?,
65 csr_der: row.try_get("csr_der")?,
66 status: row.try_get("status")?,
67 error: row.try_get("error")?,
68 created_at: row.try_get("created_at")?,
69 updated_at: row.try_get("updated_at")?,
70 client_ip: row.try_get("client_ip")?,
71 client_ptr: row.try_get("client_ptr")?,
72 user_agent: row.try_get("user_agent")?,
73 request_id: row.try_get("request_id")?,
74 })
75 }
76
77 #[must_use]
79 pub fn client(&self) -> acme_proxy_core::audit::ClientContext {
80 acme_proxy_core::audit::ClientContext {
81 ip: self.client_ip.clone(),
82 ptr: self.client_ptr.clone(),
83 user_agent: self.user_agent.clone(),
84 request_id: self.request_id.clone(),
85 }
86 }
87
88 pub async fn set_client(
104 order_id: &str,
105 client: &acme_proxy_core::audit::ClientContext,
106 database: &Database,
107 ) -> Result<(), sqlx::Error> {
108 let Some(order_id) = crate::id::parse(order_id) else {
109 return Ok(());
110 };
111 crate::sql::query(
112 "UPDATE upstream_orders \
113 SET client_ip = ?, client_ptr = ?, user_agent = ?, request_id = ? \
114 WHERE order_id = ?;",
115 )
116 .bind(&client.ip)
117 .bind(&client.ptr)
118 .bind(&client.user_agent)
119 .bind(&client.request_id)
120 .bind(order_id)
121 .execute(database)
122 .await?;
123 Ok(())
124 }
125
126 pub async fn create(
133 order_id: &str,
134 upstream_order_url: &str,
135 upstream_finalize_url: Option<&str>,
136 csr_der: &[u8],
137 database: &Database,
138 ) -> Result<Option<UpstreamOrder>, sqlx::Error> {
139 let Some(order_id) = crate::id::parse(order_id) else {
140 return Ok(None);
141 };
142 let now = now_secs();
143 let record = UpstreamOrder {
144 order_id,
145 upstream_order_url: upstream_order_url.to_string(),
146 upstream_finalize_url: upstream_finalize_url.map(str::to_string),
147 upstream_certificate_url: None,
148 csr_der: csr_der.to_vec(),
149 status: "processing".to_string(),
150 error: None,
151 created_at: now,
152 updated_at: now,
153 client_ip: None,
154 client_ptr: None,
155 user_agent: None,
156 request_id: None,
157 };
158
159 debug!(event = "db_upstream_order_create_started", outcome = "progress", order_id = ?order_id);
160 let result = crate::sql::query(
161 "INSERT INTO upstream_orders \
162 (order_id, upstream_order_url, upstream_finalize_url, csr_der, status, \
163 created_at, updated_at) \
164 VALUES (?, ?, ?, ?, ?, ?, ?) \
165 ON CONFLICT DO NOTHING;",
166 )
167 .bind(record.order_id)
168 .bind(&record.upstream_order_url)
169 .bind(&record.upstream_finalize_url)
170 .bind(&record.csr_der)
171 .bind(&record.status)
172 .bind(record.created_at)
173 .bind(record.updated_at)
174 .execute(database)
175 .await?;
176
177 if result.rows_affected() == 0 {
178 return Ok(None);
179 }
180 Ok(Some(record))
181 }
182
183 pub async fn find_by_order_id(
186 order_id: &str,
187 database: &Database,
188 ) -> Result<Option<UpstreamOrder>, sqlx::Error> {
189 let Some(order_id) = crate::id::parse(order_id) else {
190 return Ok(None);
191 };
192 let row = crate::sql::query("SELECT * FROM upstream_orders WHERE order_id = ?;")
193 .bind(order_id)
194 .fetch_optional(database)
195 .await?;
196 row.map(UpstreamOrder::from_row).transpose()
197 }
198
199 pub async fn mark_valid(
201 order_id: &str,
202 certificate_url: Option<&str>,
203 database: &Database,
204 ) -> Result<(), sqlx::Error> {
205 let Some(order_id) = crate::id::parse(order_id) else {
206 return Ok(());
207 };
208 crate::sql::query(
209 "UPDATE upstream_orders \
210 SET status = 'valid', upstream_certificate_url = ?, updated_at = ? \
211 WHERE order_id = ?;",
212 )
213 .bind(certificate_url)
214 .bind(now_secs())
215 .bind(order_id)
216 .execute(database)
217 .await?;
218 Ok(())
219 }
220
221 pub async fn mark_invalid(
224 order_id: &str,
225 error: &str,
226 database: &Database,
227 ) -> Result<(), sqlx::Error> {
228 let Some(order_id) = crate::id::parse(order_id) else {
229 return Ok(());
230 };
231 crate::sql::query(
232 "UPDATE upstream_orders SET status = 'invalid', error = ?, updated_at = ? \
233 WHERE order_id = ?;",
234 )
235 .bind(error)
236 .bind(now_secs())
237 .bind(order_id)
238 .execute(database)
239 .await?;
240 Ok(())
241 }
242
243 pub async fn list_processing(
252 profiles: &[String],
253 database: &Database,
254 ) -> Result<Vec<UpstreamOrder>, sqlx::Error> {
255 if profiles.is_empty() {
256 return Ok(Vec::new());
257 }
258
259 let placeholders = std::iter::repeat_n("?", profiles.len())
262 .collect::<Vec<_>>()
263 .join(", ");
264 let limit = MAX_PROCESSING_BATCH;
267 let sql = format!(
268 "SELECT u.* FROM upstream_orders u \
269 JOIN orders o ON o.id = u.order_id \
270 WHERE u.status = 'processing' AND o.profile IN ({placeholders}) \
271 ORDER BY u.created_at ASC LIMIT {limit};"
272 );
273
274 let mut query = crate::sql::query(sqlx::AssertSqlSafe(sql));
278 for profile in profiles {
279 query = query.bind(profile);
280 }
281 let rows = query.fetch_all(database).await?;
282 rows.into_iter().map(UpstreamOrder::from_row).collect()
283 }
284}
285
286#[derive(Debug, Clone)]
295pub struct UpstreamOrderRow {
296 pub order_id: Uuid,
297 pub upstream_order_url: String,
298 pub upstream_finalize_url: Option<String>,
299 pub upstream_certificate_url: Option<String>,
300 pub status: String,
301 pub error: Option<String>,
304 pub created_at: i64,
305 pub updated_at: i64,
306 pub client_ip: Option<String>,
307 pub client_ptr: Option<String>,
308 pub user_agent: Option<String>,
310 pub request_id: Option<String>,
311 pub profile: String,
313 pub account_id: Uuid,
314 pub identifiers: Vec<Identifier>,
315 pub local_status: OrderStatus,
316 pub local_expires: i64,
317}
318
319impl UpstreamOrderRow {
320 const COLUMNS: &'static str = "u.order_id, u.upstream_order_url, u.upstream_finalize_url, \
325 u.upstream_certificate_url, u.status, u.error, u.created_at, u.updated_at, \
326 u.client_ip, u.client_ptr, u.user_agent, u.request_id, \
327 o.profile, o.account_id, o.identifiers, o.status AS local_status, \
328 o.expires AS local_expires";
329
330 fn from_joined_row(row: Row) -> Result<Self, sqlx::Error> {
331 let identifiers_json: String = row.try_get("identifiers")?;
332 let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
333 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
334
335 Ok(UpstreamOrderRow {
336 order_id: row.try_get("order_id")?,
337 upstream_order_url: row.try_get("upstream_order_url")?,
338 upstream_finalize_url: row.try_get("upstream_finalize_url")?,
339 upstream_certificate_url: row.try_get("upstream_certificate_url")?,
340 status: row.try_get("status")?,
341 error: row.try_get("error")?,
342 created_at: row.try_get("created_at")?,
343 updated_at: row.try_get("updated_at")?,
344 client_ip: row.try_get("client_ip")?,
345 client_ptr: row.try_get("client_ptr")?,
346 user_agent: row.try_get("user_agent")?,
347 request_id: row.try_get("request_id")?,
348 profile: row.try_get("profile")?,
349 account_id: row.try_get("account_id")?,
350 identifiers,
351 local_status: status::from_column(row.try_get::<String>("local_status")?.as_str())?,
352 local_expires: row.try_get("local_expires")?,
353 })
354 }
355
356 #[must_use]
359 pub fn client(&self) -> acme_proxy_core::audit::ClientContext {
360 acme_proxy_core::audit::ClientContext {
361 ip: self.client_ip.clone(),
362 ptr: self.client_ptr.clone(),
363 user_agent: self.user_agent.clone(),
364 request_id: self.request_id.clone(),
365 }
366 }
367}
368
369#[derive(Debug, Clone, Default)]
373pub struct UpstreamOrderQuery {
374 pub profile: Option<String>,
375 pub status: Option<UpstreamOrderStatus>,
376 pub limit: i64,
377 pub offset: i64,
378}
379
380impl UpstreamOrderQuery {
381 fn push_predicates(&self, builder: &mut crate::sql::Builder) {
384 crate::query::push_equalities(
385 builder,
386 crate::query::WHERE,
387 &[
388 ("o.profile = ", self.profile.as_deref()),
389 ("u.status = ", self.status.map(UpstreamOrderStatus::as_str)),
390 ],
391 );
392 }
393}
394
395impl UpstreamOrder {
396 pub async fn search(
410 query: &UpstreamOrderQuery,
411 database: &Database,
412 ) -> Result<(Vec<UpstreamOrderRow>, i64), sqlx::Error> {
413 debug!(
414 event = "db_upstream_order_search_started",
415 outcome = "progress",
416 profile = ?query.profile,
417 status = ?query.status,
418 limit = query.limit,
419 offset = query.offset,
420 );
421
422 let mut page = crate::sql::Builder::new(
423 database.dialect(),
424 format!(
425 "SELECT {} FROM upstream_orders u JOIN orders o ON o.id = u.order_id",
426 UpstreamOrderRow::COLUMNS
427 ),
428 );
429 query.push_predicates(&mut page);
430 page.push(" ORDER BY u.created_at DESC, u.order_id DESC LIMIT ");
431 page.push_bind(query.limit);
432 page.push(" OFFSET ");
433 page.push_bind(query.offset);
434
435 let rows = page.build().fetch_all(database).await?;
436 let items: Vec<UpstreamOrderRow> = rows
437 .into_iter()
438 .map(UpstreamOrderRow::from_joined_row)
439 .collect::<Result<_, _>>()?;
440
441 let mut count = crate::sql::Builder::new(
442 database.dialect(),
443 "SELECT COUNT(*) FROM upstream_orders u JOIN orders o ON o.id = u.order_id",
444 );
445 query.push_predicates(&mut count);
446 let total: i64 = count.build().fetch_one(database).await?.try_get::<i64>(0)?;
447
448 Ok((items, total))
449 }
450
451 pub async fn find_row_by_order_id(
455 order_id: &str,
456 database: &Database,
457 ) -> Result<Option<UpstreamOrderRow>, sqlx::Error> {
458 let Some(order_id) = crate::id::parse(order_id) else {
459 return Ok(None);
460 };
461 let mut query = crate::sql::Builder::new(
462 database.dialect(),
463 format!(
464 "SELECT {} FROM upstream_orders u JOIN orders o ON o.id = u.order_id \
465 WHERE u.order_id = ",
466 UpstreamOrderRow::COLUMNS
467 ),
468 );
469 query.push_bind(order_id);
470 let row = query.build().fetch_optional(database).await?;
471 row.map(UpstreamOrderRow::from_joined_row).transpose()
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::account::Account;
479 use crate::order::Order;
480 use acme_proxy_core::audit::ClientContext;
481 use acme_proxy_core::identifier::Identifier;
482 use std::sync::Arc;
483
484 async fn order(database: &Database) -> Order {
487 let (account, _) = Account::find_or_create(
488 "default",
489 &acme_proxy_core::random::random_bytes::<16>(),
490 Vec::new(),
491 &ClientContext::default(),
492 database,
493 )
494 .await
495 .unwrap();
496 Order::create(
497 "default",
498 account.id,
499 vec![Identifier::dns("example.com")],
500 now_secs() + 3600,
501 None,
502 None,
503 database,
504 )
505 .await
506 .unwrap()
507 }
508
509 async fn database() -> Arc<Database> {
510 Arc::new(Database::connect_for_test().await.unwrap())
511 }
512
513 #[tokio::test]
518 async fn the_finalize_context_is_stored_and_handed_back_for_the_audit_row() {
519 let database = database().await;
520 let order = order(&database).await;
521
522 UpstreamOrder::create(
523 order.id.to_string().as_str(),
524 "https://up.example/order/1",
525 None,
526 b"csr",
527 &database,
528 )
529 .await
530 .unwrap()
531 .unwrap();
532
533 let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &database)
535 .await
536 .unwrap()
537 .unwrap();
538 assert_eq!(mapping.client(), ClientContext::default());
539
540 let client = ClientContext {
541 ip: Some("203.0.113.7".to_string()),
542 ptr: Some("host.example.com".to_string()),
543 user_agent: Some("lego".to_string()),
544 request_id: Some("req-1".to_string()),
545 };
546 UpstreamOrder::set_client(order.id.to_string().as_str(), &client, &database)
547 .await
548 .unwrap();
549
550 let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &database)
551 .await
552 .unwrap()
553 .unwrap();
554 assert_eq!(mapping.client(), client);
555
556 UpstreamOrder::set_client("no-such-order", &client, &database)
561 .await
562 .unwrap();
563 }
564
565 #[tokio::test]
566 async fn create_then_find_round_trips() {
567 let db = database().await;
568 let order = order(&db).await;
569
570 let created = UpstreamOrder::create(
571 order.id.to_string().as_str(),
572 "https://up.example/order/1",
573 Some("https://up.example/order/1/finalize"),
574 b"csr-bytes",
575 &db,
576 )
577 .await
578 .unwrap()
579 .expect("a first insert must succeed");
580 assert_eq!(created.status, "processing");
581
582 let found = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
583 .await
584 .unwrap()
585 .expect("the row just written must be found");
586 assert_eq!(found.upstream_order_url, "https://up.example/order/1");
587 assert_eq!(
588 found.upstream_finalize_url.as_deref(),
589 Some("https://up.example/order/1/finalize")
590 );
591 assert!(found.upstream_certificate_url.is_none());
592 }
593
594 #[tokio::test]
597 async fn a_second_create_for_one_order_is_refused() {
598 let db = database().await;
599 let order = order(&db).await;
600
601 assert!(
602 UpstreamOrder::create(
603 order.id.to_string().as_str(),
604 "https://up.example/order/1",
605 None,
606 b"csr",
607 &db
608 )
609 .await
610 .unwrap()
611 .is_some()
612 );
613 assert!(
614 UpstreamOrder::create(
615 order.id.to_string().as_str(),
616 "https://up.example/order/2",
617 None,
618 b"csr",
619 &db
620 )
621 .await
622 .unwrap()
623 .is_none(),
624 "a duplicate must be reported as already-in-flight, not inserted"
625 );
626
627 let found = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
629 .await
630 .unwrap()
631 .unwrap();
632 assert_eq!(found.upstream_order_url, "https://up.example/order/1");
633 }
634
635 #[tokio::test]
636 async fn mark_valid_records_the_certificate_url() {
637 let db = database().await;
638 let order = order(&db).await;
639 UpstreamOrder::create(
640 order.id.to_string().as_str(),
641 "https://up.example/order/1",
642 None,
643 b"csr",
644 &db,
645 )
646 .await
647 .unwrap();
648
649 UpstreamOrder::mark_valid(
650 order.id.to_string().as_str(),
651 Some("https://up.example/cert/1"),
652 &db,
653 )
654 .await
655 .unwrap();
656
657 let found = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
658 .await
659 .unwrap()
660 .unwrap();
661 assert_eq!(found.status, "valid");
662 assert_eq!(
663 found.upstream_certificate_url.as_deref(),
664 Some("https://up.example/cert/1")
665 );
666 }
667
668 #[tokio::test]
669 async fn mark_invalid_records_the_reason() {
670 let db = database().await;
671 let order = order(&db).await;
672 UpstreamOrder::create(
673 order.id.to_string().as_str(),
674 "https://up.example/order/1",
675 None,
676 b"csr",
677 &db,
678 )
679 .await
680 .unwrap();
681
682 UpstreamOrder::mark_invalid(order.id.to_string().as_str(), "upstream said no", &db)
683 .await
684 .unwrap();
685
686 let found = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
687 .await
688 .unwrap()
689 .unwrap();
690 assert_eq!(found.status, "invalid");
691 assert_eq!(found.error.as_deref(), Some("upstream said no"));
692 }
693
694 #[tokio::test]
697 async fn list_processing_skips_settled_rows() {
698 let db = database().await;
699 let still_running = order(&db).await;
700 let finished = order(&db).await;
701
702 UpstreamOrder::create(
703 still_running.id.to_string().as_str(),
704 "https://up.example/a",
705 None,
706 b"csr",
707 &db,
708 )
709 .await
710 .unwrap();
711 UpstreamOrder::create(
712 finished.id.to_string().as_str(),
713 "https://up.example/b",
714 None,
715 b"csr",
716 &db,
717 )
718 .await
719 .unwrap();
720 UpstreamOrder::mark_valid(finished.id.to_string().as_str(), None, &db)
721 .await
722 .unwrap();
723
724 let processing = UpstreamOrder::list_processing(&["default".to_string()], &db)
725 .await
726 .unwrap();
727 assert_eq!(processing.len(), 1);
728 assert_eq!(processing[0].order_id, still_running.id);
729 }
730
731 #[tokio::test]
732 async fn find_by_order_id_is_none_for_an_unknown_order() {
733 let db = database().await;
734 assert!(
735 UpstreamOrder::find_by_order_id("nope", &db)
736 .await
737 .unwrap()
738 .is_none()
739 );
740 }
741
742 use crate::status::{OrderStatus, UpstreamOrderStatus};
745
746 async fn order_on(profile: &str, names: &[&str], database: &Database) -> Order {
747 let (account, _) = Account::find_or_create(
748 profile,
749 &acme_proxy_core::random::random_bytes::<16>(),
750 Vec::new(),
751 &ClientContext::default(),
752 database,
753 )
754 .await
755 .unwrap();
756 Order::create(
757 profile,
758 account.id,
759 names.iter().map(|n| Identifier::dns(*n)).collect(),
760 now_secs() + 3600,
761 None,
762 None,
763 database,
764 )
765 .await
766 .unwrap()
767 }
768
769 #[tokio::test]
770 async fn search_joins_the_local_order_for_identifiers_and_status() {
771 let db = database().await;
772 let ord = order_on("default", &["a.example.com", "b.example.com"], &db).await;
773 UpstreamOrder::create(
774 ord.id.to_string().as_str(),
775 "https://up.example/order/1",
776 Some("https://up.example/order/1/finalize"),
777 b"secret-csr-bytes",
778 &db,
779 )
780 .await
781 .unwrap();
782
783 let (rows, total) = UpstreamOrder::search(
784 &UpstreamOrderQuery {
785 limit: 50,
786 ..UpstreamOrderQuery::default()
787 },
788 &db,
789 )
790 .await
791 .unwrap();
792 assert_eq!(total, 1);
793 let row = &rows[0];
794 assert_eq!(row.order_id, ord.id);
795 assert_eq!(row.profile, "default");
796 assert_eq!(row.local_status, OrderStatus::Pending);
797 let names: Vec<&str> = row.identifiers.iter().map(|i| i.value.as_str()).collect();
798 assert_eq!(names, vec!["a.example.com", "b.example.com"]);
799 }
802
803 #[tokio::test]
804 async fn search_filters_profile_and_status_together_and_pages() {
805 let db = database().await;
806 let a = order_on("default", &["a.example.com"], &db).await;
807 let b = order_on("default", &["b.example.com"], &db).await;
808 let c = order_on("other", &["c.example.com"], &db).await;
809 for ord in [&a, &b, &c] {
810 UpstreamOrder::create(
811 ord.id.to_string().as_str(),
812 "https://up.example/o",
813 None,
814 b"csr",
815 &db,
816 )
817 .await
818 .unwrap();
819 }
820 UpstreamOrder::mark_invalid(b.id.to_string().as_str(), "upstream said no", &db)
821 .await
822 .unwrap();
823
824 let (rows, total) = UpstreamOrder::search(
826 &UpstreamOrderQuery {
827 profile: Some("default".to_string()),
828 limit: 50,
829 ..UpstreamOrderQuery::default()
830 },
831 &db,
832 )
833 .await
834 .unwrap();
835 assert_eq!(total, 2);
836 assert_eq!(rows.len(), 2);
837
838 let (rows, total) = UpstreamOrder::search(
840 &UpstreamOrderQuery {
841 profile: Some("default".to_string()),
842 status: Some(UpstreamOrderStatus::Invalid),
843 limit: 50,
844 offset: 0,
845 },
846 &db,
847 )
848 .await
849 .unwrap();
850 assert_eq!(total, 1);
851 assert_eq!(rows[0].order_id, b.id);
852 assert_eq!(rows[0].error.as_deref(), Some("upstream said no"));
853
854 let (rows, total) = UpstreamOrder::search(
856 &UpstreamOrderQuery {
857 profile: Some("' OR 1=1 --".to_string()),
858 limit: 50,
859 ..UpstreamOrderQuery::default()
860 },
861 &db,
862 )
863 .await
864 .unwrap();
865 assert_eq!(total, 0);
866 assert!(rows.is_empty());
867
868 let mut seen = std::collections::BTreeSet::new();
870 for offset in 0..3 {
871 let (rows, total) = UpstreamOrder::search(
872 &UpstreamOrderQuery {
873 limit: 1,
874 offset,
875 ..UpstreamOrderQuery::default()
876 },
877 &db,
878 )
879 .await
880 .unwrap();
881 assert_eq!(total, 3);
882 assert!(seen.insert(rows[0].order_id));
883 }
884 assert_eq!(seen.len(), 3);
885 }
886
887 #[tokio::test]
888 async fn find_row_by_order_id_is_none_for_junk_and_for_an_unrelayed_order() {
889 let db = database().await;
890 assert!(
891 UpstreamOrder::find_row_by_order_id("nope", &db)
892 .await
893 .unwrap()
894 .is_none()
895 );
896 let ord = order_on("default", &["a.example.com"], &db).await;
897 assert!(
899 UpstreamOrder::find_row_by_order_id(ord.id.to_string().as_str(), &db)
900 .await
901 .unwrap()
902 .is_none()
903 );
904 }
905}