taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 9f177f8367f1ee83cf99fc3c3ead7295e6037510
parent 88fad287f9047d4ab097253f39bc44e82729a623
Author: Antoine A <>
Date:   Wed,  8 Jan 2025 18:48:26 +0100

magnet-bank: init wire implementation with WIP setup command

Diffstat:
M.gitignore | 5+++--
MCargo.lock | 435+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 44++++++++++++++++++++++++++------------------
Mcommon/taler-api/Cargo.toml | 4++--
Mcommon/taler-common/Cargo.toml | 4++--
Awire-gateway/magnet-bank/Cargo.toml | 23+++++++++++++++++++++++
Awire-gateway/magnet-bank/src/config.rs | 38++++++++++++++++++++++++++++++++++++++
Awire-gateway/magnet-bank/src/magnet.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awire-gateway/magnet-bank/src/magnet/error.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awire-gateway/magnet-bank/src/magnet/oauth.rs | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awire-gateway/magnet-bank/src/main.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1028 insertions(+), 24 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ .env -target -\ No newline at end of file +target +dev.conf +\ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock @@ -54,12 +54,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -85,6 +128,12 @@ dependencies = [ ] [[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] name = "auto-future" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -317,6 +366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -325,8 +375,22 @@ version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -336,6 +400,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -361,6 +431,16 @@ dependencies = [ ] [[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -638,6 +718,15 @@ dependencies = [ ] [[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -711,6 +800,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -825,6 +929,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] name = "half" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -977,6 +1100,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http 1.2.0", "http-body", "httparse", @@ -989,6 +1113,39 @@ dependencies = [ ] [[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] name = "hyper-util" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1198,6 +1355,12 @@ dependencies = [ ] [[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] name = "is-terminal" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1209,6 +1372,12 @@ dependencies = [ ] [[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.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1334,6 +1503,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] +name = "magnet-bank" +version = "0.1.0" +dependencies = [ + "base64", + "clap", + "form_urlencoded", + "getrandom", + "hmac", + "jiff", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", + "sha1", + "taler-common", + "thiserror 2.0.9", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1401,6 +1593,23 @@ dependencies = [ ] [[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1485,6 +1694,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1763,6 +2016,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 1.2.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] name = "reserve-port" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1913,12 +2210,44 @@ dependencies = [ ] [[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "semver" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2342,6 +2671,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2355,6 +2687,27 @@ dependencies = [ ] [[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "taler-api" version = "0.1.0" dependencies = [ @@ -2571,6 +2924,26 @@ dependencies = [ ] [[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2582,6 +2955,19 @@ dependencies = [ ] [[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2768,6 +3154,12 @@ 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.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2848,6 +3240,19 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2946,6 +3351,36 @@ dependencies = [ ] [[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -1,18 +1,26 @@ -[workspace] -resolver = "2" -members = ["common/taler-api", "common/taler-common", "common/test-utils"] - -[profile.dev] -debug = true - -[workspace.dependencies] -thiserror = "2.0.4" -serde_json = "1.0" -serde = "1.0" -tokio = { version = "1.42", features = ["macros"] } -axum = "0.8.1" -sqlx = { version = "0.8", default-features = false } -url = { version = "2.2", features = ["serde"] } -criterion = { version = "0.5" } -fastrand = "2.2.0" -tracing = "0.1" +[workspace] +resolver = "2" +members = [ + "common/taler-api", + "common/taler-common", + "common/test-utils", + "wire-gateway/magnet-bank", +] + +[profile.dev] +debug = true + +[workspace.dependencies] +thiserror = "2.0" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.42", features = ["macros"] } +axum = "0.8" +sqlx = { version = "0.8", default-features = false } +url = { version = "2.2", features = ["serde"] } +criterion = { version = "0.5" } +fastrand = "2.2" +tracing = "0.1" +tracing-subscriber = "0.3" +clap = { version = "4.5", features = ["derive"] } +jiff = { version = "0.1", default-features = false, features = ["std"] } diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] listenfd = "1.0.0" -tracing-subscriber = "0.3" tracing-test = "0.2" dashmap = "6.1" sqlx = { workspace = true, features = [ @@ -18,8 +17,9 @@ http-body-util = "0.1.2" libdeflater = "1.22.0" ed25519-dalek = { version = "2.1.1", default-features = false } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } -serde = { workspace = true, features = ["derive"] } +serde.workspace = true tracing.workspace= true +tracing-subscriber.workspace= true serde_json.workspace = true axum.workspace = true url.workspace = true diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml @@ -10,8 +10,8 @@ serde_urlencoded = "0.7" glob = "0.3" indexmap = "2.7" tempfile = "3.15" -jiff = { version = "0.1", default-features = false, features = ["std"] } -serde = { workspace = true, features = ["derive"] } +jiff.workspace = true +serde.workspace = true serde_json = { workspace = true, features = ["raw_value"] } url.workspace = true thiserror.workspace = true diff --git a/wire-gateway/magnet-bank/Cargo.toml b/wire-gateway/magnet-bank/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "magnet-bank" +version = "0.1.0" +edition = "2021" + +[dependencies] +reqwest = "0.12" +hmac = "0.12" +sha1 = "0.10" +getrandom = "0.2" +base64 = "0.22" +form_urlencoded = "1.2" +percent-encoding = "2.3" +serde_urlencoded = "0.7.1" +taler-common = { path = "../../common/taler-common" } +serde_json = { workspace = true, features = ["raw_value"] } +jiff = { workspace = true, features = ["serde"] } +clap.workspace = true +serde.workspace = true +thiserror.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +tokio.workspace = true diff --git a/wire-gateway/magnet-bank/src/config.rs b/wire-gateway/magnet-bank/src/config.rs @@ -0,0 +1,38 @@ +/* + 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 reqwest::Url; +use taler_common::config::{Config, ValueError}; + +use crate::magnet::Token; + +pub struct MagnetConfig { + pub api_url: Url, + pub consumer: Token, +} + +impl MagnetConfig { + pub fn parse(cfg: &Config) -> Result<Self, ValueError> { + let sect = cfg.section("magnet-bank"); + Ok(Self { + api_url: sect.parse("URL", "API_URL").require()?, + consumer: Token { + key: sect.str("CONSUMER_KEY").require()?, + secret: sect.str("CONSUMER_SECRET").require()?, + }, + }) + } +} diff --git a/wire-gateway/magnet-bank/src/magnet.rs b/wire-gateway/magnet-bank/src/magnet.rs @@ -0,0 +1,75 @@ +/* + 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 error::ApiResult; + +use crate::magnet::{error::MagnetBuilder, oauth::OAuthBuilder}; + +pub mod error; +mod oauth; + +#[derive(serde::Deserialize, Debug)] +pub struct Token { + #[serde(rename = "oauth_token")] + pub key: String, + #[serde(rename = "oauth_token_secret")] + pub secret: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct TokenAuth { + pub oauth_token: String, + pub oauth_verifier: String, +} + +pub struct AuthClient { + pub client: reqwest::Client, + pub api_url: reqwest::Url, + pub consumer: Token, +} + +impl AuthClient { + pub fn join(&self, path: &str) -> reqwest::Url { + self.api_url.join(path).unwrap() + } + + pub async fn token_request(&self) -> ApiResult<Token> { + self.client + .get(self.join("/NetBankOAuth/token/request")) + .query(&[("oauth_callback", "oob")]) + .oauth(&self.consumer, None, None) + .await + .magnet_call_encoded() + .await + } + + pub async fn token_access( + &self, + token_request: &Token, + token_auth: &TokenAuth, + ) -> ApiResult<Token> { + self.client + .get(self.join("/NetBankOAuth/token/access")) + .oauth( + &self.consumer, + Some(token_request), + Some(&token_auth.oauth_verifier), + ) + .await + .magnet_call_encoded() + .await + } +} diff --git a/wire-gateway/magnet-bank/src/magnet/error.rs b/wire-gateway/magnet-bank/src/magnet/error.rs @@ -0,0 +1,132 @@ +/* + 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 reqwest::{header, Response, StatusCode}; +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::value::RawValue; +use thiserror::Error; +use tracing::error; + +#[derive(Deserialize, Debug)] +pub struct MagnetHeader { + timestamp: jiff::civil::DateTime, + #[serde(alias = "errorCode")] + error_code: Option<u16>, +} + +#[derive(Deserialize, Error, Debug)] +#[error("{error_code} {short_message} '{long_message}'")] +pub struct MagnetError { + #[serde(alias = "errorCode")] + error_code: u16, + #[serde(alias = "shortMessage")] + short_message: String, + #[serde(alias = "longMessage")] + long_message: String, +} + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("transport: {0}")] + Transport(#[from] reqwest::Error), + #[error("magnet: {0}")] + Magnet(#[from] MagnetError), + #[error("JSON body: {0}")] + Json(#[from] serde_json::Error), + #[error("form body: {0}")] + Form(#[from] serde_urlencoded::de::Error), + #[error("status: {0}")] + Status(StatusCode), + #[error("status: {0} '{1}'")] + StatusCause(StatusCode, String), +} + +pub type ApiResult<R> = std::result::Result<R, ApiError>; + +/** Handle error from magnet API calls */ +async fn error_handling(res: reqwest::Result<Response>) -> ApiResult<String> { + let res = res?; + let status = res.status(); + match status { + StatusCode::OK => Ok(res.text().await?), + StatusCode::BAD_REQUEST => Err(ApiError::Status(status)), + StatusCode::FORBIDDEN => { + let cause = res + .headers() + .get(header::WWW_AUTHENTICATE) + .map(|s| s.to_str().unwrap_or_default()) + .unwrap_or_default(); + Err(ApiError::StatusCause(status, cause.to_string())) + } + _ => { + dbg!(&res); + let body = res.text().await?; + dbg!(body); + Err(ApiError::Status(status)) + } + } +} + +/** Parse magnet JSON response */ +async fn magnet_raw_json(res: reqwest::Result<Response>) -> ApiResult<Box<RawValue>> { + let body = error_handling(res).await?; + let raw = RawValue::from_string(body).map_err(ApiError::Json)?; + let header: MagnetHeader = serde_json::from_str(raw.get()).map_err(ApiError::Json)?; + if header.error_code.is_some_and(|it| it != 200) { + let error: MagnetError = serde_json::from_str(raw.get()).map_err(ApiError::Json)?; + Err(ApiError::Magnet(error)) + } else { + Ok(raw) + } +} + +/** Parse magnet JSON response into our own type */ +async fn magnet_json<T: DeserializeOwned>(response: reqwest::Result<Response>) -> ApiResult<T> { + let raw = magnet_raw_json(response).await?; + serde_json::from_str(raw.get()).map_err(ApiError::Json) +} + +/** Parse magnet URL encoded response into our own type */ +async fn magnet_url<T: DeserializeOwned>(response: reqwest::Result<Response>) -> ApiResult<T> { + let body = error_handling(response).await?; + serde_urlencoded::from_str(&body).map_err(ApiError::Form) +} + +pub trait MagnetBuilder { + async fn magnet_call_encoded<T: DeserializeOwned>(self) -> ApiResult<T>; + async fn magnet_call<T: DeserializeOwned>(self) -> ApiResult<T>; + async fn magnet_empty(self) -> ApiResult<()>; + async fn magnet_json<R: DeserializeOwned>(self) -> ApiResult<R>; +} + +impl MagnetBuilder for reqwest::Result<Response> { + async fn magnet_call_encoded<T: DeserializeOwned>(self) -> ApiResult<T> { + magnet_url(self).await + } + + async fn magnet_call<T: DeserializeOwned>(self) -> ApiResult<T> { + magnet_json(self).await + } + + async fn magnet_empty(self) -> ApiResult<()> { + magnet_raw_json(self).await?; + Ok(()) + } + + async fn magnet_json<R: DeserializeOwned>(self) -> ApiResult<R> { + magnet_json(self).await + } +} diff --git a/wire-gateway/magnet-bank/src/magnet/oauth.rs b/wire-gateway/magnet-bank/src/magnet/oauth.rs @@ -0,0 +1,160 @@ +/* + 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::{borrow::Cow, time::SystemTime}; + +use base64::{prelude::BASE64_STANDARD, Engine as _}; +use hmac::{Hmac, Mac}; +use percent_encoding::NON_ALPHANUMERIC; +use reqwest::header::HeaderValue; +use sha1::Sha1; + +use super::Token; + +type HmacSha1 = Hmac<Sha1>; + +/** Generate a secure OAuth nonce */ +fn oauth_nonce() -> String { + // Generate 8 secure random bytes + let mut buf = [0u8; 8]; + getrandom::getrandom(&mut buf).expect("OS rand 8 bytes"); + // Encode as base64 string + BASE64_STANDARD.encode(buf) +} + +/** Generate an OAuth timestamp */ +fn oauth_timestamp() -> u64 { + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards"); + + since_the_epoch.as_secs() +} + +/** Generate a valid OAuth Authorization header */ +fn oauth_header( + method: &reqwest::Method, + url: &reqwest::Url, + consumer: &Token, + access: Option<&Token>, + verifier: Option<&str>, +) -> String { + // Per request value + let oauth_nonce = oauth_nonce(); + let oauth_timestamp = oauth_timestamp().to_string(); + + // Base string + let base_string = { + let oauth_data = { + let mut oauth_query: Vec<(&str, &str)> = vec![ + ("oauth_consumer_key", &consumer.key), + ("oauth_nonce", &oauth_nonce), + ("oauth_signature_method", "HMAC-SHA1"), + ("oauth_timestamp", &oauth_timestamp), + ]; + if let Some(token) = &access { + oauth_query.push(("oauth_token", &token.key)); + } + if let Some(verifier) = &verifier { + oauth_query.push(("oauth_verifier", verifier)); + } + oauth_query.push(("oauth_version", "1.0")); + let mut all_query: Vec<_> = oauth_query + .into_iter() + .map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b))) + .chain(url.query_pairs()) + .collect(); + all_query.sort_unstable(); + + let mut tmp: form_urlencoded::Serializer<'_, String> = + form_urlencoded::Serializer::new(String::new()); + for (k, v) in all_query { + tmp.append_pair(&k, &v); + } + tmp.finish() + }; + let mut stripped = url.clone(); + stripped.set_query(None); + form_urlencoded::Serializer::new(String::new()) + .append_key_only(method.as_str()) + .append_key_only(stripped.as_str()) + .append_key_only(&oauth_data) + .finish() + }; + + // Signature + let key = { + let mut buf = consumer.secret.clone(); + buf.push('&'); + if let Some(token) = access { + buf.push_str(&token.secret); + } + buf + }; + let signature = HmacSha1::new_from_slice(key.as_bytes()) + .expect("HMAC can take key of any size") + .chain_update(base_string.as_bytes()) + .finalize() + .into_bytes(); + let signature_encoded = BASE64_STANDARD.encode(signature); + + // Authorization header + { + let mut buf = "OAuth ".to_string(); + let mut append = |key: &str, value: &str| { + buf.push_str(key); + buf.push_str("=\""); + for part in percent_encoding::percent_encode(value.as_bytes(), NON_ALPHANUMERIC) { + buf.push_str(part); + } + buf.push_str("\","); + }; + append("oauth_consumer_key", &consumer.key); + append("oauth_nonce", &oauth_nonce); + append("oauth_signature_method", "HMAC-SHA1"); + append("oauth_timestamp", &oauth_timestamp); + if let Some(token) = &access { + append("oauth_token", &token.key); + } + if let Some(verifier) = &verifier { + append("oauth_verifier", verifier); + } + append("oauth_version", "1.0"); + append("oauth_signature", &signature_encoded); + buf + } +} + +pub trait OAuthBuilder<T> { + async fn oauth(self, consumer: &Token, access: Option<&Token>, verifier: Option<&str>) -> T; +} + +impl OAuthBuilder<reqwest::Result<reqwest::Response>> for reqwest::RequestBuilder { + async fn oauth( + self, + consumer: &Token, + access: Option<&Token>, + verifier: Option<&str>, + ) -> reqwest::Result<reqwest::Response> { + let (client, req) = self.build_split(); + let mut req = req?; + let header = oauth_header(req.method(), req.url(), consumer, access, verifier); + req.headers_mut() + .append("Authorization", HeaderValue::from_str(&header).unwrap()); + client.execute(req).await + } +} diff --git a/wire-gateway/magnet-bank/src/main.rs b/wire-gateway/magnet-bank/src/main.rs @@ -0,0 +1,132 @@ +/* + 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::{fmt::Display, future::Future, io::BufRead, path::PathBuf}; + +use clap::Parser; +use config::MagnetConfig; +use magnet::{error::ApiError, AuthClient, TokenAuth}; +use taler_common::config::{ + parser::{ConfigSource, ParserErr}, + Config, ValueError, +}; +use tracing::{error, Level}; +use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter}; + +mod config; +mod magnet; + +#[derive(clap::Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Specifies the configuration file + #[clap(long, short)] + #[arg(global = true)] + config: Option<PathBuf>, + + /// Configure logging to use LOGLEVEL + #[clap(long, short('L'))] + #[arg(global = true)] + log: Option<tracing::Level>, + + #[command(subcommand)] + cmd: Command, +} + +#[derive(clap::Subcommand, Debug)] +enum Command { + /// Setup Magnet Bank auth token and account settings for Wire Gateway use + Setup, +} + +fn setup<E: Display>(level: Option<tracing::Level>, app: impl Future<Output = Result<(), E>>) { + // Setup logger + let level = level.unwrap_or(Level::INFO); + let filter: EnvFilter = format!("magnet-bank={level}").into(); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(filter) + .init(); + + // Setup async runtime + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + // Run app + let result = runtime.block_on(app); + if let Err(err) = result { + error!("{}", err); + std::process::exit(1); + } +} + +#[derive(Debug, thiserror::Error)] +enum MagnetError { + #[error("{0}")] + CfgParsing(#[from] ParserErr), + #[error("{0}")] + Config(#[from] ValueError), + #[error("{0}")] + Api(#[from] ApiError), +} + +async fn app(args: Args) -> Result<(), MagnetError> { + let source = ConfigSource::new("magnet-bank", "magnet-bank", "magnet-bank"); + + let cfg = Config::from_file(source, args.config)?; + let cfg = MagnetConfig::parse(&cfg)?; + match args.cmd { + Command::Setup => { + println!("Setup"); + let client = AuthClient { + client: reqwest::Client::new(), + api_url: cfg.api_url, + consumer: cfg.consumer, + }; + 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={}\nEnter the result url>", + client.join("/NetBankOAuth/authtoken.xhtml"), + token_request.key + ); + let prompt = std::io::stdin() + .lock() + .lines() + .next() + .expect("Missing auth URL line") + .expect("Reading auth URL prompt"); + let auth_url = reqwest::Url::parse(&prompt).expect("Auth URL malformed"); + let token_auth: TokenAuth = + serde_urlencoded::from_str(auth_url.query().unwrap_or_default()) + .expect("Auth URL malformed"); + assert_eq!(token_request.key, token_auth.oauth_token); + + let auth_token = client.token_access(&token_request, &token_auth).await?; + dbg!(auth_token); + } + } + Ok(()) +} + +fn main() { + let args = Args::parse(); + setup(args.log, app(args)); +}