From f6424c7d84b4dbd046e5fd4083b8ac9098a45024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Mon, 12 Aug 2024 00:31:05 +0300 Subject: [PATCH 1/7] feat: disallow unsupported file extensions --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/invoices.rs | 28 +++++++++++++++------------- src/error.rs | 5 ++++- src/mailgun/invoices.rs | 14 -------------- templates/invoice.typ | 2 +- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 904ec41..c3e8c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1507,6 +1507,7 @@ dependencies = [ "futures", "garde", "lopdf", + "regex", "reqwest", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 8dbdfd9..f46d31f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ fontdb = { version = "0.17.0", optional = true } typst-assets = { version = "0.11.1", features = ["fonts"] } reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"] } lopdf = "0.33.0" +regex = "1.10.6" [dev-dependencies] tower = { version = "0.4.13", features = ["util"] } diff --git a/src/api/invoices.rs b/src/api/invoices.rs index 14c265c..d082b2a 100644 --- a/src/api/invoices.rs +++ b/src/api/invoices.rs @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use crate::error::Error; use crate::mailgun::MailgunClient; use axum::{async_trait, body::Bytes, http::StatusCode, Json}; @@ -6,10 +8,13 @@ use axum_typed_multipart::{ }; use axum_valid::Garde; use futures::stream::Stream; -use futures::stream::{self, TryStreamExt}; use garde::Validate; +use regex::Regex; use serde_derive::{Deserialize, Serialize}; +static ALLOWED_FILENAME: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\.(jpg|jpeg|png|gif|svg|pdf)$").unwrap()); + #[async_trait] impl TryFromChunks for Invoice { async fn try_from_chunks( @@ -98,7 +103,7 @@ pub struct InvoiceAttachment { pub bytes: Vec, } -async fn try_handle_file(field: &FieldData) -> Result { +fn try_handle_file(field: FieldData) -> Result { let filename = field .metadata .file_name @@ -106,8 +111,12 @@ async fn try_handle_file(field: &FieldData) -> Result>, ) -> Result<(StatusCode, Json), Error> { let orig = multipart.data.clone(); - multipart.data.attachments = stream::iter( + multipart.data.attachments = Result::from_iter( multipart .attachments - .iter() + .into_iter() .map(try_handle_file) - .map(Ok) - // NOTE: This collect might seem harmless but - // I dare you to try removing it .collect::>(), - ) - // FIXME: Don't hardcode buffer size - .try_buffer_unordered(50) - .try_collect::>() - .await?; + )?; tokio::task::spawn(async move { if let Err(e) = client.send_mail(multipart.data).await { diff --git a/src/error.rs b/src/error.rs index 6c1bb5a..631eca3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,8 @@ pub enum Error { MultipartRejection(#[from] axum::extract::multipart::MultipartRejection), #[error("Missing filename multipart")] MissingFilename, + #[error("Unsupported file format: {0}")] + UnsupportedFileFormat(String), #[error("Error in handling json value")] JsonRejection(#[from] axum::extract::rejection::JsonRejection), #[error("Error while parsing json")] @@ -43,7 +45,8 @@ impl IntoResponse for Error { | Error::MissingFilename | Error::MultipartError(_) | Error::MultipartRejection(_) - | Error::JsonRejection(_) => StatusCode::BAD_REQUEST, + | Error::JsonRejection(_) + | Error::UnsupportedFileFormat(_) => StatusCode::BAD_REQUEST, Error::TypstError => StatusCode::INTERNAL_SERVER_ERROR, }; diff --git a/src/mailgun/invoices.rs b/src/mailgun/invoices.rs index 12e5223..9edb0dc 100644 --- a/src/mailgun/invoices.rs +++ b/src/mailgun/invoices.rs @@ -13,7 +13,6 @@ impl MailgunClient { pdfs.extend_from_slice( invoice .attachments - .clone() .into_iter() .map(|a| a.bytes) .collect::>() @@ -40,19 +39,6 @@ impl MailgunClient { reqwest::multipart::Part::bytes(pdf).file_name("invoice.pdf"), ); - let form = invoice - .attachments - .into_iter() - .try_fold(form, |form, attachment| { - Ok::( - form.part( - "attachment", - reqwest::multipart::Part::bytes(attachment.bytes) - .file_name(attachment.filename.clone()), - ), - ) - })?; - let response = self .client .post(self.url) diff --git a/templates/invoice.typ b/templates/invoice.typ index d00f2c2..7f7d0ca 100644 --- a/templates/invoice.typ +++ b/templates/invoice.typ @@ -117,7 +117,7 @@ ) #for file in data.attachments { - if regex(".*\.(jpg|png)$") in file.filename { + if regex("(?i)\.(jpg|jpeg|png|gif|svg)$") in file.filename { pagebreak() image("/attachments/" + file.filename) } From cc010c8665f9436356c96049805c480315e4925b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Tue, 13 Aug 2024 00:06:05 +0300 Subject: [PATCH 2/7] feat: add ratelimits with governor --- Cargo.lock | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/api/mod.rs | 23 ++++++++++ src/main.rs | 11 +++-- 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3e8c1c..0655cd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,19 @@ dependencies = [ "syn 2.0.73", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-url" version = "0.3.1" @@ -856,6 +869,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "futures" version = "0.3.30" @@ -927,6 +950,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -1022,6 +1051,26 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand", + "smallvec", + "spinning_top", +] + [[package]] name = "half" version = "2.4.1" @@ -1517,6 +1566,7 @@ dependencies = [ "tokio", "tower", "tower-http", + "tower_governor", "tracing", "tracing-subscriber", "typst", @@ -1725,6 +1775,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -1735,6 +1791,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2100,6 +2168,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -2204,6 +2287,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -2681,6 +2773,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3091,6 +3192,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower_governor" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" +dependencies = [ + "axum", + "forwarded-header-value", + "governor", + "http 1.1.0", + "pin-project", + "thiserror", + "tower", + "tracing", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index f46d31f..f1b30c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ typst-assets = { version = "0.11.1", features = ["fonts"] } reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"] } lopdf = "0.33.0" regex = "1.10.6" +tower_governor = { version = "0.4.2", features = ["axum"] } [dev-dependencies] tower = { version = "0.4.13", features = ["util"] } diff --git a/src/api/mod.rs b/src/api/mod.rs index 4ea6362..e90d539 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,8 +1,12 @@ use axum::{ extract::DefaultBodyLimit, + http::Method, routing::{get, post}, Router, }; +use std::sync::Arc; +use std::time::Duration; +use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer}; use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer, trace::TraceLayer}; pub mod invoices; @@ -13,6 +17,22 @@ pub fn app() -> Router { "http://localhost:3000".parse().unwrap(), ]); + let governor_config = Arc::new( + GovernorConfigBuilder::default() + .const_period(Duration::from_secs(720)) + .burst_size(5) + .use_headers() + .methods(vec![Method::POST]) + .finish() + .unwrap(), + ); + let governor_limiter = governor_config.limiter().clone(); + + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(60)); + governor_limiter.retain_recent(); + }); + Router::new() .route("/health", get(health)) .route("/invoices", post(invoices::create)) @@ -21,6 +41,9 @@ pub fn app() -> Router { .layer(DefaultBodyLimit::disable()) // Limit the body to 24 MiB since the email is limited to 25 MiB .layer(RequestBodyLimitLayer::new(24 * 1024 * 1024)) + .layer(GovernorLayer { + config: governor_config, + }) } async fn health() {} diff --git a/src/main.rs b/src/main.rs index 5cd61dc..b6e89e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,12 @@ async fn main() { .await .expect("Failed to bind TcpListener"); - axum::serve(listener, api::app().with_state(state)) - .await - .expect("Failed to start server"); + axum::serve( + listener, + api::app() + .with_state(state) + .into_make_service_with_connect_info::(), + ) + .await + .expect("Failed to start server"); } From 8972d6c172bb91ea3b85ed50dec54c96486eac67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Wed, 14 Aug 2024 19:33:06 +0300 Subject: [PATCH 3/7] fix: return "OK" from /health --- src/api/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index e90d539..3619a7b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -46,4 +46,6 @@ pub fn app() -> Router { }) } -async fn health() {} +async fn health() -> &'static str { + "OK" +} From 059ba2690d999ec5b7c981095407298e4248ca3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Wed, 14 Aug 2024 19:57:54 +0300 Subject: [PATCH 4/7] feat: validate iban account numbers --- Cargo.lock | 10 ++++++++++ Cargo.toml | 1 + src/api/invoices.rs | 10 +++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0655cd4..c43c4ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1270,6 +1270,15 @@ dependencies = [ "cc", ] +[[package]] +name = "iban_validate" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1d358f7ae89819e8656f1b495c9d760a9ca315998b12d589dc516c9f81ed08" +dependencies = [ + "arrayvec", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1555,6 +1564,7 @@ dependencies = [ "fontdb 0.17.0", "futures", "garde", + "iban_validate", "lopdf", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index f1b30c4..0201835 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ reqwest = { version = "0.12.5", default-features = false, features = ["multipart lopdf = "0.33.0" regex = "1.10.6" tower_governor = { version = "0.4.2", features = ["axum"] } +iban_validate = "4.0.1" [dev-dependencies] tower = { version = "0.4.13", features = ["util"] } diff --git a/src/api/invoices.rs b/src/api/invoices.rs index d082b2a..434583a 100644 --- a/src/api/invoices.rs +++ b/src/api/invoices.rs @@ -9,6 +9,7 @@ use axum_typed_multipart::{ use axum_valid::Garde; use futures::stream::Stream; use garde::Validate; +use iban::Iban; use regex::Regex; use serde_derive::{Deserialize, Serialize}; @@ -27,6 +28,13 @@ impl TryFromChunks for Invoice { } } +fn is_valid_iban(value: &str, _: &()) -> garde::Result { + match value.parse::() { + Err(e) => Err(garde::Error::new(e)), + _ => Ok(()), + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Validate)] pub struct Address { #[garde(byte_length(max = 128))] @@ -51,7 +59,7 @@ pub struct Invoice { pub address: Address, /// The recipient's bank account number // TODO: maybe validate with https://crates.io/crates/iban_validate/ - #[garde(byte_length(max = 128))] + #[garde(byte_length(max = 128), custom(is_valid_iban))] pub bank_account_number: String, #[garde(byte_length(min = 1, max = 128))] pub subject: String, From 8a6074d660e2feca7719accf4d28bd1bb154a066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Thu, 15 Aug 2024 10:37:56 +0300 Subject: [PATCH 5/7] chore: sort dependencies in Cargo.toml --- Cargo.toml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0201835..ba35841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,29 +20,29 @@ axum-valid = { version = "0.14.0", features = [ "typed_multipart", ], default-features = false } axum_typed_multipart = "0.11.0" +comemo = { version = "0.4.0" } dotenv = "0.15.0" +fontdb = { version = "0.17.0", optional = true } +futures = "0.3.30" +garde = "0.17.0" +iban_validate = "4.0.1" +lopdf = "0.33.0" +regex = "1.10.6" +reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"] } serde = "1.0.195" serde_derive = "1.0.195" +serde_json = "1.0.111" thiserror = "1.0.56" +time = { version = "0.3.36" } tokio = { version = "1.35.1", features = ["full"] } tower-http = { version = "0.5.1", features = ["trace", "limit", "cors"] } +tower_governor = { version = "0.4.2", features = ["axum"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -serde_json = "1.0.111" -futures = "0.3.30" -garde = "0.17.0" typst = { version = "0.11.1" } -typst-pdf = { version = "0.11.1" } -comemo = { version = "0.4.0" } -time = { version = "0.3.36" } -fontdb = { version = "0.17.0", optional = true } typst-assets = { version = "0.11.1", features = ["fonts"] } -reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"] } -lopdf = "0.33.0" -regex = "1.10.6" -tower_governor = { version = "0.4.2", features = ["axum"] } -iban_validate = "4.0.1" +typst-pdf = { version = "0.11.1" } [dev-dependencies] -tower = { version = "0.4.13", features = ["util"] } axum-test = "14.2.2" +tower = { version = "0.4.13", features = ["util"] } From 07bf0d03ae7f17b6ee29a6fb724c09328284e7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Fri, 23 Aug 2024 23:57:49 +0300 Subject: [PATCH 6/7] fix(api): return error on failed send_email --- src/api/invoices.rs | 7 +------ src/error.rs | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/api/invoices.rs b/src/api/invoices.rs index 434583a..e5fb08e 100644 --- a/src/api/invoices.rs +++ b/src/api/invoices.rs @@ -142,11 +142,6 @@ pub async fn create( .collect::>(), )?; - tokio::task::spawn(async move { - if let Err(e) = client.send_mail(multipart.data).await { - error!("Sending invoice failed: {}", e); - } - }); - + client.send_mail(multipart.data).await?; Ok((StatusCode::CREATED, axum::Json(orig))) } diff --git a/src/error.rs b/src/error.rs index 631eca3..37960f8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -38,7 +38,7 @@ impl IntoResponse for Error { error!(%self); let status = match self { - Error::InternalServerError(_) | Error::ReqwestError(_) => { + Error::InternalServerError(_) | Error::ReqwestError(_) | Error::TypstError => { StatusCode::INTERNAL_SERVER_ERROR } Error::JsonError(_) @@ -47,7 +47,6 @@ impl IntoResponse for Error { | Error::MultipartRejection(_) | Error::JsonRejection(_) | Error::UnsupportedFileFormat(_) => StatusCode::BAD_REQUEST, - Error::TypstError => StatusCode::INTERNAL_SERVER_ERROR, }; ( From aaeefc2597cee67708289ae78c0d797f3de84e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luukas=20P=C3=B6rtfors?= Date: Sat, 24 Aug 2024 14:31:43 +0300 Subject: [PATCH 7/7] feat: use clap for parsing configuration --- Cargo.lock | 108 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/api/mod.rs | 13 +++--- src/mailgun/mod.rs | 27 +++++++++--- src/main.rs | 53 ++++++++++++++++------ src/state.rs | 11 +---- 6 files changed, 178 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c43c4ff..e51d380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -457,6 +506,46 @@ dependencies = [ "serde", ] +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.73", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cobs" version = "0.2.3" @@ -469,6 +558,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "comemo" version = "0.4.0" @@ -1503,6 +1598,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -1559,6 +1660,7 @@ dependencies = [ "axum-test", "axum-valid", "axum_typed_multipart", + "clap", "comemo", "dotenv", "fontdb 0.17.0", @@ -3649,6 +3751,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index ba35841..b18667f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ axum-valid = { version = "0.14.0", features = [ "typed_multipart", ], default-features = false } axum_typed_multipart = "0.11.0" +clap = { version = "4.5.16", features = ["env", "derive"] } comemo = { version = "0.4.0" } dotenv = "0.15.0" fontdb = { version = "0.17.0", optional = true } diff --git a/src/api/mod.rs b/src/api/mod.rs index 3619a7b..adb8f97 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,6 @@ use axum::{ extract::DefaultBodyLimit, - http::Method, + http::{HeaderValue, Method}, routing::{get, post}, Router, }; @@ -12,10 +12,13 @@ use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer, trace::TraceLaye pub mod invoices; pub fn app() -> Router { - let cors_layer = CorsLayer::new().allow_origin([ - "https://tietokilta.fi".parse().unwrap(), - "http://localhost:3000".parse().unwrap(), - ]); + let cors_layer = CorsLayer::new().allow_origin( + crate::CONFIG + .allowed_origins + .iter() + .map(|c| c.parse::().unwrap()) + .collect::>(), + ); let governor_config = Arc::new( GovernorConfigBuilder::default() diff --git a/src/mailgun/mod.rs b/src/mailgun/mod.rs index 18423bb..97cf4c4 100644 --- a/src/mailgun/mod.rs +++ b/src/mailgun/mod.rs @@ -7,14 +7,27 @@ use axum::{ mod invoices; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct MailgunClient { - pub client: reqwest::Client, - pub url: String, - pub api_user: String, - pub api_key: String, - pub default_to: String, - pub from: String, + client: reqwest::Client, + url: String, + api_user: String, + api_key: String, + default_to: String, + from: String, +} + +impl From for MailgunClient { + fn from(config: crate::MailgunConfig) -> Self { + Self { + client: reqwest::Client::new(), + url: config.url, + api_user: config.user, + api_key: config.password, + default_to: config.to, + from: config.from, + } + } } #[async_trait] diff --git a/src/main.rs b/src/main.rs index b6e89e7..2d6cc0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use clap::Parser; use std::net::SocketAddr; +use std::sync::LazyLock; mod api; mod error; @@ -16,6 +18,43 @@ mod tests; #[macro_use] extern crate tracing; +#[derive(Parser, Clone, Debug)] +struct MailgunConfig { + /// Url used by mailgun + #[clap(long = "mailgun-url", env = "MAILGUN_URL")] + url: String, + /// Username used by mailgun + #[clap(long = "mailgun-user", env = "MAILGUN_USER")] + user: String, + /// Password used by mailgun + #[clap(long = "mailgun-password", env = "MAILGUN_PASSWORD")] + password: String, + /// Initial To-value used by mailgun + #[clap(long = "mailgun-to", env = "MAILGUN_TO")] + to: String, + /// From-value used by mailgun + #[clap(long = "mailgun-from", env = "MAILGUN_FROM")] + from: String, +} + +#[derive(Parser, Clone, Debug)] +#[command(version, about, long_about = None)] +struct LaskugenConfig { + #[clap(flatten)] + mailgun: MailgunConfig, + /// The listen port for the HTTP server + #[clap(long, env, required = false, default_value = "3000")] + port: u16, + /// The ip address to bound by the HTTP server + #[clap(long, env, required = false, default_value = "127.0.0.1")] + bind_addr: std::net::IpAddr, + /// A comma-separated list of allowed origins + #[clap(long, env, required = false, value_delimiter = ',')] + allowed_origins: Vec, +} + +static CONFIG: LazyLock = LazyLock::new(|| LaskugenConfig::parse()); + #[tokio::main] async fn main() { dotenv::dotenv().ok(); @@ -27,19 +66,7 @@ async fn main() { .init(); let state = state::new().await; - - let ip = if std::env::var("EXPOSE").unwrap_or("0".into()) == "1" { - [0, 0, 0, 0] - } else { - [127, 0, 0, 1] - }; - - let addr = SocketAddr::from(( - ip, - std::env::var("PORT") - .map(|p| p.parse::().unwrap()) - .unwrap_or(3000), - )); + let addr = SocketAddr::from((CONFIG.bind_addr, CONFIG.port)); debug!("Listening on {addr}"); let listener = tokio::net::TcpListener::bind(addr) .await diff --git a/src/state.rs b/src/state.rs index ed14fd2..83d2a02 100644 --- a/src/state.rs +++ b/src/state.rs @@ -10,17 +10,8 @@ pub struct State { pub async fn new() -> State { dotenv::dotenv().ok(); - let mailgun_client = MailgunClient { - client: reqwest::Client::new(), - url: std::env::var("MAILGUN_URL").expect("No MAILGUN_URL in env"), - api_user: std::env::var("MAILGUN_USER").expect("No MAILGUN_USER in env"), - api_key: std::env::var("MAILGUN_PASSWORD").expect("No MAILGUN_PASSWORD in env"), - default_to: std::env::var("MAILGUN_TO").expect("No MAILGUN_TO in env"), - from: std::env::var("MAILGUN_FROM").expect("No MAILGUN_FROM in env"), - }; - State { - mailgun_client, + mailgun_client: MailgunClient::from(crate::CONFIG.mailgun.clone()), for_garde: (), } }