lib.rs (6911B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025 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, IbanErrorKind, ParseIbanError}, 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, cfg.auth.method()); 49 } 50 router.serve(cfg.serve, None).await?; 51 Ok(()) 52 } 53 54 #[derive( 55 Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 56 )] 57 pub struct HuIban(IBAN); 58 59 impl HuIban { 60 #[allow(clippy::identity_op)] 61 pub fn checksum(b: &[u8]) -> Result<(), (u8, u8)> { 62 let expected_digit = b[7] - b'0'; 63 let sum = ((b[0] - b'0') * 9) as u16 64 + ((b[1] - b'0') * 7) as u16 65 + ((b[2] - b'0') * 3) as u16 66 + ((b[3] - b'0') * 1) as u16 67 + ((b[4] - b'0') * 9) as u16 68 + ((b[5] - b'0') * 7) as u16 69 + ((b[6] - b'0') * 3) as u16; 70 let modulo = ((10 - (sum % 10)) % 10) as u8; 71 if expected_digit != modulo { 72 Err((expected_digit, modulo)) 73 } else { 74 Ok(()) 75 } 76 } 77 78 fn check_bban(bban: &str) -> Result<(), HuIbanErr> { 79 let bban = bban.as_bytes(); 80 if bban.len() != 16 && bban.len() != 24 { 81 return Err(HuIbanErr::BbanSize(bban.len())); 82 } else if !bban.iter().all(u8::is_ascii_digit) { 83 return Err(HuIbanErr::Invalid); 84 } 85 Self::checksum(&bban[..8]).map_err(|e| HuIbanErr::checksum("bank-branch number", e))?; 86 if bban.len() == 16 { 87 Self::checksum(&bban[8..]).map_err(|e| HuIbanErr::checksum("account number", e))?; 88 } else { 89 Self::checksum(&bban[8..16]) 90 .map_err(|e| HuIbanErr::checksum("account number first group", e))?; 91 Self::checksum(&bban[16..]) 92 .map_err(|e| HuIbanErr::checksum("account number second group", e))?; 93 } 94 Ok(()) 95 } 96 97 pub fn from_bban(bban: &str) -> Result<Self, HuIbanErr> { 98 Self::check_bban(bban)?; 99 let full_bban = if bban.len() == 16 { 100 Cow::Owned(format!("{bban}00000000")) 101 } else { 102 Cow::Borrowed(bban) 103 }; 104 let iban = IBAN::from_parts(Country::HU, &full_bban); 105 Ok(Self(iban)) 106 } 107 108 pub fn bban(&self) -> &str { 109 let bban = self.0.bban(); 110 bban.strip_suffix("00000000").unwrap_or(bban) 111 } 112 113 pub fn iban(&self) -> &str { 114 self.0.as_ref() 115 } 116 } 117 118 #[derive(Debug, thiserror::Error)] 119 pub enum HuIbanErr { 120 #[error("contains illegal characters (only 0-9 allowed)")] 121 Invalid, 122 #[error("expected an hungarian IBAN starting with HU got {0}")] 123 Country(Country), 124 #[error("invalid length expected 16 or 24 chars got {0}")] 125 BbanSize(usize), 126 #[error("invalid checksum for {0} expected {1} got {2}")] 127 Checksum(&'static str, u8, u8), 128 #[error(transparent)] 129 Iban(IbanErrorKind), 130 } 131 132 impl From<ParseIbanError> for HuIbanErr { 133 fn from(value: ParseIbanError) -> Self { 134 Self::Iban(value.kind) 135 } 136 } 137 138 impl HuIbanErr { 139 fn checksum(part: &'static str, (expected, checksum): (u8, u8)) -> Self { 140 Self::Checksum(part, expected, checksum) 141 } 142 } 143 144 impl TryFrom<IBAN> for HuIban { 145 type Error = HuIbanErr; 146 147 fn try_from(iban: IBAN) -> Result<Self, Self::Error> { 148 let country = iban.country(); 149 if country != Country::HU { 150 return Err(HuIbanErr::Country(country)); 151 } 152 153 Self::check_bban(iban.bban())?; 154 155 Ok(Self(iban)) 156 } 157 } 158 159 impl PaytoImpl for HuIban { 160 fn as_payto(&self) -> PaytoURI { 161 PaytoURI::from_parts("iban", format_args!("/{}", self.0)) 162 } 163 164 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 165 let iban_payto = IbanPayto::try_from(raw).map_err(PaytoErr::custom)?; 166 HuIban::try_from(iban_payto.into_inner().iban).map_err(PaytoErr::custom) 167 } 168 } 169 170 impl FromStr for HuIban { 171 type Err = HuIbanErr; 172 173 fn from_str(s: &str) -> Result<Self, Self::Err> { 174 let iban: IBAN = s.parse()?; 175 Self::try_from(iban) 176 } 177 } 178 179 impl std::fmt::Display for HuIban { 180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 181 self.0.fmt(f) 182 } 183 } 184 185 /// Parse a magnet payto URI, panic if malformed 186 pub fn magnet_payto(url: impl AsRef<str>) -> FullHuPayto { 187 url.as_ref().parse().expect("invalid magnet payto") 188 } 189 190 pub type HuPayto = Payto<HuIban>; 191 pub type FullHuPayto = FullPayto<HuIban>; 192 pub type TransferHuPayto = TransferPayto<HuIban>; 193 194 #[cfg(test)] 195 mod test { 196 use taler_common::types::{ 197 iban::IBAN, 198 payto::{Payto, PaytoImpl, payto}, 199 }; 200 201 use crate::HuIban; 202 203 #[test] 204 fn hu_iban() { 205 for (valid, account) in [ 206 ( 207 payto("payto://iban/HU30162000031000163100000000"), 208 "1620000310001631", 209 ), 210 ( 211 payto("payto://iban/HU02162000031000164800000000"), 212 "1620000310001648", 213 ), 214 ( 215 payto("payto://iban/HU60162000101006446300000000"), 216 "1620001010064463", 217 ), 218 ] { 219 // Parsing 220 let iban_payto: Payto<IBAN> = (&valid).try_into().unwrap(); 221 let hu_payto: HuIban = iban_payto.into_inner().try_into().unwrap(); 222 assert_eq!(hu_payto.bban(), account); 223 // Roundtrip 224 let iban = HuIban::from_bban(&account).unwrap(); 225 let payto = iban.as_payto(); 226 assert_eq!(payto, valid); 227 } 228 } 229 }