commit aeb633aa5bfae3373fc8dba312840d94aee3eeaf
parent 7a64f76dc2b923b730b5d38c24c00901538a7653
Author: Antoine A <>
Date: Thu, 13 Feb 2025 17:00:24 +0100
magnet-bank: setup improvements and fixes
Diffstat:
10 files changed, 229 insertions(+), 193 deletions(-)
diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs
@@ -400,7 +400,6 @@ pub mod parser {
let bin_path = path_dir.join(self.exec_name);
if bin_path.exists() {
if let Some(parent) = path_dir.parent() {
- dbg!(parent);
return parent.canonicalize().map_err(|e| (parent.to_path_buf(), e));
}
}
diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs
@@ -27,11 +27,11 @@ pub mod api_revenue;
pub mod api_wire;
pub mod cli;
pub mod config;
+pub mod db;
pub mod error_code;
pub mod json_file;
mod log;
pub mod types;
-pub mod db;
#[derive(clap::Parser, Debug, Clone)]
pub struct CommonArgs {
diff --git a/taler-magnet-bank/src/config.rs b/taler-magnet-bank/src/config.rs
@@ -20,9 +20,12 @@ use base64::{prelude::BASE64_STANDARD, Engine};
use reqwest::Url;
use sqlx::postgres::PgConnectOptions;
use taler_api::{auth::AuthMethod, Serve};
-use taler_common::config::{map_config, Config, Section, ValueErr};
+use taler_common::{
+ config::{map_config, Config, Section, ValueErr},
+ types::payto::PaytoURI,
+};
-use crate::magnet::Token;
+use crate::{magnet::Token, FullHuPayto, HuIban};
pub struct DbCfg {
pub cfg: PgConnectOptions,
@@ -39,6 +42,14 @@ impl DbCfg {
}
}
+pub fn parse_account_payto(cfg: &Config) -> Result<FullHuPayto, ValueErr> {
+ let sect = cfg.section("magnet-bank");
+ let iban: HuIban = sect.parse("iban", "IBAN").require()?;
+ let name = sect.str("NAME").require()?;
+
+ Ok(FullHuPayto::new(iban, name))
+}
+
pub struct ApiCfg {
pub auth: AuthMethod,
}
@@ -70,6 +81,7 @@ impl ApiCfg {
}
pub struct ServeCfg {
+ pub payto: PaytoURI,
pub serve: Serve,
pub wire_gateway: Option<ApiCfg>,
pub revenue: Option<ApiCfg>,
@@ -77,6 +89,8 @@ pub struct ServeCfg {
impl ServeCfg {
pub fn parse(cfg: &Config) -> Result<Self, ValueErr> {
+ let payto = parse_account_payto(cfg)?;
+
let sect = cfg.section("magnet-bank-httpd");
let serve = map_config!(sect, "serve", "SERVE",
@@ -97,6 +111,7 @@ impl ServeCfg {
let revenue = ApiCfg::parse(cfg.section("magnet-bank-httpd-revenue-api"))?;
Ok(Self {
+ payto: payto.as_payto(),
serve,
wire_gateway,
revenue,
@@ -105,6 +120,7 @@ impl ServeCfg {
}
pub struct WorkerCfg {
+ pub payto: FullHuPayto,
pub api_url: Url,
pub consumer: Token,
pub keys_path: String,
@@ -112,8 +128,10 @@ pub struct WorkerCfg {
impl WorkerCfg {
pub fn parse(cfg: &Config) -> Result<Self, ValueErr> {
+ let payto = parse_account_payto(cfg)?;
let sect = cfg.section("magnet-bank-worker");
Ok(Self {
+ payto,
api_url: sect.parse("URL", "API_URL").require()?,
consumer: Token {
key: sect.str("CONSUMER_KEY").require()?,
diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs
@@ -19,16 +19,16 @@ use clap::ValueEnum;
use jiff::Zoned;
use taler_common::{
config::Config,
- types::{amount::Amount, iban::IBAN, payto::PaytoImpl},
+ types::{amount::Amount, payto::PaytoImpl},
};
use tracing::info;
use crate::{
config::WorkerCfg,
- keys,
magnet::{AuthClient, Direction},
+ setup,
worker::{extract_tx_info, Tx},
- FullHuPayto, HuPayto, TransferHuPayto,
+ HuPayto, TransferHuPayto,
};
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
@@ -51,9 +51,9 @@ pub enum DevCmd {
},
Transfer {
#[clap(long)]
- debtor: TransferHuPayto,
+ debtor: HuPayto,
#[clap(long)]
- creditor: FullHuPayto,
+ creditor: TransferHuPayto,
#[clap(long)]
amount: Option<Amount>,
#[clap(long)]
@@ -63,7 +63,7 @@ pub enum DevCmd {
pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
let cfg = WorkerCfg::parse(&cfg)?;
- let keys = keys::load(&cfg)?;
+ let keys = setup::load(&cfg)?;
let client = reqwest::Client::new();
let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer).upgrade(&keys.access_token);
match cmd {
@@ -71,8 +71,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
let res = client.list_accounts().await?;
for partner in res.partners {
for account in partner.bank_accounts {
- let iban: IBAN = account.iban.parse()?;
- let payto = iban.as_full_payto(&partner.partner.name);
+ let payto = account.iban.as_full_payto(&partner.partner.name);
info!("{} {} {}", account.code, account.currency.symbol, payto);
}
}
@@ -109,12 +108,14 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
subject,
} => {
let account = client.account(debtor.bban()).await?;
- let amount = debtor
+ let amount = creditor
.amount
+ .clone()
.or(amount)
.ok_or_else(|| anyhow!("Missing amount"))?;
- let subject = debtor
+ let subject = creditor
.subject
+ .clone()
.or(subject)
.ok_or_else(|| anyhow!("Missing subject"))?;
diff --git a/taler-magnet-bank/src/keys.rs b/taler-magnet-bank/src/keys.rs
@@ -1,147 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2025 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-use std::io::ErrorKind;
-
-use p256::ecdsa::SigningKey;
-use taler_common::{json_file, types::base32::Base32};
-use tracing::info;
-
-use crate::{
- config::WorkerCfg,
- magnet::{
- error::{ApiError, MagnetError},
- AuthClient, Token, TokenAuth,
- },
-};
-
-#[derive(Default, Debug, serde::Deserialize, serde::Serialize)]
-struct KeysFile {
- access_token: Option<Token>,
- signing_key: Option<Base32<32>>,
-}
-
-#[derive(Debug)]
-pub struct Keys {
- pub access_token: Token,
- pub signing_key: SigningKey,
-}
-
-pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> {
- // Load JSON file
- let file: KeysFile = match json_file::load(&cfg.keys_path) {
- Ok(file) => file,
- Err(e) => return Err(anyhow::anyhow!("Could not magnet keys: {e}")),
- };
-
- fn incomplete_err() -> anyhow::Error {
- anyhow::anyhow!("Missing magnet keys, run 'taler-magnet-bank setup' first")
- }
-
- // Check full
- let access_token = file.access_token.ok_or_else(incomplete_err)?;
- let signing_key = file.signing_key.ok_or_else(incomplete_err)?;
-
- // Load signing key
-
- let signing_key = SigningKey::from_slice(&*signing_key)?;
-
- Ok(Keys {
- access_token,
- signing_key,
- })
-}
-
-pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> {
- if reset {
- if let Err(e) = std::fs::remove_file(&cfg.keys_path) {
- if e.kind() != ErrorKind::NotFound {
- Err(e)?;
- }
- }
- }
- let mut keys = match json_file::load(&cfg.keys_path) {
- Ok(existing) => existing,
- Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(),
- Err(e) => Err(e)?,
- };
- let client = reqwest::Client::new();
- let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer);
-
- info!("Setup OAuth access token");
- if keys.access_token.is_none() {
- let token_request = client.token_request().await?;
-
- // TODO how to do it in a generic way ?
- // TODO Ask MagnetBank if they could support out-of-band configuration
- println!(
- "Login at {}?oauth_token={}",
- client.join("/NetBankOAuth/authtoken.xhtml"),
- token_request.key
- );
- let auth_url = passterm::prompt_password_tty(Some("Enter the result URL>"))?;
- let auth_url = reqwest::Url::parse(&auth_url)?;
- let token_auth: TokenAuth =
- serde_urlencoded::from_str(auth_url.query().unwrap_or_default())?;
- assert_eq!(token_request.key, token_auth.oauth_token);
-
- let access_token = client.token_access(&token_request, &token_auth).await?;
- keys.access_token = Some(access_token);
- json_file::persist(&cfg.keys_path, &keys)?;
- }
-
- let client = client.upgrade(keys.access_token.as_ref().unwrap());
-
- info!("Setup Strong Customer Authentication");
- // TODO find a proper way to check if SCA is required without triggering SCA.GLOBAL_FEATURE_NOT_ENABLED
- let request = client.request_sms_code().await?;
- println!(
- "A SCA code have been sent through {} to {}",
- request.channel,
- request.sent_to.join(", ")
- );
- let sca_code = passterm::prompt_password_tty(Some("Enter the code>"))?;
- if let Err(e) = client.perform_sca(&sca_code).await {
- // Ignore error if SCA already performed
- if !matches!(e, ApiError::Magnet(MagnetError { ref short_message, .. }) if short_message == "TOKEN_SCA_HITELESITETT")
- {
- return Err(e.into());
- }
- }
-
- info!("Setup public key");
- // TODO find a proper way to check if a public key have been setup
- // TODO use the better from/to_array API in the next version of the crypto lib
- let signing_key = match keys.signing_key {
- Some(bytes) => SigningKey::from_slice(bytes.as_ref())?,
- None => {
- let rand = SigningKey::random(&mut rand_core::OsRng);
- let array: [u8; 32] = rand.to_bytes().as_slice().try_into().unwrap();
- keys.signing_key = Some(Base32::from(array));
- json_file::persist(&cfg.keys_path, &keys)?;
- rand
- }
- };
- if let Err(e) = client.upload_public_key(&signing_key).await {
- // Ignore error if public key already uploaded
- if !matches!(e, ApiError::Magnet(MagnetError { ref short_message, .. }) if short_message == "KULCS_MAR_HASZNALATBAN")
- {
- return Err(e.into());
- }
- }
-
- Ok(())
-}
diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs
@@ -26,8 +26,8 @@ pub mod config;
pub mod constant;
pub mod db;
pub mod dev;
-pub mod keys;
pub mod magnet;
+pub mod setup;
pub mod worker;
pub mod failure_injection {
pub fn fail_point(_name: &'static str) {
diff --git a/taler-magnet-bank/src/magnet.rs b/taler-magnet-bank/src/magnet.rs
@@ -24,7 +24,7 @@ use p256::{
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use spki::EncodePublicKey;
-use taler_common::types::amount;
+use taler_common::types::{amount, iban::IBAN};
use crate::magnet::{error::MagnetBuilder, oauth::OAuthBuilder};
@@ -123,7 +123,7 @@ pub struct Account {
#[serde(rename = "deviza")]
pub currency: Currency,
#[serde(rename = "ibanSzamlaszam")]
- pub iban: String,
+ pub iban: IBAN,
#[serde(rename = "kod")]
pub code: u64,
#[serde(rename = "szamlaszam")]
diff --git a/taler-magnet-bank/src/magnet/error.rs b/taler-magnet-bank/src/magnet/error.rs
@@ -21,7 +21,7 @@ use tracing::{error, Level};
#[derive(Deserialize, Debug)]
struct Header {
- #[serde(alias = "errorCode")]
+ #[serde(rename = "errorCode")]
pub error_code: Option<u16>,
}
@@ -31,11 +31,11 @@ struct Empty {}
#[derive(Deserialize, Error, Debug)]
#[error("{error_code} {short_message} '{long_message}'")]
pub struct MagnetError {
- #[serde(alias = "errorCode")]
+ #[serde(rename = "errorCode")]
pub error_code: u16,
- #[serde(alias = "shortMessage")]
+ #[serde(rename = "shortMessage")]
pub short_message: String,
- #[serde(alias = "longMessage")]
+ #[serde(rename = "longMessage")]
pub long_message: String,
}
@@ -129,9 +129,10 @@ fn parse<'de, T: Deserialize<'de>>(str: &'de str) -> ApiResult<T> {
async fn magnet_json<T: DeserializeOwned>(res: reqwest::Result<Response>) -> ApiResult<T> {
let body = error_handling(res).await?;
let header: Header = parse(&body)?;
- match header.error_code {
- Some(_) => Err(ApiError::Magnet(parse(&body)?)),
- None => parse(&body),
+ if header.error_code.unwrap_or(200) == 200 {
+ parse(&body)
+ } else {
+ Err(ApiError::Magnet(parse(&body)?))
}
}
diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs
@@ -22,18 +22,15 @@ use taler_common::{
cli::ConfigCmd,
config::{parser::ConfigSource, Config},
db::{dbinit, pool},
- taler_main,
- types::payto::{payto, PaytoURI},
- CommonArgs,
+ taler_main, CommonArgs,
};
use taler_magnet_bank::{
adapter::MagnetApi,
config::{DbCfg, ServeCfg, WorkerCfg},
dev::{self, DevCmd},
- keys,
magnet::AuthClient,
+ setup,
worker::Worker,
- HuPayto,
};
pub fn long_version() -> &'static str {
@@ -76,8 +73,6 @@ enum Command {
/// Execute once and return
#[clap(long, short)]
transient: bool,
- // TODO account in config
- account: PaytoURI,
},
/// Run taler-magnet-bank HTTP server
Serve {
@@ -97,7 +92,7 @@ async fn app(args: Args, cfg: Config) -> anyhow::Result<()> {
match args.cmd {
Command::Setup { reset } => {
let cfg = WorkerCfg::parse(&cfg)?;
- keys::setup(cfg, reset).await?
+ setup::setup(cfg, reset).await?
}
Command::Dbinit { reset } => {
let cfg = DbCfg::parse(&cfg)?;
@@ -115,13 +110,7 @@ async fn app(args: Args, cfg: Config) -> anyhow::Result<()> {
let db = DbCfg::parse(&cfg)?;
let pool = pool(db.cfg, "magnet_bank").await?;
let cfg = ServeCfg::parse(&cfg)?;
- let api = Arc::new(
- MagnetApi::start(
- pool,
- payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
- )
- .await,
- );
+ let api = Arc::new(MagnetApi::start(pool, cfg.payto).await);
let mut builder = TalerApiBuilder::new();
if let Some(cfg) = cfg.wire_gateway {
builder = builder.wire_gateway(api.clone(), cfg.auth);
@@ -132,19 +121,15 @@ async fn app(args: Args, cfg: Config) -> anyhow::Result<()> {
builder.serve(cfg.serve, None).await?;
}
}
- Command::Worker {
- account,
- transient: _,
- } => {
+ Command::Worker { transient: _ } => {
let db = DbCfg::parse(&cfg)?;
let pool = pool(db.cfg, "magnet_bank").await?;
let cfg = WorkerCfg::parse(&cfg)?;
- let keys = keys::load(&cfg)?;
+ let keys = setup::load(&cfg)?;
let client = reqwest::Client::new();
let client =
AuthClient::new(&client, &cfg.api_url, &cfg.consumer).upgrade(&keys.access_token);
- let account = HuPayto::try_from(&account)?;
- let account = client.account(account.bban()).await?;
+ let account = client.account(cfg.payto.bban()).await?;
let mut db = pool.acquire().await?.detach();
// TODO run in loop and handle errors
let mut worker = Worker {
diff --git a/taler-magnet-bank/src/setup.rs b/taler-magnet-bank/src/setup.rs
@@ -0,0 +1,179 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::io::ErrorKind;
+
+use p256::ecdsa::SigningKey;
+use taler_common::{json_file, types::base32::Base32};
+use tracing::{info, warn};
+
+use crate::{
+ config::WorkerCfg,
+ magnet::{
+ error::{ApiError, MagnetError},
+ AuthClient, Token, TokenAuth,
+ },
+};
+
+#[derive(Default, Debug, serde::Deserialize, serde::Serialize)]
+struct KeysFile {
+ access_token: Option<Token>,
+ signing_key: Option<Base32<32>>,
+}
+
+#[derive(Debug)]
+pub struct Keys {
+ pub access_token: Token,
+ pub signing_key: SigningKey,
+}
+
+pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> {
+ // Load JSON file
+ let file: KeysFile = match json_file::load(&cfg.keys_path) {
+ Ok(file) => file,
+ Err(e) => {
+ return Err(anyhow::anyhow!(
+ "Could not read magnet keys at '{}': {}",
+ cfg.keys_path,
+ e.kind()
+ ))
+ }
+ };
+
+ fn incomplete_err() -> anyhow::Error {
+ anyhow::anyhow!("Missing magnet keys, run 'taler-magnet-bank setup' first")
+ }
+
+ // Check full
+ let access_token = file.access_token.ok_or_else(incomplete_err)?;
+ let signing_key = file.signing_key.ok_or_else(incomplete_err)?;
+
+ // Load signing key
+
+ let signing_key = SigningKey::from_slice(&*signing_key)?;
+
+ Ok(Keys {
+ access_token,
+ signing_key,
+ })
+}
+
+pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> {
+ if reset {
+ if let Err(e) = std::fs::remove_file(&cfg.keys_path) {
+ if e.kind() != ErrorKind::NotFound {
+ Err(e)?;
+ }
+ }
+ }
+ let mut keys = match json_file::load(&cfg.keys_path) {
+ Ok(existing) => existing,
+ Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(),
+ Err(e) => Err(e)?,
+ };
+ let client = reqwest::Client::new();
+ let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer);
+
+ info!("Setup OAuth access token");
+ if keys.access_token.is_none() {
+ let token_request = client.token_request().await?;
+
+ // TODO how to do it in a generic way ?
+ // TODO Ask MagnetBank if they could support out-of-band configuration
+ println!(
+ "Login at {}?oauth_token={}",
+ client.join("/NetBankOAuth/authtoken.xhtml"),
+ token_request.key
+ );
+ let auth_url = passterm::prompt_password_tty(Some("Enter the result URL>"))?;
+ let auth_url = reqwest::Url::parse(&auth_url)?;
+ let token_auth: TokenAuth =
+ serde_urlencoded::from_str(auth_url.query().unwrap_or_default())?;
+ assert_eq!(token_request.key, token_auth.oauth_token);
+
+ let access_token = client.token_access(&token_request, &token_auth).await?;
+ keys.access_token = Some(access_token);
+ json_file::persist(&cfg.keys_path, &keys)?;
+ }
+
+ let client = client.upgrade(keys.access_token.as_ref().unwrap());
+
+ info!("Setup Strong Customer Authentication");
+ // TODO find a proper way to check if SCA is required without triggering SCA.GLOBAL_FEATURE_NOT_ENABLED
+ let request = client.request_sms_code().await?;
+ println!(
+ "A SCA code have been sent through {} to {}",
+ request.channel,
+ request.sent_to.join(", ")
+ );
+ let sca_code = passterm::prompt_password_tty(Some("Enter the code>"))?;
+ if let Err(e) = client.perform_sca(&sca_code).await {
+ // Ignore error if SCA already performed
+ if !matches!(e, ApiError::Magnet(MagnetError { ref short_message, .. }) if short_message == "TOKEN_SCA_HITELESITETT")
+ {
+ return Err(e.into());
+ }
+ }
+
+ info!("Setup public key");
+ // TODO find a proper way to check if a public key have been setup
+ // TODO use the better from/to_array API in the next version of the crypto lib
+ let signing_key = match keys.signing_key {
+ Some(bytes) => SigningKey::from_slice(bytes.as_ref())?,
+ None => {
+ let rand = SigningKey::random(&mut rand_core::OsRng);
+ let array: [u8; 32] = rand.to_bytes().as_slice().try_into().unwrap();
+ keys.signing_key = Some(Base32::from(array));
+ json_file::persist(&cfg.keys_path, &keys)?;
+ rand
+ }
+ };
+ if let Err(e) = client.upload_public_key(&signing_key).await {
+ // Ignore error if public key already uploaded
+ if !matches!(e, ApiError::Magnet(MagnetError { ref short_message, .. }) if short_message== "KULCS_MAR_HASZNALATBAN")
+ {
+ return Err(e.into());
+ }
+ }
+
+ info!("Check account");
+ let res = client.list_accounts().await?;
+ let mut ibans = Vec::new();
+ for partner in res.partners {
+ for account in partner.bank_accounts {
+ if cfg.payto.0 == account.iban {
+ if partner.partner.name != cfg.payto.name {
+ warn!(
+ "Expected name '{}' from config got '{}' from bank",
+ cfg.payto.name, partner.partner.name
+ );
+ }
+ return Ok(());
+ } else {
+ ibans.push(account.iban);
+ }
+ }
+ }
+ Err(anyhow::anyhow!(
+ "Unknown account {} from config, expected one of the following account {}",
+ cfg.payto.0,
+ ibans
+ .into_iter()
+ .map(|it| it.to_string())
+ .collect::<Vec<_>>()
+ .join(", ")
+ ))
+}