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:
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));
+}