lib.rs (7040B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{borrow::Cow, str::FromStr, sync::Arc}; 18 19 use sqlx::PgPool; 20 use taler_api::api::{Router, TalerRouter as _}; 21 use taler_common::{ 22 config::Config, 23 types::{ 24 iban::{Country, IBAN, IbanErrKind, ParseIbanErr}, 25 payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto}, 26 }, 27 }; 28 29 use crate::{api::MagnetApi, config::ServeCfg}; 30 31 pub mod api; 32 pub mod config; 33 pub mod constants; 34 pub mod db; 35 pub mod dev; 36 pub mod magnet_api; 37 pub mod setup; 38 pub mod worker; 39 40 pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> { 41 let cfg = ServeCfg::parse(cfg)?; 42 let api = Arc::new(MagnetApi::start(pool, cfg.payto).await); 43 let mut router = Router::new(); 44 if let Some(cfg) = cfg.wire_gateway { 45 router = router.wire_gateway(api.clone(), cfg.auth.method()); 46 } 47 if let Some(cfg) = cfg.revenue { 48 router = router.revenue(api.clone(), cfg.auth.method()); 49 } 50 if let Some(cfg) = cfg.observability { 51 router = router.observability(api, cfg.auth.method()); 52 } 53 router.serve(&cfg.serve, None).await?; 54 Ok(()) 55 } 56 57 #[derive( 58 Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 59 )] 60 pub struct HuIban(IBAN); 61 62 impl HuIban { 63 #[allow(clippy::identity_op)] 64 pub fn checksum(b: &[u8]) -> Result<(), (u8, u8)> { 65 let expected_digit = b[7] - b'0'; 66 let sum = ((b[0] - b'0') * 9) as u16 67 + ((b[1] - b'0') * 7) as u16 68 + ((b[2] - b'0') * 3) as u16 69 + ((b[3] - b'0') * 1) as u16 70 + ((b[4] - b'0') * 9) as u16 71 + ((b[5] - b'0') * 7) as u16 72 + ((b[6] - b'0') * 3) as u16; 73 let modulo = ((10 - (sum % 10)) % 10) as u8; 74 if expected_digit != modulo { 75 Err((expected_digit, modulo)) 76 } else { 77 Ok(()) 78 } 79 } 80 81 fn check_bban(bban: &str) -> Result<(), HuIbanErr> { 82 let bban = bban.as_bytes(); 83 if bban.len() != 16 && bban.len() != 24 { 84 return Err(HuIbanErr::BbanSize(bban.len())); 85 } else if !bban.iter().all(u8::is_ascii_digit) { 86 return Err(HuIbanErr::Invalid); 87 } 88 Self::checksum(&bban[..8]).map_err(|e| HuIbanErr::checksum("bank-branch number", e))?; 89 if bban.len() == 16 { 90 Self::checksum(&bban[8..]).map_err(|e| HuIbanErr::checksum("account number", e))?; 91 } else { 92 Self::checksum(&bban[8..16]) 93 .map_err(|e| HuIbanErr::checksum("account number first group", e))?; 94 Self::checksum(&bban[16..]) 95 .map_err(|e| HuIbanErr::checksum("account number second group", e))?; 96 } 97 Ok(()) 98 } 99 100 pub fn from_bban(bban: &str) -> Result<Self, HuIbanErr> { 101 Self::check_bban(bban)?; 102 let full_bban = if bban.len() == 16 { 103 Cow::Owned(format!("{bban}00000000")) 104 } else { 105 Cow::Borrowed(bban) 106 }; 107 let iban = IBAN::from_parts(Country::HU, &full_bban); 108 Ok(Self(iban)) 109 } 110 111 pub fn bban(&self) -> &str { 112 let bban = self.0.bban(); 113 bban.strip_suffix("00000000").unwrap_or(bban) 114 } 115 116 pub fn iban(&self) -> &str { 117 self.0.as_ref() 118 } 119 } 120 121 #[derive(Debug, thiserror::Error)] 122 pub enum HuIbanErr { 123 #[error("contains illegal characters (only 0-9 allowed)")] 124 Invalid, 125 #[error("expected an hungarian IBAN starting with HU got {0}")] 126 Country(Country), 127 #[error("invalid length expected 16 or 24 chars got {0}")] 128 BbanSize(usize), 129 #[error("invalid checksum for {0} expected {1} got {2}")] 130 Checksum(&'static str, u8, u8), 131 #[error(transparent)] 132 Iban(IbanErrKind), 133 } 134 135 impl From<ParseIbanErr> for HuIbanErr { 136 fn from(value: ParseIbanErr) -> Self { 137 Self::Iban(value.kind) 138 } 139 } 140 141 impl HuIbanErr { 142 const fn checksum(part: &'static str, (expected, checksum): (u8, u8)) -> Self { 143 Self::Checksum(part, expected, checksum) 144 } 145 } 146 147 impl TryFrom<IBAN> for HuIban { 148 type Error = HuIbanErr; 149 150 fn try_from(iban: IBAN) -> Result<Self, Self::Error> { 151 let country = iban.country(); 152 if country != Country::HU { 153 return Err(HuIbanErr::Country(country)); 154 } 155 156 Self::check_bban(iban.bban())?; 157 158 Ok(Self(iban)) 159 } 160 } 161 162 impl PaytoImpl for HuIban { 163 fn as_uri(&self) -> PaytoURI { 164 PaytoURI::from_parts("iban", format_args!("/{}", self.0)) 165 } 166 167 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 168 let iban_payto = IbanPayto::try_from(raw)?; 169 Self::try_from(iban_payto.into_inner().iban) 170 .map_err(|e| PaytoErr::malformed_segment("iban", e)) 171 } 172 } 173 174 impl FromStr for HuIban { 175 type Err = HuIbanErr; 176 177 fn from_str(s: &str) -> Result<Self, Self::Err> { 178 let iban: IBAN = s.parse()?; 179 Self::try_from(iban) 180 } 181 } 182 183 impl std::fmt::Display for HuIban { 184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 185 self.0.fmt(f) 186 } 187 } 188 189 /// Parse a magnet payto URI, panic if malformed 190 pub fn magnet_payto(url: impl AsRef<str>) -> FullHuPayto { 191 url.as_ref().parse().expect("invalid magnet payto") 192 } 193 194 pub type HuPayto = Payto<HuIban>; 195 pub type FullHuPayto = FullPayto<HuIban>; 196 pub type TransferHuPayto = TransferPayto<HuIban>; 197 198 #[cfg(test)] 199 mod test { 200 use taler_common::types::{ 201 iban::IBAN, 202 payto::{Payto, PaytoImpl, payto}, 203 }; 204 205 use crate::HuIban; 206 207 #[test] 208 fn hu_iban() { 209 for (valid, account) in [ 210 ( 211 payto("payto://iban/HU30162000031000163100000000"), 212 "1620000310001631", 213 ), 214 ( 215 payto("payto://iban/HU02162000031000164800000000"), 216 "1620000310001648", 217 ), 218 ( 219 payto("payto://iban/HU60162000101006446300000000"), 220 "1620001010064463", 221 ), 222 ] { 223 // Parsing 224 let iban_payto: Payto<IBAN> = (&valid).try_into().unwrap(); 225 let hu_payto: HuIban = iban_payto.into_inner().try_into().unwrap(); 226 assert_eq!(hu_payto.bban(), account); 227 // Roundtrip 228 let iban = HuIban::from_bban(account).unwrap(); 229 let payto = iban.as_uri(); 230 assert_eq!(payto, valid); 231 } 232 } 233 }