diff --git a/Cargo.lock b/Cargo.lock index 987e01c682..24f0e34659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,7 @@ dependencies = [ "actix-utils", "ahash 0.8.3", "base64 0.21.4", - "bitflags 2.4.0", + "bitflags 2.4.1", "brotli", "bytes", "bytestring", @@ -613,7 +613,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.24", + "rustix 0.37.25", "slab", "socket2 0.4.9", "waker-fn", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", @@ -799,18 +799,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-macros" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "backtrace" version = "0.3.69" @@ -923,9 +911,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bitvec" @@ -2085,9 +2073,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.67+curl-8.3.0" +version = "0.4.68+curl-8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" +checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" dependencies = [ "cc", "libc", @@ -2329,7 +2317,7 @@ dependencies = [ "hashbrown 0.14.1", "lock_api", "once_cell", - "parking_lot_core 0.9.8", + "parking_lot_core 0.9.9", ] [[package]] @@ -2409,10 +2397,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ + "powerfmt", "serde", ] @@ -2497,7 +2486,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -2681,9 +2670,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", "signature 2.1.0", @@ -2724,7 +2713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ "curve25519-dalek 4.1.1", - "ed25519 2.2.2", + "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.8", @@ -2913,7 +2902,7 @@ dependencies = [ "unsigned-varint", "utoipa", "utoipa-swagger-ui", - "uuid 1.4.1", + "uuid 1.5.0", ] [[package]] @@ -3151,9 +3140,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "libz-sys", @@ -3866,6 +3855,20 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-api-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + [[package]] name = "http-body" version = "0.4.5" @@ -3877,6 +3880,12 @@ dependencies = [ "pin-project-lite 0.2.13", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -4012,16 +4021,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows-core", ] [[package]] @@ -4072,9 +4081,9 @@ dependencies = [ [[package]] name = "if-watch" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9465340214b296cd17a0009acdb890d6160010b8adf8f78a00d0d7ab270f79f" +checksum = "bbb892e5777fe09e16f3d44de7802f4daa7267ecbe8c466f19d94e25bb0c303e" dependencies = [ "async-io", "core-foundation", @@ -4086,7 +4095,7 @@ dependencies = [ "rtnetlink", "system-configuration", "tokio", - "windows 0.34.0", + "windows 0.51.1", ] [[package]] @@ -4103,7 +4112,7 @@ checksum = "bfbcff6ae46750b15cc594bfd277b188cbddcfdc1817848f97f03f26f8625b9e" dependencies = [ "cfg-if", "js-sys", - "uuid 1.4.1", + "uuid 1.5.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4298,7 +4307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.3", - "rustix 0.38.18", + "rustix 0.38.19", "windows-sys 0.48.0", ] @@ -5360,9 +5369,9 @@ checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -5552,13 +5561,14 @@ dependencies = [ name = "mix-fetch-wasm" version = "1.2.0" dependencies = [ + "async-trait", "futures", + "http-api-client", "js-sys", "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-requests", "rand 0.7.3", - "reqwest", "serde", "serde-wasm-bindgen", "thiserror", @@ -5814,7 +5824,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "libc", ] @@ -5985,6 +5995,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-name-service-common", + "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", "nym-service-provider-directory-common", @@ -5995,7 +6006,6 @@ dependencies = [ "nym-vesting-contract-common", "okapi", "pin-project", - "pretty_env_logger", "rand 0.7.3", "rand 0.8.5", "reqwest", @@ -6015,7 +6025,7 @@ dependencies = [ "tokio-stream", "ts-rs", "url", - "uuid 1.4.1", + "uuid 1.5.0", "zeroize", ] @@ -6029,6 +6039,7 @@ dependencies = [ "getset", "nym-coconut-interface", "nym-mixnet-contract-common", + "nym-node-requests", "schemars", "serde", "ts-rs", @@ -6062,12 +6073,14 @@ dependencies = [ "opentelemetry", "opentelemetry-jaeger", "pretty_env_logger", + "schemars", "semver 0.11.0", "serde", "serde_json", "tracing-opentelemetry", "tracing-subscriber", "tracing-tree", + "utoipa", "vergen", ] @@ -6496,9 +6509,6 @@ dependencies = [ "anyhow", "async-trait", "atty", - "axum", - "axum-macros", - "base64 0.21.4", "bip39", "bs58 0.4.0", "clap 4.4.6", @@ -6506,9 +6516,7 @@ dependencies = [ "dashmap", "dirs 4.0.0", "dotenvy", - "fastrand 2.0.1", "futures", - "hmac 0.12.1", "humantime-serde", "hyper", "lazy_static", @@ -6524,6 +6532,7 @@ dependencies = [ "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", + "nym-node", "nym-pemstore", "nym-sphinx", "nym-statistics-common", @@ -6537,7 +6546,6 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.8", "sqlx 0.5.13", "subtle-encoding", "thiserror", @@ -6547,7 +6555,6 @@ dependencies = [ "tokio-util", "tower", "url", - "x25519-dalek 2.0.0", "zeroize", ] @@ -6615,21 +6622,6 @@ dependencies = [ "serde", ] -[[package]] -name = "nym-http-requests" -version = "0.1.0" -dependencies = [ - "bytecodec", - "bytes", - "http", - "httpcodec", - "nym-ordered-buffer", - "nym-service-providers-common", - "nym-socks5-requests", - "thiserror", - "url", -] - [[package]] name = "nym-inclusion-probability" version = "0.1.0" @@ -6854,6 +6846,56 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-node" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bytes", + "colored", + "dashmap", + "fastrand 2.0.1", + "hmac 0.12.1", + "hyper", + "mime", + "nym-config", + "nym-crypto", + "nym-node-requests", + "nym-task", + "nym-wireguard-types", + "rand 0.7.3", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "utoipa", + "utoipa-swagger-ui", + "x25519-dalek 2.0.0", +] + +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.21.4", + "http-api-client", + "nym-bin-common", + "nym-crypto", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "thiserror", + "tokio", + "utoipa", +] + [[package]] name = "nym-node-tester-utils" version = "0.1.0" @@ -7343,6 +7385,7 @@ dependencies = [ "async-trait", "bs58 0.4.0", "log", + "nym-api-requests", "nym-bin-common", "nym-config", "nym-crypto", @@ -7365,10 +7408,8 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.4", - "boringtun", "cosmrs", "cosmwasm-std", - "dashmap", "eyre", "hmac 0.12.1", "itertools 0.11.0", @@ -7410,6 +7451,7 @@ dependencies = [ "eyre", "flate2", "futures", + "http-api-client", "itertools 0.10.5", "log", "nym-api-requests", @@ -7498,7 +7540,7 @@ dependencies = [ "ip_network_table", "log", "nym-task", - "nym-types", + "nym-wireguard-types", "serde", "tap", "thiserror", @@ -7506,6 +7548,24 @@ dependencies = [ "tokio-tun", ] +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "boringtun", + "dashmap", + "hmac 0.12.1", + "nym-crypto", + "rand 0.7.3", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror", + "utoipa", + "x25519-dalek 2.0.0", +] + [[package]] name = "object" version = "0.32.1" @@ -7575,7 +7635,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -7730,9 +7790,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "overload" @@ -7782,9 +7842,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -7804,7 +7864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.8", + "parking_lot_core 0.9.9", ] [[package]] @@ -7823,13 +7883,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -8167,6 +8227,12 @@ dependencies = [ "universal-hash 0.5.1", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -8704,6 +8770,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -8781,14 +8856,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.1", - "regex-syntax 0.8.0", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -8802,13 +8877,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.0", + "regex-syntax 0.8.2", ] [[package]] @@ -8819,9 +8894,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" @@ -8910,9 +8985,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.3" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +checksum = "fce3045ffa7c981a6ee93f640b538952e155f1ae3a1a02b84547fc7a56b7059a" dependencies = [ "cc", "getrandom 0.2.10", @@ -9188,9 +9263,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.24" +version = "0.37.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4279d76516df406a8bd37e7dff53fd37d1a093f997a3c34a5c21658c126db06d" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" dependencies = [ "bitflags 1.3.2", "errno", @@ -9202,11 +9277,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys 0.4.10", @@ -9538,9 +9613,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] @@ -9585,9 +9660,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -9669,6 +9744,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -10439,7 +10527,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.18", + "rustix 0.38.19", "windows-sys 0.48.0", ] @@ -10451,7 +10539,7 @@ checksum = "3f0a7d05cf78524782337f8edd55cbc578d159a16ad4affe2135c92f7dbac7f0" dependencies = [ "bytes", "digest 0.10.7", - "ed25519 2.2.2", + "ed25519 2.2.3", "ed25519-consensus", "flex-error", "futures", @@ -10556,7 +10644,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.38.18", + "rustix 0.38.19", "windows-sys 0.48.0", ] @@ -10620,13 +10708,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", "js-sys", + "powerfmt", "serde", "time-core", "time-macros", @@ -10802,7 +10891,9 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "native-tls", "tokio", + "tokio-native-tls", "tungstenite", ] @@ -10963,6 +11054,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite 0.2.13", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -10977,11 +11093,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "log", "pin-project-lite 0.2.13", "tracing-attributes", @@ -10990,9 +11105,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -11001,9 +11116,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -11231,6 +11346,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand 0.8.5", "sha1", "thiserror", @@ -11393,6 +11509,12 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "unsigned-varint" version = "0.7.2" @@ -11502,6 +11624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" dependencies = [ "actix-web", + "axum", "mime_guess", "regex", "rust-embed", @@ -11519,9 +11642,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom 0.2.10", "serde", @@ -11822,7 +11945,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.3", + "ring 0.17.4", "untrusted 0.9.0", ] @@ -11964,7 +12087,7 @@ dependencies = [ "tokio", "turn", "url", - "uuid 1.4.1", + "uuid 1.5.0", "waitgroup", "webrtc-mdns", "webrtc-util", @@ -12067,7 +12190,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.18", + "rustix 0.38.19", ] [[package]] @@ -12109,22 +12232,28 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.34.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", + "windows-targets 0.48.5", ] [[package]] name = "windows" -version = "0.48.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets 0.48.5", ] @@ -12189,12 +12318,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -12207,12 +12330,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_i686_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -12225,12 +12342,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -12243,12 +12354,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -12273,12 +12378,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_msvc" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -12293,9 +12392,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" +checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" dependencies = [ "memchr", ] @@ -12488,11 +12587,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 1c3108ba15..4b18c15e2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ members = [ "common/crypto", "common/dkg", "common/execute", - "common/http-requests", + "common/http-api-client", "common/inclusion-probability", "common/ledger", "common/mixnode-common", @@ -78,6 +78,7 @@ members = [ "common/wasm/storage", "common/wasm/utils", "common/wireguard", + "common/wireguard-types", "explorer-api", "explorer-api/explorer-api-requests", "explorer-api/explorer-client", @@ -93,6 +94,8 @@ members = [ "nym-api", "nym-browser-extension/storage", "nym-api/nym-api-requests", + "nym-node", + "nym-node/nym-node-requests", "nym-outfox", "tools/internal/ssl-inject", "tools/internal/sdk-version-bump", @@ -129,6 +132,8 @@ license = "Apache-2.0" [workspace.dependencies] anyhow = "1.0.71" async-trait = "0.1.68" +axum = "0.6.20" +base64 = "0.21.4" bip39 = { version = "2.0.0", features = ["zeroize"] } boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "e1d6360d6ab4529fc942a078e4c54df107abe2ba" } cfg-if = "1.0.0" @@ -152,22 +157,27 @@ dotenvy = "0.15.6" futures = "0.3.28" generic-array = "0.14.7" getrandom = "0.2.10" +hyper = "0.14.27" k256 = "0.13" lazy_static = "1.4.0" log = "0.4" once_cell = "1.7.2" parking_lot = "0.12.1" rand = "0.8.5" -reqwest = "0.11.18" +reqwest = "0.11.22" +schemars = "0.8.1" serde = "1.0.152" serde_json = "1.0.91" tap = "1.0.1" tendermint-rpc = "0.32" # same version as used by cosmrs -thiserror = "1.0.38" +thiserror = "1.0.48" tokio = "1.24.1" tokio-tungstenite = "0.20.1" +tracing = "0.1.37" tungstenite = { version = "0.20.1", default-features = false } ts-rs = "7.0.0" +utoipa = "3.5.0" +utoipa-swagger-ui = "3.1.5" url = "2.4" zeroize = "1.6.0" diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 3ce1f01454..7b8e4a51cb 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] bip39 = { workspace = true } rand = "0.7.3" -thiserror = "1.0" +thiserror = { workspace = true } url = { workspace = true } nym-coconut-interface = { path = "../coconut-interface" } diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index 5e817b6afd..7f9ae7fe89 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -15,6 +15,7 @@ clap_complete_fig = "4.0" log = { workspace = true } pretty_env_logger = "0.4.0" semver = "0.11" +schemars = { workspace = true, features = ["preserve_order"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } @@ -29,6 +30,7 @@ opentelemetry-jaeger = { version = "0.18.0", optional = true, features = [ "isahc_collector_client", ] } tracing-opentelemetry = { version = "0.19.0", optional = true } +utoipa = { workspace = true, optional = true } opentelemetry = { version = "0.19.0", optional = true, features = ["rt-tokio"] } @@ -42,7 +44,9 @@ vergen = { version = "=7.4.3", default-features = false, features = [ [features] default = [] +openapi = ["utoipa"] output_format = ["serde_json"] +bin_info_schema = ["schemars"] tracing = [ "tracing-subscriber", "tracing-tree", diff --git a/common/bin-common/src/build_information/mod.rs b/common/bin-common/src/build_information/mod.rs index 064f8a60fa..06dc1da51c 100644 --- a/common/bin-common/src/build_information/mod.rs +++ b/common/bin-common/src/build_information/mod.rs @@ -81,6 +81,8 @@ impl BinaryBuildInformation { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "bin_info_schema", derive(schemars::JsonSchema))] pub struct BinaryBuildInformationOwned { /// Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable. pub binary_name: String, diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 5de16663de..824294793b 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -69,7 +69,7 @@ impl NymApiTopologyProvider { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_cached_gateways().await { + let gateways = match self.validator_client.get_cached_described_gateways().await { Err(err) => { error!("failed to get network gateways - {err}"); return None; diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index cdce63c4c6..ce6d8d4f91 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -281,29 +281,24 @@ impl GatewayEndpointConfig { .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId) } - pub fn from_node(node: nym_topology::gateway::Node, use_tls: bool) -> Self { - // TODO: in the future this shall return a Result and explicit `use_tls` will be removed in favour of the tls info being available on the struct - if use_tls { - Self::from_topology_node_tls(node) + pub fn from_node( + node: nym_topology::gateway::Node, + must_use_tls: bool, + ) -> Result { + let gateway_listener = if must_use_tls { + node.clients_address_tls() + .ok_or(ClientCoreError::UnsupportedWssProtocol { + gateway: node.identity_key.to_base58_string(), + })? } else { - Self::from_topology_node_no_tls(node) - } - } + node.clients_address() + }; - pub fn from_topology_node_no_tls(node: nym_topology::gateway::Node) -> Self { - GatewayEndpointConfig { + Ok(GatewayEndpointConfig { gateway_id: node.identity_key.to_base58_string(), - gateway_listener: node.clients_address(), + gateway_listener, gateway_owner: node.owner, - } - } - - pub fn from_topology_node_tls(node: nym_topology::gateway::Node) -> Self { - GatewayEndpointConfig { - gateway_id: node.identity_key.to_base58_string(), - gateway_listener: node.clients_address_tls(), - gateway_owner: node.owner, - } + }) } } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 15b6c1a1bf..8118936730 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -126,6 +126,12 @@ pub enum ClientCoreError { #[error("this client has performed gateway initialisation in another session")] NoInitClientPresent, + + #[error("there are no gateways supporting the wss protocol available")] + NoWssGateways, + + #[error("the specified gateway '{gateway}' does not support the wss protocol")] + UnsupportedWssProtocol { gateway: String }, } /// Set of messages that the client can send to listeners via the task manager diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 27516e7750..4b50f0864b 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -67,7 +67,7 @@ pub async fn current_gateways( log::trace!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_cached_gateways().await?; + let gateways = client.get_cached_described_gateways().await?; let valid_gateways = gateways .into_iter() .filter_map(|gateway| gateway.try_into().ok()) @@ -174,7 +174,10 @@ async fn measure_latency(gateway: &gateway::Node) -> Result( rng: &mut R, gateways: &[gateway::Node], + must_use_tls: bool, ) -> Result { + let gateways = filter_by_tls(gateways, must_use_tls)?; + info!( "choosing gateway by latency, pinging {} gateways ...", gateways.len() @@ -210,28 +213,57 @@ pub async fn choose_gateway_by_latency( Ok(chosen.gateway.clone()) } +fn filter_by_tls( + gateways: &[gateway::Node], + must_use_tls: bool, +) -> Result, ClientCoreError> { + if must_use_tls { + let filtered = gateways + .iter() + .filter(|g| g.clients_wss_port.is_some()) + .collect::>(); + + if filtered.is_empty() { + return Err(ClientCoreError::NoWssGateways); + } + + Ok(filtered) + } else { + Ok(gateways.iter().collect()) + } +} + pub(super) fn uniformly_random_gateway( rng: &mut R, gateways: &[gateway::Node], + must_use_tls: bool, ) -> Result { - gateways + filter_by_tls(gateways, must_use_tls)? .choose(rng) .ok_or(ClientCoreError::NoGatewaysOnNetwork) - .cloned() + .map(|&r| r.clone()) } pub(super) fn get_specified_gateway( gateway_identity: IdentityKeyRef, gateways: &[gateway::Node], + must_use_tls: bool, ) -> Result { let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; - gateways + let gateway = gateways .iter() .find(|gateway| gateway.identity_key == user_gateway) - .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string())) - .cloned() + .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string()))?; + + if must_use_tls && gateway.clients_wss_port.is_none() { + return Err(ClientCoreError::UnsupportedWssProtocol { + gateway: gateway_identity.to_string(), + }); + } + + Ok(gateway.clone()) } pub(super) async fn register_with_gateway( diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 27279cfadb..395fdd45c2 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -108,19 +108,20 @@ where let gateway_details = match selection_specification { GatewaySelectionSpecification::UniformRemote { must_use_tls } => { - let gateway = uniformly_random_gateway(&mut rng, &available_gateways)?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)) + let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?; + GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) } GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => { - let gateway = choose_gateway_by_latency(&mut rng, &available_gateways).await?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)) + let gateway = + choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?; + GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) } GatewaySelectionSpecification::Specified { must_use_tls, identity, } => { - let gateway = get_specified_gateway(&identity, &available_gateways)?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)) + let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?; + GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) } GatewaySelectionSpecification::Custom { gateway_identity, diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 0840d5882f..92b1134512 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -45,6 +45,9 @@ features = ["net", "sync", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite] workspace = true +# the choice of this particular tls feature was arbitrary; +# if you reckon a different one would be more appropriate, feel free to change it +features = ["native-tls"] # wasm-only dependencies [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 4b3533ef7e..e16345f446 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -24,6 +24,7 @@ nym-service-provider-directory-common = { path = "../../cosmwasm-smart-contracts serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } reqwest = { workspace = true, features = ["json"] } +http-api-client = { path = "../../../common/http-api-client"} thiserror = { workspace = true } log = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c5ae83a6bf..a04b8f05d5 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -11,7 +11,7 @@ use crate::{ use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; -use nym_api_requests::models::MixNodeBondAnnotated; +use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, @@ -19,6 +19,7 @@ use nym_api_requests::models::{ use nym_network_defaults::NymNetworkDetails; use url::Url; +pub use crate::nym_api::NymApiClientExt; pub use nym_mixnet_contract_common::{ mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, MixId, }; @@ -147,7 +148,7 @@ impl Client { impl Client { pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self { - let nym_api_client = nym_api::Client::new(config.api_url.clone()); + let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -161,7 +162,7 @@ impl Client { where S: OfflineSigner, { - let nym_api_client = nym_api::Client::new(config.api_url.clone()); + let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -177,7 +178,7 @@ impl Client { } pub fn change_nym_api(&mut self, new_endpoint: Url) { - self.nym_api.change_url(new_endpoint) + self.nym_api.change_base_url(new_endpoint) } pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { @@ -241,7 +242,7 @@ pub struct NymApiClient { impl NymApiClient { pub fn new(api_url: Url) -> Self { - let nym_api = nym_api::Client::new(api_url); + let nym_api = nym_api::Client::new(api_url, None); NymApiClient { nym_api } } @@ -251,7 +252,7 @@ impl NymApiClient { } pub fn change_nym_api(&mut self, new_endpoint: Url) { - self.nym_api.change_url(new_endpoint); + self.nym_api.change_base_url(new_endpoint); } pub async fn get_cached_active_mixnodes( @@ -274,6 +275,12 @@ impl NymApiClient { Ok(self.nym_api.get_gateways().await?) } + pub async fn get_cached_described_gateways( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_gateways_described().await?) + } + pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, diff --git a/common/client-libs/validator-client/src/nym_api/error.rs b/common/client-libs/validator-client/src/nym_api/error.rs index 34053daeaf..554a0826d4 100644 --- a/common/client-libs/validator-client/src/nym_api/error.rs +++ b/common/client-libs/validator-client/src/nym_api/error.rs @@ -1,20 +1,7 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use http_api_client::HttpClientError; use nym_api_requests::models::RequestError; -use thiserror::Error; -#[derive(Error, Debug)] -pub enum NymAPIError { - #[error("There was an issue with the REST request - {source}")] - ReqwestClientError { - #[from] - source: reqwest::Error, - }, - - #[error("Not found")] - NotFound, - - #[error("Request failed with error message - {0}")] - GenericRequestFailure(String), - - #[error("The nym API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())] - ApiRequestFailure { status: u16, error: RequestError }, -} +pub type NymAPIError = HttpClientError; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index a7d2f1c1c1..21ac049840 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -3,140 +3,38 @@ use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; +use async_trait::async_trait; +use http_api_client::{ApiClient, NO_PARAMS}; use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use nym_api_requests::models::{ - ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, + ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, RewardEstimationResponse, + MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use nym_name_service_common::response::NamesListResponse; use nym_service_provider_directory_common::response::ServicesListResponse; -use reqwest::{Response, StatusCode}; -use serde::{Deserialize, Serialize}; -use url::Url; pub mod error; pub mod routes; -type PathSegments<'a> = &'a [&'a str]; -type Params<'a, K, V> = &'a [(K, V)]; +pub use http_api_client::Client; -const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; - -#[derive(Clone)] -pub struct Client { - url: Url, - reqwest_client: reqwest::Client, -} - -impl Client { - pub fn new(url: Url) -> Self { - let reqwest_client = reqwest::Client::new(); - Self { - url, - reqwest_client, - } - } - - pub fn change_url(&mut self, new_url: Url) { - self.url = new_url - } - - pub fn current_url(&self) -> &Url { - &self.url - } - - async fn send_get_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> Result - where - K: AsRef, - V: AsRef, - { - let url = create_api_url(&self.url, path, params); - Ok(self.reqwest_client.get(url).send().await?) - } - - async fn query_nym_api( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> Result - where - for<'a> T: Deserialize<'a>, - K: AsRef, - V: AsRef, - { - let res = self.send_get_request(path, params).await?; - if res.status().is_success() { - Ok(res.json().await?) - } else if res.status() == StatusCode::NOT_FOUND { - Err(NymAPIError::NotFound) - } else { - Err(NymAPIError::GenericRequestFailure(res.text().await?)) - } - } - - // This works for endpoints returning Result, ErrorResponse> - async fn query_nym_api_fallible( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> Result - where - for<'a> T: Deserialize<'a>, - K: AsRef, - V: AsRef, - { - let res = self.send_get_request(path, params).await?; - let status = res.status(); - if res.status().is_success() { - Ok(res.json().await?) - } else { - let request_error: RequestError = res.json().await?; - Err(NymAPIError::ApiRequestFailure { - status: status.as_u16(), - error: request_error, - }) - } - } - - async fn post_nym_api( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - json_body: &B, - ) -> Result - where - B: Serialize + ?Sized, - for<'a> T: Deserialize<'a>, - K: AsRef, - V: AsRef, - { - let url = create_api_url(&self.url, path, params); - let response = self.reqwest_client.post(url).json(json_body).send().await?; - if response.status().is_success() { - Ok(response.json().await?) - } else { - Err(NymAPIError::GenericRequestFailure(response.text().await?)) - } - } - - pub async fn get_mixnodes(&self) -> Result, NymAPIError> { - self.query_nym_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait NymApiClientExt: ApiClient { + async fn get_mixnodes(&self) -> Result, NymAPIError> { + self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } - pub async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -148,8 +46,8 @@ impl Client { .await } - pub async fn get_gateways_detailed(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_gateways_detailed(&self) -> Result, NymAPIError> { + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -161,10 +59,10 @@ impl Client { .await } - pub async fn get_mixnodes_detailed_unfiltered( + async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -176,23 +74,29 @@ impl Client { .await } - pub async fn get_gateways(&self) -> Result, NymAPIError> { - self.query_nym_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) + async fn get_gateways(&self) -> Result, NymAPIError> { + self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } - pub async fn get_active_mixnodes(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_gateways_described(&self) -> Result, NymAPIError> { + self.get_json( + &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], + NO_PARAMS, + ) + .await + } + + async fn get_active_mixnodes(&self) -> Result, NymAPIError> { + self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, ) .await } - pub async fn get_active_mixnodes_detailed( - &self, - ) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -205,19 +109,19 @@ impl Client { .await } - pub async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { - self.query_nym_api( + async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { + self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], NO_PARAMS, ) .await } - pub async fn get_mixnode_report( + async fn get_mixnode_report( &self, mix_id: MixId, ) -> Result { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -230,11 +134,11 @@ impl Client { .await } - pub async fn get_gateway_report( + async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, ) -> Result { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -247,11 +151,11 @@ impl Client { .await } - pub async fn get_mixnode_history( + async fn get_mixnode_history( &self, mix_id: MixId, ) -> Result { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -264,11 +168,11 @@ impl Client { .await } - pub async fn get_gateway_history( + async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, ) -> Result { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -281,10 +185,10 @@ impl Client { .await } - pub async fn get_rewarded_mixnodes_detailed( + async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS, @@ -297,13 +201,13 @@ impl Client { .await } - pub async fn get_gateway_core_status_count( + async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, since: Option, ) -> Result { if let Some(since) = since { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -315,7 +219,7 @@ impl Client { ) .await } else { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -328,13 +232,13 @@ impl Client { } } - pub async fn get_mixnode_core_status_count( + async fn get_mixnode_core_status_count( &self, mix_id: MixId, since: Option, ) -> Result { if let Some(since) = since { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -346,7 +250,7 @@ impl Client { ) .await } else { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -360,11 +264,11 @@ impl Client { } } - pub async fn get_mixnode_status( + async fn get_mixnode_status( &self, mix_id: MixId, ) -> Result { - self.query_nym_api( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -377,11 +281,11 @@ impl Client { .await } - pub async fn get_mixnode_reward_estimation( + async fn get_mixnode_reward_estimation( &self, mix_id: MixId, ) -> Result { - self.query_nym_api_fallible( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -394,12 +298,12 @@ impl Client { .await } - pub async fn compute_mixnode_reward_estimation( + async fn compute_mixnode_reward_estimation( &self, mix_id: MixId, request_body: &ComputeRewardEstParam, ) -> Result { - self.post_nym_api( + self.post_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -413,11 +317,11 @@ impl Client { .await } - pub async fn get_mixnode_stake_saturation( + async fn get_mixnode_stake_saturation( &self, mix_id: MixId, ) -> Result { - self.query_nym_api_fallible( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -430,11 +334,11 @@ impl Client { .await } - pub async fn get_mixnode_inclusion_probability( + async fn get_mixnode_inclusion_probability( &self, mix_id: MixId, ) -> Result { - self.query_nym_api_fallible( + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -447,11 +351,8 @@ impl Client { .await } - pub async fn get_mixnode_avg_uptime( - &self, - mix_id: MixId, - ) -> Result { - self.query_nym_api_fallible( + async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result { + self.get_json( &[ routes::API_VERSION, routes::STATUS_ROUTES, @@ -464,11 +365,11 @@ impl Client { .await } - pub async fn blind_sign( + async fn blind_sign( &self, request_body: &BlindSignRequestBody, ) -> Result { - self.post_nym_api( + self.post_json( &[ routes::API_VERSION, routes::COCONUT_ROUTES, @@ -481,11 +382,11 @@ impl Client { .await } - pub async fn verify_bandwidth_credential( + async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, ) -> Result { - self.post_nym_api( + self.post_json( &[ routes::API_VERSION, routes::COCONUT_ROUTES, @@ -498,118 +399,20 @@ impl Client { .await } - pub async fn get_service_providers(&self) -> Result { + async fn get_service_providers(&self) -> Result { log::trace!("Getting service providers"); - self.query_nym_api(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS) + self.get_json(&[routes::API_VERSION, routes::SERVICE_PROVIDERS], NO_PARAMS) .await } - //pub async fn get_registered_names(&self) -> Result, NymAPIError> { - pub async fn get_registered_names(&self) -> Result { + //async fn get_registered_names(&self) -> Result, NymAPIError> { + async fn get_registered_names(&self) -> Result { log::trace!("Getting registered names"); - self.query_nym_api(&[routes::API_VERSION, routes::REGISTERED_NAMES], NO_PARAMS) + self.get_json(&[routes::API_VERSION, routes::REGISTERED_NAMES], NO_PARAMS) .await } } -// utility function that should solve the double slash problem in validator API forever. -fn create_api_url, V: AsRef>( - base: &Url, - segments: PathSegments<'_>, - params: Params<'_, K, V>, -) -> Url { - let mut url = base.clone(); - let mut path_segments = url - .path_segments_mut() - .expect("provided validator url does not have a base!"); - for segment in segments { - let segment = segment.strip_prefix('/').unwrap_or(segment); - let segment = segment.strip_suffix('/').unwrap_or(segment); - - path_segments.push(segment); - } - // I don't understand why compiler couldn't figure out that it's no longer used - // and can be dropped - drop(path_segments); - - if !params.is_empty() { - url.query_pairs_mut().extend_pairs(params); - } - - url -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn creating_api_path() { - let base_url: Url = "http://foomp.com".parse().unwrap(); - - // works with 1 segment - assert_eq!( - "http://foomp.com/foo", - create_api_url(&base_url, &["foo"], NO_PARAMS).as_str() - ); - - // works with 2 segments - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() - ); - - // works with leading slash - assert_eq!( - "http://foomp.com/foo", - create_api_url(&base_url, &["/foo"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() - ); - - // works with trailing slash - assert_eq!( - "http://foomp.com/foo", - create_api_url(&base_url, &["foo/"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() - ); - - // works with both leading and trailing slash - assert_eq!( - "http://foomp.com/foo", - create_api_url(&base_url, &["/foo/"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - create_api_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() - ); - - // adds params - assert_eq!( - "http://foomp.com/foo/bar?foomp=baz", - create_api_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar?arg1=val1&arg2=val2", - create_api_url( - &base_url, - &["/foo/", "/bar/"], - &[("arg1", "val1"), ("arg2", "val2")] - ) - .as_str() - ); - } -} +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NymApiClientExt for Client {} diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index fd97047b6a..df063a114d 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -6,6 +6,7 @@ use nym_network_defaults::NYM_API_VERSION; pub const API_VERSION: &str = NYM_API_VERSION; pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; +pub const DESCRIBED: &str = "described"; pub const DETAILED: &str = "detailed"; pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered"; diff --git a/common/coconut-interface/Cargo.toml b/common/coconut-interface/Cargo.toml index 8f62dcc9b5..e749824e51 100644 --- a/common/coconut-interface/Cargo.toml +++ b/common/coconut-interface/Cargo.toml @@ -8,6 +8,6 @@ description = "Crutch library until there is proper SerDe support for coconut st bs58 = "0.4.0" getset = "0.1.1" serde = { workspace = true, features = ["derive"] } -thiserror = "1" +thiserror = { workspace = true } nym-coconut = {path = "../nymcoconut" } diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index 17f1d4d48d..16f0e44285 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Parser; -use comfy_table::Table; - use crate::context::QueryClientWithNyxd; use crate::utils::{pretty_cosmwasm_coin, show_error}; +use clap::Parser; +use comfy_table::Table; +use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index 3b23f2092e..9bb20cc8d4 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Parser; -use comfy_table::Table; - use crate::context::QueryClientWithNyxd; use crate::utils::{pretty_decimal_with_denom, show_error}; +use clap::Parser; +use comfy_table::Table; +use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { diff --git a/common/commands/src/validator/mixnet/query/query_all_names.rs b/common/commands/src/validator/mixnet/query/query_all_names.rs index 5a17cad30d..fa75752c6c 100644 --- a/common/commands/src/validator/mixnet/query/query_all_names.rs +++ b/common/commands/src/validator/mixnet/query/query_all_names.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Parser; -use comfy_table::Table; -use nym_validator_client::nym_api::error::NymAPIError; - use crate::context::QueryClientWithNyxd; use crate::utils::show_error; +use clap::Parser; +use comfy_table::Table; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::nym_api::error::NymAPIError; #[derive(Debug, Parser)] pub struct Args { diff --git a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs index e13f483f93..74ef07a637 100644 --- a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs +++ b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Parser; -use comfy_table::Table; -use nym_validator_client::nym_api::error::NymAPIError; - use crate::context::QueryClientWithNyxd; use crate::utils::show_error; +use clap::Parser; +use comfy_table::Table; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::nym_api::error::NymAPIError; #[derive(Debug, Parser)] pub struct Args { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index e82be77e29..205b3d7d01 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -18,7 +18,7 @@ serde_repr = "0.1" # we still have to preserve that import for `JsonSchema` for `Layer` type (since we can't use cw_serde macro due to custom serde impl) schemars = "0.8" -thiserror = "1.0" +thiserror = { workspace = true } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } serde-json-wasm = { workspace = true } humantime-serde = "1.1.1" diff --git a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml index d419e25086..6ffc7deb14 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml @@ -14,4 +14,4 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.23" } +thiserror = { workspace = true } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index 3fd7e8a377..a975df45ae 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -14,7 +14,7 @@ cw2 = { workspace = true, optional = true } mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" +thiserror = { workspace = true } ts-rs = { workspace = true, optional = true} [features] diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index f86d919197..a5c6f9e71d 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" async-trait = { workspace = true } log = { workspace = true } -thiserror = "1.0" +thiserror = { workspace = true } tokio = { version = "1.24.1", features = ["sync"]} [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 992005f2d4..481ecbf1f7 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -23,7 +23,7 @@ rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true } serde_bytes = { version = "0.11.6", optional = true } serde_crate = { version = "1.0", optional = true, default_features = false, features = ["derive"], package = "serde" } subtle-encoding = { version = "0.5", features = ["bech32-preview"]} -thiserror = "1.0.37" +thiserror = { workspace = true } zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] } # internal diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index ae001c2df6..a56abdee32 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -145,8 +145,12 @@ impl PublicKey { Self::from_bytes(&bytes) } - pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SignatureError> { - self.0.verify(message, &signature.0) + pub fn verify>( + &self, + message: M, + signature: &Signature, + ) -> Result<(), SignatureError> { + self.0.verify(message.as_ref(), &signature.0) } } @@ -239,16 +243,16 @@ impl PrivateKey { Self::from_bytes(&bytes) } - pub fn sign(&self, message: &[u8]) -> Signature { + pub fn sign>(&self, message: M) -> Signature { let expanded_secret_key = ed25519_dalek::ExpandedSecretKey::from(&self.0); let public_key: PublicKey = self.into(); - let sig = expanded_secret_key.sign(message, &public_key.0); + let sig = expanded_secret_key.sign(message.as_ref(), &public_key.0); Signature(sig) } /// Signs text with the provided Ed25519 private key, returning a base58 signature pub fn sign_text(&self, text: &str) -> String { - let signature_bytes = self.sign(text.as_ref()).to_bytes(); + let signature_bytes = self.sign(text).to_bytes(); bs58::encode(signature_bytes).into_string() } } diff --git a/common/dkg/Cargo.toml b/common/dkg/Cargo.toml index dc1f4c29e9..26808724bf 100644 --- a/common/dkg/Cargo.toml +++ b/common/dkg/Cargo.toml @@ -21,10 +21,10 @@ rand = { version = "0.8.5", default-features = false} rand_chacha = "0.3" rand_core = "0.6.3" sha2 = "0.9" -serde = "1.0" +serde = { workspace = true } serde_derive = "1.0" -thiserror = "1.0" -zeroize = { version = "1.4", features = ["zeroize_derive"] } +thiserror = { workspace = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } nym-pemstore = { path = "../pemstore" } diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml new file mode 100644 index 0000000000..ac43b59576 --- /dev/null +++ b/common/http-api-client/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "http-api-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +# for request timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed +[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] +workspace = true +features = ["tokio"] \ No newline at end of file diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs new file mode 100644 index 0000000000..2d816cc142 --- /dev/null +++ b/common/http-api-client/src/lib.rs @@ -0,0 +1,512 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use reqwest::{IntoUrl, Response, StatusCode}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::time::Duration; +use thiserror::Error; +use tracing::warn; +use url::Url; + +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); + +pub type PathSegments<'a> = &'a [&'a str]; +pub type Params<'a, K, V> = &'a [(K, V)]; + +pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; + +#[derive(Debug, Error)] +pub enum HttpClientError { + #[error("there was an issue with the REST request: {source}")] + ReqwestClientError { + #[from] + source: reqwest::Error, + }, + + #[error("provided url is malformed: {source}")] + MalformedUrl { + #[from] + source: url::ParseError, + }, + + #[error("the requested resource could not be found")] + NotFound, + + #[error("request failed with error message: {0}")] + GenericRequestFailure(String), + + #[error("the request failed with status '{status}'. no additional error message provided")] + RequestFailure { status: StatusCode }, + + #[error("the returned response was empty. status: '{status}'")] + EmptyResponse { status: StatusCode }, + + #[error("failed to resolve request. status: '{status}', additional error message: {error}")] + EndpointFailure { status: StatusCode, error: E }, + + #[cfg(target_arch = "wasm32")] + #[error("the request has timed out")] + RequestTimeout, +} + +/// A simple extendable client wrapper for http request with extra url sanitization. +#[derive(Debug, Clone)] +pub struct Client { + base_url: Url, + reqwest_client: reqwest::Client, + + #[cfg(target_arch = "wasm32")] + request_timeout: Duration, +} + +impl Client { + // no timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed + pub fn new(base_url: Url, timeout: Option) -> Self { + #[cfg(target_arch = "wasm32")] + let reqwest_client = reqwest::Client::new(); + + // TODO: we should probably be propagating the error rather than panicking, + // but that'd break bunch of things due to type changes + #[cfg(not(target_arch = "wasm32"))] + let reqwest_client = reqwest::ClientBuilder::new() + .timeout(timeout.unwrap_or(DEFAULT_TIMEOUT)) + .user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))) + .build() + .expect("Client::new()"); + + Client { + base_url, + reqwest_client, + + #[cfg(target_arch = "wasm32")] + request_timeout: timeout.unwrap_or(DEFAULT_TIMEOUT), + } + } + + pub fn new_url(url: U, timeout: Option) -> Result> + where + U: IntoUrl, + E: Display, + { + // a naive check: if the provided URL does not start with http(s), add that scheme + let str_url = url.as_str(); + + if !str_url.starts_with("http") { + let alt = format!("http://{str_url}"); + warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."); + // TODO: or should we maybe default to https? + Self::new_url(alt, timeout) + } else { + Ok(Self::new(url.into_url()?, timeout)) + } + } + + pub fn change_base_url(&mut self, new_url: Url) { + self.base_url = new_url + } + + pub fn current_url(&self) -> &Url { + &self.base_url + } + + async fn send_get_request( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + K: AsRef, + V: AsRef, + E: Display, + { + let url = sanitize_url(&self.base_url, path, params); + + #[cfg(target_arch = "wasm32")] + { + Ok( + wasmtimer::tokio::timeout( + self.request_timeout, + self.reqwest_client.get(url).send(), + ) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??, + ) + } + + #[cfg(not(target_arch = "wasm32"))] + { + Ok(self.reqwest_client.get(url).send().await?) + } + } + + async fn send_post_request( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized, + K: AsRef, + V: AsRef, + E: Display, + { + let url = sanitize_url(&self.base_url, path, params); + + #[cfg(target_arch = "wasm32")] + { + Ok(wasmtimer::tokio::timeout( + self.request_timeout, + self.reqwest_client.post(url).json(json_body).send(), + ) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??) + } + + #[cfg(not(target_arch = "wasm32"))] + { + Ok(self.reqwest_client.post(url).json(json_body).send().await?) + } + } + + pub async fn get_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + E: Display + DeserializeOwned, + { + let res = self.send_get_request(path, params).await?; + parse_response(res, false).await + } + + pub async fn post_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + E: Display + DeserializeOwned, + { + let res = self.send_post_request(path, params, json_body).await?; + parse_response(res, true).await + } + + pub async fn get_json_endpoint(&self, endpoint: S) -> Result> + where + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef, + { + #[cfg(target_arch = "wasm32")] + let res = { + wasmtimer::tokio::timeout( + self.request_timeout, + self.reqwest_client + .get(self.base_url.join(endpoint.as_ref())?) + .send(), + ) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)?? + }; + + #[cfg(not(target_arch = "wasm32"))] + let res = { + self.reqwest_client + .get(self.base_url.join(endpoint.as_ref())?) + .send() + .await? + }; + + parse_response(res, false).await + } + + pub async fn post_json_endpoint( + &self, + endpoint: S, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef, + { + #[cfg(target_arch = "wasm32")] + let res = { + wasmtimer::tokio::timeout( + self.request_timeout, + self.reqwest_client + .post(self.base_url.join(endpoint.as_ref())?) + .json(json_body) + .send(), + ) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)?? + }; + + #[cfg(not(target_arch = "wasm32"))] + let res = { + self.reqwest_client + .post(self.base_url.join(endpoint.as_ref())?) + .json(json_body) + .send() + .await? + }; + + parse_response(res, true).await + } +} + +// define those methods on the trait for nicer extensions (and not having to type the thing twice) +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ApiClient { + /// 'get' json data from the segment-defined path, i.e. for example `["api", "v1", "mixnodes"]`, + /// with tuple defined key-value parameters, i.e. for example `[("since", "12345")]` + async fn get_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned; + + async fn post_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned; + + /// `get` json data from the provided absolute endpoint, i.e. for example `"/api/v1/mixnodes?since=12345"` + async fn get_json_from(&self, endpoint: S) -> Result> + where + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef + Sync + Send; + + async fn post_json_data_to( + &self, + endpoint: S, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef + Sync + Send; +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ApiClient for Client { + async fn get_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned, + { + self.get_json(path, params).await + } + + async fn post_json( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned, + { + self.post_json(path, params, json_body).await + } + + async fn get_json_from(&self, endpoint: S) -> Result> + where + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef + Sync + Send, + { + self.get_json_endpoint(endpoint).await + } + + async fn post_json_data_to( + &self, + endpoint: S, + json_body: &B, + ) -> Result> + where + B: Serialize + ?Sized + Sync, + for<'a> T: Deserialize<'a>, + E: Display + DeserializeOwned, + S: AsRef + Sync + Send, + { + self.post_json_endpoint(endpoint, json_body).await + } +} + +// utility function that should solve the double slash problem in API urls forever. +pub fn sanitize_url, V: AsRef>( + base: &Url, + segments: PathSegments<'_>, + params: Params<'_, K, V>, +) -> Url { + let mut url = base.clone(); + let mut path_segments = url + .path_segments_mut() + .expect("provided validator url does not have a base!"); + for segment in segments { + let segment = segment.strip_prefix('/').unwrap_or(segment); + let segment = segment.strip_suffix('/').unwrap_or(segment); + + path_segments.push(segment); + } + // I don't understand why compiler couldn't figure out that it's no longer used + // and can be dropped + drop(path_segments); + + if !params.is_empty() { + url.query_pairs_mut().extend_pairs(params); + } + + url +} + +async fn parse_response(res: Response, allow_empty: bool) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + let status = res.status(); + + if !allow_empty { + if let Some(0) = res.content_length() { + return Err(HttpClientError::EmptyResponse { status }); + } + } + + if res.status().is_success() { + Ok(res.json().await?) + } else if res.status() == StatusCode::NOT_FOUND { + Err(HttpClientError::NotFound) + } else { + let Ok(plaintext) = res.text().await else { + return Err(HttpClientError::RequestFailure { status }); + }; + + if let Ok(request_error) = serde_json::from_str(&plaintext) { + Err(HttpClientError::EndpointFailure { + status, + error: request_error, + }) + } else { + Err(HttpClientError::GenericRequestFailure(plaintext)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sanitizing_urls() { + let base_url: Url = "http://foomp.com".parse().unwrap(); + + // works with 1 segment + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str() + ); + + // works with 2 segments + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() + ); + + // works with leading slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() + ); + + // works with trailing slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() + ); + + // works with both leading and trailing slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() + ); + + // adds params + assert_eq!( + "http://foomp.com/foo/bar?foomp=baz", + sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar?arg1=val1&arg2=val2", + sanitize_url( + &base_url, + &["/foo/", "/bar/"], + &[("arg1", "val1"), ("arg2", "val2")] + ) + .as_str() + ); + } +} diff --git a/common/http-requests/Cargo.toml b/common/http-requests/Cargo.toml deleted file mode 100644 index 6245eeedf4..0000000000 --- a/common/http-requests/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "nym-http-requests" -version = "0.1.0" -description = "Helper library for sending HTTP requesters over the Nym mixnet" -edition = { workspace = true } -authors = { workspace = true } -license = { workspace = true } -repository = { workspace = true } - -[dependencies] -nym-socks5-requests = { path = "../socks5/requests" } -nym-ordered-buffer = { path = "../socks5/ordered-buffer" } -nym-service-providers-common = { path = "../../service-providers/common" } -bytecodec = "0.4.15" -httpcodec = "0.2.3" -bytes = "1" -http = "0.2.9" -thiserror = "1" -url = "2" diff --git a/common/http-requests/src/error.rs b/common/http-requests/src/error.rs deleted file mode 100644 index 180a4fd44a..0000000000 --- a/common/http-requests/src/error.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::io; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum MixHttpRequestError { - #[error("invalid Socks5 response")] - InvalidSocks5Response, - - #[error("the received Socks5 response was empty")] - EmptySocks5Response, - - #[error("bytecodec Error: {0}")] - ByteCodecError(#[from] bytecodec::Error), - - #[error("Url parse error: {0}")] - UrlParseError(#[from] url::ParseError), - - #[error("could not resolve socket address from the provided URL")] - SocketAddrResolveError { - #[source] - source: io::Error, - }, -} diff --git a/common/http-requests/src/lib.rs b/common/http-requests/src/lib.rs deleted file mode 100644 index 1905396279..0000000000 --- a/common/http-requests/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use bytecodec::bytes::BytesEncoder; -use bytecodec::io::IoEncodeExt; -use bytecodec::Encode; -use httpcodec::{BodyEncoder, Request, RequestEncoder}; - -pub mod error; -pub mod socks; - -pub fn encode_http_request_as_string( - request: Request>, -) -> Result { - // Encode HTTP request as bytes - let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new())); - encoder.start_encoding(request)?; - let mut buf = Vec::new(); - encoder.encode_all(&mut buf)?; - - Ok(String::from_utf8_lossy(&buf).to_string()) -} - -#[cfg(test)] -mod http_requests_tests { - use super::*; - use httpcodec::{HeaderField, HttpVersion, Method, RequestTarget}; - - fn create_http_get_request() -> Request> { - let mut request = Request::new( - Method::new("GET").unwrap(), - RequestTarget::new("/.wellknown/wallet/validators.json").unwrap(), - HttpVersion::V1_1, - b"".to_vec(), - ); - let mut headers = request.header_mut(); - headers.add_field(HeaderField::new("Host", "nymtech.net").unwrap()); - - request - } - - #[test] - fn http_request_ok() { - // Encode HTTP request as bytes - let request = create_http_get_request(); - let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new())); - encoder.start_encoding(request).unwrap(); - let mut buf = Vec::new(); - encoder.encode_all(&mut buf).unwrap(); - - let body_as_string = String::from_utf8(buf).unwrap(); - - // replace newlines with \r\n - let expected = r"GET /.wellknown/wallet/validators.json HTTP/1.1 -Host: nymtech.net -Content-Length: 0 - -" - .replace('\n', "\r\n"); - - assert_eq!(expected, body_as_string); - } -} diff --git a/common/http-requests/src/socks.rs b/common/http-requests/src/socks.rs deleted file mode 100644 index 952ff1fc52..0000000000 --- a/common/http-requests/src/socks.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error; -use bytecodec::bytes::BytesEncoder; -use bytecodec::bytes::RemainingBytesDecoder; -use bytecodec::io::IoEncodeExt; -use bytecodec::{DecodeExt, Encode}; -use httpcodec::{BodyDecoder, ResponseDecoder}; -use httpcodec::{BodyEncoder, Request, RequestEncoder}; -use nym_service_providers_common::interface::ProviderInterfaceVersion; -use nym_socks5_requests::{SocketData, Socks5ProtocolVersion, Socks5ProviderRequest}; - -pub fn encode_http_request_as_socks_send_request( - provider_interface: ProviderInterfaceVersion, - socks5_protocol: Socks5ProtocolVersion, - conn_id: u64, - request: Request>, - seq: Option, - local_closed: bool, -) -> Result { - // Encode HTTP request as bytes - let mut encoder = RequestEncoder::new(BodyEncoder::new(BytesEncoder::new())); - encoder.start_encoding(request)?; - let mut buf = Vec::new(); - encoder.encode_all(&mut buf)?; - - // Wrap it as SOCKS send request - let request_content = nym_socks5_requests::request::Socks5Request::new_send( - socks5_protocol, - SocketData::new(seq.unwrap_or_default(), conn_id, local_closed, buf), - ); - - // and wrap it in provider request - Ok(Socks5ProviderRequest::new_provider_data( - provider_interface, - request_content, - )) -} - -#[derive(Debug)] -pub struct MixHttpResponse { - // pub connection_id: u64, - // #[deprecated] - // pub is_closed: bool, - pub http_response: httpcodec::Response>, - // #[deprecated] - // pub seq: u64, -} - -impl MixHttpResponse { - pub fn try_from_bytes(b: &[u8]) -> Result { - if b.is_empty() { - Err(error::MixHttpRequestError::EmptySocks5Response) - } else { - let mut decoder = ResponseDecoder::>::default(); - let http_response = decoder.decode_from_bytes(b)?; - - Ok(MixHttpResponse { http_response }) - } - } -} - -// impl TryFrom for MixHttpResponse { -// type Error = error::MixHttpRequestError; -// -// fn try_from(value: Socks5Response) -> Result { -// if let Socks5ResponseContent::NetworkData { content } = value.content { -// content.try_into() -// } else { -// Err(error::MixHttpRequestError::InvalidSocks5Response) -// } -// } -// } -// -// impl TryFrom for MixHttpResponse { -// type Error = error::MixHttpRequestError; -// -// fn try_from(value: SocketData) -> Result { -// if value.data.is_empty() { -// Err(error::MixHttpRequestError::EmptySocks5Response) -// } else { -// let mut decoder = ResponseDecoder::>::default(); -// let http_response = decoder.decode_from_bytes(value.data.as_ref())?; -// -// Ok(MixHttpResponse { -// connection_id: value.header.connection_id, -// is_closed: value.header.local_socket_closed, -// http_response, -// seq: value.header.seq, -// }) -// } -// } -// } - -// pub fn decode_socks_response_as_http_response( -// socks5_response: Socks5Response, -// ) -> Result { -// socks5_response.try_into() -// } -// -// #[cfg(test)] -// mod http_requests_tests { -// use super::*; -// use httpcodec::{HeaderField, HttpVersion, Method, RequestTarget}; -// use nym_service_providers_common::interface::Serializable; -// use nym_socks5_requests::Socks5Response; -// -// fn create_http_get_request() -> Request> { -// let mut request = Request::new( -// Method::new("GET").unwrap(), -// RequestTarget::new("/.wellknown/wallet/validators.json").unwrap(), -// HttpVersion::V1_1, -// b"".to_vec(), -// ); -// let mut headers = request.header_mut(); -// headers.add_field(HeaderField::new("Host", "nymtech.net").unwrap()); -// -// request -// } -// -// fn create_socks5_request_buffer() -> Vec { -// let request = create_http_get_request(); -// let socks5_request = encode_http_request_as_socks_send_request( -// ProviderInterfaceVersion::new_current(), -// Socks5ProtocolVersion::new_current(), -// 99u64, -// request, -// Some(42u64), -// true, -// ) -// .unwrap(); -// socks5_request.into_bytes() -// } -// -// #[test] -// fn request_http_request_content_ok() { -// let buffer = create_socks5_request_buffer(); -// -// // HTTP request string content is as expected -// assert_eq!( -// [71u8, 69u8, 84u8, 32u8, 47u8, 46u8, 119u8, 101u8], -// buffer[19..27] -// ); -// } -// -// /// This test will fail if the framing of the request buffer changes, e.g. when OrderedMessage -// /// changes to have the `index` value as a field, instead of packed with the `data` -// #[test] -// fn request_size_as_expected_ok() { -// let buffer = create_socks5_request_buffer(); -// // println!("{:?}", buffer) // uncomment and run `cargo test -- --nocapture` to view -// -// assert_eq!(108, buffer.len()); // version set to SOCKS5 -// } -// -// #[test] -// fn request_socks5_headers_ok() { -// let buffer = create_socks5_request_buffer(); -// -// assert_eq!(5u8, buffer[0]); // version set to SOCKS5 -// assert_eq!(1u8, buffer[1]); // type is SEND -// assert_eq!(99u8, buffer[9]); // ConnectionId is correct -// assert_eq!(1u8, buffer[10]); // local_closed is true -// } -// -// #[test] -// fn request_ordered_message_ok() { -// let buffer = create_socks5_request_buffer(); -// -// // OrderedMessage index is correct -// assert_eq!(42u8, buffer[18]); -// } -// -// fn create_socks_response() -> Socks5Response { -// // HTTP response is just a string -// let http_response_string = "HTTP/1.1 200 OK\r\nServer: foo/0.0.1\r\n\r\n"; -// -// let data = http_response_string.as_bytes().to_vec(); -// -// // wrap in `NetworkData`, then Socks5Response -// Socks5Response::new( -// Socks5ProtocolVersion::new_current(), -// Socks5ResponseContent::NetworkData { -// content: SocketData::new(42, 99u64, false, data), -// }, -// ) -// } -// -// /// This test will fail is anything in the framing of the socks5_response byte -// /// representation changes -// #[test] -// fn response_byte_size_is_as_expected() { -// let socks5_response = create_socks_response(); -// let buf = socks5_response.into_bytes(); -// -// assert_eq!(57, buf.len()); -// } -// -// #[test] -// fn response_parses() { -// unimplemented!() -// // let socks5_response = create_socks_response(); -// // let response = decode_socks_response_as_http_response(socks5_response).unwrap(); -// // -// // assert_eq!(42u64, response.seq); // OrderedMessage index as expected -// // assert_eq!(HttpVersion::V1_1, response.http_response.http_version()); -// // assert_eq!(200u16, response.http_response.status_code().as_u16()); -// // assert_eq!( -// // "foo/0.0.1", -// // response.http_response.header().get_field("Server").unwrap() -// // ); -// } -// } diff --git a/common/ledger/Cargo.toml b/common/ledger/Cargo.toml index 5ea9badd52..dbbce3e920 100644 --- a/common/ledger/Cargo.toml +++ b/common/ledger/Cargo.toml @@ -10,4 +10,4 @@ bip32 = "0.5.1" k256 = { workspace = true } ledger-transport = "0.10.0" ledger-transport-hid = "0.10.0" -thiserror = "1" \ No newline at end of file +thiserror = { workspace = true } \ No newline at end of file diff --git a/common/mixnode-common/src/verloc/packet.rs b/common/mixnode-common/src/verloc/packet.rs index 80d7f59ac9..3951a7f6b0 100644 --- a/common/mixnode-common/src/verloc/packet.rs +++ b/common/mixnode-common/src/verloc/packet.rs @@ -23,7 +23,7 @@ impl EchoPacket { .chain(keys.public_key().to_bytes().iter().cloned()) .collect::>(); - let signature = keys.private_key().sign(&bytes_to_sign); + let signature = keys.private_key().sign(bytes_to_sign); EchoPacket { sequence_number, @@ -67,7 +67,7 @@ impl EchoPacket { pub(crate) fn construct_reply(self, private_key: &identity::PrivateKey) -> ReplyPacket { let bytes = self.to_bytes(); - let signature = private_key.sign(&bytes); + let signature = private_key.sign(bytes); ReplyPacket { base_packet: self, signature, diff --git a/common/mobile-storage/Cargo.toml b/common/mobile-storage/Cargo.toml index bd58629110..4b02821498 100644 --- a/common/mobile-storage/Cargo.toml +++ b/common/mobile-storage/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" [dependencies] async-trait = { workspace = true } -thiserror = "1.0" +thiserror = { workspace = true } diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index bc5bddb497..5d5e071343 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -12,7 +12,7 @@ cfg-if = { workspace = true } dotenvy = { workspace = true } hex-literal = "0.3.3" once_cell = { workspace = true } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"]} thiserror = { workspace = true } url = { workspace = true } diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index fe353b16ae..b45b5483e9 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -462,6 +462,9 @@ pub const DEFAULT_NYM_API_PORT: u16 = 8080; pub const NYM_API_VERSION: &str = "v1"; +// NYM-NODE +pub const DEFAULT_NYM_NODE_HTTP_PORT: u16 = 8080; + // REWARDING /// We'll be assuming a few more things, profit margin and cost function. Since we don't have reliable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate interval costs to Nyms. We'll also assume a cost of 40$ per interval(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml index 73e427f547..7aed2bb595 100644 --- a/common/nymcoconut/Cargo.toml +++ b/common/nymcoconut/Cargo.toml @@ -11,8 +11,8 @@ bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-seriali itertools = "0.10" digest = "0.9" rand = "0.8" -thiserror = "1.0" -serde = "1.0" +thiserror = { workspace = true } +serde = { workspace = true } serde_derive = "1.0" bs58 = "0.4.0" sha2 = "0.9" diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index 4420d7f258..c73e692e62 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -11,7 +11,7 @@ repository = { workspace = true } nym-crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys nym-sphinx-types = { path = "../types", features = ["sphinx"] } # we need to be able to refer to some types defined inside sphinx crate serde = "1.0" # implementing serialization/deserialization for some types, like `Recipient` -thiserror = "1.0.37" +thiserror = { workspace = true } [dev-dependencies] rand = "0.7" diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index 49367da152..58c7c44890 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -10,8 +10,8 @@ repository = { workspace = true } [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } bs58 = "0.4" -serde = "1.0" -thiserror = "1" +serde = { workspace = true } +thiserror = { workspace = true } nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] } nym-sphinx-addressing = { path = "../addressing" } diff --git a/common/nymsphinx/chunking/Cargo.toml b/common/nymsphinx/chunking/Cargo.toml index 94bec5886b..542c8d28e2 100644 --- a/common/nymsphinx/chunking/Cargo.toml +++ b/common/nymsphinx/chunking/Cargo.toml @@ -12,7 +12,7 @@ repository = { workspace = true } [dependencies] log = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } -thiserror = "1.0.37" +thiserror = { workspace = true } nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } diff --git a/common/nymsphinx/cover/Cargo.toml b/common/nymsphinx/cover/Cargo.toml index e700327c93..d3da16d2db 100644 --- a/common/nymsphinx/cover/Cargo.toml +++ b/common/nymsphinx/cover/Cargo.toml @@ -9,7 +9,7 @@ repository = { workspace = true } [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } -thiserror = "1.0.37" +thiserror = { workspace = true } nym-crypto = { path = "../../crypto" } nym-sphinx-acknowledgements = { path = "../acknowledgements" } diff --git a/common/nymsphinx/forwarding/Cargo.toml b/common/nymsphinx/forwarding/Cargo.toml index 4060cbf076..c3f3e0dc9f 100644 --- a/common/nymsphinx/forwarding/Cargo.toml +++ b/common/nymsphinx/forwarding/Cargo.toml @@ -12,4 +12,4 @@ nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } nym-outfox = { path = "../../../nym-outfox" } -thiserror = "1" +thiserror = { workspace = true } diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml index 3915f204a2..bf2c030e2e 100644 --- a/common/nymsphinx/framing/Cargo.toml +++ b/common/nymsphinx/framing/Cargo.toml @@ -10,7 +10,7 @@ repository = { workspace = true } [dependencies] bytes = "1.0" tokio-util = { version = "0.7.4", features = ["codec"] } -thiserror = "1.0.37" +thiserror = { workspace = true } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] } diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml index 9d9a5a24e2..7933943630 100644 --- a/common/nymsphinx/params/Cargo.toml +++ b/common/nymsphinx/params/Cargo.toml @@ -8,7 +8,7 @@ license = { workspace = true } repository = { workspace = true } [dependencies] -thiserror = "1.0.37" +thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } nym-crypto = { path = "../../crypto", features = ["hashing", "symmetric"] } diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index a756d6b1ba..cbf3db277c 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -294,7 +294,8 @@ mod message_receiver { owner: "foomp4".to_string(), host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), - clients_port: 9000, + clients_ws_port: 9000, + clients_wss_port: None, identity_key: identity::PublicKey::from_base58_string( "FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML", ) diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml index ae348113a0..47ad47c2d2 100644 --- a/common/nymsphinx/types/Cargo.toml +++ b/common/nymsphinx/types/Cargo.toml @@ -10,7 +10,7 @@ repository = { workspace = true } [dependencies] sphinx-packet = { version = "0.1.0", optional = true } nym-outfox = { path = "../../../nym-outfox", optional = true } -thiserror = "1" +thiserror = { workspace = true } [features] default = ["sphinx"] diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index 51b779be29..e883ed60c0 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -51,7 +51,11 @@ where if T::pem_type() != key_pem.tag { return Err(io::Error::new( io::ErrorKind::Other, - "unexpected key pem tag", + format!( + "unexpected key pem tag. Got '{}', expected: '{}'", + key_pem.tag, + T::pem_type() + ), )); } diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index ca4c53023d..a02684958f 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -13,10 +13,10 @@ log = { workspace = true } pin-project = "1.0" rand = { version = "0.7.3", features = ["wasm-bindgen"] } reqwest = { workspace = true } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization tap = "1.0.1" -thiserror = "1.0.34" +thiserror = { workspace = true } tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal"] } url = { workspace = true } diff --git a/common/socks5/ordered-buffer/Cargo.toml b/common/socks5/ordered-buffer/Cargo.toml index faa16769db..b92662f633 100644 --- a/common/socks5/ordered-buffer/Cargo.toml +++ b/common/socks5/ordered-buffer/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] log = { workspace = true } -thiserror = "1.0.37" +thiserror = { workspace = true } diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 1e762b88f1..8a6974096d 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -15,5 +15,5 @@ reqwest = { workspace = true, features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1" sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "chrono"]} -thiserror = "1" +thiserror = { workspace = true } tokio = { version = "1.24.1", features = [ "time" ] } diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 079429cc69..3d43cfe229 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -15,7 +15,7 @@ documentation = { workspace = true } bs58 = "0.4" log = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } -thiserror = "1.0.37" +thiserror = { workspace = true } async-trait = { workspace = true, optional = true } semver = "0.11" @@ -35,6 +35,10 @@ nym-sphinx-types = { path = "../nymsphinx/types", features = ["sphinx", "outfox" nym-sphinx-routing = { path = "../nymsphinx/routing" } nym-bin-common = { path = "../bin-common" } +# I'm not sure how to feel about pulling in this dependency here... +nym-api-requests = { path = "../../nym-api/nym-api-requests" } + + # 'serializable' feature nym-config = { path = "../config", optional = true } diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index ce5acc7638..9d10b8d2a0 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; +use nym_api_requests::models::DescribedGateway; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::GatewayBond; use nym_sphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; @@ -10,6 +11,7 @@ use std::convert::{TryFrom, TryInto}; use std::fmt; use std::fmt::Formatter; use std::io; +use std::net::AddrParseError; use std::net::SocketAddr; use thiserror::Error; @@ -27,6 +29,17 @@ pub enum GatewayConversionError { #[source] source: io::Error, }, + + #[error("'{gateway}' has not provided any valid ip addresses")] + NoIpAddressesProvided { gateway: String }, + + #[error("'{gateway}' has provided a malformed ip address: {err}")] + MalformedIpAddress { + gateway: String, + + #[source] + err: AddrParseError, + }, } #[derive(Clone)] @@ -36,7 +49,13 @@ pub struct Node { // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node pub mix_host: SocketAddr, - pub clients_port: u16, + + // #[serde(alias = "clients_port")] + pub clients_ws_port: u16, + + // #[serde(default)] + pub clients_wss_port: Option, + pub identity_key: identity::PublicKey, pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 pub version: NodeVersion, @@ -48,7 +67,8 @@ impl std::fmt::Debug for Node { .field("host", &self.host) .field("owner", &self.owner) .field("mix_host", &self.mix_host) - .field("clients_port", &self.clients_port) + .field("clients_ws_port", &self.clients_ws_port) + .field("clients_wss_port", &self.clients_wss_port) .field("identity_key", &self.identity_key.to_base58_string()) .field("sphinx_key", &self.sphinx_key.to_base58_string()) .field("version", &self.version) @@ -82,11 +102,17 @@ impl Node { } pub fn clients_address(&self) -> String { - format!("ws://{}:{}", self.host, self.clients_port) + self.clients_address_tls() + .unwrap_or_else(|| self.clients_address_no_tls()) } - pub fn clients_address_tls(&self) -> String { - format!("wss://{}:443", self.host) + pub fn clients_address_no_tls(&self) -> String { + format!("ws://{}:{}", self.host, self.clients_ws_port) + } + + pub fn clients_address_tls(&self) -> Option { + self.clients_wss_port + .map(|p| format!("wss://{}:{p}", self.host)) } } @@ -131,7 +157,8 @@ impl<'a> TryFrom<&'a GatewayBond> for Node { owner: bond.owner.as_str().to_owned(), host, mix_host, - clients_port: bond.gateway.clients_port, + clients_ws_port: bond.gateway.clients_port, + clients_wss_port: None, identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, version: bond.gateway.version.as_str().into(), @@ -146,3 +173,56 @@ impl TryFrom for Node { Node::try_from(&bond) } } + +impl<'a> TryFrom<&'a DescribedGateway> for Node { + type Error = GatewayConversionError; + + fn try_from(value: &'a DescribedGateway) -> Result { + let Some(ref self_described) = value.self_described else { + return (&value.bond).try_into(); + }; + + let ips = &self_described.host_information.ip_address; + if ips.is_empty() { + return Err(GatewayConversionError::NoIpAddressesProvided { + gateway: value.bond.gateway.identity_key.clone(), + }); + } + + let host = match &self_described.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], value.bond.gateway.mix_port); + + Ok(Node { + owner: value.bond.owner.as_str().to_owned(), + host, + mix_host, + clients_ws_port: self_described.mixnet_websockets.ws_port, + clients_wss_port: self_described.mixnet_websockets.wss_port, + identity_key: identity::PublicKey::from_base58_string( + &self_described.host_information.keys.ed25519, + )?, + sphinx_key: encryption::PublicKey::from_base58_string( + &self_described.host_information.keys.x25519, + )?, + version: self_described + .build_information + .build_version + .as_str() + .into(), + }) + } +} + +impl TryFrom for Node { + type Error = GatewayConversionError; + + fn try_from(value: DescribedGateway) -> Result { + Node::try_from(&value) + } +} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 0273cf4a3a..039e30eb94 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -19,6 +19,7 @@ use std::str::FromStr; #[cfg(feature = "serializable")] use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; +use nym_api_requests::models::DescribedGateway; pub mod error; pub mod filter; @@ -403,10 +404,33 @@ impl<'de> Deserialize<'de> for NymTopology { } } -pub fn nym_topology_from_detailed( +pub trait IntoGatewayNode: TryInto +where + >::Error: Display, +{ + fn identity(&self) -> IdentityKeyRef; +} + +impl IntoGatewayNode for GatewayBond { + fn identity(&self) -> IdentityKeyRef { + &self.gateway.identity_key + } +} + +impl IntoGatewayNode for DescribedGateway { + fn identity(&self) -> IdentityKeyRef { + &self.bond.gateway.identity_key + } +} + +pub fn nym_topology_from_detailed( mix_details: Vec, - gateway_bonds: Vec, -) -> NymTopology { + gateway_bonds: Vec, +) -> NymTopology +where + G: IntoGatewayNode, + >::Error: Display, +{ let mut mixes = BTreeMap::new(); for bond in mix_details .into_iter() @@ -435,7 +459,7 @@ pub fn nym_topology_from_detailed( let mut gateways = Vec::with_capacity(gateway_bonds.len()); for bond in gateway_bonds.into_iter() { - let gate_id = bond.gateway.identity_key.clone(); + let gate_id = bond.identity().to_owned(); match bond.try_into() { Ok(gate) => gateways.push(gate), Err(err) => { diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs index 89e943e03e..84770a4868 100644 --- a/common/topology/src/serde.rs +++ b/common/topology/src/serde.rs @@ -190,7 +190,12 @@ pub struct SerializableGateway { #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] #[serde(alias = "clients_port")] - pub clients_port: Option, + #[serde(alias = "clients_ws_port")] + pub clients_ws_port: Option, + + #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] + #[serde(alias = "clients_wss_port")] + pub clients_wss_port: Option, #[serde(alias = "identity_key")] pub identity_key: String, @@ -209,7 +214,9 @@ impl TryFrom for gateway::Node { let host = gateway::Node::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); - let clients_port = value.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT); + let clients_ws_port = value + .clients_ws_port + .unwrap_or(DEFAULT_CLIENT_LISTENING_PORT); let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); // try to completely resolve the host in the mix situation to avoid doing it every @@ -224,7 +231,8 @@ impl TryFrom for gateway::Node { owner: value.owner, host, mix_host, - clients_port, + clients_ws_port, + clients_wss_port: value.clients_wss_port, identity_key: identity::PublicKey::from_base58_string(&value.identity_key) .map_err(GatewayConversionError::from)?, sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) @@ -241,7 +249,8 @@ impl<'a> From<&'a gateway::Node> for SerializableGateway { host: value.host.to_string(), explicit_ip: Some(value.mix_host.ip()), mix_port: Some(value.mix_host.port()), - clients_port: Some(value.clients_port), + clients_ws_port: Some(value.clients_ws_port), + clients_wss_port: value.clients_wss_port, identity_key: value.identity_key.to_base58_string(), sphinx_key: value.sphinx_key.to_base58_string(), version: Some(value.version.to_string()), diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml index f7c79daf7b..4ac66b8a9b 100644 --- a/common/types/Cargo.toml +++ b/common/types/Cargo.toml @@ -8,8 +8,6 @@ rust-version = "1.58" [dependencies] base64 = "0.21.4" -boringtun = { workspace = true, optional = true } -dashmap = "5.5.3" eyre = "0.6.5" hmac = "0.12.1" itertools = "0.11" @@ -41,4 +39,3 @@ tempfile = "3.3.0" [features] default = [] generate-ts = ["nym-mixnet-contract-common/generate-ts"] -wireguard = ["boringtun"] diff --git a/common/types/src/gateway_client_registration.rs b/common/types/src/gateway_client_registration.rs deleted file mode 100644 index e558cd043f..0000000000 --- a/common/types/src/gateway_client_registration.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{net::SocketAddr, ops::Deref, str::FromStr}; - -use crate::wireguard::PeerPublicKey; -use base64::{engine::general_purpose, Engine}; -use dashmap::DashMap; -use hmac::{Hmac, Mac}; -use nym_crypto::asymmetric::encryption::PrivateKey; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use x25519_dalek::StaticSecret; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub enum ClientMessage { - Init(InitMessage), - Final(GatewayClient), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct InitMessage { - pub_key: PeerPublicKey, -} - -impl InitMessage { - pub fn pub_key(&self) -> &PeerPublicKey { - &self.pub_key - } - #[allow(dead_code)] - pub fn new(pub_key: PeerPublicKey) -> Self { - InitMessage { pub_key } - } -} - -// Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. -// Gateway can then verify pub_key payload using the sme process -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GatewayClient { - // base64 encoded public key, using x25519-dalek for impl - pub pub_key: PeerPublicKey, - pub socket: SocketAddr, - pub mac: ClientMac, -} - -pub type HmacSha256 = Hmac; - -impl GatewayClient { - // Reusable secret should be gateways Wireguard PK - // Client should perform this step when generating its payload, using its own WG PK - pub fn verify( - &self, - gateway_key: &PrivateKey, - nonce: u64, - ) -> Result<(), Box> { - #[allow(clippy::expect_used)] - let static_secret = - StaticSecret::try_from(gateway_key.to_bytes()).expect("This is infalliable"); - let dh = static_secret.diffie_hellman(&self.pub_key); - let mut mac = HmacSha256::new_from_slice(dh.as_bytes())?; - mac.update(self.pub_key.as_bytes()); - mac.update(self.socket.ip().to_string().as_bytes()); - mac.update(self.socket.port().to_string().as_bytes()); - mac.update(&nonce.to_le_bytes()); - Ok(mac.verify_slice(&self.mac)?) - } - - pub fn pub_key(&self) -> &PeerPublicKey { - &self.pub_key - } - - pub fn socket(&self) -> SocketAddr { - self.socket - } -} - -#[derive(Debug, Clone)] -pub struct ClientMac(Vec); - -impl ClientMac { - #[allow(dead_code)] - pub fn new(mac: Vec) -> Self { - ClientMac(mac) - } -} - -impl Deref for ClientMac { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl FromStr for ClientMac { - type Err = String; - - fn from_str(s: &str) -> Result { - let mac_bytes: Vec = general_purpose::STANDARD - .decode(s) - .map_err(|_| "Could not base64 decode public key mac representation".to_string())?; - Ok(ClientMac(mac_bytes)) - } -} - -impl Serialize for ClientMac { - fn serialize(&self, serializer: S) -> Result { - let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); - serializer.serialize_str(&encoded_key) - } -} - -impl<'de> Deserialize<'de> for ClientMac { - fn deserialize>(deserializer: D) -> Result { - let encoded_key = String::deserialize(deserializer)?; - ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) - } -} - -pub type GatewayClientRegistry = DashMap; diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs index 956036000a..e44fa517df 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -9,12 +9,8 @@ pub mod error; pub mod fees; pub mod gas; pub mod gateway; -#[cfg(feature = "wireguard")] -pub mod gateway_client_registration; pub mod helpers; pub mod mixnode; pub mod pending_events; pub mod transaction; pub mod vesting; -#[cfg(feature = "wireguard")] -pub mod wireguard; diff --git a/common/types/src/wireguard.rs b/common/types/src/wireguard.rs deleted file mode 100644 index 279b8392ad..0000000000 --- a/common/types/src/wireguard.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::{ - fmt, - hash::{Hash, Hasher}, - ops::Deref, - str::FromStr, -}; - -use base64::{engine::general_purpose, Engine}; -use boringtun::x25519::PublicKey; -use serde::Serialize; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PeerPublicKey(PublicKey); - -impl fmt::Display for PeerPublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", general_purpose::STANDARD.encode(self.0.as_bytes())) - } -} - -impl Hash for PeerPublicKey { - fn hash(&self, state: &mut H) { - self.0.as_bytes().hash(state) - } -} - -impl PeerPublicKey { - #[allow(dead_code)] - pub fn new(key: PublicKey) -> Self { - PeerPublicKey(key) - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Deref for PeerPublicKey { - type Target = PublicKey; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl FromStr for PeerPublicKey { - type Err = String; - - fn from_str(s: &str) -> Result { - let key_bytes: [u8; 32] = general_purpose::STANDARD - .decode(s) - .map_err(|_| "Could not base64 decode public key representation".to_string())? - .try_into() - .map_err(|_| "Invalid key length".to_string())?; - Ok(PeerPublicKey(PublicKey::from(key_bytes))) - } -} - -impl Serialize for PeerPublicKey { - fn serialize(&self, serializer: S) -> Result { - let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes()); - serializer.serialize_str(&encoded_key) - } -} - -impl<'de> serde::Deserialize<'de> for PeerPublicKey { - fn deserialize>(deserializer: D) -> Result { - let encoded_key = String::deserialize(deserializer)?; - Ok(PeerPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))? - } -} diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index 38a9e3f5f9..3e8ec57163 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -14,7 +14,7 @@ js-sys = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { workspace = true, features = ["derive"] } serde-wasm-bindgen = { workspace = true } -thiserror = { workspace = true } +thiserror = { workspace = true } tsify = { workspace = true, features = ["js"] } url = { workspace = true } wasm-bindgen = { workspace = true } diff --git a/common/wireguard-types/Cargo.toml b/common/wireguard-types/Cargo.toml new file mode 100644 index 0000000000..41c5a19b16 --- /dev/null +++ b/common/wireguard-types/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "nym-wireguard-types" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = { workspace = true } +dashmap = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } + +nym-crypto = { path = "../crypto", features = ["asymmetric"] } + +# feature-specific dependencies: + +## verify: +hmac = { version = "0.12.1", optional = true } +sha2 = { version = "0.10.8", optional = true } + +## openapi: +utoipa = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + +# target-specific dependencies +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.boringtun] +workspace = true + +[target."cfg(target_arch = \"wasm32\")".dependencies.x25519-dalek] +version = "2.0.0" + + +[dev-dependencies] +rand = "0.7.3" +nym-crypto = { path = "../crypto", features = ["rand"]} + + +[features] +default = ["verify"] +openapi = ["utoipa", "serde_json"] +# this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm) +verify = ["hmac", "sha2"] \ No newline at end of file diff --git a/common/wireguard-types/src/error.rs b/common/wireguard-types/src/error.rs new file mode 100644 index 0000000000..0bd4a8d3f6 --- /dev/null +++ b/common/wireguard-types/src/error.rs @@ -0,0 +1,35 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("the provided base64-encoded client MAC ('{mac}') was malformed: {source}")] + MalformedClientMac { + mac: String, + #[source] + source: base64::DecodeError, + }, + + #[error("the provided base64-encoded client x25519 public key ('{pub_key}') was malformed: {source}")] + MalformedPeerPublicKeyEncoding { + pub_key: String, + #[source] + source: base64::DecodeError, + }, + + #[error("the provided base64-encoded client x25519 public key ('{pub_key}') has invalid length: {decoded_length}. expected 32 bytes")] + InvalidPeerPublicKeyLength { + pub_key: String, + decoded_length: usize, + }, + + #[cfg(feature = "verify")] + #[error("failed to verify mac provided by '{client}': {source}")] + FailedClientMacVerification { + client: String, + #[source] + source: hmac::digest::MacError, + }, +} diff --git a/common/wireguard-types/src/lib.rs b/common/wireguard-types/src/lib.rs new file mode 100644 index 0000000000..acb3366f86 --- /dev/null +++ b/common/wireguard-types/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +pub mod public_key; +pub mod registration; + +pub use error::Error; +pub use public_key::PeerPublicKey; +pub use registration::{ + ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce, +}; + +#[cfg(feature = "verify")] +pub use registration::HmacSha256; diff --git a/common/wireguard-types/src/public_key.rs b/common/wireguard-types/src/public_key.rs new file mode 100644 index 0000000000..7843f35c4c --- /dev/null +++ b/common/wireguard-types/src/public_key.rs @@ -0,0 +1,90 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::Error; +use base64::engine::general_purpose; +use base64::Engine; +use serde::Serialize; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::str::FromStr; + +// underneath the same library is being used, i.e. x25519-dalek 2.0, +// which is being reexported by boringtun but wasm hates internals of boringtun +#[cfg(target_arch = "wasm32")] +use x25519_dalek::PublicKey as BoringtunPublicKey; + +#[cfg(not(target_arch = "wasm32"))] +use boringtun::x25519::PublicKey as BoringtunPublicKey; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct PeerPublicKey(BoringtunPublicKey); + +impl PeerPublicKey { + #[allow(dead_code)] + pub fn new(key: BoringtunPublicKey) -> Self { + PeerPublicKey(key) + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl fmt::Display for PeerPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(self.0.as_bytes())) + } +} + +impl Hash for PeerPublicKey { + fn hash(&self, state: &mut H) { + self.0.as_bytes().hash(state) + } +} + +impl Deref for PeerPublicKey { + type Target = BoringtunPublicKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for PeerPublicKey { + type Err = Error; + + fn from_str(s: &str) -> Result { + let key_bytes: Vec = general_purpose::STANDARD.decode(s).map_err(|source| { + Error::MalformedPeerPublicKeyEncoding { + pub_key: s.to_string(), + source, + } + })?; + + let decoded_length = key_bytes.len(); + let Ok(key_arr): Result<[u8; 32], _> = key_bytes.try_into() else { + return Err(Error::InvalidPeerPublicKeyLength { + pub_key: s.to_string(), + decoded_length, + })?; + }; + + Ok(PeerPublicKey(BoringtunPublicKey::from(key_arr))) + } +} + +impl Serialize for PeerPublicKey { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> serde::Deserialize<'de> for PeerPublicKey { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + Ok(PeerPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))? + } +} diff --git a/common/wireguard-types/src/registration.rs b/common/wireguard-types/src/registration.rs new file mode 100644 index 0000000000..c899be5d02 --- /dev/null +++ b/common/wireguard-types/src/registration.rs @@ -0,0 +1,231 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use crate::PeerPublicKey; +use base64::{engine::general_purpose, Engine}; +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::encryption::{PrivateKey, PublicKey}; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type GatewayClientRegistry = DashMap; +pub type PendingRegistrations = DashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum ClientMessage { + Initial(InitMessage), + Final(GatewayClient), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct InitMessage { + /// Base64 encoded x25519 public key + #[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))] + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum ClientRegistrationResponse { + PendingRegistration { nonce: u64 }, + Registered { success: bool }, +} + +/// Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + #[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))] + pub pub_key: PeerPublicKey, + + /// Client's socket address + #[cfg_attr(feature = "openapi", schema(example = "1.2.3.4:51820", value_type = String))] + pub socket: SocketAddr, + + /// Sha256 hmac on the data (alongside the prior nonce) + #[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))] + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: PublicKey, + socket_address: SocketAddr, + nonce: u64, + ) -> Self { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = boringtun::x25519::StaticSecret::try_from(local_secret.to_bytes()) + .expect("conversion between x25519 private keys is infallible"); + let local_public: boringtun::x25519::PublicKey = (&static_secret).into(); + + let remote_public = boringtun::x25519::PublicKey::from(remote_public.to_bytes()); + + let dh = static_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(socket_address.ip().to_string().as_bytes()); + mac.update(socket_address.port().to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public), + socket: socket_address, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = boringtun::x25519::StaticSecret::try_from(gateway_key.to_bytes()) + .expect("conversion between x25519 private keys is infallible"); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.socket.ip().to_string().as_bytes()); + mac.update(self.socket.port().to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + pub fn socket(&self) -> SocketAddr { + self.socket + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::encryption; + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let socket: SocketAddr = "1.2.3.4:5678".parse().unwrap(); + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + *gateway_key_pair.public_key(), + socket, + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index c82f52141a..25cb796cc4 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -30,7 +30,7 @@ tap.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } serde = { workspace = true, features = ["derive"] } -nym-types = { path = "../types", features = ["wireguard"] } +nym-wireguard-types = { path = "../wireguard-types" } [target.'cfg(target_os = "linux")'.dependencies] tokio-tun = "0.9.0" diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index a20dc23fa4..5a4520d6c2 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -13,7 +13,7 @@ mod setup; mod udp_listener; mod wg_tunnel; -use nym_types::gateway_client_registration::GatewayClientRegistry; +use nym_wireguard_types::registration::GatewayClientRegistry; use std::sync::Arc; // Currently the module related to setting up the virtual network device is platform specific. diff --git a/common/wireguard/src/registered_peers.rs b/common/wireguard/src/registered_peers.rs index 248f8f85a5..a0a5b7f6dc 100644 --- a/common/wireguard/src/registered_peers.rs +++ b/common/wireguard/src/registered_peers.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, sync::Arc}; use ip_network::IpNetwork; -use nym_types::wireguard::PeerPublicKey; +use nym_wireguard_types::PeerPublicKey; pub(crate) type PeerIdx = u32; @@ -30,7 +30,7 @@ impl RegisteredPeers { pub(crate) async fn insert(&mut self, peer: Arc>) { let peer_idx = { peer.lock().await.index }; - let public_key = { peer.lock().await.public_key.clone() }; + let public_key = { peer.lock().await.public_key }; self.peers.insert(public_key, Arc::clone(&peer)); self.peers_by_idx.insert(peer_idx, peer); } diff --git a/common/wireguard/src/udp_listener.rs b/common/wireguard/src/udp_listener.rs index a62c279f9d..045dd7e892 100644 --- a/common/wireguard/src/udp_listener.rs +++ b/common/wireguard/src/udp_listener.rs @@ -7,7 +7,7 @@ use boringtun::{ use futures::StreamExt; use log::error; use nym_task::TaskClient; -use nym_types::{gateway_client_registration::GatewayClientRegistry, wireguard::PeerPublicKey}; +use nym_wireguard_types::{registration::GatewayClientRegistry, PeerPublicKey}; use tap::TapFallible; use tokio::{ net::UdpSocket, @@ -37,7 +37,7 @@ async fn add_test_peer(registered_peers: &mut RegisteredPeers) { let peer_index = 0; let peer_allowed_ips = setup::peer_allowed_ips(); let test_peer = Arc::new(tokio::sync::Mutex::new(RegisteredPeer { - public_key: peer_static_public.clone(), + public_key: peer_static_public, index: peer_index, allowed_ips: peer_allowed_ips, })); @@ -247,7 +247,7 @@ async fn parse_peer<'a>( } else if gateway_client_registry.contains_key(&peer_public_key) { let peer_idx = registered_peers.next_idx(); let peer = Arc::new(Mutex::new(RegisteredPeer { - public_key: peer_public_key.clone(), + public_key: peer_public_key, index: peer_idx, allowed_ips: setup::peer_allowed_ips(), })); diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index cfe8bc7e30..5855dd5322 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -810,7 +810,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.32", ] [[package]] @@ -1628,9 +1628,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb" dependencies = [ "proc-macro2", ] @@ -1894,7 +1894,7 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.32", ] [[package]] @@ -1927,7 +1927,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.32", ] [[package]] @@ -2041,9 +2041,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.12" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -2065,22 +2065,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.32", ] [[package]] @@ -2427,5 +2427,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.32", ] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 9ec0049b20..effb7d9df7 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -48,3 +48,5 @@ cw3 = "=1.1.0" cw3-fixed-multisig = "=1.1.0" cw4 = "=1.1.0" cw20 = "=1.1.0" + +thiserror = "1.0.48" diff --git a/contracts/coconut-bandwidth/Cargo.toml b/contracts/coconut-bandwidth/Cargo.toml index 5ed8e5f98b..86a6a1bbfb 100644 --- a/contracts/coconut-bandwidth/Cargo.toml +++ b/contracts/coconut-bandwidth/Cargo.toml @@ -23,7 +23,7 @@ cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" +thiserror = { workspace = true } [features] schema-gen = ["nym-coconut-bandwidth-contract-common/schema", "cosmwasm-schema"] \ No newline at end of file diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml index 794c1f3a0f..dbdd097553 100644 --- a/contracts/coconut-dkg/Cargo.toml +++ b/contracts/coconut-dkg/Cargo.toml @@ -22,7 +22,7 @@ cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } cw4 = { workspace = true } serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" +thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/coconut-test/Cargo.toml b/contracts/coconut-test/Cargo.toml index 7cafdc200f..b7497b060b 100644 --- a/contracts/coconut-test/Cargo.toml +++ b/contracts/coconut-test/Cargo.toml @@ -22,7 +22,7 @@ cw-utils = { workspace = true } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" +thiserror = { workspace = true } nym-coconut-bandwidth = { path = "../coconut-bandwidth" } nym-coconut-dkg = { path = "../coconut-dkg" } diff --git a/contracts/ephemera/Cargo.toml b/contracts/ephemera/Cargo.toml index 376eec374e..edcde5d054 100644 --- a/contracts/ephemera/Cargo.toml +++ b/contracts/ephemera/Cargo.toml @@ -23,7 +23,7 @@ cw-controllers = { workspace = true } cw4 = { workspace = true } serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" +thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index ecc25f459e..4f1a4d3bec 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -243,7 +243,7 @@ impl TestSetup { ContractMessageContent::new(Addr::unchecked(owner), proxy, vec![stake], payload); let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content); let plaintext = sign_payload.to_plaintext().unwrap(); - let signature = keypair.private_key().sign(&plaintext); + let signature = keypair.private_key().sign(plaintext); let msg_signature = MessageSignature::from(signature.to_bytes().as_ref()); (mixnode, msg_signature) diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 6824f2e309..a6f1ba5d0e 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -39,7 +39,7 @@ cw-storage-plus = { workspace = true } bs58 = "0.4.0" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.23" } +thiserror = { workspace = true } time = { version = "0.3", features = ["macros"] } semver = { version = "1.0.16", default-features = false } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 851d394bfe..2bd98eb175 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -206,7 +206,7 @@ pub mod test_helpers { let sig_bytes = family_owner_keys .private_key() - .sign(&msg.to_plaintext().unwrap()) + .sign(msg.to_plaintext().unwrap()) .to_bytes(); MessageSignature::from(sig_bytes.as_ref()) } @@ -956,7 +956,7 @@ pub mod test_helpers { match message.algorithm { SigningAlgorithm::Ed25519 => { let plaintext = message.to_plaintext().unwrap(); - let signature = private_key.sign(&plaintext); + let signature = private_key.sign(plaintext); MessageSignature::from(signature.to_bytes().as_ref()) } SigningAlgorithm::Secp256k1 => { diff --git a/contracts/multisig/cw4-group/Cargo.toml b/contracts/multisig/cw4-group/Cargo.toml index a2897779b9..6c63d707ef 100644 --- a/contracts/multisig/cw4-group/Cargo.toml +++ b/contracts/multisig/cw4-group/Cargo.toml @@ -41,5 +41,5 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.23" } +thiserror = { workspace = true } diff --git a/contracts/name-service/Cargo.toml b/contracts/name-service/Cargo.toml index f680b41c35..e2b5ddb753 100644 --- a/contracts/name-service/Cargo.toml +++ b/contracts/name-service/Cargo.toml @@ -22,7 +22,7 @@ nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" } semver = { version = "1.0.16", default-features = false } serde = { version = "1.0.155", default-features = false, features = ["derive"] } -thiserror = "1.0.39" +thiserror = { workspace = true } [build-dependencies] vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] } diff --git a/contracts/name-service/src/test_helpers/signing.rs b/contracts/name-service/src/test_helpers/signing.rs index d7431fc7ed..2a2a280290 100644 --- a/contracts/name-service/src/test_helpers/signing.rs +++ b/contracts/name-service/src/test_helpers/signing.rs @@ -29,7 +29,7 @@ pub fn ed25519_sign_message( match message.algorithm { SigningAlgorithm::Ed25519 => { let plaintext = message.to_plaintext().unwrap(); - let signature = private_key.sign(&plaintext); + let signature = private_key.sign(plaintext); MessageSignature::from(signature.to_bytes().as_ref()) } SigningAlgorithm::Secp256k1 => { diff --git a/contracts/service-provider-directory/Cargo.toml b/contracts/service-provider-directory/Cargo.toml index 39ffcaf6f7..e6b36d3346 100644 --- a/contracts/service-provider-directory/Cargo.toml +++ b/contracts/service-provider-directory/Cargo.toml @@ -22,7 +22,7 @@ nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts nym-service-provider-directory-common = { path = "../../common/cosmwasm-smart-contracts/service-provider-directory" } semver = { version = "1.0.16", default-features = false } serde = { version = "1.0.155", default-features = false, features = ["derive"] } -thiserror = "1.0.39" +thiserror = { workspace = true } [build-dependencies] vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] } diff --git a/contracts/service-provider-directory/src/test_helpers/signing.rs b/contracts/service-provider-directory/src/test_helpers/signing.rs index 4bd1708bff..1d9aed1266 100644 --- a/contracts/service-provider-directory/src/test_helpers/signing.rs +++ b/contracts/service-provider-directory/src/test_helpers/signing.rs @@ -31,7 +31,7 @@ pub fn ed25519_sign_message( match message.algorithm { SigningAlgorithm::Ed25519 => { let plaintext = message.to_plaintext().unwrap(); - let signature = private_key.sign(&plaintext); + let signature = private_key.sign(plaintext); MessageSignature::from(signature.to_bytes().as_ref()) } SigningAlgorithm::Secp256k1 => { diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index f9a4f53d2a..efd3f73006 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -35,7 +35,7 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true, features = ["iterator"] } serde = { version = "1.0", default-features = false, features = ["derive"] } -thiserror = { version = "1.0" } +thiserror ={ workspace = true } semver = { version = "1.0.16", default-features = false } [dev-dependencies] diff --git a/ephemera/Cargo.toml b/ephemera/Cargo.toml index ee991b3876..63ccaa9ab7 100644 --- a/ephemera/Cargo.toml +++ b/ephemera/Cargo.toml @@ -36,7 +36,7 @@ nym-config = { path = "../common/config" } nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } pretty_env_logger = "0.4" refinery = { version = "0.8.7", features = ["rusqlite"], optional = true } -reqwest = { version = "0.11.6", features = ["json"] } +reqwest = { version = "0.11.22", features = ["json"] } # Rocksdb kills compilation times and we're not currently using it. The reason # we comment it out is that rust-analyzer runs with --all-features #rocksdb = { version = "0.21.0", optional = true } @@ -44,14 +44,14 @@ rusqlite = { version = "0.27.0", features = ["bundled"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0.149" serde_json = "1.0.91" -thiserror = "1.0.37" +thiserror = { workspace = true } tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] } tokio-tungstenite = { workspace = true } tokio-util = { version = "0.7.4", features = ["full"] } toml = "0.7.0" unsigned-varint = "0.7.1" -utoipa = { version = "3.0.1", features = ["actix_extras"] } -utoipa-swagger-ui = { version = "3.0.2", features = ["actix-web"] } +utoipa = { workspace = true, features = ["actix_extras"] } +utoipa-swagger-ui = { workspace = true, features = ["actix-web"] } uuid = { version = "1.2.2", features = ["v4"] } # Temporary fix to https://github.com/bluejekyll/trust-dns/issues/1946 diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 160ac37bd4..935a76cb52 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -24,10 +24,10 @@ reqwest = { workspace = true } rocket = { version = "0.5.0-rc.2", features = ["json"] } rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" } rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } -schemars = { version = "0.8", features = ["preserve_order"] } -serde = "1.0.126" -serde_json = "1.0.66" -thiserror = "1.0.29" +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } tokio = {version = "1.21.2", features = ["full"] } nym-bin-common = { path = "../common/bin-common"} diff --git a/explorer-api/explorer-api-requests/Cargo.toml b/explorer-api/explorer-api-requests/Cargo.toml index d7139b9f78..825af93a91 100644 --- a/explorer-api/explorer-api-requests/Cargo.toml +++ b/explorer-api/explorer-api-requests/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } ts-rs = { workspace = true, optional = true } diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 66a4676376..0b1c901a40 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -6,6 +6,7 @@ use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; use nym_contracts_common::truncate_decimal; use nym_mixnet_contract_common::MixId; +use nym_validator_client::client::NymApiClientExt; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 43549cb78b..b968ee7b7e 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -15,13 +15,6 @@ rust-version = "1.56" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = "0.6.20" -sha2 = "0.10.8" -hmac = "0.12.1" -axum-macros = "0.3.8" # Useful for debugging axum Handler trait errors -fastrand = "2" -x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } -base64 = "0.21.4" anyhow = { workspace = true } async-trait = { workspace = true } atty = "0.2" @@ -48,7 +41,7 @@ sqlx = { version = "0.5", features = [ "migrate", ] } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } -thiserror = "1" +thiserror = { workspace = true } tokio = { workspace = true, features = [ "rt-multi-thread", "net", @@ -63,6 +56,8 @@ url = { version = "2.2", features = ["serde"] } zeroize = { workspace = true } # internal +nym-node = { path = "../nym-node" } + nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } nym-coconut-interface = { path = "../common/coconut-interface" } @@ -97,4 +92,4 @@ sqlx = { version = "0.5", features = [ ] } [features] -wireguard = ["nym-wireguard", "nym-types/wireguard"] +wireguard = ["nym-wireguard"] diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index df6c59414a..fc46005eba 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -17,7 +17,7 @@ log = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -thiserror = "1.0" +thiserror = { workspace = true } zeroize = { workspace = true } nym-crypto = { path = "../../common/crypto" } diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 97f88c570a..a224e24720 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -137,7 +137,7 @@ impl<'a, S> State<'a, S> { .chain(remote_ephemeral_key.to_bytes().iter().cloned()) .collect(); - let signature = self.identity.private_key().sign(&message); + let signature = self.identity.private_key().sign(message); let zero_iv = stream_cipher::zero_iv::(); stream_cipher::encrypt::( self.derived_shared_keys.as_ref().unwrap().encryption_key(), @@ -191,7 +191,7 @@ impl<'a, S> State<'a, S> { self.remote_pubkey .as_ref() .unwrap() - .verify(&signed_payload, &signature) + .verify(signed_payload, &signature) .map_err(|_| HandshakeError::InvalidSignature) } diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index f8b9e326bd..c58020ca3c 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -25,7 +25,10 @@ use std::path::PathBuf; // Configuration that can be overridden. #[derive(Default)] pub(crate) struct OverrideConfig { - pub(crate) host: Option, + pub(crate) listening_address: Option, + pub(crate) public_ips: Option>, + pub(crate) hostname: Option, + pub(crate) mix_port: Option, pub(crate) clients_port: Option, pub(crate) datastore: Option, @@ -41,7 +44,9 @@ pub(crate) struct OverrideConfig { impl OverrideConfig { pub(crate) fn do_override(self, mut config: Config) -> Result { config = config - .with_optional(Config::with_listening_address, self.host) + .with_optional(Config::with_hostname, self.hostname) + .with_optional(Config::with_public_ips, self.public_ips) + .with_optional(Config::with_listening_address, self.listening_address) .with_optional(Config::with_mix_port, self.mix_port) .with_optional(Config::with_clients_port, self.clients_port) .with_optional_custom_env( diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index c5123703ab..645fbd7ab3 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -17,32 +17,41 @@ use std::{fs, io}; #[derive(Args, Clone)] pub struct Init { /// Id of the gateway we want to create config for - #[arg(long)] + #[clap(long)] id: String, - /// The custom host on which the gateway will be running for receiving sphinx packets - #[arg(long)] - host: IpAddr, + /// The listening address on which the gateway will be receiving sphinx packets and listening for client data + #[clap(long, alias = "host")] + listening_address: IpAddr, + + /// Comma separated list of public ip addresses that will announced to the nym-api and subsequently to the clients. + /// In nearly all circumstances, it's going to be identical to the address you're going to use for bonding. + #[clap(long, value_delimiter = ',')] + public_ips: Option>, + + /// Optional hostname associated with this gateway that will announced to the nym-api and subsequently to the clients + #[clap(long)] + hostname: Option, /// The port on which the gateway will be listening for sphinx packets - #[arg(long)] + #[clap(long)] mix_port: Option, /// The port on which the gateway will be listening for clients gateway-requests - #[arg(long)] + #[clap(long)] clients_port: Option, /// Path to sqlite database containing all gateway persistent data - #[arg(long)] + #[clap(long)] datastore: Option, /// Comma separated list of endpoints of nym APIs - #[arg(long, alias = "validator_apis", value_delimiter = ',')] + #[clap(long, alias = "validator_apis", value_delimiter = ',')] // the alias here is included for backwards compatibility (1.1.4 and before) nym_apis: Option>, /// Comma separated list of endpoints of the validator - #[arg( + #[clap( long, alias = "validators", alias = "nyxd_validators", @@ -53,43 +62,43 @@ pub struct Init { nyxd_urls: Option>, /// Cosmos wallet mnemonic needed for double spending protection - #[arg(long)] + #[clap(long)] mnemonic: Option, /// Set this gateway to work only with coconut credentials; that would disallow clients to /// bypass bandwidth credential requirement - #[arg(long, hide = true)] + #[clap(long, hide = true)] only_coconut_credentials: Option, /// Enable/disable gateway anonymized statistics that get sent to a statistics aggregator server - #[arg(long)] + #[clap(long)] enabled_statistics: Option, /// URL where a statistics aggregator is running. The default value is a Nym aggregator server - #[arg(long)] + #[clap(long)] statistics_service_url: Option, /// Allows this gateway to run an embedded network requester for minimal network overhead - #[arg(long)] + #[clap(long)] with_network_requester: bool, // ##### NETWORK REQUESTER FLAGS ##### /// Specifies whether this network requester should run in 'open-proxy' mode - #[arg(long, requires = "with_network_requester")] + #[clap(long, requires = "with_network_requester")] open_proxy: Option, /// Enable service anonymized statistics that get sent to a statistics aggregator server - #[arg(long, requires = "with_network_requester")] + #[clap(long, requires = "with_network_requester")] enable_statistics: Option, /// Mixnet client address where a statistics aggregator is running. The default value is a Nym /// aggregator client - #[arg(long, requires = "with_network_requester")] + #[clap(long, requires = "with_network_requester")] statistics_recipient: Option, /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init - #[arg( + #[clap( long, hide = true, conflicts_with = "medium_toggle", @@ -98,7 +107,7 @@ pub struct Init { fastmode: bool, /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) - #[arg( + #[clap( long, hide = true, conflicts_with = "medium_toggle", @@ -108,7 +117,7 @@ pub struct Init { /// Enable medium mixnet traffic, for experiments only. /// This includes things like disabling cover traffic, no per hop delays, etc. - #[arg( + #[clap( long, hide = true, conflicts_with = "no_cover", @@ -117,14 +126,16 @@ pub struct Init { )] medium_toggle: bool, - #[arg(short, long, default_value_t = OutputFormat::default())] + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } impl From for OverrideConfig { fn from(init_config: Init) -> Self { OverrideConfig { - host: Some(init_config.host), + listening_address: Some(init_config.listening_address), + public_ips: init_config.public_ips, + hostname: init_config.hostname, mix_port: init_config.mix_port, clients_port: init_config.clients_port, datastore: init_config.datastore, @@ -244,7 +255,9 @@ mod tests { async fn create_gateway_with_in_mem_storage() { let args = Init { id: "foo-id".to_string(), - host: "1.1.1.1".parse().unwrap(), + listening_address: "1.1.1.1".parse().unwrap(), + public_ips: None, + hostname: None, mix_port: Some(42), clients_port: Some(43), datastore: Some("/foo-datastore".parse().unwrap()), diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 6dd01e90bc..95dd0c5707 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -6,9 +6,12 @@ use crate::commands::helpers::{ }; use crate::node::helpers::node_details; use crate::support::config::build_config; +use anyhow::bail; use clap::Args; +use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_config::helpers::SPECIAL_ADDRESSES; +use nym_node::error::NymNodeError; use std::net::IpAddr; use std::path::PathBuf; @@ -18,9 +21,18 @@ pub struct Run { #[clap(long)] id: String, - /// The custom host on which the gateway will be running for receiving sphinx packets + /// The custom listening address on which the gateway will be running for receiving sphinx packets + #[clap(long, alias = "host")] + listening_address: Option, + + /// Comma separated list of public ip addresses that will announced to the nym-api and subsequently to the clients. + /// In nearly all circumstances, it's going to be identical to the address you're going to use for bonding. + #[clap(long, value_delimiter = ',')] + public_ips: Option>, + + /// Optional hostname associated with this gateway that will announced to the nym-api and subsequently to the clients #[clap(long)] - host: Option, + hostname: Option, /// The port on which the gateway will be listening for sphinx packets #[clap(long)] @@ -116,12 +128,18 @@ pub struct Run { #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, + + /// Flag specifying this node will be running in a local setting. + #[clap(long)] + local: bool, } impl From for OverrideConfig { fn from(run_config: Run) -> Self { OverrideConfig { - host: run_config.host, + listening_address: run_config.listening_address, + public_ips: run_config.public_ips, + hostname: run_config.hostname, mix_port: run_config.mix_port, clients_port: run_config.clients_port, datastore: run_config.datastore, @@ -150,7 +168,7 @@ impl<'a> From<&'a Run> for OverrideNetworkRequesterConfig { } } -fn show_binding_warning(address: &str) { +fn show_binding_warning(address: IpAddr) { eprintln!("\n##### NOTE #####"); eprintln!( "\nYou are trying to bind to {address} - you might not be accessible to other nodes\n\ @@ -160,8 +178,33 @@ fn show_binding_warning(address: &str) { eprintln!("\n\n"); } +fn check_public_ips(ips: &[IpAddr], local: bool) -> anyhow::Result<()> { + let mut suspicious_ip = Vec::new(); + for ip in ips { + if SPECIAL_ADDRESSES.contains(ip) { + if !local { + return Err(NymNodeError::InvalidPublicIp { address: *ip }.into()); + } + suspicious_ip.push(ip); + } + } + + if !suspicious_ip.is_empty() { + warn!("\n##### WARNING #####"); + for ip in suspicious_ip { + warn!("The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients.\ + Please make sure this is what you intended to announce.\ + You can ignore this warning if you're running setup on a local network ") + } + warn!("\n##### WARNING #####\n"); + } + Ok(()) +} + pub async fn execute(args: Run) -> anyhow::Result<()> { let id = args.id.clone(); + let local = args.local; + eprintln!("Starting gateway {id}..."); let output = args.output; @@ -171,15 +214,24 @@ pub async fn execute(args: Run) -> anyhow::Result<()> { let config = build_config(id, args)?; ensure_config_version_compatibility(&config)?; + let public_ips = &config.host.public_ips; + if public_ips.is_empty() { + return Err(NymNodeError::NoPublicIps.into()); + } + check_public_ips(public_ips, local)?; + if config.gateway.clients_wss_port.is_some() && config.host.hostname.is_none() { + bail!("attempted to announce 'wss' port without a valid hostname") + } + if SPECIAL_ADDRESSES.contains(&config.gateway.listening_address) { - show_binding_warning(&config.gateway.listening_address.to_string()); + show_binding_warning(config.gateway.listening_address); } let node_details = node_details(&config)?; let gateway = crate::node::create_gateway(config, Some(nr_opts), custom_mixnet).await?; eprintln!( "\nTo bond your gateway you will need to install the Nym wallet, go to https://nymtech.net/get-involved and select the Download button.\n\ - Select the correct version and install it to your machine. You will need to provide the following: \n "); + Select the correct version and install it to your machine. You will need to provide some of the following: \n "); output.to_stdout(&node_details); gateway.run().await diff --git a/gateway/src/commands/upgrade_helpers.rs b/gateway/src/commands/upgrade_helpers.rs index d8555ca215..7b13cc3f53 100644 --- a/gateway/src/commands/upgrade_helpers.rs +++ b/gateway/src/commands/upgrade_helpers.rs @@ -3,6 +3,7 @@ use crate::config::old_config_v1_1_20::ConfigV1_1_20; use crate::config::old_config_v1_1_28::ConfigV1_1_28; +use crate::config::old_config_v1_1_29::ConfigV1_1_29; use crate::config::{default_config_filepath, Config}; use crate::error::GatewayError; use log::info; @@ -20,7 +21,8 @@ fn try_upgrade_v1_1_20_config(id: &str) -> Result { info!("It is going to get updated to the current specification."); let updated_step1: ConfigV1_1_28 = old_config.into(); - let updated: Config = updated_step1.into(); + let updated_step2: ConfigV1_1_29 = updated_step1.into(); + let updated: Config = updated_step2.into(); updated .save_to_default_location() .map_err(|err| GatewayError::ConfigSaveFailure { @@ -42,6 +44,29 @@ fn try_upgrade_v1_1_28_config(id: &str) -> Result { info!("It seems the gateway is using <= v1.1.28 config template."); info!("It is going to get updated to the current specification."); + let updated_step1: ConfigV1_1_29 = old_config.into(); + let updated: Config = updated_step1.into(); + updated + .save_to_default_location() + .map_err(|err| GatewayError::ConfigSaveFailure { + path: default_config_filepath(id), + id: id.to_string(), + source: err, + })?; + + Ok(true) +} + +fn try_upgrade_v1_1_29_config(id: &str) -> Result { + // explicitly load it as v1.1.29 (which is incompatible with the current, i.e. 1.1.30+) + let Ok(old_config) = ConfigV1_1_29::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the gateway is using <= v1.1.29 config template."); + info!("It is going to get updated to the current specification."); + let updated: Config = old_config.into(); updated .save_to_default_location() @@ -61,6 +86,9 @@ pub(crate) fn try_upgrade_config(id: &str) -> Result<(), GatewayError> { if try_upgrade_v1_1_28_config(id)? { return Ok(()); } + if try_upgrade_v1_1_29_config(id)? { + return Ok(()); + } Ok(()) } diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 23620953d6..134964fe94 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -12,9 +12,10 @@ use nym_config::{ DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, }; use nym_network_defaults::mainnet; -use serde::{Deserialize, Serialize}; +use nym_node::config; +use serde::{Deserialize, Deserializer, Serialize}; use std::io; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; use url::Url; @@ -22,6 +23,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) mod old_config_v1_1_20; pub(crate) mod old_config_v1_1_28; +pub(crate) mod old_config_v1_1_29; pub mod persistence; mod template; @@ -38,6 +40,18 @@ const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +fn de_maybe_port<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let port = u16::deserialize(deserializer)?; + if port == 0 { + Ok(None) + } else { + Ok(Some(port)) + } +} + /// Derive default path to gateway's config directory. /// It should get resolved to `$HOME/.nym/gateways//config` pub fn default_config_directory>(id: P) -> PathBuf { @@ -71,8 +85,17 @@ pub struct Config { #[serde(skip)] pub(crate) save_path: Option, + pub host: config::Host, + + #[serde(default)] + pub http: config::Http, + pub gateway: Gateway, + #[serde(default)] + // currently not really used for anything useful + pub wireguard: config::Wireguard, + pub storage_paths: GatewayPaths, pub network_requester: NetworkRequester, @@ -92,9 +115,17 @@ impl NymConfigTemplate for Config { impl Config { pub fn new>(id: S) -> Self { + let default_gateway = Gateway::new_default(id.as_ref()); Config { save_path: None, - gateway: Gateway::new_default(id.as_ref()), + host: config::Host { + // this is a very bad default! + public_ips: vec![default_gateway.listening_address], + hostname: None, + }, + http: Default::default(), + gateway: default_gateway, + wireguard: Default::default(), storage_paths: GatewayPaths::new_default(id.as_ref()), network_requester: Default::default(), logging: Default::default(), @@ -140,6 +171,18 @@ impl Config { } } + #[must_use] + pub fn with_hostname(mut self, hostname: String) -> Self { + self.host.hostname = Some(hostname); + self + } + + #[must_use] + pub fn with_public_ips(mut self, public_ips: Vec) -> Self { + self.host.public_ips = public_ips; + self + } + pub fn with_enabled_network_requester(mut self, enabled_network_requester: bool) -> Self { self.network_requester.enabled = enabled_network_requester; self @@ -184,6 +227,12 @@ impl Config { pub fn with_listening_address(mut self, listening_address: IpAddr) -> Self { self.gateway.listening_address = listening_address; + + let http_port = self.http.bind_address.port(); + self.http.bind_address = SocketAddr::new(listening_address, http_port); + let wg_port = self.wireguard.bind_address.port(); + self.wireguard.bind_address = SocketAddr::new(listening_address, wg_port); + self } @@ -245,6 +294,11 @@ pub struct Gateway { /// (default: 9000) pub clients_port: u16, + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub clients_wss_port: Option, + /// Whether gateway collects and sends anonymized statistics pub enabled_statistics: bool, @@ -279,6 +333,7 @@ impl Gateway { listening_address: inaddr_any(), mix_port: DEFAULT_MIX_LISTENING_PORT, clients_port: DEFAULT_CLIENT_LISTENING_PORT, + clients_wss_port: None, enabled_statistics: false, statistics_service_url: mainnet::STATISTICS_SERVICE_DOMAIN_ADDRESS .parse() diff --git a/gateway/src/config/old_config_v1_1_28.rs b/gateway/src/config/old_config_v1_1_28.rs index 7639d860f9..dff9cc896c 100644 --- a/gateway/src/config/old_config_v1_1_28.rs +++ b/gateway/src/config/old_config_v1_1_28.rs @@ -1,9 +1,10 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::persistence::paths::{GatewayPaths, KeysPaths}; -use crate::config::{Config, Debug, Gateway}; -use nym_bin_common::logging::LoggingSettings; +use crate::config::old_config_v1_1_29::{ + ConfigV1_1_29, DebugV1_1_29, GatewayPathsV1_1_29, GatewayV1_1_29, KeysPathsV1_1_29, + LoggingSettingsV1_1_29, +}; use nym_config::{ must_get_home, read_config_from_toml_file, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR, }; @@ -84,11 +85,13 @@ impl ConfigV1_1_28 { } } -impl From for Config { +impl From for ConfigV1_1_29 { fn from(value: ConfigV1_1_28) -> Self { - Config { + ConfigV1_1_29 { + // \/ ADDED save_path: None, - gateway: Gateway { + // /\ ADDED + gateway: GatewayV1_1_29 { version: value.gateway.version, id: value.gateway.id, only_coconut_credentials: value.gateway.only_coconut_credentials, @@ -101,19 +104,25 @@ impl From for Config { statistics_service_url: value.gateway.statistics_service_url, cosmos_mnemonic: value.gateway.cosmos_mnemonic, }, - storage_paths: GatewayPaths { - keys: KeysPaths { + storage_paths: GatewayPathsV1_1_29 { + keys: KeysPathsV1_1_29 { private_identity_key_file: value.storage_paths.keys.private_identity_key_file, public_identity_key_file: value.storage_paths.keys.public_identity_key_file, private_sphinx_key_file: value.storage_paths.keys.private_sphinx_key_file, public_sphinx_key_file: value.storage_paths.keys.public_sphinx_key_file, }, clients_storage: value.storage_paths.clients_storage, + + // \/ ADDED network_requester_config: None, + // /\ ADDED }, + + // \/ ADDED network_requester: Default::default(), - logging: LoggingSettings {}, - debug: Debug { + // /\ ADDED + logging: LoggingSettingsV1_1_29 {}, + debug: DebugV1_1_29 { packet_forwarding_initial_backoff: value.debug.packet_forwarding_initial_backoff, packet_forwarding_maximum_backoff: value.debug.packet_forwarding_maximum_backoff, initial_connection_timeout: value.debug.initial_connection_timeout, diff --git a/gateway/src/config/old_config_v1_1_29.rs b/gateway/src/config/old_config_v1_1_29.rs new file mode 100644 index 0000000000..e38ff2a57c --- /dev/null +++ b/gateway/src/config/old_config_v1_1_29.rs @@ -0,0 +1,279 @@ +// Copyright 2020-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::persistence::paths::{GatewayPaths, KeysPaths}; +use crate::config::{Config, Debug, Gateway, NetworkRequester}; +use nym_bin_common::logging::LoggingSettings; +use nym_config::{ + must_get_home, read_config_from_toml_file, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR, +}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::io; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use url::Url; + +const DEFAULT_GATEWAYS_DIR: &str = "gateways"; + +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_PRESENCE_SENDING_DELAY: Duration = Duration::from_millis(10_000); +const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); +const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); +const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); +const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; + +const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; +const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + +/// Derive default path to gateway's config directory. +/// It should get resolved to `$HOME/.nym/gateways//config` +pub fn default_config_directory>(id: P) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_GATEWAYS_DIR) + .join(id) + .join(DEFAULT_CONFIG_DIR) +} + +/// Derive default path to gateways's config file. +/// It should get resolved to `$HOME/.nym/gateways//config/config.toml` +pub fn default_config_filepath>(id: P) -> PathBuf { + default_config_directory(id).join(DEFAULT_CONFIG_FILENAME) +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV1_1_29 { + pub private_identity_key_file: PathBuf, + pub public_identity_key_file: PathBuf, + pub private_sphinx_key_file: PathBuf, + pub public_sphinx_key_file: PathBuf, +} + +fn de_maybe_path<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let path = PathBuf::deserialize(deserializer)?; + if path.as_os_str().is_empty() { + Ok(None) + } else { + Ok(Some(path)) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayPathsV1_1_29 { + pub keys: KeysPathsV1_1_29, + + #[serde(alias = "persistent_storage")] + pub clients_storage: PathBuf, + + #[serde(deserialize_with = "de_maybe_path")] + pub network_requester_config: Option, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV1_1_29 {} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_29 { + #[serde(skip)] + pub save_path: Option, + + pub gateway: GatewayV1_1_29, + + pub storage_paths: GatewayPathsV1_1_29, + + pub network_requester: NetworkRequesterV1_1_29, + + #[serde(default)] + pub logging: LoggingSettingsV1_1_29, + + #[serde(default)] + pub debug: DebugV1_1_29, +} + +impl ConfigV1_1_29 { + pub fn read_from_default_path>(id: P) -> io::Result { + read_config_from_toml_file(default_config_filepath(id)) + } +} + +impl From for Config { + fn from(value: ConfigV1_1_29) -> Self { + Config { + save_path: value.save_path, + + // \/ ADDED + host: nym_node::config::Host { + // this is a very bad default! + public_ips: vec![value.gateway.listening_address], + hostname: None, + }, + // /\ ADDED + + // \/ ADDED + http: Default::default(), + // /\ ADDED + gateway: Gateway { + version: value.gateway.version, + id: value.gateway.id, + only_coconut_credentials: value.gateway.only_coconut_credentials, + listening_address: value.gateway.listening_address, + mix_port: value.gateway.mix_port, + clients_port: value.gateway.clients_port, + + // \/ ADDED + clients_wss_port: None, + // /\ ADDED + enabled_statistics: value.gateway.enabled_statistics, + nym_api_urls: value.gateway.nym_api_urls, + nyxd_urls: value.gateway.nyxd_urls, + statistics_service_url: value.gateway.statistics_service_url, + cosmos_mnemonic: value.gateway.cosmos_mnemonic, + }, + // \/ ADDED + wireguard: Default::default(), + // /\ ADDED + storage_paths: GatewayPaths { + keys: KeysPaths { + private_identity_key_file: value.storage_paths.keys.private_identity_key_file, + public_identity_key_file: value.storage_paths.keys.public_identity_key_file, + private_sphinx_key_file: value.storage_paths.keys.private_sphinx_key_file, + public_sphinx_key_file: value.storage_paths.keys.public_sphinx_key_file, + }, + clients_storage: value.storage_paths.clients_storage, + network_requester_config: value.storage_paths.network_requester_config, + }, + network_requester: NetworkRequester { + enabled: value.network_requester.enabled, + }, + logging: LoggingSettings {}, + debug: Debug { + packet_forwarding_initial_backoff: value.debug.packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: value.debug.packet_forwarding_maximum_backoff, + initial_connection_timeout: value.debug.initial_connection_timeout, + maximum_connection_buffer_size: value.debug.maximum_connection_buffer_size, + presence_sending_delay: value.debug.presence_sending_delay, + stored_messages_filename_length: value.debug.stored_messages_filename_length, + message_retrieval_limit: value.debug.message_retrieval_limit, + use_legacy_framed_packet_version: value.debug.use_legacy_framed_packet_version, + }, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct GatewayV1_1_29 { + /// Version of the gateway for which this configuration was created. + pub version: String, + + /// ID specifies the human readable ID of this particular gateway. + pub id: String, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the + /// the mixnet, or if it also accepts non-paying clients + #[serde(default)] + pub only_coconut_credentials: bool, + + /// Address to which this mixnode will bind to and will be listening for packets. + pub listening_address: IpAddr, + + /// Port used for listening for all mixnet traffic. + /// (default: 1789) + pub mix_port: u16, + + /// Port used for listening for all client-related traffic. + /// (default: 9000) + pub clients_port: u16, + + /// Whether gateway collects and sends anonymized statistics + pub enabled_statistics: bool, + + /// Domain address of the statistics service + pub statistics_service_url: Url, + + /// Addresses to APIs from which the node gets the view of the network. + #[serde(alias = "validator_api_urls")] + pub nym_api_urls: Vec, + + /// Addresses to validators which the node uses to check for double spending of ERC20 tokens. + #[serde(alias = "validator_nymd_urls")] + pub nyxd_urls: Vec, + + /// Mnemonic of a cosmos wallet used in checking for double spending. + // #[deprecated(note = "move to storage")] + // TODO: I don't think this should be stored directly in the config... + pub cosmos_mnemonic: bip39::Mnemonic, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct NetworkRequesterV1_1_29 { + pub enabled: bool, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV1_1_29 { + fn default() -> Self { + NetworkRequesterV1_1_29 { enabled: false } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct DebugV1_1_29 { + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Delay between each subsequent presence data being sent. + #[serde(with = "humantime_serde")] + pub presence_sending_delay: Duration, + + /// Length of filenames for new client messages. + pub stored_messages_filename_length: u16, + + /// Number of messages from offline client that can be pulled at once from the storage. + pub message_retrieval_limit: i64, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + pub use_legacy_framed_packet_version: bool, +} + +impl Default for DebugV1_1_29 { + fn default() -> Self { + DebugV1_1_29 { + packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, + presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, + maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, + message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + use_legacy_framed_packet_version: false, + } + } +} diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index 7e7563edf9..0f77d499fc 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -9,7 +9,18 @@ pub(crate) const CONFIG_TEMPLATE: &str = r#" # This is a TOML config file. # For more information, see https://github.com/toml-lang/toml -##### main base mixnode config options ##### +##### main base gateway config options ##### + +[host] +# Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. +public_ips = [ + {{#each host.public_ips }} + '{{this}}', + {{/each}} +] + +# (temporary) Optional hostname of this node, for example nymtech.net. +hostname = '{{ host.hostname }}' [gateway] # Version of the gateway for which this configuration was created. @@ -33,6 +44,10 @@ mix_port = {{ gateway.mix_port }} # (default: 9000) clients_port = {{ gateway.clients_port }} +# If applicable, announced port for listening for secure websocket client traffic. +# (default: 0 - disabled) +clients_wss_port ={{#if gateway.clients_wss_port }} {{ gateway.clients_wss_port }} {{else}} 0 {{/if}} + # Wheather gateway collects and sends anonymized statistics enabled_statistics = {{ gateway.enabled_statistics }} @@ -55,6 +70,14 @@ nyxd_urls = [ cosmos_mnemonic = '{{ gateway.cosmos_mnemonic }}' +[http] +# Socket address this node will use for binding its http API. +# default: `0.0.0.0:8080` +bind_address = '{{ http.bind_address }}' + +# Path to assets directory of custom landing page of this node +landing_page_assets_path = '{{ http.landing_page_assets_path }}' + [network_requester] # Specifies whether network requester service is enabled in this process. enabled = {{ network_requester.enabled }} diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 80fee0a7fa..438b684981 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -106,16 +106,10 @@ pub(crate) enum GatewayError { #[from] source: NyxdError, }, - #[error("Error verifying hmac digest")] - HmacDigestError { - #[from] - source: hmac::digest::MacError, - }, - #[error("Invalid hmac length")] - HmacInvalidLength { - #[from] - source: hmac::digest::InvalidLength, - }, + + // TODO: in the future this should work the other way, i.e. NymNode depending on Gateway errors + #[error(transparent)] + NymNodeError(#[from] nym_node::error::NymNodeError), } impl From for GatewayError { diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs new file mode 100644 index 0000000000..eabb377456 --- /dev/null +++ b/gateway/src/http/mod.rs @@ -0,0 +1,141 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::Config; +use crate::error::GatewayError; +use crate::node::helpers::load_public_key; +use nym_bin_common::bin_info_owned; +use nym_crypto::asymmetric::{encryption, identity}; +use nym_node::error::NymNodeError; +use nym_node::http::api::api_requests; +use nym_node::http::api::api_requests::SignedHostInformation; +use nym_node::http::router::WireguardAppState; +use nym_node::wireguard::types::GatewayClientRegistry; +use nym_sphinx::addressing::clients::Recipient; +use nym_task::TaskClient; +use std::sync::Arc; + +fn load_gateway_details( + config: &Config, +) -> Result { + let wireguard = if config.wireguard.enabled { + Some(api_requests::v1::gateway::models::Wireguard { + port: config.wireguard.announced_port, + public_key: "placeholder key value".to_string(), + }) + } else { + None + }; + + Ok(api_requests::v1::gateway::models::Gateway { + client_interfaces: api_requests::v1::gateway::models::ClientInterfaces { + wireguard, + mixnet_websockets: Some(api_requests::v1::gateway::models::WebSockets { + ws_port: config.gateway.clients_port, + wss_port: config.gateway.clients_wss_port, + }), + }, + }) +} + +fn load_host_details( + config: &Config, + sphinx_key: &encryption::PublicKey, + identity_keypair: &identity::KeyPair, +) -> Result { + let host_info = api_requests::v1::node::models::HostInformation { + ip_address: config.host.public_ips.clone(), + hostname: config.host.hostname.clone(), + keys: api_requests::v1::node::models::HostKeys { + ed25519: identity_keypair.public_key().to_base58_string(), + x25519: sphinx_key.to_base58_string(), + }, + }; + + let signed_info = SignedHostInformation::new(host_info, identity_keypair.private_key()) + .map_err(NymNodeError::from)?; + Ok(signed_info) +} + +fn load_network_requester_details( + config: &Config, + network_requester_config: &nym_network_requester::Config, +) -> Result { + let identity_public_key: identity::PublicKey = load_public_key( + &network_requester_config + .storage_paths + .common_paths + .keys + .public_identity_key_file, + "network requester identity", + )?; + + let dh_public_key: encryption::PublicKey = load_public_key( + &network_requester_config + .storage_paths + .common_paths + .keys + .public_encryption_key_file, + "network requester diffie hellman", + )?; + + let gateway_identity_public_key: identity::PublicKey = load_public_key( + &config.storage_paths.keys.public_identity_key_file, + "gateway identity", + )?; + + Ok( + api_requests::v1::network_requester::models::NetworkRequester { + encoded_identity_key: identity_public_key.to_base58_string(), + encoded_x25519_key: dh_public_key.to_base58_string(), + address: Recipient::new( + identity_public_key, + dh_public_key, + gateway_identity_public_key, + ) + .to_string(), + }, + ) +} + +pub(crate) fn start_http_api( + gateway_config: &Config, + network_requester_config: Option<&nym_network_requester::Config>, + client_registry: Arc, + identity_keypair: &identity::KeyPair, + // TODO: this should be a wg specific key and not re-used sphinx + sphinx_keypair: Arc, + + task_client: TaskClient, +) -> Result<(), GatewayError> { + // is it suboptimal to load all the keys, etc for the second time after they've already been + // retrieved during startup of the rest of the components? + // yes, a bit. + // but in the grand scheme of things performance penalty is negligible since it's only happening on startup + // and makes the code a bit nicer to manage. on top of it, all of it will refactored anyway at some point + // (famous last words, eh? - 22.09.23) + let mut config = nym_node::http::Config::new( + bin_info_owned!(), + load_host_details( + gateway_config, + sphinx_keypair.public_key(), + identity_keypair, + )?, + ) + .with_gateway(load_gateway_details(gateway_config)?) + .with_landing_page_assets(gateway_config.http.landing_page_assets_path.as_ref()); + + if let Some(nr_config) = network_requester_config { + config = config + .with_network_requester(load_network_requester_details(gateway_config, nr_config)?) + } + + let wg_state = WireguardAppState::new(sphinx_keypair, client_registry, Default::default()); + let router = nym_node::http::NymNodeRouter::new(config, Some(wg_state)); + + let server = router + .build_server(&gateway_config.http.bind_address)? + .with_task_client(task_client); + tokio::spawn(async move { server.run().await }); + Ok(()) +} diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 1f3f54b398..16ef053776 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -17,6 +17,7 @@ use std::error::Error; mod commands; mod config; pub(crate) mod error; +mod http; mod node; pub(crate) mod support; @@ -46,13 +47,14 @@ struct Cli { #[tokio::main] async fn main() -> Result<(), Box> { + setup_logging(); + let args = Cli::parse(); setup_env(args.config_env_file.as_ref()); if !args.no_banner { maybe_print_banner(crate_name!(), crate_version!()); } - setup_logging(); commands::execute(args).await.map_err(|err| { if atty::is(atty::Stream::Stdout) { diff --git a/gateway/src/node/http/api/mod.rs b/gateway/src/node/http/api/mod.rs deleted file mode 100644 index c0753a12f8..0000000000 --- a/gateway/src/node/http/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod v1; diff --git a/gateway/src/node/http/api/v1/client_registry.rs b/gateway/src/node/http/api/v1/client_registry.rs deleted file mode 100644 index fc8d3a8a94..0000000000 --- a/gateway/src/node/http/api/v1/client_registry.rs +++ /dev/null @@ -1,105 +0,0 @@ -use axum::extract::Path; -use nym_types::gateway_client_registration::{ClientMessage, GatewayClient, InitMessage}; -use nym_types::wireguard::PeerPublicKey; -use std::sync::Arc; - -use axum::http::StatusCode; -use axum::{extract::State, Json}; -use std::str::FromStr; - -// use axum_macros::debug_handler; - -use crate::node::http::ApiState; - -async fn process_final_message(client: GatewayClient, state: Arc) -> StatusCode { - let preshared_nonce = { - if let Some(nonce) = state.registration_in_progress.get(client.pub_key()) { - *nonce - } else { - return StatusCode::BAD_REQUEST; - } - }; - - if client - .verify(state.sphinx_key_pair.private_key(), preshared_nonce) - .is_ok() - { - { - state.registration_in_progress.remove(client.pub_key()); - } - { - state - .client_registry - .insert(client.pub_key().clone(), client); - } - return StatusCode::OK; - } - - StatusCode::BAD_REQUEST -} - -async fn process_init_message(init_message: InitMessage, state: Arc) -> u64 { - let nonce: u64 = fastrand::u64(..); - state - .registration_in_progress - .insert(init_message.pub_key().clone(), nonce); - nonce -} - -// #[debug_handler] -pub(crate) async fn register_client( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json>) { - match payload { - ClientMessage::Init(i) => ( - StatusCode::OK, - Json(Some(process_init_message(i, Arc::clone(&state)).await)), - ), - ClientMessage::Final(client) => ( - process_final_message(client, Arc::clone(&state)).await, - Json(None), - ), - } -} - -pub(crate) async fn get_all_clients( - State(state): State>, -) -> (StatusCode, Json>) { - ( - StatusCode::OK, - Json( - state - .client_registry - .iter() - .map(|c| c.pub_key().clone()) - .collect::>(), - ), - ) -} - -pub(crate) async fn get_client( - Path(pub_key): Path, - State(state): State>, -) -> (StatusCode, Json>) { - let pub_key = match PeerPublicKey::from_str(&pub_key) { - Ok(pub_key) => pub_key, - Err(_) => return (StatusCode::BAD_REQUEST, Json(vec![])), - }; - let clients = state - .client_registry - .iter() - .filter_map(|r| { - let client = r.value(); - if client.pub_key() == &pub_key { - Some(client.clone()) - } else { - None - } - }) - .collect::>(); - if clients.is_empty() { - return (StatusCode::NOT_FOUND, Json(clients)); - } - (StatusCode::OK, Json(clients)) -} diff --git a/gateway/src/node/http/api/v1/mod.rs b/gateway/src/node/http/api/v1/mod.rs deleted file mode 100644 index 082c8dd954..0000000000 --- a/gateway/src/node/http/api/v1/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod client_registry; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 6d7ddf44cd..f902bb66d7 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -5,6 +5,7 @@ use self::storage::PersistentStorage; use crate::commands::helpers::{override_network_requester_config, OverrideNetworkRequesterConfig}; use crate::config::Config; use crate::error::GatewayError; +use crate::http::start_http_api; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_network_requester::{ LocalNetworkRequesterHandle, MessageRouter, @@ -12,7 +13,6 @@ use crate::node::client_handling::embedded_network_requester::{ use crate::node::client_handling::websocket; use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; -use crate::node::http::start_http_api; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::statistics::collector::GatewayStatisticsCollector; use crate::node::storage::Storage; @@ -24,9 +24,9 @@ use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::{LocalGateway, NRServiceProviderBuilder}; +use nym_node::wireguard::types::GatewayClientRegistry; use nym_statistics_common::collector::StatisticsSender; use nym_task::{TaskClient, TaskManager}; -use nym_types::gateway_client_registration::GatewayClientRegistry; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; @@ -37,7 +37,6 @@ use std::sync::Arc; pub(crate) mod client_handling; pub(crate) mod helpers; -pub(crate) mod http; pub(crate) mod mixnet_handling; pub(crate) mod statistics; pub(crate) mod storage; @@ -341,6 +340,15 @@ impl Gateway { CoconutVerifier::new(nyxd_client) }; + start_http_api( + &self.config, + self.network_requester_opts.as_ref().map(|o| &o.config), + self.client_registry.clone(), + self.identity_keypair.as_ref(), + self.sphinx_keypair.clone(), + shutdown.subscribe().named("http-api"), + )?; + let mix_forwarding_channel = self.start_packet_forwarder(shutdown.subscribe().named("PacketForwarder")); @@ -395,12 +403,6 @@ impl Gateway { bail!("{err}") } - // This should likely be wireguard feature gated, but its easier to test if it hangs in here - tokio::spawn(start_http_api( - Arc::clone(&self.client_registry), - Arc::clone(&self.sphinx_keypair), - )); - info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); if let Err(err) = Self::wait_for_interrupt(shutdown).await { diff --git a/integrations/bity/Cargo.toml b/integrations/bity/Cargo.toml index db8bdc9420..d2b8e9ee10 100644 --- a/integrations/bity/Cargo.toml +++ b/integrations/bity/Cargo.toml @@ -7,7 +7,7 @@ rust-version = "1.56" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -thiserror = "1.0" +thiserror = { workspace = true } k256 = { workspace = true, features = ["ecdsa", "sha256"] } eyre = "0.6.5" diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4389d27de8..4691bf250f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version = "1.56" [dependencies] async-trait = { workspace = true } -bs58 = {version = "0.4.0" } +bs58 = { version = "0.4.0" } bip39 = { workspace = true } cfg-if = "1.0" clap = { version = "4.0", features = ["cargo", "derive"] } @@ -27,7 +27,6 @@ humantime-serde = "1.0" lazy_static = "1.4.0" log = { workspace = true } pin-project = "1.0" -pretty_env_logger = "0.4.0" rand = "0.8.5" rand-07 = { package = "rand", version = "0.7.3" } # required for compatibility reqwest = { workspace = true, features = ["json"] } @@ -49,7 +48,7 @@ url = { workspace = true } ts-rs = { workspace = true, optional = true} -anyhow = "1.0" +anyhow = { workspace = true } getset = "0.1.1" sqlx = { version = "0.6.2", features = [ @@ -61,7 +60,7 @@ sqlx = { version = "0.6.2", features = [ okapi = { version = "0.7.0-rc.1", features = ["impl_json_schema"] } rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } ## ephemera-specific @@ -106,13 +105,14 @@ nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-bin-common = { path = "../common/bin-common" } nym-node-tester-utils = { path = "../common/node-tester-utils" } +nym-node-requests = { path = "../nym-node/nym-node-requests" } [features] no-reward = [] generate-ts = ["ts-rs"] [build-dependencies] -tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.6.2", features = [ "runtime-tokio-rustls", "sqlite", diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 2d39074adb..d9869bd7ae 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -10,13 +10,14 @@ bs58 = "0.4.0" cosmrs = { workspace = true } cosmwasm-std = { workspace = true } getset = "0.1.1" -schemars = { version = "0.8", features = ["preserve_order"] } -serde = { version = "1.0", features = ["derive"] } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } nym-coconut-interface = { path = "../../common/coconut-interface" } nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } [features] default = [] -generate-ts = ["ts-rs"] +generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 319d49edd1..3ab82c320a 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -9,8 +9,11 @@ use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ GatewayBond, IdentityKey, Interval, MixId, MixNode, Percent, RewardedSetNodeStatus, }; +use nym_node_requests::api::v1::gateway::models::WebSockets; +use nym_node_requests::api::v1::node::models::{BinaryBuildInformationOwned, HostInformation}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; use std::{fmt, time::Duration}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -30,6 +33,12 @@ impl RequestError { } } +impl Display for RequestError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.message.fmt(f) + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -135,6 +144,10 @@ impl MixNodeBondAnnotated { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct GatewayBondAnnotated { pub gateway_bond: GatewayBond, + + #[serde(default)] + pub self_described: Option, + // NOTE: the performance field is deprecated in favour of node_performance pub performance: Performance, pub node_performance: NodePerformance, @@ -151,6 +164,11 @@ impl GatewayBondAnnotated { } } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct GatewayDescription { + // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) +} + #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ComputeRewardEstParam { pub performance: Option, @@ -337,3 +355,29 @@ pub struct CirculatingSupplyResponse { pub vesting_tokens: Coin, pub circulating_supply: Coin, } + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +pub struct NymNodeDescription { + pub host_information: HostInformation, + + // TODO: do we really care about ALL build info or just the version? + pub build_information: BinaryBuildInformationOwned, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSockets, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct DescribedGateway { + pub bond: GatewayBond, + pub self_described: Option, +} + +impl From for DescribedGateway { + fn from(bond: GatewayBond) -> Self { + DescribedGateway { + bond, + self_described: None, + } + } +} diff --git a/nym-api/src/circulating_supply_api/cache/data.rs b/nym-api/src/circulating_supply_api/cache/data.rs index 162429bf58..0ca5b7a23a 100644 --- a/nym-api/src/circulating_supply_api/cache/data.rs +++ b/nym-api/src/circulating_supply_api/cache/data.rs @@ -30,9 +30,9 @@ impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse { fn from(value: &'a CirculatingSupplyCacheData) -> Self { CirculatingSupplyResponse { total_supply: value.total_supply.clone().into(), - mixmining_reserve: value.mixmining_reserve.clone().into_inner().into(), - vesting_tokens: value.vesting_tokens.clone().into_inner().into(), - circulating_supply: value.circulating_supply.clone().into_inner().into(), + mixmining_reserve: value.mixmining_reserve.clone().into(), + vesting_tokens: value.vesting_tokens.clone().into(), + circulating_supply: value.circulating_supply.clone().into(), } } } diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs index 70794bc72e..a9f6279e8c 100644 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ b/nym-api/src/circulating_supply_api/cache/mod.rs @@ -85,8 +85,10 @@ impl CirculatingSupplyCache { log::info!("the number of tokens still vesting is now {vesting_tokens}"); log::info!("the circulating supply is now {circulating_supply}"); - cache.mixmining_reserve.update(mixmining_reserve); - cache.vesting_tokens.update(vesting_tokens); - cache.circulating_supply.update(circulating_supply); + cache.mixmining_reserve.unchecked_update(mixmining_reserve); + cache.vesting_tokens.unchecked_update(vesting_tokens); + cache + .circulating_supply + .unchecked_update(circulating_supply); } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index a259879ace..1320efa590 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -6,7 +6,9 @@ extern crate rocket; use crate::epoch_operations::RewardedSetUpdater; use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::support::caching::cache::SharedCache; use crate::support::cli; use crate::support::cli::CliArgs; use crate::support::config::Config; @@ -34,8 +36,10 @@ mod ephemera; mod epoch_operations; pub(crate) mod network; mod network_monitor; +pub(crate) mod node_describe_cache; pub(crate) mod node_status_api; pub(crate) mod nym_contract_cache; +pub(crate) mod nym_nodes; pub(crate) mod support; struct ShutdownHandles { @@ -90,6 +94,19 @@ async fn start_nym_api_tasks( let node_status_cache_state = rocket.state::().unwrap(); let circulating_supply_cache_state = rocket.state::().unwrap(); let maybe_storage = rocket.state::(); + let described_nodes_state = rocket.state::>().unwrap(); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + nym_contract_cache_state.clone(), + described_nodes_state.to_owned(), + ) + .named("node-self-described-data-refresher") + .start(shutdown.subscribe_named("node-self-described-data-refresher")); // start all the caches first let nym_contract_cache_listener = nym_contract_cache::start_refresher( @@ -98,6 +115,7 @@ async fn start_nym_api_tasks( nyxd_client.clone(), &shutdown, ); + node_status_api::start_cache_refresh( &config.node_status_api, nym_contract_cache_state, diff --git a/nym-api/src/network/routes.rs b/nym-api/src/network/routes.rs index 77f60f0fa9..05dd0b8524 100644 --- a/nym-api/src/network/routes.rs +++ b/nym-api/src/network/routes.rs @@ -25,14 +25,13 @@ pub(crate) async fn nym_contracts( ) -> Json>> { let info = cache.contract_details().await; Json( - info.value - .into_iter() + info.iter() .map(|(contract, info)| { ( - contract, + contract.to_owned(), ContractInformation { - address: info.address.map(|a| a.to_string()), - details: info.base, + address: info.address.as_ref().map(|a| a.to_string()), + details: info.base.clone(), }, ) }) @@ -47,14 +46,13 @@ pub(crate) async fn nym_contracts_detailed( ) -> Json>> { let info = cache.contract_details().await; Json( - info.value - .into_iter() + info.iter() .map(|(contract, info)| { ( - contract, + contract.to_owned(), ContractInformation { - address: info.address.map(|a| a.to_string()), - details: info.detailed, + address: info.address.as_ref().map(|a| a.to_string()), + details: info.detailed.clone(), }, ) }) diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs new file mode 100644 index 0000000000..fc1c697cac --- /dev/null +++ b/nym-api/src/node_describe_cache/mod.rs @@ -0,0 +1,241 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::{SharedCache, UninitialisedCache}; +use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; +use crate::support::config; +use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use futures_util::{stream, StreamExt}; +use nym_api_requests::models::NymNodeDescription; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_contracts_common::IdentityKey; +use nym_mixnet_contract_common::Gateway; +use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use std::collections::HashMap; +use thiserror::Error; + +// type alias for ease of use +pub type DescribedNodes = HashMap; + +#[derive(Debug, Error)] +pub enum NodeDescribeCacheError { + #[error("contract cache hasn't been initialised")] + UninitialisedContractCache { + #[from] + source: UninitialisedCache, + }, + + #[error("gateway {gateway} has provided malformed host information ({host}: {source}")] + MalformedHost { + host: String, + + gateway: IdentityKey, + + #[source] + source: NymNodeApiClientError, + }, + + #[error("gateway '{gateway}' with host '{host}' doesn't seem to expose any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, gateway: IdentityKey }, + + #[error("failed to query gateway '{gateway}': {source}")] + ApiFailure { + gateway: IdentityKey, + + #[source] + source: NymNodeApiClientError, + }, + + // TODO: perhaps include more details here like whether key/signature/payload was malformed + #[error("could not verify signed host information for gateway '{gateway}'")] + MissignedHostInformation { gateway: IdentityKey }, +} + +pub struct NodeDescriptionProvider { + // for now we only care about gateways, nothing more + // network_gateways: SharedCache>, + contract_cache: NymContractCache, + + batch_size: usize, +} + +impl NodeDescriptionProvider { + pub(crate) fn new(contract_cache: NymContractCache) -> NodeDescriptionProvider { + NodeDescriptionProvider { + contract_cache, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + } + } + + #[must_use] + pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size = batch_size; + self + } +} + +async fn try_get_client( + gateway: &Gateway, +) -> Result { + let gateway_host = &gateway.host; + + // first try the standard port in case the operator didn't put the node behind the proxy, + // then default https (443) + // finally default http (80) + let addresses_to_try = vec![ + format!("http://{gateway_host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), + format!("https://{gateway_host}"), + format!("http://{gateway_host}"), + ]; + + for address in addresses_to_try { + // if provided host was malformed, no point in continuing + let client = match nym_node_requests::api::Client::new_url(address, None) { + Ok(client) => client, + Err(err) => { + return Err(NodeDescribeCacheError::MalformedHost { + host: gateway_host.clone(), + gateway: gateway.identity_key.clone(), + source: err, + }); + } + }; + if let Ok(health) = client.get_health().await { + if health.status.is_up() { + return Ok(client); + } + } + } + + Err(NodeDescribeCacheError::NoHttpPortsAvailable { + host: gateway_host.clone(), + gateway: gateway.identity_key.clone(), + }) +} + +async fn get_gateway_description( + gateway: Gateway, +) -> Result<(IdentityKey, NymNodeDescription), NodeDescribeCacheError> { + let client = try_get_client(&gateway).await?; + + let host_info = + client + .get_host_information() + .await + .map_err(|err| NodeDescribeCacheError::ApiFailure { + gateway: gateway.identity_key.clone(), + source: err, + })?; + + if !host_info.verify_host_information() { + return Err(NodeDescribeCacheError::MissignedHostInformation { + gateway: gateway.identity_key, + }); + } + + let build_info = + client + .get_build_information() + .await + .map_err(|err| NodeDescribeCacheError::ApiFailure { + gateway: gateway.identity_key.clone(), + source: err, + })?; + + let websockets = + client + .get_mixnet_websockets() + .await + .map_err(|err| NodeDescribeCacheError::ApiFailure { + gateway: gateway.identity_key.clone(), + source: err, + })?; + + let description = NymNodeDescription { + host_information: host_info.data, + build_information: build_info, + mixnet_websockets: websockets, + }; + + Ok((gateway.identity_key, description)) +} + +#[async_trait] +impl CacheItemProvider for NodeDescriptionProvider { + type Item = HashMap; + type Error = NodeDescribeCacheError; + + async fn wait_until_ready(&self) { + self.contract_cache.wait_for_initial_values().await + } + + async fn try_refresh(&self) -> Result { + let gateways = self.contract_cache.gateways_all().await; + + // let guard = self.network_gateways.get().await?; + // let gateways = &*guard; + + if gateways.is_empty() { + return Ok(HashMap::new()); + } + + // TODO: somehow bypass the 'higher-ranked lifetime error' and remove that redundant clone + let websockets = stream::iter( + gateways + // .deref() + // .clone() + .into_iter() + .map(|bond| bond.gateway) + .map(get_gateway_description), + ) + .buffer_unordered(self.batch_size) + .filter_map(|res| async move { + match res { + Ok((identity, description)) => Some((identity, description)), + Err(err) => { + debug!("{err}"); + None + } + } + }) + .collect::>() + .await; + + Ok(websockets) + } +} + +// currently dead code : ( +#[allow(dead_code)] +pub(crate) fn new_refresher( + config: &config::TopologyCacher, + contract_cache: NymContractCache, + // hehe. we can't do that yet + // network_gateways: SharedCache>, +) -> CacheRefresher { + CacheRefresher::new( + Box::new( + NodeDescriptionProvider::new(contract_cache) + .with_batch_size(config.debug.node_describe_batch_size), + ), + config.debug.node_describe_caching_interval, + ) +} + +pub(crate) fn new_refresher_with_initial_value( + config: &config::TopologyCacher, + contract_cache: NymContractCache, + // hehe. we can't do that yet + // network_gateways: SharedCache>, + initial: SharedCache, +) -> CacheRefresher { + CacheRefresher::new_with_initial_value( + Box::new( + NodeDescriptionProvider::new(contract_cache) + .with_batch_size(config.debug.node_describe_batch_size), + ), + config.debug.node_describe_caching_interval, + initial, + ) +} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index ba74ab561c..c1f2d93c61 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -63,13 +63,13 @@ impl NodeStatusCache { ) { match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_annotated.update(mixnodes); - cache.rewarded_set_annotated.update(rewarded_set); - cache.active_set_annotated.update(active_set); - cache.gateways_annotated.update(gateways); + cache.mixnodes_annotated.unchecked_update(mixnodes); + cache.rewarded_set_annotated.unchecked_update(rewarded_set); + cache.active_set_annotated.unchecked_update(active_set); + cache.gateways_annotated.unchecked_update(gateways); cache .inclusion_probabilities - .update(inclusion_probabilities); + .unchecked_update(inclusion_probabilities); } Err(e) => error!("{e}"), } @@ -90,33 +90,33 @@ impl NodeStatusCache { } pub(crate) async fn mixnodes_annotated_full(&self) -> Option>> { - self.get(|c| c.mixnodes_annotated.clone()).await + self.get(|c| c.mixnodes_annotated.clone_cache()).await } pub(crate) async fn mixnodes_annotated_filtered(&self) -> Option> { let full = self.mixnodes_annotated_full().await?; - Some(full.value.into_iter().filter(|m| !m.blacklisted).collect()) + Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } pub(crate) async fn rewarded_set_annotated(&self) -> Option>> { - self.get(|c| c.rewarded_set_annotated.clone()).await + self.get(|c| c.rewarded_set_annotated.clone_cache()).await } pub(crate) async fn active_set_annotated(&self) -> Option>> { - self.get(|c| c.active_set_annotated.clone()).await + self.get(|c| c.active_set_annotated.clone_cache()).await } pub(crate) async fn gateways_annotated_full(&self) -> Option>> { - self.get(|c| c.gateways_annotated.clone()).await + self.get(|c| c.gateways_annotated.clone_cache()).await } pub(crate) async fn gateways_annotated_filtered(&self) -> Option> { let full = self.gateways_annotated_full().await?; - Some(full.value.into_iter().filter(|m| !m.blacklisted).collect()) + Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } pub(crate) async fn inclusion_probabilities(&self) -> Option> { - self.get(|c| c.inclusion_probabilities.clone()).await + self.get(|c| c.inclusion_probabilities.clone_cache()).await } pub async fn mixnode_details( diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index a80c84d2d1..f871505016 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -180,6 +180,7 @@ pub(crate) async fn annotate_gateways_with_details( annotated.push(GatewayBondAnnotated { blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), gateway_bond, + self_described: None, performance, node_performance, }); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index d3488eb99a..c87b7e5b1c 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -1,6 +1,12 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use self::cache::refresher::NodeStatusCacheRefresher; +use crate::support::config; +use crate::{ + nym_contract_cache::cache::NymContractCache, + support::{self, storage}, +}; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; use okapi::openapi3::OpenApi; @@ -8,13 +14,6 @@ use rocket::Route; use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; -use crate::support::config; -use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, -}; - -use self::cache::refresher::NodeStatusCacheRefresher; pub(crate) mod cache; pub(crate) mod helpers; pub(crate) mod local_guard; diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index ec0ce7f8e5..250ba423ac 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -59,17 +59,25 @@ impl NymContractCache { ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes.update(mixnodes); - cache.gateways.update(gateways); - cache.rewarded_set.update(rewarded_set); - cache.active_set.update(active_set); - cache.current_reward_params.update(Some(rewarding_params)); - cache.current_interval.update(Some(current_interval)); - cache.mix_to_family.update(mix_to_family); + cache.mixnodes.unchecked_update(mixnodes); + cache.gateways.unchecked_update(gateways); + cache.rewarded_set.unchecked_update(rewarded_set); + cache.active_set.unchecked_update(active_set); + cache + .current_reward_params + .unchecked_update(Some(rewarding_params)); + cache + .current_interval + .unchecked_update(Some(current_interval)); + cache.mix_to_family.unchecked_update(mix_to_family); // Just return empty lists when these are not available - cache.service_providers.update(services.unwrap_or_default()); - cache.registered_names.update(names.unwrap_or_default()); - cache.contracts_info.update(nym_contracts_info) + cache + .service_providers + .unchecked_update(services.unwrap_or_default()); + cache + .registered_names + .unchecked_update(names.unwrap_or_default()); + cache.contracts_info.unchecked_update(nym_contracts_info) } Err(err) => { error!("{err}"); @@ -79,7 +87,7 @@ impl NymContractCache { pub async fn mixnodes_blacklist(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes_blacklist.clone(), + Ok(cache) => cache.mixnodes_blacklist.clone_cache(), Err(err) => { error!("{err}"); Cache::new(HashSet::new()) @@ -89,7 +97,7 @@ impl NymContractCache { pub async fn gateways_blacklist(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways_blacklist.clone(), + Ok(cache) => cache.gateways_blacklist.clone_cache(), Err(err) => { error!("{err}"); Cache::new(HashSet::new()) @@ -99,11 +107,7 @@ impl NymContractCache { pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.mixnodes_blacklist().await; - let mut blacklist = blacklist - .value - .union(&add) - .cloned() - .collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() @@ -113,7 +117,7 @@ impl NymContractCache { } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_blacklist.update(blacklist); + cache.mixnodes_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update mixnodes blacklist: {err}"); @@ -128,7 +132,6 @@ impl NymContractCache { ) { let blacklist = self.gateways_blacklist().await; let mut blacklist = blacklist - .value .union(&add) .cloned() .collect::>(); @@ -141,7 +144,7 @@ impl NymContractCache { } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.gateways_blacklist.update(blacklist); + cache.gateways_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update gateways blacklist: {err}"); @@ -159,7 +162,7 @@ impl NymContractCache { if !blacklist.is_empty() { mixnodes .into_iter() - .filter(|mix| !blacklist.value.contains(&mix.mix_id())) + .filter(|mix| !blacklist.contains(&mix.mix_id())) .collect() } else { mixnodes @@ -168,7 +171,7 @@ impl NymContractCache { pub async fn mixnodes_all(&self) -> Vec { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes.clone().value, + Ok(cache) => cache.mixnodes.clone(), Err(err) => { error!("{err}"); Vec::new() @@ -181,7 +184,6 @@ impl NymContractCache { Ok(cache) => cache .mixnodes .clone() - .into_inner() .into_iter() .map(|bond| bond.bond_information) .collect(), @@ -203,7 +205,7 @@ impl NymContractCache { if !blacklist.is_empty() { gateways .into_iter() - .filter(|mix| !blacklist.value.contains(mix.identity())) + .filter(|mix| !blacklist.contains(mix.identity())) .collect() } else { gateways @@ -212,7 +214,7 @@ impl NymContractCache { pub async fn gateways_all(&self) -> Vec { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways.value.clone(), + Ok(cache) => cache.gateways.clone(), Err(err) => { error!("{err}"); Vec::new() @@ -222,7 +224,7 @@ impl NymContractCache { pub async fn rewarded_set(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.rewarded_set.clone(), + Ok(cache) => cache.rewarded_set.clone_cache(), Err(err) => { error!("{err}"); Cache::new(Vec::new()) @@ -232,7 +234,7 @@ impl NymContractCache { pub async fn active_set(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.active_set.clone(), + Ok(cache) => cache.active_set.clone_cache(), Err(err) => { error!("{err}"); Cache::new(Vec::new()) @@ -242,7 +244,7 @@ impl NymContractCache { pub async fn mix_to_family(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mix_to_family.clone(), + Ok(cache) => cache.mix_to_family.clone_cache(), Err(err) => { error!("{err}"); Cache::new(Vec::new()) @@ -252,7 +254,7 @@ impl NymContractCache { pub(crate) async fn interval_reward_params(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_reward_params.clone(), + Ok(cache) => cache.current_reward_params.clone_cache(), Err(err) => { error!("{err}"); Cache::new(None) @@ -262,7 +264,7 @@ impl NymContractCache { pub(crate) async fn current_interval(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_interval.clone(), + Ok(cache) => cache.current_interval.clone_cache(), Err(err) => { error!("{err}"); Cache::new(None) @@ -272,7 +274,7 @@ impl NymContractCache { pub(crate) async fn contract_details(&self) -> Cache { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.contracts_info.clone(), + Ok(cache) => cache.contracts_info.clone_cache(), Err(err) => { error!("{err}"); Cache::default() @@ -284,12 +286,12 @@ impl NymContractCache { // it might not be the most optimal to possibly iterate the entire vector to find (or not) // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - let active_set = &self.active_set().await.value; + let active_set = &self.active_set().await; if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set().await.value; + let rewarded_set = &self.rewarded_set().await; if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { return (Some(bond.clone()), MixnodeStatus::Standby); } @@ -308,7 +310,7 @@ impl NymContractCache { pub(crate) async fn services(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.service_providers.clone(), + Ok(cache) => cache.service_providers.clone_cache(), Err(err) => { error!("{err}"); Cache::new(Vec::new()) @@ -318,7 +320,7 @@ impl NymContractCache { pub(crate) async fn names(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.registered_names.clone(), + Ok(cache) => cache.registered_names.clone_cache(), Err(err) => { error!("{err}"); Cache::new(Vec::new()) diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs index 56f1ec2bff..9840e51e97 100644 --- a/nym-api/src/nym_contract_cache/routes.rs +++ b/nym-api/src/nym_contract_cache/routes.rs @@ -49,7 +49,7 @@ pub async fn get_gateways(cache: &State) -> Json) -> Json> { - Json(cache.rewarded_set().await.value) + Json(cache.rewarded_set().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -70,7 +70,7 @@ pub async fn get_rewarded_set_detailed( #[openapi(tag = "contract-cache")] #[get("/mixnodes/active")] pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await.value) + Json(cache.active_set().await.clone()) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -93,7 +93,7 @@ pub async fn get_active_set_detailed( pub async fn get_blacklisted_mixnodes( cache: &State, ) -> Json>> { - let blacklist = cache.mixnodes_blacklist().await.value; + let blacklist = cache.mixnodes_blacklist().await.clone(); if blacklist.is_empty() { Json(None) } else { @@ -106,7 +106,7 @@ pub async fn get_blacklisted_mixnodes( pub async fn get_blacklisted_gateways( cache: &State, ) -> Json>> { - let blacklist = cache.gateways_blacklist().await.value; + let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { Json(None) } else { @@ -119,25 +119,25 @@ pub async fn get_blacklisted_gateways( pub async fn get_interval_reward_params( cache: &State, ) -> Json> { - Json(cache.interval_reward_params().await.value) + Json(*cache.interval_reward_params().await) } #[openapi(tag = "contract-cache")] #[get("/epoch/current")] pub async fn get_current_epoch(cache: &State) -> Json> { - Json(cache.current_interval().await.value) + Json(*cache.current_interval().await) } #[openapi(tag = "contract-cache")] #[get("/services")] pub async fn get_services(cache: &State) -> Json { - let services = cache.services().await.value; + let services = cache.services().await.clone(); Json(services.as_slice().into()) } #[openapi(tag = "contract-cache")] #[get("/names")] pub async fn get_registered_names(cache: &State) -> Json { - let names = cache.names().await.value; + let names = cache.names().await.clone(); Json(names.as_slice().into()) } diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs new file mode 100644 index 0000000000..4e2a10c5b0 --- /dev/null +++ b/nym-api/src/nym_nodes/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use okapi::openapi3::OpenApi; +use rocket::Route; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; + +pub(crate) mod routes; + +/// Merges the routes with http information and returns it to Rocket for serving +pub(crate) fn nym_node_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![ + settings: routes::get_gateways_described + ] +} diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs new file mode 100644 index 0000000000..cad4549b45 --- /dev/null +++ b/nym-api/src/nym_nodes/routes.rs @@ -0,0 +1,44 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_describe_cache::DescribedNodes; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use nym_api_requests::models::DescribedGateway; +use rocket::serde::json::Json; +use rocket::State; +use rocket_okapi::openapi; +use std::ops::Deref; + +// obviously this should get refactored later on because gateways will go away. +// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD + +#[openapi(tag = "Nym Nodes")] +#[get("/gateways/described")] +pub async fn get_gateways_described( + contract_cache: &State, + describe_cache: &State>, +) -> Json> { + let gateways = contract_cache.gateways_filtered().await; + if gateways.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(gateways.into_iter().map(Into::into).collect()); + }; + + // TODO: this is extremely inefficient, but given we don't have many gateways, + // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, + // but at that point (I hope) the whole caching situation should get refactored + Json( + gateways + .into_iter() + .map(|bond| DescribedGateway { + self_described: self_descriptions.deref().get(bond.identity()).cloned(), + bond, + }) + .collect(), + ) +} diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs new file mode 100644 index 0000000000..d330d2f667 --- /dev/null +++ b/nym-api/src/support/caching/cache.rs @@ -0,0 +1,156 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; +use thiserror::Error; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Debug, Error)] +#[error("the cache item has not been initialised")] +pub struct UninitialisedCache; + +pub struct SharedCache(Arc>>); + +impl Clone for SharedCache { + fn clone(&self) -> Self { + SharedCache(Arc::clone(&self.0)) + } +} + +impl Default for SharedCache { + fn default() -> Self { + SharedCache(Arc::new(RwLock::new(CachedItem { inner: None }))) + } +} + +impl SharedCache { + pub(crate) fn new() -> Self { + SharedCache::default() + } + + pub(crate) async fn update(&self, value: T) { + let mut guard = self.0.write().await; + if let Some(ref mut existing) = guard.inner { + existing.unchecked_update(value) + } else { + guard.inner = Some(Cache::new(value)) + } + } + + pub(crate) async fn get(&self) -> Result>, UninitialisedCache> { + let guard = self.0.read().await; + RwLockReadGuard::try_map(guard, |a| a.inner.as_ref()).map_err(|_| UninitialisedCache) + } + + // ignores expiration data + #[allow(dead_code)] + pub(crate) async fn unchecked_get_inner( + &self, + ) -> Result, UninitialisedCache> { + Ok(RwLockReadGuard::map(self.get().await?, |a| &a.value)) + } +} + +impl From> for SharedCache { + fn from(value: Cache) -> Self { + SharedCache(Arc::new(RwLock::new(value.into()))) + } +} + +#[derive(Default)] +pub(crate) struct CachedItem { + inner: Option>, +} + +impl CachedItem { + #[allow(dead_code)] + fn get(&self) -> Result<&Cache, UninitialisedCache> { + self.inner.as_ref().ok_or(UninitialisedCache) + } +} + +impl From> for CachedItem { + fn from(value: Cache) -> Self { + CachedItem { inner: Some(value) } + } +} + +// don't use this directly! +// opt for SharedCache instead +pub struct Cache { + value: T, + as_at: i64, +} + +impl Cache { + // ugh. I hate to expose it, but it'd have broken pre-existing code + pub(crate) fn new(value: T) -> Self { + Cache { + value, + as_at: current_unix_timestamp(), + } + } + + // ugh. I hate to expose it, but it'd have broken pre-existing code + pub(crate) fn clone_cache(&self) -> Self + where + T: Clone, + { + Cache { + value: self.value.clone(), + as_at: self.as_at, + } + } + + // ugh. I hate to expose it, but it'd have broken pre-existing code + pub(crate) fn unchecked_update(&mut self, value: T) { + self.value = value; + self.as_at = current_unix_timestamp() + } + + #[allow(dead_code)] + pub fn has_expired(&self, ttl: Duration, now: Option) -> bool { + let now = now.unwrap_or(OffsetDateTime::now_utc()).unix_timestamp(); + let diff = now - self.as_at; + + diff > (ttl.as_secs() as i64) + } + + #[allow(dead_code)] + pub fn timestamp(&self) -> i64 { + self.as_at + } + + #[allow(dead_code)] + pub fn into_inner(self) -> T { + self.value + } +} + +impl Deref for Cache { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Default for Cache +where + T: Default, +{ + fn default() -> Self { + Cache { + value: T::default(), + as_at: 0, + } + } +} + +fn current_unix_timestamp() -> i64 { + let now = OffsetDateTime::now_utc(); + now.unix_timestamp() +} diff --git a/nym-api/src/support/caching/mod.rs b/nym-api/src/support/caching/mod.rs index 0d2615d327..7d647c6666 100644 --- a/nym-api/src/support/caching/mod.rs +++ b/nym-api/src/support/caching/mod.rs @@ -1,59 +1,11 @@ -use serde::Serialize; -use std::ops::Deref; -use time::OffsetDateTime; +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -#[derive(Serialize, Clone)] -pub struct Cache { - pub value: T, - as_at: i64, -} +pub(crate) mod cache; +pub(crate) mod refresher; -impl Cache { - pub fn new(value: T) -> Self { - Cache { - value, - as_at: current_unix_timestamp(), - } - } - - pub(crate) fn update(&mut self, value: T) { - self.value = value; - self.as_at = current_unix_timestamp() - } - - pub fn timestamp(&self) -> i64 { - self.as_at - } - - pub fn into_inner(self) -> T { - self.value - } -} - -impl Deref for Cache { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - -impl Default for Cache -where - T: Default, -{ - fn default() -> Self { - Cache { - value: T::default(), - as_at: 0, - } - } -} - -fn current_unix_timestamp() -> i64 { - let now = OffsetDateTime::now_utc(); - now.unix_timestamp() -} +// don't break existing imports +pub(crate) use cache::Cache; // The cache can emit notifications to listeners about the current state #[derive(Debug, PartialEq, Eq)] diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs new file mode 100644 index 0000000000..bb0435a6cf --- /dev/null +++ b/nym-api/src/support/caching/refresher.rs @@ -0,0 +1,136 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::caching::cache::SharedCache; +use nym_task::TaskClient; +use std::time::Duration; +use tokio::time::interval; + +pub struct CacheRefresher { + name: String, + refreshing_interval: Duration, + + // TODO: the Send + Sync bounds are only required for the `start` method. could we maybe make it less restrictive? + provider: Box + Send + Sync>, + shared_cache: SharedCache, + // triggers: Vec>, +} + +#[async_trait] +pub trait CacheItemProvider { + type Item; + type Error: std::error::Error; + + async fn wait_until_ready(&self) {} + + async fn try_refresh(&self) -> Result; +} + +// pub struct TriggerFailure; +// +// #[async_trait] +// pub trait RefreshTriggerTrait { +// async fn triggerred(&mut self) -> Result<(), TriggerFailure>; +// } +// +// // TODO: how to get rid of `T: Send + Sync`? it really doesn't need to be Send + Sync +// // since it's wrapped in Shared internally anyway +// #[async_trait] +// impl RefreshTriggerTrait for watch::Receiver +// where +// T: Send + Sync, +// { +// async fn triggerred(&mut self) -> Result<(), TriggerFailure> { +// self.changed().await.map_err(|err| { +// error!("failed to process refresh trigger: {err}"); +// TriggerFailure +// }) +// } +// } + +impl CacheRefresher +where + E: std::error::Error, +{ + pub(crate) fn new( + item_provider: Box + Send + Sync>, + refreshing_interval: Duration, + ) -> Self { + CacheRefresher { + name: "GenericCacheRefresher".to_string(), + refreshing_interval, + provider: item_provider, + shared_cache: SharedCache::new(), + } + } + + pub(crate) fn new_with_initial_value( + item_provider: Box + Send + Sync>, + refreshing_interval: Duration, + shared_cache: SharedCache, + ) -> Self { + CacheRefresher { + name: "GenericCacheRefresher".to_string(), + refreshing_interval, + provider: item_provider, + shared_cache, + } + } + + #[must_use] + pub(crate) fn named(mut self, name: impl Into) -> Self { + self.name = name.into(); + self + } + + #[allow(dead_code)] + pub(crate) fn get_shared_cache(&self) -> SharedCache { + self.shared_cache.clone() + } + + async fn do_refresh_cache(&self) { + match self.provider.try_refresh().await { + Ok(updated_items) => { + self.shared_cache.update(updated_items).await; + } + Err(err) => { + error!("{}: failed to refresh the cache: {err}", self.name) + } + } + } + + pub async fn refresh(&self, task_client: &mut TaskClient) { + log::info!("{}: refreshing cache state", self.name); + + tokio::select! { + biased; + _ = task_client.recv() => { + log::trace!("{}: Received shutdown while refreshing cache", self.name) + } + _ = self.do_refresh_cache() => (), + } + } + + pub async fn run(&self, mut task_client: TaskClient) { + self.provider.wait_until_ready().await; + + let mut refresh_interval = interval(self.refreshing_interval); + while !task_client.is_shutdown() { + tokio::select! { + biased; + _ = task_client.recv() => { + log::trace!("{}: Received shutdown", self.name) + } + _ = refresh_interval.tick() => self.refresh(&mut task_client).await, + } + } + } + + pub fn start(self, task_client: TaskClient) + where + T: Send + Sync + 'static, + E: Send + Sync + 'static, + { + tokio::spawn(async move { self.run(task_client).await }); + } +} diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 9cf1404b38..43eb0ddf8a 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -49,6 +49,10 @@ const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); + +pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from_secs(4500); +pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; + const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20; @@ -451,12 +455,19 @@ pub struct TopologyCacher { pub struct TopologyCacherDebug { #[serde(with = "humantime_serde")] pub caching_interval: Duration, + + #[serde(with = "humantime_serde")] + pub node_describe_caching_interval: Duration, + + pub node_describe_batch_size: usize, } impl Default for TopologyCacherDebug { fn default() -> Self { TopologyCacherDebug { caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, + node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, } } } diff --git a/nym-api/src/support/config/old_config_v1_1_21.rs b/nym-api/src/support/config/old_config_v1_1_21.rs index 2f13baaad0..be8c06d7c8 100644 --- a/nym-api/src/support/config/old_config_v1_1_21.rs +++ b/nym-api/src/support/config/old_config_v1_1_21.rs @@ -126,6 +126,7 @@ impl From for Config { topology_cacher: TopologyCacher { debug: TopologyCacherDebug { caching_interval: value.topology_cacher.caching_interval, + ..Default::default() }, }, circulating_supply_cacher: CirculatingSupplyCacher { diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index b21ccdb381..5d0b646107 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -5,11 +5,13 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::coconut::{self, comm::QueryCommunicationChannel, InternalSignRequest}; use crate::network::models::NetworkDetails; use crate::network::network_routes; +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::{self, NodeStatusCache}; use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache}; +use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; use anyhow::Result; use rocket::http::Method; use rocket::{Ignite, Rocket}; @@ -39,10 +41,12 @@ pub(crate) async fn setup_rocket( "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), "/network" => network_routes(&openapi_settings), + "" => nym_node_routes(&openapi_settings), } let rocket = rocket .manage(network_details) + .manage(SharedCache::::new()) .mount("/swagger", make_swagger_ui(&openapi::get_docs())) .attach(setup_cors()?) .attach(NymContractCache::stage()) diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 00e1040483..83a2c97fe9 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -422,9 +422,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -454,7 +454,7 @@ dependencies = [ "pbkdf2", "rand_core 0.6.4", "ripemd", - "sha2 0.10.7", + "sha2 0.10.8", "subtle 2.4.1", "zeroize", ] @@ -515,6 +515,15 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake3" version = "1.4.1" @@ -602,6 +611,29 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" +[[package]] +name = "boringtun" +version = "0.6.0" +source = "git+https://github.com/cloudflare/boringtun?rev=e1d6360d6ab4529fc942a078e4c54df107abe2ba#e1d6360d6ab4529fc942a078e4c54df107abe2ba" +dependencies = [ + "aead", + "base64 0.13.1", + "blake2 0.10.6", + "chacha20poly1305", + "hex", + "hmac 0.12.1", + "ip_network", + "ip_network_table", + "libc", + "nix 0.25.1", + "parking_lot 0.12.1", + "rand_core 0.6.4", + "ring", + "tracing", + "untrusted 0.9.0", + "x25519-dalek 2.0.0", +] + [[package]] name = "brotli" version = "3.3.4" @@ -635,7 +667,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" dependencies = [ - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -1130,7 +1162,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -1367,6 +1399,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle 2.4.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1794,7 +1853,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "ed25519 1.5.3", "rand 0.7.3", "serde", @@ -1808,7 +1867,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -2061,6 +2120,12 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "fiat-crypto" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" + [[package]] name = "field-offset" version = "0.3.6" @@ -2964,6 +3029,20 @@ dependencies = [ "itoa 1.0.9", ] +[[package]] +name = "http-api-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + [[package]] name = "http-body" version = "0.4.5" @@ -3237,6 +3316,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipnet" version = "2.8.0" @@ -3357,7 +3458,7 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3370,7 +3471,7 @@ dependencies = [ "ecdsa 0.16.8", "elliptic-curve 0.13.5", "once_cell", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.1.0", ] @@ -3513,7 +3614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" dependencies = [ "arrayref", - "blake2", + "blake2 0.8.1", "chacha", "keystream", ] @@ -3730,6 +3831,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.26.2" @@ -3874,6 +3987,7 @@ dependencies = [ "getset", "nym-coconut-interface", "nym-mixnet-contract-common", + "nym-node-requests", "schemars", "serde", ] @@ -3904,6 +4018,7 @@ dependencies = [ "clap_complete_fig", "log", "pretty_env_logger", + "schemars", "semver 0.11.0", "serde", "vergen", @@ -3914,7 +4029,7 @@ name = "nym-client-core" version = "1.1.15" dependencies = [ "async-trait", - "base64 0.21.2", + "base64 0.21.4", "cfg-if", "dashmap", "dirs 4.0.0", @@ -3940,7 +4055,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx 0.6.3", "tap", "thiserror", @@ -4128,7 +4243,7 @@ dependencies = [ "rand 0.7.3", "subtle-encoding", "thiserror", - "x25519-dalek", + "x25519-dalek 1.1.1", "zeroize", ] @@ -4310,6 +4425,20 @@ dependencies = [ "url", ] +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "nym-bin-common", + "nym-crypto", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "nym-nonexhaustive-delayqueue" version = "0.1.0" @@ -4335,7 +4464,7 @@ dependencies = [ "blake3", "chacha20", "chacha20poly1305", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "getrandom 0.2.10", "log", "rand 0.7.3", @@ -4602,6 +4731,7 @@ dependencies = [ "async-trait", "bs58 0.4.0", "log", + "nym-api-requests", "nym-bin-common", "nym-config", "nym-crypto", @@ -4634,6 +4764,7 @@ dependencies = [ "eyre", "flate2", "futures", + "http-api-client", "itertools", "log", "nym-api-requests", @@ -4676,6 +4807,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "boringtun", + "dashmap", + "nym-crypto", + "serde", + "thiserror", + "x25519-dalek 2.0.0", +] + [[package]] name = "objc" version = "0.2.7" @@ -5043,7 +5187,7 @@ checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" dependencies = [ "once_cell", "pest", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -5202,13 +5346,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" + [[package]] name = "plist" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "indexmap 1.9.3", "line-wrap", "quick-xml 0.29.0", @@ -5598,11 +5748,11 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -5623,6 +5773,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tokio-socks", @@ -5633,7 +5784,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg 0.10.1", + "winreg 0.50.0", ] [[package]] @@ -5691,7 +5842,7 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] @@ -5736,7 +5887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" dependencies = [ "globset", - "sha2 0.10.7", + "sha2 0.10.8", "walkdir", ] @@ -5825,7 +5976,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", ] [[package]] @@ -5908,7 +6059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -5918,7 +6069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -6247,7 +6398,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "chrono", "hex", "indexmap 1.9.3", @@ -6328,9 +6479,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -6468,11 +6619,11 @@ checksum = "cc43eda802856ee82a7555c7b75ceb9e07451741c7a2f5f23d036020e01189d4" dependencies = [ "aes 0.7.5", "arrayref", - "blake2", + "blake2 0.8.1", "bs58 0.4.0", "byteorder", "chacha", - "curve25519-dalek", + "curve25519-dalek 3.2.0", "digest 0.9.0", "hkdf 0.11.0", "hmac 0.11.0", @@ -6594,7 +6745,7 @@ dependencies = [ "paste", "percent-encoding", "rustls 0.19.1", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "sqlformat 0.1.8", "sqlx-rt 0.5.13", @@ -6641,7 +6792,7 @@ dependencies = [ "percent-encoding", "rustls 0.20.8", "rustls-pemfile", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "sqlformat 0.2.1", "sqlx-rt 0.6.3", @@ -6664,7 +6815,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx-core 0.5.13", "sqlx-rt 0.5.13", "syn 1.0.109", @@ -6683,7 +6834,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx-core 0.6.3", "sqlx-rt 0.6.3", "syn 1.0.109", @@ -6833,6 +6984,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -6949,7 +7121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbe522898e35407a8e60dc3870f7579fea2fc262a6a6072eccdd37ae1e1d91e" dependencies = [ "anyhow", - "base64 0.21.2", + "base64 0.21.4", "bytes", "cocoa", "dirs-next", @@ -7020,7 +7192,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "brotli", "ico", "json-patch", @@ -7032,7 +7204,7 @@ dependencies = [ "semver 1.0.18", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "tauri-utils", "thiserror", "time", @@ -7179,7 +7351,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.1.0", "subtle 2.4.1", "subtle-encoding", @@ -7283,18 +7455,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", @@ -7450,7 +7622,9 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "native-tls", "tokio", + "tokio-native-tls", "tungstenite", ] @@ -7630,6 +7804,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand 0.8.5", "sha1", "thiserror", @@ -7717,13 +7892,19 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "log", "native-tls", "once_cell", @@ -7963,9 +8144,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" dependencies = [ "futures-util", "js-sys", @@ -8067,7 +8248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -8077,7 +8258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -8427,15 +8608,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.11.0" @@ -8446,6 +8618,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wry" version = "0.24.3" @@ -8472,7 +8654,7 @@ dependencies = [ "once_cell", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "soup2", "tao", "thiserror", @@ -8520,11 +8702,23 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "rand_core 0.5.1", "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek 4.1.1", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "xattr" version = "1.0.1" @@ -8540,7 +8734,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" dependencies = [ - "nix", + "nix 0.26.2", "winapi", ] @@ -8577,7 +8771,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.2", "once_cell", "ordered-stream", "rand 0.8.5", diff --git a/nym-connect/desktop/src-tauri/Cargo.toml b/nym-connect/desktop/src-tauri/Cargo.toml index 3947434d65..193d3a53a3 100644 --- a/nym-connect/desktop/src-tauri/Cargo.toml +++ b/nym-connect/desktop/src-tauri/Cargo.toml @@ -31,7 +31,7 @@ log = { version = "0.4", features = ["serde"] } pretty_env_logger = "0.4.0" rand = "0.8" rand-07 = { package = "rand", version = "0.7.3" } -reqwest = { version = "0.11.18", features = ["json", "socks"] } +reqwest = { version = "0.11.22", features = ["json", "socks"] } rust-embed = { version = "6.4.2", features = ["include-exclude"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs b/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs index 56bc16ab67..fca43e8fff 100644 --- a/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs +++ b/nym-connect/desktop/src-tauri/src/operations/directory/gateways.rs @@ -8,6 +8,7 @@ use nym_bin_common::version_checker::is_minor_version_compatible; use nym_config::defaults::var_names::NYM_API; use nym_contracts_common::types::Percent; use nym_topology::gateway; +use nym_validator_client::client::NymApiClientExt; use nym_validator_client::nym_api::Client as ApiClient; use std::str::FromStr; use url::Url; @@ -16,7 +17,7 @@ use url::Url; const GATEWAY_PERFORMANCE_SCORE_THRESHOLD: u64 = 90; async fn fetch_all_gateways() -> Result> { - let api_client = ApiClient::new(Url::from_str(&std::env::var(NYM_API)?)?); + let api_client = ApiClient::new(Url::from_str(&std::env::var(NYM_API)?)?, None); let gateways = api_client.get_gateways_detailed().await?; if gateways.is_empty() { Err(BackendError::NoGatewaysFoundInDirectory) @@ -87,9 +88,12 @@ async fn select_gateway_by_latency(gateways: Vec) -> Resul .collect(); let mut rng = rand_07::rngs::OsRng; - let selected_gateway = - nym_client_core::init::helpers::choose_gateway_by_latency(&mut rng, &gateways_as_nodes) - .await?; + let selected_gateway = nym_client_core::init::helpers::choose_gateway_by_latency( + &mut rng, + &gateways_as_nodes, + false, + ) + .await?; Ok(selected_gateway) } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml new file mode 100644 index 0000000000..7b70ae86e0 --- /dev/null +++ b/nym-node/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "nym-node" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +bytes = "1.5.0" +colored = "2" +serde = { workspace = true, features = ["derive"] } +serde_yaml = "0.9.25" +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true, features = ["macros", "sync"] } + +# Wireguard: +fastrand = "2" + +# HTTP API: +axum = { workspace = true } +# useful for `#[axum_macros::debug_handler]` +#axum-macros = "0.3.8" +mime = "0.3.17" +hyper = { workspace = true } +tower = { version = "0.4.13" } +tower-http = { version = "0.4.4", features = ["fs"] } + +# `actix_extras`? what the hell? +# hear me out first! +# we can't use `axum_extras` because of freaking ephemera that depends on `actix_extras`. +# however, it seems that pulling in `actix_extras` pulls in just enough shared features to improve `IntoParams` for our Query attributes +utoipa = { workspace = true, features = ["actix_extras"] } # can't use `"axum_extras"` feature because ephemera uses `"actix_extras"` -.-' +utoipa-swagger-ui = { workspace = true, features = ["axum"] } + +# if we ever wanted redoc/rapidoc bridges: +#utoipa-redoc = { version = "0.1.0", features = ["axum"] } +#utoipa-rapidoc = { version = "0.1.0", features = ["axum"] } + +nym-config = { path = "../common/config" } +nym-crypto = { path = "../common/crypto", features = ["asymmetric" ]} +nym-node-requests = { path = "nym-node-requests", default-features = false, features = ["openapi"]} +nym-task = { path = "../common/task" } +nym-wireguard-types = { path = "../common/wireguard-types", features = ["verify"] } + +[dev-dependencies] +dashmap = { workspace = true } +rand = "0.7.3" +nym-crypto = { path = "../common/crypto", features = ["rand"]} +hmac = "0.12.1" +x25519-dalek = { version = "2.0.0" } diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml new file mode 100644 index 0000000000..ff7cb13336 --- /dev/null +++ b/nym-node/nym-node-requests/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "nym-node-requests" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } + +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } + +# feature-specific dependencies: + +## client: +async-trait = { workspace = true, optional = true } +http-api-client = { path = "../../common/http-api-client", optional = true } + +## openapi: +utoipa = { workspace = true, optional = true } +nym-bin-common = { path = "../../common/bin-common", features = ["bin_info_schema"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } + +[features] +default = ["client"] +client = ["http-api-client", "async-trait"] +openapi = ["utoipa", "nym-bin-common/openapi", "nym-wireguard-types/openapi"] diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs new file mode 100644 index 0000000000..1967b6963e --- /dev/null +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -0,0 +1,47 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::api::v1::gateway::models::WebSockets; +use crate::api::v1::node::models::SignedHostInformation; +use crate::api::ErrorResponse; +use crate::routes; +use async_trait::async_trait; +use http_api_client::{ApiClient, HttpClientError}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; + +use crate::api::v1::health::models::NodeHealth; +pub use http_api_client::Client; + +pub type NymNodeApiClientError = HttpClientError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait NymNodeApiClientExt: ApiClient { + async fn get_health(&self) -> Result { + self.get_json_from(routes::api::v1::health_absolute()).await + } + + async fn get_host_information(&self) -> Result { + self.get_json_from(routes::api::v1::host_info_absolute()) + .await + } + + async fn get_build_information( + &self, + ) -> Result { + self.get_json_from(routes::api::v1::build_info_absolute()) + .await + } + + // TODO: implement calls for other endpoints; for now I only care about the wss + async fn get_mixnet_websockets(&self) -> Result { + self.get_json_from( + routes::api::v1::gateway::client_interfaces::mixnet_websockets_absolute(), + ) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NymNodeApiClientExt for Client {} diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs new file mode 100644 index 0000000000..8da95074a9 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -0,0 +1,85 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::api::v1::node::models::HostInformation; +use crate::error::Error; +use nym_crypto::asymmetric::identity; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; + +#[cfg(feature = "client")] +pub mod client; +pub mod v1; + +#[cfg(feature = "client")] +pub use client::Client; + +// create the type alias manually if openapi is not enabled +#[cfg(not(feature = "openapi"))] +pub type SignedHostInformation = SignedData; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", aliases(SignedHostInformation = SignedData))] +pub struct SignedData { + // #[serde(flatten)] + pub data: T, + pub signature: String, +} + +impl SignedData { + pub fn new(data: T, key: &identity::PrivateKey) -> Result + where + T: Serialize, + { + let plaintext = serde_json::to_string(&data)?; + let signature = key.sign(plaintext).to_base58_string(); + Ok(SignedData { data, signature }) + } + + pub fn verify(&self, key: &identity::PublicKey) -> bool + where + T: Serialize, + { + let Ok(plaintext) = serde_json::to_string(&self.data) else { + return false; + }; + let Ok(signature) = identity::Signature::from_base58_string(&self.signature) else { + return false; + }; + + key.verify(plaintext, &signature).is_ok() + } +} + +impl SignedHostInformation { + pub fn verify_host_information(&self) -> bool { + let Ok(pub_key) = identity::PublicKey::from_base58_string(&self.keys.ed25519) else { + return false; + }; + + self.verify(&pub_key) + } +} + +impl Deref for SignedData { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ErrorResponse { + pub message: String, +} + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.message.fmt(f) + } +} diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/mod.rs new file mode 100644 index 0000000000..487c41bb7d --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod wireguard; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/models.rs new file mode 100644 index 0000000000..27c086d8c8 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interfaces/wireguard/models.rs @@ -0,0 +1,7 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_wireguard_types::{ + ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce, + PeerPublicKey, +}; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs new file mode 100644 index 0000000000..6c690d4f82 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod client_interfaces; +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs new file mode 100644 index 0000000000..6287e9612a --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs @@ -0,0 +1,38 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Gateway { + pub client_interfaces: ClientInterfaces, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Wireguard { + #[cfg_attr(feature = "openapi", schema(example = 51820, default = 51820))] + pub port: u16, + + pub public_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ClientInterfaces { + pub wireguard: Option, + + pub mixnet_websockets: Option, + // pub mixnet_tcp: +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct WebSockets { + #[cfg_attr(feature = "openapi", schema(example = 9000, default = 9000))] + pub ws_port: u16, + + pub wss_port: Option, +} diff --git a/nym-node/nym-node-requests/src/api/v1/health/mod.rs b/nym-node/nym-node-requests/src/api/v1/health/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/health/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/health/models.rs b/nym-node/nym-node-requests/src/api/v1/health/models.rs new file mode 100644 index 0000000000..30d9889c11 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/health/models.rs @@ -0,0 +1,35 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct NodeHealth { + pub status: NodeStatus, + pub uptime: u64, +} + +impl NodeHealth { + pub fn new_healthy(uptime: Duration) -> Self { + NodeHealth { + status: NodeStatus::Up, + uptime: uptime.as_secs(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub enum NodeStatus { + Up, +} + +impl NodeStatus { + pub fn is_up(&self) -> bool { + matches!(self, NodeStatus::Up) + } +} diff --git a/nym-node/nym-node-requests/src/api/v1/mixnode/mod.rs b/nym-node/nym-node-requests/src/api/v1/mixnode/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/mixnode/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs b/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs new file mode 100644 index 0000000000..76b8c9cc5b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs @@ -0,0 +1,15 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Mixnode { + // /// Base58 encoded ed25519 EdDSA public key of the mixnode. + // pub encoded_identity_key: String, + // + // /// Base58-encoded x25519 public key used for sphinx key derivation. + // pub encoded_sphinx_key: String, +} diff --git a/nym-node/nym-node-requests/src/api/v1/mod.rs b/nym-node/nym-node-requests/src/api/v1/mod.rs new file mode 100644 index 0000000000..b07fdbb7aa --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod gateway; +pub mod health; +pub mod mixnode; +pub mod network_requester; +pub mod node; diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs new file mode 100644 index 0000000000..b8aa29236b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs @@ -0,0 +1,31 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct NetworkRequester { + /// Base58 encoded ed25519 EdDSA public key of the network requester. + pub encoded_identity_key: String, + + /// Base58-encoded x25519 public key used for performing key exchange with remote clients. + pub encoded_x25519_key: String, + + /// Nym address of this network requester. + pub address: String, +} + +// #[derive(Serialize, Debug, Clone, ToSchema)] +// pub struct ExitPolicy { +// // pub allowed_ports: +// // pub deny_list: DenyList, +// } +// +// #[derive(Serialize, Debug, Clone, ToSchema)] +// pub struct DenyListEntry { +// // pub ports: +// // pub ips: +// pub description: String, +// } diff --git a/nym-node/nym-node-requests/src/api/v1/node/mod.rs b/nym-node/nym-node-requests/src/api/v1/node/mod.rs new file mode 100644 index 0000000000..c57081432b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/node/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs new file mode 100644 index 0000000000..260752436b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -0,0 +1,43 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; + +pub use crate::api::SignedHostInformation; +pub use nym_bin_common::build_information::BinaryBuildInformationOwned; + +#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct NodeRoles { + pub mixnode_enabled: bool, + pub gateway_enabled: bool, + pub network_requester_enabled: bool, +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct HostInformation { + /// Ip address(es) of this host, such as `1.1.1.1`. + #[cfg_attr(feature = "openapi", schema(value_type = Vec, format = Byte, example = json!(["1.1.1.1"])))] + pub ip_address: Vec, + + /// Optional hostname of this node, for example `nymtech.net`. + #[cfg_attr(feature = "openapi", schema(example = "nymtech.net"))] + pub hostname: Option, + + /// Public keys associated with this node. + pub keys: HostKeys, +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct HostKeys { + /// Base58-encoded ed25519 public key of this node. Currently it corresponds to either mixnode's or gateway's identity. + pub ed25519: String, + + /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. + /// Currently it corresponds to either mixnode's or gateway's key. + pub x25519: String, +} diff --git a/nym-node/nym-node-requests/src/error.rs b/nym-node/nym-node-requests/src/error.rs new file mode 100644 index 0000000000..d14e252270 --- /dev/null +++ b/nym-node/nym-node-requests/src/error.rs @@ -0,0 +1,19 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("failed to serialize json data: {source}")] + JsonSerializationFailure { + #[from] + source: serde_json::Error, + }, + + #[error(transparent)] + WireguardError { + #[from] + source: nym_wireguard_types::Error, + }, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs new file mode 100644 index 0000000000..e1922f5d3b --- /dev/null +++ b/nym-node/nym-node-requests/src/lib.rs @@ -0,0 +1,142 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +pub mod api; +pub mod error; + +// still thinking how to nicely organise it +pub mod routes { + use crate::absolute_route; + + pub const LANDING_PAGE: &str = "/"; + pub const ROOT: &str = "/"; + pub const API: &str = "/api"; + + pub mod api { + use super::*; + + pub const V1: &str = "/v1"; + + absolute_route!(v1_absolute, super::API, V1); + + pub mod v1 { + use super::*; + + pub const ROLES: &str = "/roles"; + pub const BUILD_INFO: &str = "/build-information"; + pub const HOST_INFO: &str = "/host-information"; + + pub const HEALTH: &str = "/health"; + pub const GATEWAY: &str = "/gateway"; + pub const MIXNODE: &str = "/mixnode"; + pub const NETWORK_REQUESTER: &str = "/network-requester"; + pub const SWAGGER: &str = "/swagger"; + + // define helper functions to get absolute routes + absolute_route!(health_absolute, v1_absolute(), HEALTH); + absolute_route!(roles_absolute, v1_absolute(), ROLES); + absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO); + absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO); + absolute_route!(gateway_absolute, v1_absolute(), GATEWAY); + absolute_route!(mixnode_absolute, v1_absolute(), MIXNODE); + absolute_route!(network_requester_absolute, v1_absolute(), NETWORK_REQUESTER); + absolute_route!(swagger_absolute, v1_absolute(), SWAGGER); + + pub mod gateway { + use super::*; + + pub const CLIENT_INTERFACES: &str = "/client-interfaces"; + + absolute_route!( + client_interfaces_absolute, + gateway_absolute(), + CLIENT_INTERFACES + ); + + pub mod client_interfaces { + use super::*; + + pub const WIREGUARD: &str = "/wireguard"; + pub const WEBSOCKETS: &str = "/mixnet-websockets"; + + absolute_route!(wireguard_absolute, client_interfaces_absolute(), WIREGUARD); + absolute_route!( + mixnet_websockets_absolute, + client_interfaces_absolute(), + WEBSOCKETS + ); + + pub mod wireguard { + use super::*; + + pub const CLIENT: &str = "/client"; + pub const CLIENTS: &str = "/clients"; + + absolute_route!(client_absolute, wireguard_absolute(), CLIENT); + absolute_route!(clients_absolute, wireguard_absolute(), CLIENTS); + } + } + } + + pub mod mixnode { + // use super::*; + } + + pub mod network_requester { + // use super::*; + } + } + } +} + +#[macro_export] +macro_rules! absolute_route { + ( $name:ident, $parent:expr, $suffix:expr ) => { + pub fn $name() -> String { + format!("{}{}", $parent, $suffix) + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn route_expansion_works() { + assert_eq!("/api/v1", routes::api::v1_absolute()); + + assert_eq!( + "/api/v1/build-information", + routes::api::v1::build_info_absolute() + ); + assert_eq!( + "/api/v1/host-information", + routes::api::v1::host_info_absolute() + ); + assert_eq!("/api/v1/roles", routes::api::v1::roles_absolute()); + + assert_eq!("/api/v1/gateway", routes::api::v1::gateway_absolute()); + assert_eq!( + "/api/v1/gateway/client-interfaces", + routes::api::v1::gateway::client_interfaces_absolute() + ); + assert_eq!( + "/api/v1/gateway/client-interfaces/wireguard", + routes::api::v1::gateway::client_interfaces::wireguard_absolute() + ); + assert_eq!( + "/api/v1/gateway/client-interfaces/mixnet-websockets", + routes::api::v1::gateway::client_interfaces::mixnet_websockets_absolute() + ); + + assert_eq!("/api/v1/mixnode", routes::api::v1::mixnode_absolute()); + assert_eq!( + "/api/v1/network-requester", + routes::api::v1::network_requester_absolute() + ); + } +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs new file mode 100644 index 0000000000..87b464c48e --- /dev/null +++ b/nym-node/src/config/mod.rs @@ -0,0 +1,92 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use serde::{Deserialize, Serialize}; +use serde_helpers::*; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; + +pub mod persistence; +mod serde_helpers; + +pub const DEFAULT_WIREGUARD_PORT: u16 = 51820; +pub const DEFAULT_HTTP_PORT: u16 = DEFAULT_NYM_NODE_HTTP_PORT; + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Host { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + pub public_ips: Vec, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_string")] + pub hostname: Option, +} + +impl Host { + pub fn validate(&self) -> bool { + if self.public_ips.is_empty() { + return false; + } + + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct Http { + /// Socket address this node will use for binding its http API. + /// default: `0.0.0.0:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_path")] + pub landing_page_assets_path: Option, +} + +impl Default for Http { + fn default() -> Self { + Http { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct Wireguard { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `0.0.0.0:51820` + pub bind_address: SocketAddr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: persistence::WireguardPaths, +} + +impl Default for Wireguard { + fn default() -> Self { + Wireguard { + enabled: false, + bind_address: SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + DEFAULT_WIREGUARD_PORT, + ), + announced_port: DEFAULT_WIREGUARD_PORT, + storage_paths: persistence::WireguardPaths {}, + } + } +} diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs new file mode 100644 index 0000000000..2ceed4b3cc --- /dev/null +++ b/nym-node/src/config/persistence.rs @@ -0,0 +1,10 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPaths { + // pub keys: +} diff --git a/nym-node/src/config/serde_helpers.rs b/nym-node/src/config/serde_helpers.rs new file mode 100644 index 0000000000..df0f42bb44 --- /dev/null +++ b/nym-node/src/config/serde_helpers.rs @@ -0,0 +1,29 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Deserializer}; +use std::path::PathBuf; + +pub(super) fn de_maybe_path<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let path = PathBuf::deserialize(deserializer)?; + if path.as_os_str().is_empty() { + Ok(None) + } else { + Ok(Some(path)) + } +} + +pub(super) fn de_maybe_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let path = String::deserialize(deserializer)?; + if path.is_empty() { + Ok(None) + } else { + Ok(Some(path)) + } +} diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs new file mode 100644 index 0000000000..09cbbe8952 --- /dev/null +++ b/nym-node/src/error.rs @@ -0,0 +1,36 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::wireguard::error::WireguardError; +use std::net::{IpAddr, SocketAddr}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymNodeError { + #[error("failed to bind the HTTP API to {bind_address}: {source}")] + HttpBindFailure { + bind_address: SocketAddr, + source: hyper::Error, + }, + + #[error("this node hasn't set any valid public addresses to announce. Please modify [host.public_ips] section of your config")] + NoPublicIps, + + #[error("this node attempted to announce an invalid public address: {address}. Please modify [host.public_ips] section of your config. Alternatively, if you wanted to use it in the local setting, run the node with the '--local' flag.")] + InvalidPublicIp { address: IpAddr }, + + #[error("failed to use nym-node requests: {source}")] + RequestError { + #[from] + source: nym_node_requests::error::Error, + }, + + #[error(transparent)] + WireguardError { + #[from] + source: WireguardError, + }, + + #[error("unimplemented")] + Unimplemented, +} diff --git a/nym-node/src/http/middleware/logging.rs b/nym-node/src/http/middleware/logging.rs new file mode 100644 index 0000000000..68b12b4155 --- /dev/null +++ b/nym-node/src/http/middleware/logging.rs @@ -0,0 +1,49 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::{ + extract::ConnectInfo, + http::{HeaderValue, Request}, + middleware::Next, + response::IntoResponse, +}; +use colored::*; +use hyper::header::{HOST, USER_AGENT}; +use std::net::SocketAddr; +use tracing::info; + +/// Simple logger for requests +pub async fn logger( + ConnectInfo(addr): ConnectInfo, + req: Request, + next: Next, +) -> impl IntoResponse { + let method = req.method().to_string().green(); + let uri = req.uri().to_string().blue(); + let agent = header_map( + req.headers().get(USER_AGENT), + "Unknown User Agent".to_string(), + ); + + let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); + + let res = next.run(req).await; + let status = res.status(); + let print_status = if status.is_client_error() || status.is_server_error() { + status.to_string().red() + } else if status.is_success() { + status.to_string().green() + } else { + status.to_string().yellow() + }; + + info!(target: "incoming request", "[{addr} -> {host}] {method} '{uri}': {print_status} / agent: {agent}"); + + res +} + +fn header_map(header: Option<&HeaderValue>, msg: String) -> String { + header + .map(|x| x.to_str().unwrap_or(&msg).to_string()) + .unwrap_or(msg) +} diff --git a/nym-node/src/http/middleware/mod.rs b/nym-node/src/http/middleware/mod.rs new file mode 100644 index 0000000000..872a6d3520 --- /dev/null +++ b/nym-node/src/http/middleware/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod logging; diff --git a/nym-node/src/http/mod.rs b/nym-node/src/http/mod.rs new file mode 100644 index 0000000000..a7a4fa0d4b --- /dev/null +++ b/nym-node/src/http/mod.rs @@ -0,0 +1,64 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::extract::connect_info::IntoMakeServiceWithConnectInfo; +use axum::Router; +use hyper::server::conn::AddrIncoming; +use hyper::Server; +use nym_task::TaskClient; +use std::net::SocketAddr; +use tracing::{debug, error, info}; + +pub mod middleware; +pub mod router; +pub mod state; + +pub use router::{api, landing_page, Config, NymNodeRouter}; + +pub struct NymNodeHTTPServer { + task_client: Option, + inner: Server>, +} + +impl NymNodeHTTPServer { + pub(crate) fn new( + inner: Server>, + ) -> Self { + NymNodeHTTPServer { + task_client: None, + inner, + } + } + + #[must_use] + pub fn with_task_client(mut self, task_client: TaskClient) -> Self { + self.task_client = Some(task_client); + self + } + + async fn run_server_forever( + server: Server>, + ) { + if let Err(err) = server.await { + error!("the HTTP server has terminated with the error: {err}"); + } else { + error!("the HTTP server has terminated with producing any errors"); + } + } + + pub async fn run(self) { + info!("Started NymNodeHTTPServer on {}", self.inner.local_addr()); + if let Some(mut task_client) = self.task_client { + tokio::select! { + _ = task_client.recv_with_delay() => { + debug!("NymNodeHTTPServer: Received shutdown"); + } + _ = Self::run_server_forever(self.inner) => { } + } + } else { + Self::run_server_forever(self.inner).await + } + + debug!("NymNodeHTTPServer: Exiting"); + } +} diff --git a/nym-node/src/http/router/api/mod.rs b/nym-node/src/http/router/api/mod.rs new file mode 100644 index 0000000000..757ec48e97 --- /dev/null +++ b/nym-node/src/http/router/api/mod.rs @@ -0,0 +1,108 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::{Json, Router}; +use bytes::{BufMut, BytesMut}; +use serde::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; + +pub mod v1; + +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; +use crate::http::state::AppState; +pub use nym_node_requests::api as api_requests; +use nym_node_requests::routes; + +#[derive(Debug, Clone)] +pub struct Config { + pub v1_config: v1::Config, +} + +pub(super) fn routes(config: Config, initial_wg_state: WireguardAppState) -> Router { + Router::new().nest( + routes::api::V1, + v1::routes(config.v1_config, initial_wg_state), + ) +} + +#[derive(Debug, Clone, ToSchema)] +pub enum FormattedResponse { + Json(Json), + Yaml(Yaml), +} + +impl IntoResponse for FormattedResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + match self { + FormattedResponse::Json(json_response) => json_response.into_response(), + FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum Output { + #[default] + Json, + Yaml, +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] +#[serde(default)] +pub struct OutputParams { + pub output: Option, +} + +impl Output { + pub fn to_response(self, data: T) -> FormattedResponse { + match self { + Output::Json => FormattedResponse::Json(Json(data)), + Output::Yaml => FormattedResponse::Yaml(Yaml(data)), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[must_use] +pub struct Yaml(pub T); + +impl From for Yaml { + fn from(inner: T) -> Self { + Self(inner) + } +} + +impl IntoResponse for Yaml +where + T: Serialize, +{ + // replicates axum's Json + fn into_response(self) -> Response { + let mut buf = BytesMut::with_capacity(128).writer(); + match serde_yaml::to_writer(&mut buf, &self.0) { + Ok(()) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/yaml"), + )], + buf.into_inner().freeze(), + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response(), + } + } +} diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs new file mode 100644 index 0000000000..a9b4f6beb7 --- /dev/null +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; +use crate::http::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use axum::http::StatusCode; +use axum::routing::get; +use axum::Router; +use nym_node_requests::api::v1::gateway::models::{ClientInterfaces, WebSockets, Wireguard}; +use nym_node_requests::routes::api::v1::gateway::client_interfaces; + +pub(crate) mod wireguard; + +pub(crate) fn routes( + interfaces: Option, + initial_wg_state: WireguardAppState, +) -> Router { + Router::new() + .route( + "/", + get({ + let interfaces = interfaces.clone(); + move |query| client_interfaces(interfaces, query) + }), + ) + .route( + client_interfaces::WEBSOCKETS, + get({ + let websockets = interfaces.as_ref().and_then(|i| i.mixnet_websockets); + move |query| mixnet_websockets(websockets, query) + }), + ) + .nest( + client_interfaces::WIREGUARD, + wireguard::routes(initial_wg_state), + ) + .route( + client_interfaces::WIREGUARD, + get({ + let wireguard_cfg_info = interfaces.and_then(|i| i.wireguard); + move |query| wireguard_info(wireguard_cfg_info, query) + }), + ) +} + +/// Returns client interfaces supported by this gateway. +#[utoipa::path( + get, + path = "/client-interfaces", + context_path = "/api/v1/gateway", + tag = "Gateway", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = ClientInterfaces), + ("application/yaml" = ClientInterfaces) + )) + ), + params(OutputParams) +)] +pub(crate) async fn client_interfaces( + interfaces: Option, + Query(output): Query, +) -> Result { + let interfaces = interfaces.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(interfaces)) +} + +pub type ClientInterfacesResponse = FormattedResponse; + +/// Returns client interfaces supported by this gateway. +#[utoipa::path( + get, + path = "/wireguard", + context_path = "/api/v1/gateway/client-interfaces", + tag = "Gateway", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = Wireguard), + ("application/yaml" = Wireguard) + )) + ), + params(OutputParams) +)] +pub(crate) async fn wireguard_info( + wireguard: Option, + Query(output): Query, +) -> Result { + let wireguard = wireguard.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(wireguard)) +} + +pub type WireguardResponse = FormattedResponse; + +/// Returns client interfaces supported by this gateway. +#[utoipa::path( + get, + path = "/mixnet-websockets", + context_path = "/api/v1/gateway/client-interfaces", + tag = "Gateway", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = WebSockets), + ("application/yaml" = WebSockets) + )) + ), + params(OutputParams) +)] +pub(crate) async fn mixnet_websockets( + websockets: Option, + Query(output): Query, +) -> Result { + let websockets = websockets.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(websockets)) +} + +pub type MixnetWebSocketsResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs new file mode 100644 index 0000000000..f21a1e719f --- /dev/null +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs @@ -0,0 +1,194 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::{ + WireguardAppState, WireguardAppStateInner, +}; +use crate::http::api::{FormattedResponse, OutputParams}; +use crate::http::router::types::RequestError; +use crate::wireguard::error::WireguardError; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::Json; +use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{ + ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce, PeerPublicKey, +}; + +async fn process_final_message( + client: GatewayClient, + state: &WireguardAppStateInner, +) -> Result { + let preshared_nonce = { + if let Some(nonce) = state.registration_in_progress.get(&client.pub_key()) { + *nonce + } else { + return Err(RequestError::from_err( + WireguardError::RegistrationNotInProgress, + StatusCode::BAD_REQUEST, + )); + } + }; + + if client + .verify(state.dh_keypair.private_key(), preshared_nonce) + .is_ok() + { + state.registration_in_progress.remove(&client.pub_key()); + state.client_registry.insert(client.pub_key(), client); + + Ok(StatusCode::OK) + } else { + Err(RequestError::from_err( + WireguardError::MacVerificationFailure, + StatusCode::BAD_REQUEST, + )) + } +} + +async fn process_init_message(init_message: InitMessage, state: &WireguardAppStateInner) -> Nonce { + let nonce: u64 = fastrand::u64(..); + state + .registration_in_progress + .insert(init_message.pub_key(), nonce); + nonce +} + +/// Perform wireguard client registration. +#[utoipa::path( + post, + path = "/client", + context_path = "/api/v1/gateway/client-interfaces/wireguard", + tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)", + request_body( + content = ClientMessage, + description = "Data used for proceeding with client wireguard registration", + content_type = "application/json" + ), + responses( + (status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"), + (status = 400, body = ErrorResponse), + (status = 200, content( + ("application/json" = ClientRegistrationResponse), + ("application/yaml" = ClientRegistrationResponse) + )) + ), + params(OutputParams) +)] +pub(crate) async fn register_client( + State(state): State, + Query(output): Query, + Json(payload): Json, +) -> Result { + let output = output.output.unwrap_or_default(); + + let Some(state) = state.inner() else { + return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED)); + }; + + match payload { + ClientMessage::Initial(init) => { + let nonce = process_init_message(init, state).await; + let response = ClientRegistrationResponse::PendingRegistration { nonce }; + Ok(output.to_response(response)) + } + ClientMessage::Final(finalize) => { + let result = process_final_message(finalize, state).await?; + if result.is_success() { + let response = ClientRegistrationResponse::Registered { success: true }; + Ok(output.to_response(response)) + } else { + Err(RequestError::new_status(result)) + } + } + } +} + +pub type RegisterClientResponse = FormattedResponse; + +/// Get public keys of all registered wireguard clients. +#[utoipa::path( + get, + path = "/clients", + context_path = "/api/v1/gateway/client-interfaces/wireguard", + tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)", + responses( + (status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = Vec), + ("application/yaml" = Vec) + )) + ), + params(OutputParams) +)] +pub(crate) async fn get_all_clients( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + let Some(state) = state.inner() else { + return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED)); + }; + + let clients = state + .client_registry + .iter() + .map(|c| c.pub_key()) + .collect::>(); + + Ok(output.to_response(clients)) +} + +pub type AllClientsResponse = FormattedResponse>; + +/// Get client details of the registered wireguard client by its public key. +#[utoipa::path( + get, + path = "/client/{pub_key}", + context_path = "/api/v1/gateway/client-interfaces/wireguard", + tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)", + params( + ("pub_key", Path, description = "The public key of the client"), + OutputParams + ), + responses( + (status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"), + (status = 404, body = ErrorResponse, description = "there are no clients with the provided public key"), + (status = 400, body = ErrorResponse), + (status = 200, content( + ("application/json" = Vec), + ("application/yaml" = Vec) + )) + ), +)] +pub(crate) async fn get_client( + Path(pub_key): Path, + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + let Some(state) = state.inner() else { + return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED)); + }; + + let clients = state + .client_registry + .iter() + .filter_map(|c| { + if c.pub_key() == pub_key { + Some(c.clone()) + } else { + None + } + }) + .collect::>(); + + if clients.is_empty() { + return Err(RequestError::new_status(StatusCode::NOT_FOUND)); + } + + Ok(output.to_response(clients)) +} + +pub type ClientResponse = FormattedResponse>; diff --git a/gateway/src/node/http/mod.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs similarity index 51% rename from gateway/src/node/http/mod.rs rename to nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs index 91f73561d2..8e29d8c0fb 100644 --- a/gateway/src/node/http/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs @@ -1,88 +1,112 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::client_registry::{ + get_all_clients, get_client, register_client, +}; +use crate::wireguard::types::{GatewayClientRegistry, PendingRegistrations}; +use axum::routing::{get, post}; +use axum::Router; +use nym_crypto::asymmetric::encryption; +use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; use std::sync::Arc; -use axum::{ - routing::{get, post}, - Router, -}; -use dashmap::DashMap; -use log::info; -use nym_crypto::asymmetric::encryption; +pub(crate) mod client_registry; -mod api; -use api::v1::client_registry::*; -use nym_types::gateway_client_registration::GatewayClientRegistry; -use nym_types::wireguard::PeerPublicKey; - -const ROUTE_PREFIX: &str = "/api/v1/gateway/client-interfaces/wireguard"; - -pub struct ApiState { - client_registry: Arc, - sphinx_key_pair: Arc, - registration_in_progress: Arc>, +// I don't see any reason why this state should be accessible to any routes outside /wireguard +// if anyone finds compelling reason, it could be moved to the `AppState` struct instead +#[derive(Clone, Default)] +pub struct WireguardAppState { + inner: Option, } -fn router_with_state(state: Arc) -> Router { +impl WireguardAppState { + pub fn new( + dh_keypair: Arc, + client_registry: Arc, + registration_in_progress: Arc, + ) -> Self { + WireguardAppState { + inner: Some(WireguardAppStateInner { + dh_keypair, + client_registry, + registration_in_progress, + }), + } + } + + // #[allow(dead_code)] + // pub(crate) fn dh_keypair(&self) -> Option<&encryption::KeyPair> { + // self.inner.as_ref().map(|s| s.dh_keypair.as_ref()) + // } + // + // #[allow(dead_code)] + // pub(crate) fn client_registry(&self) -> Option<&RwLock> { + // self.inner.as_ref().map(|s| s.client_registry.as_ref()) + // } + // + // #[allow(dead_code)] + // pub(crate) fn registration_in_progress(&self) -> Option<&RwLock> { + // self.inner + // .as_ref() + // .map(|s| s.registration_in_progress.as_ref()) + // } + + // not sure what to feel about exposing this method + pub(crate) fn inner(&self) -> Option<&WireguardAppStateInner> { + self.inner.as_ref() + } +} + +// helper macro to deal with missing wg state (if not being exposed by the node) +#[macro_export] +macro_rules! get_state { + ( $state: ident, $field: ident ) => {{ + let Some(ref inner) = $state.inner else { + return ::axum::http::StatusCode::NOT_IMPLEMENTED; + }; + inner.$field.as_ref() + }}; +} + +#[derive(Clone)] +pub(crate) struct WireguardAppStateInner { + dh_keypair: Arc, + client_registry: Arc, + registration_in_progress: Arc, +} + +pub(crate) fn routes(initial_state: WireguardAppState) -> Router { Router::new() - .route(&format!("{}/clients", ROUTE_PREFIX), get(get_all_clients)) - .route(&format!("{}/client", ROUTE_PREFIX), post(register_client)) - .route( - &format!("{}/client/:pub_key", ROUTE_PREFIX), - get(get_client), - ) - .with_state(state) -} - -pub(crate) async fn start_http_api( - client_registry: Arc, - sphinx_key_pair: Arc, -) { - // Port should be 80 post smoosh - let port = 8000; - - info!("Started HTTP API on port {}", port); - - let client_registry = Arc::clone(&client_registry); - - let state = Arc::new(ApiState { - client_registry, - sphinx_key_pair, - registration_in_progress: Arc::new(DashMap::new()), - }); - - let routes = router_with_state(state); - - #[allow(clippy::unwrap_used)] - axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap()) - .serve(routes.into_make_service()) - .await - .unwrap(); + // .route("/", get()) + .route(wireguard::CLIENTS, get(get_all_clients)) + .route(wireguard::CLIENT, post(register_client)) + .route(&format!("{}/:pub_key", wireguard::CLIENT), get(get_client)) + .with_state(initial_state) } #[cfg(test)] mod test { - use std::net::SocketAddr; - use std::str::FromStr; - use std::sync::Arc; - + use crate::http::api::v1::gateway::client_interfaces::wireguard::{ + routes, WireguardAppState, WireguardAppStateInner, + }; use axum::body::Body; use axum::http::Request; use axum::http::StatusCode; use dashmap::DashMap; use hmac::Mac; - use nym_types::gateway_client_registration::ClientMac; - use nym_types::gateway_client_registration::ClientMessage; - use nym_types::gateway_client_registration::GatewayClient; - use nym_types::gateway_client_registration::HmacSha256; - use nym_types::gateway_client_registration::InitMessage; - use nym_types::wireguard::PeerPublicKey; + use nym_crypto::asymmetric::encryption; + use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{ + ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, + PeerPublicKey, + }; + use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; + use nym_wireguard_types::registration::HmacSha256; + use std::sync::Arc; use tower::Service; use tower::ServiceExt; - - use nym_crypto::asymmetric::encryption; use x25519_dalek::{PublicKey, StaticSecret}; - use crate::node::http::{router_with_state, ApiState, ROUTE_PREFIX}; - #[tokio::test] async fn registration() { // 1. Provision random keys for gateway and client @@ -110,22 +134,25 @@ mod test { let registration_in_progress = Arc::new(DashMap::new()); let client_registry = Arc::new(DashMap::new()); - let state = Arc::new(ApiState { - client_registry: Arc::clone(&client_registry), - sphinx_key_pair: Arc::new(gateway_key_pair), - registration_in_progress: Arc::clone(®istration_in_progress), - }); + let state = WireguardAppState { + inner: Some(WireguardAppStateInner { + client_registry: Arc::clone(&client_registry), + dh_keypair: Arc::new(gateway_key_pair), + registration_in_progress: Arc::clone(®istration_in_progress), + }), + }; // `Router` implements `tower::Service>` so we can // call it like any tower service, no need to run an HTTP server. - let mut app = router_with_state(state); + let mut app = routes(state); - let init_message = - ClientMessage::Init(InitMessage::new(PeerPublicKey::new(client_static_public))); + let init_message = ClientMessage::Initial(InitMessage { + pub_key: PeerPublicKey::new(client_static_public), + }); let init_request = Request::builder() .method("POST") - .uri(format!("{}/client", ROUTE_PREFIX)) + .uri(wireguard::CLIENT) .header("Content-type", "application/json") .body(Body::from(serde_json::to_vec(&init_message).unwrap())) .unwrap(); @@ -140,27 +167,29 @@ mod test { assert_eq!(response.status(), StatusCode::OK); assert!(!registration_in_progress.is_empty()); - let nonce: Option = + let ClientRegistrationResponse::PendingRegistration { nonce } = serde_json::from_slice(&hyper::body::to_bytes(response.into_body()).await.unwrap()) - .unwrap(); - assert!(nonce.is_some()); + .unwrap() + else { + panic!("invalid response") + }; let mut mac = HmacSha256::new_from_slice(client_dh.as_bytes()).unwrap(); mac.update(client_static_public.as_bytes()); mac.update("127.0.0.1".as_bytes()); mac.update("8080".as_bytes()); - mac.update(&nonce.unwrap().to_le_bytes()); + mac.update(&nonce.to_le_bytes()); let mac = mac.finalize().into_bytes(); let finalized_message = ClientMessage::Final(GatewayClient { pub_key: PeerPublicKey::new(client_static_public), - socket: SocketAddr::from_str("127.0.0.1:8080").unwrap(), + socket: "127.0.0.1:8080".parse().unwrap(), mac: ClientMac::new(mac.as_slice().to_vec()), }); let final_request = Request::builder() .method("POST") - .uri(format!("{}/client", ROUTE_PREFIX)) + .uri(wireguard::CLIENT) .header("Content-type", "application/json") .body(Body::from(serde_json::to_vec(&finalized_message).unwrap())) .unwrap(); @@ -177,7 +206,7 @@ mod test { let clients_request = Request::builder() .method("GET") - .uri(format!("{}/clients", ROUTE_PREFIX)) + .uri(wireguard::CLIENTS) .body(Body::empty()) .unwrap(); @@ -199,7 +228,7 @@ mod test { assert_eq!( client_registry .iter() - .map(|c| c.value().pub_key().clone()) + .map(|c| c.value().pub_key()) .collect::>(), clients ) diff --git a/nym-node/src/http/router/api/v1/gateway/mod.rs b/nym-node/src/http/router/api/v1/gateway/mod.rs new file mode 100644 index 0000000000..d2bba50118 --- /dev/null +++ b/nym-node/src/http/router/api/v1/gateway/mod.rs @@ -0,0 +1,37 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; +use axum::routing::get; +use axum::Router; +use nym_node_requests::api::v1::gateway::models; +use nym_node_requests::routes::api::v1::gateway; + +pub mod client_interfaces; +pub mod root; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub details: Option, +} + +pub(crate) fn routes( + config: Config, + initial_wg_state: WireguardAppState, +) -> Router { + Router::new() + .route( + "/", + get({ + let gateway_details = config.details.clone(); + move |query| root::root_gateway(gateway_details, query) + }), + ) + .nest( + gateway::CLIENT_INTERFACES, + client_interfaces::routes( + config.details.map(|g| g.client_interfaces), + initial_wg_state, + ), + ) +} diff --git a/nym-node/src/http/router/api/v1/gateway/root.rs b/nym-node/src/http/router/api/v1/gateway/root.rs new file mode 100644 index 0000000000..526d1825dd --- /dev/null +++ b/nym-node/src/http/router/api/v1/gateway/root.rs @@ -0,0 +1,33 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use axum::http::StatusCode; +use nym_node_requests::api::v1::gateway::models::Gateway; + +/// Returns root gateway information +#[utoipa::path( + get, + path = "", + context_path = "/api/v1/gateway", + tag = "Gateway", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = Gateway), + ("application/yaml" = Gateway) + )) + ), + params(OutputParams) +)] +pub(crate) async fn root_gateway( + details: Option, + Query(output): Query, +) -> Result { + let details = details.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(details)) +} + +pub type GatewayResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/health.rs b/nym-node/src/http/router/api/v1/health.rs new file mode 100644 index 0000000000..08e9ff28d5 --- /dev/null +++ b/nym-node/src/http/router/api/v1/health.rs @@ -0,0 +1,34 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::{FormattedResponse, OutputParams}; +use crate::http::state::AppState; +use axum::extract::{Query, State}; +use nym_node_requests::api::v1::health::models::NodeHealth; + +/// Returns health status of this node. +#[utoipa::path( + get, + path = "/health", + context_path = "/api/v1", + tag = "Health", + responses( + (status = 200, content( + ("application/json" = Vec), + ("application/yaml" = Vec) + ), description = "the api is available and healthy") + ), + params(OutputParams) +)] +pub(crate) async fn root_health( + Query(output): Query, + State(state): State, +) -> HealthResponse { + let output = output.output.unwrap_or_default(); + let uptime = state.startup_time.elapsed(); + let health = NodeHealth::new_healthy(uptime); + + output.to_response(health) +} + +pub type HealthResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/mixnode/mod.rs b/nym-node/src/http/router/api/v1/mixnode/mod.rs new file mode 100644 index 0000000000..998a0edd3e --- /dev/null +++ b/nym-node/src/http/router/api/v1/mixnode/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::routing::get; +use axum::Router; +use nym_node_requests::api::v1::mixnode::models; + +pub mod root; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub details: Option, +} + +pub(crate) fn routes(config: Config) -> Router { + Router::new().route( + "/", + get({ + let mixnode_details = config.details; + move |query| root::root_mixnode(mixnode_details, query) + }), + ) +} diff --git a/nym-node/src/http/router/api/v1/mixnode/root.rs b/nym-node/src/http/router/api/v1/mixnode/root.rs new file mode 100644 index 0000000000..2b71fea15c --- /dev/null +++ b/nym-node/src/http/router/api/v1/mixnode/root.rs @@ -0,0 +1,33 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use axum::http::StatusCode; +use nym_node_requests::api::v1::mixnode::models::Mixnode; + +/// Returns root mixnode information +#[utoipa::path( + get, + path = "", + context_path = "/api/v1/mixnode", + tag = "Mixnode", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = Mixnode), + ("application/yaml" = Mixnode) + )) + ), + params(OutputParams) +)] +pub(crate) async fn root_mixnode( + details: Option, + Query(output): Query, +) -> Result { + let details = details.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(details)) +} + +pub type MixnodeResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/mod.rs b/nym-node/src/http/router/api/v1/mod.rs new file mode 100644 index 0000000000..cc81f9948f --- /dev/null +++ b/nym-node/src/http/router/api/v1/mod.rs @@ -0,0 +1,39 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; +use crate::http::state::AppState; +use axum::routing::get; +use axum::Router; +use nym_node_requests::routes::api::v1; + +pub mod gateway; +pub mod health; +pub mod mixnode; +pub mod network_requester; +pub mod node; +pub mod openapi; + +#[derive(Debug, Clone)] +pub struct Config { + pub node: node::Config, + pub gateway: gateway::Config, + pub mixnode: mixnode::Config, + pub network_requester: network_requester::Config, +} + +pub(super) fn routes(config: Config, initial_wg_state: WireguardAppState) -> Router { + Router::new() + .route(v1::HEALTH, get(health::root_health)) + .nest( + v1::GATEWAY, + gateway::routes(config.gateway, initial_wg_state), + ) + .nest(v1::MIXNODE, mixnode::routes(config.mixnode)) + .nest( + v1::NETWORK_REQUESTER, + network_requester::routes(config.network_requester), + ) + .merge(node::routes(config.node)) + .merge(openapi::route()) +} diff --git a/nym-node/src/http/router/api/v1/network_requester/mod.rs b/nym-node/src/http/router/api/v1/network_requester/mod.rs new file mode 100644 index 0000000000..aced86bad3 --- /dev/null +++ b/nym-node/src/http/router/api/v1/network_requester/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::routing::get; +use axum::Router; +use nym_node_requests::api::v1::network_requester::models; + +pub mod root; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub details: Option, +} + +pub(crate) fn routes(config: Config) -> Router { + Router::new().route( + "/", + get({ + let network_requester_details = config.details; + move |query| root::root_network_requester(network_requester_details, query) + }), + ) +} diff --git a/nym-node/src/http/router/api/v1/network_requester/root.rs b/nym-node/src/http/router/api/v1/network_requester/root.rs new file mode 100644 index 0000000000..a1849e2560 --- /dev/null +++ b/nym-node/src/http/router/api/v1/network_requester/root.rs @@ -0,0 +1,33 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use axum::http::StatusCode; +use nym_node_requests::api::v1::network_requester::models::NetworkRequester; + +/// Returns root network requester information +#[utoipa::path( + get, + path = "", + context_path = "/api/v1/network-requester", + tag = "Network Requester", + responses( + (status = 501, description = "the endpoint hasn't been implemented yet"), + (status = 200, content( + ("application/json" = NetworkRequester), + ("application/yaml" = NetworkRequester) + )) + ), + params(OutputParams) +)] +pub(crate) async fn root_network_requester( + details: Option, + Query(output): Query, +) -> Result { + let details = details.ok_or(StatusCode::NOT_IMPLEMENTED)?; + let output = output.output.unwrap_or_default(); + Ok(output.to_response(details)) +} + +pub type NetworkRequesterResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/node/build_information.rs b/nym-node/src/http/router/api/v1/node/build_information.rs new file mode 100644 index 0000000000..1288d94dda --- /dev/null +++ b/nym-node/src/http/router/api/v1/node/build_information.rs @@ -0,0 +1,30 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; + +/// Returns build metadata of the binary running the API +#[utoipa::path( + get, + path = "/build-information", + context_path = "/api/v1", + tag = "Node", + responses( + (status = 200, content( + ("application/json" = BinaryBuildInformationOwned), + ("application/yaml" = BinaryBuildInformationOwned) + )) + ), + params(OutputParams) +)] +pub(crate) async fn build_information( + build_information: BinaryBuildInformationOwned, + Query(output): Query, +) -> BuildInformationResponse { + let output = output.output.unwrap_or_default(); + output.to_response(build_information) +} + +pub type BuildInformationResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/node/host_information.rs b/nym-node/src/http/router/api/v1/node/host_information.rs new file mode 100644 index 0000000000..3581de10bc --- /dev/null +++ b/nym-node/src/http/router/api/v1/node/host_information.rs @@ -0,0 +1,30 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use nym_node_requests::api::v1::node::models::SignedHostInformation; + +/// Returns host information of this node. +#[utoipa::path( + get, + path = "/host-information", + context_path = "/api/v1", + tag = "Node", + responses( + (status = 200, content( + ("application/json" = SignedHostInformation), + ("application/yaml" = SignedHostInformation) + )) + ), + params(OutputParams) +)] +pub(crate) async fn host_information( + host_information: SignedHostInformation, + Query(output): Query, +) -> HostInformationResponse { + let output = output.output.unwrap_or_default(); + output.to_response(host_information) +} + +pub type HostInformationResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/node/mod.rs b/nym-node/src/http/router/api/v1/node/mod.rs new file mode 100644 index 0000000000..6644e75572 --- /dev/null +++ b/nym-node/src/http/router/api/v1/node/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::node::build_information::build_information; +use crate::http::api::v1::node::host_information::host_information; +use crate::http::api::v1::node::roles::roles; +use axum::routing::get; +use axum::Router; +use nym_node_requests::api::v1::node::models; +use nym_node_requests::routes::api::v1; + +pub mod build_information; +pub mod host_information; +pub mod roles; + +#[derive(Debug, Clone)] +pub struct Config { + pub build_information: models::BinaryBuildInformationOwned, + pub host_information: models::SignedHostInformation, + pub roles: models::NodeRoles, +} + +pub(super) fn routes(config: Config) -> Router { + Router::new() + .route( + v1::BUILD_INFO, + get({ + let build_info = config.build_information; + move |query| build_information(build_info, query) + }), + ) + .route( + v1::ROLES, + get({ + let node_roles = config.roles; + move |query| roles(node_roles, query) + }), + ) + .route( + v1::HOST_INFO, + get({ + let host_info = config.host_information; + move |query| host_information(host_info, query) + }), + ) +} diff --git a/nym-node/src/http/router/api/v1/node/roles.rs b/nym-node/src/http/router/api/v1/node/roles.rs new file mode 100644 index 0000000000..811954ed52 --- /dev/null +++ b/nym-node/src/http/router/api/v1/node/roles.rs @@ -0,0 +1,30 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api::{FormattedResponse, OutputParams}; +use axum::extract::Query; +use nym_node_requests::api::v1::node::models::NodeRoles; + +/// Returns roles supported by this node +#[utoipa::path( + get, + path = "/roles", + context_path = "/api/v1", + tag = "Node", + responses( + (status = 200, content( + ("application/json" = NodeRoles), + ("application/yaml" = NodeRoles) + )) + ), + params(OutputParams) +)] +pub(crate) async fn roles( + node_roles: NodeRoles, + Query(output): Query, +) -> RolesResponse { + let output = output.output.unwrap_or_default(); + output.to_response(node_roles) +} + +pub type RolesResponse = FormattedResponse; diff --git a/nym-node/src/http/router/api/v1/openapi.rs b/nym-node/src/http/router/api/v1/openapi.rs new file mode 100644 index 0000000000..8678c613d7 --- /dev/null +++ b/nym-node/src/http/router/api/v1/openapi.rs @@ -0,0 +1,65 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::router::api; +use crate::http::router::types::{ErrorResponse, RequestError}; +use axum::Router; +use nym_node_requests::api as api_requests; +use nym_node_requests::routes::api::v1; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +#[derive(OpenApi)] +#[openapi( + info(title = "NymNode API"), + paths( + api::v1::node::build_information::build_information, + api::v1::node::host_information::host_information, + api::v1::node::roles::roles, + api::v1::health::root_health, + api::v1::gateway::root::root_gateway, + api::v1::gateway::client_interfaces::client_interfaces, + api::v1::gateway::client_interfaces::wireguard_info, + api::v1::gateway::client_interfaces::mixnet_websockets, + api::v1::gateway::client_interfaces::wireguard::client_registry::register_client, + api::v1::gateway::client_interfaces::wireguard::client_registry::get_all_clients, + api::v1::gateway::client_interfaces::wireguard::client_registry::get_client, + api::v1::mixnode::root::root_mixnode, + api::v1::network_requester::root::root_network_requester, + ), + components( + schemas( + ErrorResponse, + api::Output, + api::OutputParams, + api_requests::v1::health::models::NodeHealth, + api_requests::v1::health::models::NodeStatus, + api_requests::v1::node::models::BinaryBuildInformationOwned, + api_requests::v1::node::models::SignedHostInformation, + api_requests::v1::node::models::HostInformation, + api_requests::v1::node::models::HostKeys, + api_requests::v1::node::models::NodeRoles, + api_requests::v1::gateway::models::Gateway, + api_requests::v1::gateway::models::Wireguard, + api_requests::v1::gateway::models::ClientInterfaces, + api_requests::v1::gateway::models::WebSockets, + api_requests::v1::gateway::client_interfaces::wireguard::models::ClientMessage, + api_requests::v1::gateway::client_interfaces::wireguard::models::InitMessage, + api_requests::v1::gateway::client_interfaces::wireguard::models::GatewayClient, + api_requests::v1::gateway::client_interfaces::wireguard::models::ClientRegistrationResponse, + api_requests::v1::mixnode::models::Mixnode, + api_requests::v1::network_requester::models::NetworkRequester, + ), + responses(RequestError) + ) +)] +pub(crate) struct ApiDoc; + +pub(crate) fn route() -> Router { + // provide absolute path to the openapi.json + let config = utoipa_swagger_ui::Config::from("/api/v1/api-docs/openapi.json"); + SwaggerUi::new(v1::SWAGGER) + .url("/api-docs/openapi.json", ApiDoc::openapi()) + .config(config) + .into() +} diff --git a/nym-node/src/http/router/landing_page.rs b/nym-node/src/http/router/landing_page.rs new file mode 100644 index 0000000000..6ac6a66b80 --- /dev/null +++ b/nym-node/src/http/router/landing_page.rs @@ -0,0 +1,32 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::response::Html; +use axum::routing::get; +use axum::Router; +use std::path::PathBuf; +use tower_http::services::ServeDir; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub assets_path: Option, +} + +pub(super) fn routes(config: Config) -> Router { + if let Some(assets) = config.assets_path { + Router::new().nest_service("/", ServeDir::new(assets)) + } else { + Router::new().route("/", get(default)) + } +} + +pub(super) async fn default() -> Html<&'static str> { + Html( + r#" +

Nym Node

+
+

default page of the nym node - you can customize it by setting the 'assets' path under '[http]' section of your config.

+
+ "#, + ) +} diff --git a/nym-node/src/http/router/mod.rs b/nym-node/src/http/router/mod.rs new file mode 100644 index 0000000000..a25c6427f8 --- /dev/null +++ b/nym-node/src/http/router/mod.rs @@ -0,0 +1,136 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NymNodeError; +pub use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; +use crate::http::middleware::logging; +use crate::http::state::AppState; +use crate::http::NymNodeHTTPServer; +use axum::Router; +use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard}; +use nym_node_requests::api::v1::mixnode::models::Mixnode; +use nym_node_requests::api::v1::network_requester::models::NetworkRequester; +use nym_node_requests::api::v1::node::models; +use nym_node_requests::api::SignedHostInformation; +use nym_node_requests::routes; +use std::net::SocketAddr; +use std::path::Path; +use tracing::warn; + +pub mod api; +pub mod landing_page; +pub mod types; + +#[derive(Debug, Clone)] +pub struct Config { + pub landing: landing_page::Config, + pub api: api::Config, +} + +impl Config { + pub fn new( + build_information: models::BinaryBuildInformationOwned, + host_information: SignedHostInformation, + ) -> Self { + Config { + landing: Default::default(), + api: api::Config { + v1_config: api::v1::Config { + node: api::v1::node::Config { + build_information, + host_information, + roles: Default::default(), + }, + gateway: Default::default(), + mixnode: Default::default(), + network_requester: Default::default(), + }, + }, + } + } + + pub fn with_wireguard_interface(mut self, wireguard: Wireguard) -> Self { + match &mut self.api.v1_config.gateway.details { + Some(gw) => gw.client_interfaces.wireguard = Some(wireguard), + None => { + warn!( + "can't add wireguard interface information as the gateway role is not enabled." + ); + } + } + self + } + + #[must_use] + pub fn with_landing_page_assets>(mut self, assets_path: Option

) -> Self { + self.landing.assets_path = assets_path.map(|p| p.as_ref().to_path_buf()); + self + } + + #[must_use] + pub fn with_gateway(mut self, gateway: Gateway) -> Self { + self.api.v1_config.node.roles.gateway_enabled = true; + self.api.v1_config.gateway.details = Some(gateway); + self + } + + #[must_use] + pub fn with_mixnode(mut self, mixnode: Mixnode) -> Self { + self.api.v1_config.node.roles.mixnode_enabled = true; + self.api.v1_config.mixnode.details = Some(mixnode); + self + } + + #[must_use] + pub fn with_network_requester(mut self, network_requester: NetworkRequester) -> Self { + self.api.v1_config.node.roles.network_requester_enabled = true; + self.api.v1_config.network_requester.details = Some(network_requester); + self + } +} + +pub struct NymNodeRouter { + inner: Router, +} + +impl NymNodeRouter { + // TODO: move the wg state to a builder + pub fn new(config: Config, initial_wg_state: Option) -> NymNodeRouter { + let state = AppState::new(); + + NymNodeRouter { + inner: Router::new() + .nest(routes::LANDING_PAGE, landing_page::routes(config.landing)) + .nest( + routes::API, + api::routes(config.api, initial_wg_state.unwrap_or_default()), + ) + .layer(axum::middleware::from_fn(logging::logger)) + .with_state(state), + } + } + + // this is only a temporary method until everything is properly moved into the nym-node itself + #[must_use] + pub fn with_route(mut self, path: &str, router: Router) -> Self { + self.inner = self.inner.nest(path, router); + self + } + + pub fn build_server( + self, + bind_address: &SocketAddr, + ) -> Result { + let axum_server = axum::Server::try_bind(bind_address) + .map_err(|source| NymNodeError::HttpBindFailure { + bind_address: *bind_address, + source, + })? + .serve( + self.inner + .into_make_service_with_connect_info::(), + ); + + Ok(NymNodeHTTPServer::new(axum_server)) + } +} diff --git a/nym-node/src/http/router/types.rs b/nym-node/src/http/router/types.rs new file mode 100644 index 0000000000..6533a00a25 --- /dev/null +++ b/nym-node/src/http/router/types.rs @@ -0,0 +1,46 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +pub use nym_node_requests::api::ErrorResponse; +use utoipa::ToResponse; + +#[derive(Debug, Clone, ToResponse)] +#[response(description = "Error response with additional message")] +pub(crate) struct RequestError { + pub(crate) inner: ErrorResponse, + + pub(crate) status: StatusCode, +} + +impl RequestError { + pub(crate) fn new>(message: S, status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + message: message.into(), + }, + status, + } + } + + pub(crate) fn new_status(status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + message: String::new(), + }, + status, + } + } + + pub(crate) fn from_err(err: E, status: StatusCode) -> Self { + Self::new(err.to_string(), status) + } +} + +impl IntoResponse for RequestError { + fn into_response(self) -> Response { + (self.status, Json(self.inner)).into_response() + } +} diff --git a/nym-node/src/http/state.rs b/nym-node/src/http/state.rs new file mode 100644 index 0000000000..ee7cd3aad2 --- /dev/null +++ b/nym-node/src/http/state.rs @@ -0,0 +1,38 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use tokio::time::Instant; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + pub(crate) startup_time: Instant, + // wireguard: WireguardAppState, +} + +// #[derive(Debug, Clone)] +// pub struct WireguardAppState { +// // inner: Option, +// } +// +// #[derive(Debug)] +// pub(crate) struct WireguardAppStateInner { +// // +// } +// +// impl FromRef for WireguardAppState { +// fn from_ref(app_state: &AppState) -> Self { +// app_state.wireguard.clone() +// } +// } + +impl AppState { + pub fn new() -> Self { + AppState { + // is it 100% accurate? + // no. + // does it have to be? + // also no. + startup_time: Instant::now(), + } + } +} diff --git a/nym-node/src/lib.rs b/nym-node/src/lib.rs new file mode 100644 index 0000000000..e32c757b15 --- /dev/null +++ b/nym-node/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +// this crate will eventually get converted into proper binary + +pub mod config; +pub mod error; +pub mod http; +pub mod wireguard; diff --git a/nym-node/src/wireguard/error.rs b/nym-node/src/wireguard/error.rs new file mode 100644 index 0000000000..47b24af92c --- /dev/null +++ b/nym-node/src/wireguard/error.rs @@ -0,0 +1,13 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WireguardError { + #[error("the client is currently not in the process of being registered")] + RegistrationNotInProgress, + + #[error("the client mac failed to get verified correctly")] + MacVerificationFailure, +} diff --git a/nym-node/src/wireguard/mod.rs b/nym-node/src/wireguard/mod.rs new file mode 100644 index 0000000000..ad3a0d85cd --- /dev/null +++ b/nym-node/src/wireguard/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// obviously most of the features currently reside in the gateway, +// but let's start putting everything in here + +pub mod error; +pub mod types; diff --git a/nym-node/src/wireguard/types.rs b/nym-node/src/wireguard/types.rs new file mode 100644 index 0000000000..0243133005 --- /dev/null +++ b/nym-node/src/wireguard/types.rs @@ -0,0 +1,5 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::*; +pub use nym_wireguard_types::registration::{GatewayClientRegistry, PendingRegistrations}; diff --git a/nym-outfox/Cargo.toml b/nym-outfox/Cargo.toml index abf7104fc2..bfcd49c233 100644 --- a/nym-outfox/Cargo.toml +++ b/nym-outfox/Cargo.toml @@ -17,7 +17,7 @@ chacha20 = "0.9.0" curve25519-dalek = "3.2" chacha20poly1305 = "0.10.1" getrandom = { workspace = true, features = ["js"] } -thiserror = "1" +thiserror = { workspace = true } sphinx-packet = "0.1.0" rand = "0.7.3" log = "0.4" diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 75fff483b3..7b33510a97 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2436,6 +2436,20 @@ dependencies = [ "itoa 1.0.9", ] +[[package]] +name = "http-api-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + [[package]] name = "http-body" version = "0.4.5" @@ -3192,6 +3206,7 @@ dependencies = [ "getset", "nym-coconut-interface", "nym-mixnet-contract-common", + "nym-node-requests", "schemars", "serde", ] @@ -3206,6 +3221,7 @@ dependencies = [ "clap_complete_fig", "log", "pretty_env_logger", + "schemars", "semver 0.11.0", "serde", "vergen", @@ -3405,6 +3421,20 @@ dependencies = [ "url", ] +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "base64 0.21.4", + "nym-bin-common", + "nym-crypto", + "schemars", + "serde", + "serde_json", + "thiserror", + "x25519-dalek 2.0.0", +] + [[package]] name = "nym-pemstore" version = "0.3.0" @@ -3495,6 +3525,7 @@ dependencies = [ "eyre", "flate2", "futures", + "http-api-client", "itertools 0.10.5", "log", "nym-api-requests", @@ -4454,9 +4485,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64 0.21.4", "bytes", @@ -4479,6 +4510,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", @@ -5276,6 +5308,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -5687,18 +5740,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", @@ -6633,11 +6686,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 3c66fcc62d..24c79219b7 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -32,7 +32,7 @@ itertools = "0.10" log = { version = "0.4", features = ["serde"] } once_cell = "1.7.2" pretty_env_logger = "0.4" -reqwest = {version = "0.11.18", features = ["json"] } +reqwest = {version = "0.11.22", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index 35dacbb190..9f66f29289 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -107,7 +107,7 @@ pub(crate) async fn verify_mixnode_bonding_sign_payload { custom_topology_provider: Option>, custom_gateway_transceiver: Option>, custom_shutdown: Option, + force_tls: bool, // TODO: incorporate it properly into `MixnetClientStorage` (I will need it in wasm anyway) gateway_endpoint_config_path: Option, @@ -86,6 +87,7 @@ impl MixnetClientBuilder { gateway_endpoint_config_path: None, custom_shutdown: None, custom_gateway_transceiver: None, + force_tls: false, }) } } @@ -111,6 +113,7 @@ where custom_topology_provider: None, custom_gateway_transceiver: None, custom_shutdown: None, + force_tls: false, gateway_endpoint_config_path: None, storage, } @@ -128,6 +131,7 @@ where custom_topology_provider: self.custom_topology_provider, custom_gateway_transceiver: self.custom_gateway_transceiver, custom_shutdown: self.custom_shutdown, + force_tls: self.force_tls, gateway_endpoint_config_path: self.gateway_endpoint_config_path, storage, } @@ -156,6 +160,13 @@ where self } + /// Attempt to only choose a gateway that supports wss protocol. + #[must_use] + pub fn force_tls(mut self, must_use_tls: bool) -> Self { + self.force_tls = must_use_tls; + self + } + /// Enable paid coconut bandwidth credentials mode. #[must_use] pub fn enable_credentials_mode(mut self) -> Self { @@ -224,7 +235,8 @@ where .custom_gateway_transceiver(self.custom_gateway_transceiver) .custom_topology_provider(self.custom_topology_provider) .custom_shutdown(self.custom_shutdown) - .wait_for_gateway(self.wait_for_gateway); + .wait_for_gateway(self.wait_for_gateway) + .force_tls(self.force_tls); Ok(client) } @@ -265,6 +277,9 @@ where /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. wait_for_gateway: bool, + /// Force the client to connect using wss protocol with the gateway. + force_tls: bool, + /// Allows passing an externally controlled shutdown handle. custom_shutdown: Option, } @@ -312,6 +327,7 @@ where custom_topology_provider: None, custom_gateway_transceiver: None, wait_for_gateway: false, + force_tls: false, custom_shutdown: None, }) } @@ -346,6 +362,12 @@ where self } + #[must_use] + pub fn force_tls(mut self, must_use_tls: bool) -> Self { + self.force_tls = must_use_tls; + self + } + fn get_api_endpoints(&self) -> Vec { self.config .network_details @@ -422,7 +444,7 @@ where let selection_spec = GatewaySelectionSpecification::new( self.config.user_chosen_gateway.clone(), None, - false, + self.force_tls, ); let mut rng = OsRng; @@ -486,8 +508,11 @@ where .with_wait_for_gateway(self.wait_for_gateway); if !known_gateway { - let selection_spec = - GatewaySelectionSpecification::new(self.config.user_chosen_gateway, None, false); + let selection_spec = GatewaySelectionSpecification::new( + self.config.user_chosen_gateway, + None, + self.force_tls, + ); let mut rng = OsRng; let setup = GatewaySetup::New { diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 0cd084f931..c34975a3f3 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -33,7 +33,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "chrono"]} tap = { workspace = true } -thiserror = "1.0" +thiserror = { workspace = true } tokio = { workspace = true, features = [ "net", "rt-multi-thread", "macros" ] } tokio-tungstenite = { workspace = true } url = { workspace = true } diff --git a/service-providers/network-statistics/Cargo.toml b/service-providers/network-statistics/Cargo.toml index 97d9d36cbc..280a231309 100644 --- a/service-providers/network-statistics/Cargo.toml +++ b/service-providers/network-statistics/Cargo.toml @@ -12,7 +12,7 @@ pretty_env_logger = "0.4" rocket = { version = "0.5.0-rc.2", features = ["json"] } serde = { workspace = true, features = ["derive"] } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "chrono"]} -thiserror = "1" +thiserror = { workspace = true } tokio = { version = "1.4", features = [ "net", "rt-multi-thread", "macros", "time" ] } nym-bin-common = { path = "../../common/bin-common"} nym-statistics-common = { path = "../../common/statistics" } diff --git a/wasm/mix-fetch/Cargo.toml b/wasm/mix-fetch/Cargo.toml index 957fcde528..50e5d61af2 100644 --- a/wasm/mix-fetch/Cargo.toml +++ b/wasm/mix-fetch/Cargo.toml @@ -14,10 +14,10 @@ rust-version = "1.70" crate-type = ["cdylib", "rlib"] [dependencies] +async-trait = { workspace = true } futures = { workspace = true } js-sys = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } -reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde-wasm-bindgen = { workspace = true } tokio = { workspace = true, features = ["sync"] } @@ -27,6 +27,7 @@ wasm-bindgen-futures = { workspace = true } thiserror = { workspace = true } tsify = { workspace = true, features = ["js"] } +http-api-client = { path = "../../common/http-api-client" } nym-socks5-requests = { path = "../../common/socks5/requests" } nym-ordered-buffer = { path = "../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../service-providers/common" } diff --git a/wasm/mix-fetch/go-mix-conn/build/go_conn.wasm b/wasm/mix-fetch/go-mix-conn/build/go_conn.wasm deleted file mode 100755 index 0066e6d4fd..0000000000 Binary files a/wasm/mix-fetch/go-mix-conn/build/go_conn.wasm and /dev/null differ diff --git a/wasm/mix-fetch/src/client.rs b/wasm/mix-fetch/src/client.rs index 0f8ce4a682..9e4bff5e32 100644 --- a/wasm/mix-fetch/src/client.rs +++ b/wasm/mix-fetch/src/client.rs @@ -4,9 +4,7 @@ use crate::active_requests::ActiveRequests; use crate::config::MixFetchConfig; use crate::error::MixFetchError; -use crate::fetch::HackOpts; use crate::go_bridge::goWasmSetMixFetchRequestTimeout; -use crate::helpers::{_hack__get_topology_provider, get_combined_gateways}; use crate::request_writer::RequestWriter; use crate::socks_helpers::{socks5_connect_request, socks5_data_request}; use crate::{config, RequestId}; @@ -18,13 +16,12 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use wasm_client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; use wasm_client_core::client::inbound_messages::InputMessage; -use wasm_client_core::helpers::setup_gateway_wasm; +use wasm_client_core::helpers::setup_gateway_from_api; use wasm_client_core::init::types::GatewaySetup; use wasm_client_core::nym_task::connections::TransmissionLane; use wasm_client_core::nym_task::TaskManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; use wasm_client_core::storage::ClientStorage; -use wasm_client_core::topology::SerializableGateway; use wasm_client_core::{IdentityKey, QueryReqwestRpcNyxdClient, Recipient}; use wasm_utils::console_log; use wasm_utils::error::PromisableResult; @@ -50,7 +47,6 @@ pub struct MixFetchClientBuilder { config: MixFetchConfig, preferred_gateway: Option, force_tls: bool, - hidden_gateways: Vec, storage_passphrase: Option, } @@ -63,16 +59,11 @@ impl MixFetchClientBuilder { force_tls: bool, preferred_gateway: Option, storage_passphrase: Option, - hack_opts: Option, ) -> Self { MixFetchClientBuilder { config, preferred_gateway, force_tls, - hidden_gateways: hack_opts - .unwrap_or_default() - .hidden_gateways - .unwrap_or_default(), storage_passphrase, } } @@ -105,11 +96,14 @@ impl MixFetchClientBuilder { ClientStorage::new_async(&self.config.base.client.id, self.storage_passphrase.take()) .await?; - let uses_hidden = !self.hidden_gateways.is_empty(); let user_chosen = self.preferred_gateway.clone(); - let gateways = get_combined_gateways(self.hidden_gateways, &nym_api_endpoints).await?; - let init_res = - setup_gateway_wasm(&client_store, self.force_tls, user_chosen, &gateways).await?; + let init_res = setup_gateway_from_api( + &client_store, + self.force_tls, + user_chosen, + &nym_api_endpoints, + ) + .await?; let storage = Self::initialise_storage(&self.config, client_store); @@ -122,12 +116,6 @@ impl MixFetchClientBuilder { if let Ok(reuse_setup) = GatewaySetup::try_reuse_connection(init_res) { base_builder = base_builder.with_gateway_setup(reuse_setup); } - if uses_hidden { - base_builder = base_builder.with_topology_provider(Box::new( - _hack__get_topology_provider(gateways, &nym_api_endpoints).await?, - )) - } - let mut started_client = base_builder.start_base().await?; let self_address = started_client.address; @@ -158,17 +146,10 @@ impl MixFetchClient { force_tls: bool, preferred_gateway: Option, storage_passphrase: Option, - hack_opts: Option, ) -> Result { - MixFetchClientBuilder::new( - config, - force_tls, - preferred_gateway, - storage_passphrase, - hack_opts, - ) - .start_client_async() - .await + MixFetchClientBuilder::new(config, force_tls, preferred_gateway, storage_passphrase) + .start_client_async() + .await } #[wasm_bindgen(constructor)] @@ -178,18 +159,11 @@ impl MixFetchClient { force_tls: bool, preferred_gateway: Option, storage_passphrase: Option, - hack_opts: Option, ) -> Promise { future_to_promise(async move { - Self::new_async( - config, - force_tls, - preferred_gateway, - storage_passphrase, - hack_opts, - ) - .await - .into_promise_result() + Self::new_async(config, force_tls, preferred_gateway, storage_passphrase) + .await + .into_promise_result() }) } diff --git a/wasm/mix-fetch/src/fetch.rs b/wasm/mix-fetch/src/fetch.rs index 995b27e2b2..2fd8da80dc 100644 --- a/wasm/mix-fetch/src/fetch.rs +++ b/wasm/mix-fetch/src/fetch.rs @@ -13,7 +13,6 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; use wasm_bindgen_futures::future_to_promise; use wasm_client_core::config::r#override::DebugWasmOverride; -use wasm_client_core::topology::SerializableGateway; use wasm_utils::console_log; use wasm_utils::error::PromisableResultError; @@ -44,9 +43,6 @@ pub struct MixFetchOpts { #[tsify(optional)] pub(crate) mix_fetch_override: Option, - - #[tsify(optional)] - pub(crate) extra: HackOpts, } #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] @@ -67,15 +63,6 @@ pub struct MixFetchOptsSimple { pub(crate) storage_passphrase: Option, } -#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -#[doc(hidden)] -pub struct HackOpts { - #[tsify(optional)] - pub(crate) hidden_gateways: Option>, -} - impl<'a> From<&'a MixFetchOpts> for MixFetchConfigOpts { fn from(value: &'a MixFetchOpts) -> Self { MixFetchConfigOpts { @@ -118,7 +105,7 @@ pub fn setup_mix_fetch(opts: MixFetchOpts) -> Promise { config.override_mix_fetch_debug(dbg) } - setup_mix_fetch_async(config, opts.base, opts.extra) + setup_mix_fetch_async(config, opts.base) .await .map(|_| JsValue::undefined()) .map_promise_err() @@ -132,7 +119,7 @@ pub fn setup_mix_fetch_with_config(config: MixFetchConfig, opts: MixFetchOptsSim } future_to_promise(async move { - setup_mix_fetch_async(config, opts, Default::default()) + setup_mix_fetch_async(config, opts) .await .map(|_| JsValue::undefined()) .map_promise_err() @@ -171,19 +158,12 @@ pub(super) fn mix_fetch_client() -> Result<&'static MixFetchClient, MixFetchErro async fn setup_mix_fetch_async( config: MixFetchConfig, opts: MixFetchOptsSimple, - hack_opts: HackOpts, ) -> Result<(), MixFetchError> { let preferred_gateway = opts.preferred_gateway; let storage_passphrase = opts.storage_passphrase; let force_tls = opts.force_tls.unwrap_or_default(); - let client = MixFetchClient::new_async( - config, - force_tls, - preferred_gateway, - storage_passphrase, - Some(hack_opts), - ) - .await?; + let client = + MixFetchClient::new_async(config, force_tls, preferred_gateway, storage_passphrase).await?; set_mix_fetch_client(client)?; Ok(()) } diff --git a/wasm/mix-fetch/src/harbourmaster.rs b/wasm/mix-fetch/src/harbourmaster.rs index 862f6a316a..089919d9d5 100644 --- a/wasm/mix-fetch/src/harbourmaster.rs +++ b/wasm/mix-fetch/src/harbourmaster.rs @@ -4,10 +4,13 @@ // this exists inside mix-fetch rather than being made into repo-wide library since it's a temporary solution // until the API is finalised and endpoints are moved to nym-api -use reqwest::{IntoUrl, Response, StatusCode}; +use async_trait::async_trait; +use http_api_client::{ApiClient, HttpClientError, NO_PARAMS}; use serde::Deserialize; -use thiserror::Error; -use url::Url; + +pub use http_api_client::Client; + +pub type HarbourMasterApiError = HttpClientError; mod routes { pub const API_VERSION: &str = "v1"; @@ -17,79 +20,12 @@ mod routes { pub const NEW: &str = "new"; } -// most of it is copied from the nym-api client -type PathSegments<'a> = &'a [&'a str]; -type Params<'a, K, V> = &'a [(K, V)]; - -const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; - -#[derive(Debug, Error)] -pub enum HarbourMasterApiError { - #[error("there was an issue with the REST request: {source}")] - ReqwestClientError { - #[from] - source: reqwest::Error, - }, - - #[error("not found")] - NotFound, - - #[error("request failed with error message: {0}")] - GenericRequestFailure(String), -} - -pub struct Client { - url: Url, - reqwest_client: reqwest::Client, -} - -impl Client { - pub fn new(url: U) -> Result { - let reqwest_client = reqwest::Client::new(); - Ok(Self { - url: url.into_url()?, - reqwest_client, - }) - } - - async fn send_get_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> Result - where - K: AsRef, - V: AsRef, - { - let url = create_api_url(&self.url, path, params); - Ok(self.reqwest_client.get(url).send().await?) - } - - async fn query_harbourmaster( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> Result - where - for<'a> T: Deserialize<'a>, - K: AsRef, - V: AsRef, - { - let res = self.send_get_request(path, params).await?; - if res.status().is_success() { - Ok(res.json().await?) - } else if res.status() == StatusCode::NOT_FOUND { - Err(HarbourMasterApiError::NotFound) - } else { - Err(HarbourMasterApiError::GenericRequestFailure( - res.text().await?, - )) - } - } - +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait HarbourMasterApiClientExt: ApiClient { // since it's a temporary thing don't worry about paging. - pub async fn get_services_new(&self) -> Result, HarbourMasterApiError> { - self.query_harbourmaster( + async fn get_services_new(&self) -> Result, HarbourMasterApiError> { + self.get_json( &[routes::API_VERSION, routes::SERVICES, routes::NEW], NO_PARAMS, ) @@ -97,29 +33,9 @@ impl Client { } } -fn create_api_url, V: AsRef>( - base: &Url, - segments: PathSegments<'_>, - params: Params<'_, K, V>, -) -> Url { - let mut url = base.clone(); - let mut path_segments = url - .path_segments_mut() - .expect("provided validator url does not have a base!"); - for segment in segments { - let segment = segment.strip_prefix('/').unwrap_or(segment); - let segment = segment.strip_suffix('/').unwrap_or(segment); - - path_segments.push(segment); - } - drop(path_segments); - - if !params.is_empty() { - url.query_pairs_mut().extend_pairs(params); - } - - url -} +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl HarbourMasterApiClientExt for Client {} // https://gitlab.nymte.ch/nym/shipyard-test-and-earn/-/blob/main/harbour-master/src/http/mod.rs#L13 #[derive(Debug, Deserialize)] diff --git a/wasm/mix-fetch/src/helpers.rs b/wasm/mix-fetch/src/helpers.rs index 0ba09bacea..e675c7d005 100644 --- a/wasm/mix-fetch/src/helpers.rs +++ b/wasm/mix-fetch/src/helpers.rs @@ -3,14 +3,10 @@ use crate::error::MixFetchError; use crate::harbourmaster; +use crate::harbourmaster::HarbourMasterApiClientExt; use rand::seq::SliceRandom; use rand::thread_rng; -use std::collections::HashMap; -use url::Url; -use wasm_client_core::init::helpers::{current_gateways, current_mixnodes}; -use wasm_client_core::topology::{gateway, SerializableGateway}; -use wasm_client_core::{HardcodedTopologyProvider, NymTopology}; -use wasm_utils::{console_log, console_warn}; +use wasm_utils::console_log; // since this client is temporary (and will be properly integrated into nym-api eventually), // we're using hardcoded URL for mainnet @@ -23,7 +19,7 @@ pub(crate) async fn get_network_requester( return Ok(sp); } - let client = harbourmaster::Client::new(HARBOUR_MASTER)?; + let client = harbourmaster::Client::new_url(HARBOUR_MASTER, None)?; let providers = client.get_services_new().await?; console_log!( "obtained list of {} service providers on the network", @@ -39,55 +35,3 @@ pub(crate) async fn get_network_requester( .cloned() .ok_or(MixFetchError::NoNetworkRequesters) } - -pub(crate) async fn get_combined_gateways( - hidden: Vec, - nym_apis: &[Url], -) -> Result, MixFetchError> { - let mut rng = thread_rng(); - - let mut api_gateways = current_gateways(&mut rng, nym_apis).await?; - if !hidden.is_empty() { - // make sure to override duplicates - let mut gateways: HashMap<_, _> = api_gateways - .into_iter() - .map(|g| (g.identity_key.to_base58_string(), g)) - .collect(); - - for node in hidden { - let id = node.identity_key.clone(); - let converted: Result = node.try_into(); - match converted { - Err(err) => { - console_warn!("failed to add gateway '{id}' into the topology: {err}"); - } - Ok(gateway) => { - if gateways - .insert(gateway.identity_key.to_base58_string(), gateway) - .is_some() - { - console_warn!("overridden gateway '{id}'") - } - } - } - } - - api_gateways = gateways.into_values().collect(); - } - - Ok(api_gateways) -} - -#[allow(non_snake_case)] -pub(crate) async fn _hack__get_topology_provider( - combined_gateways: Vec, - nym_apis: &[Url], -) -> Result { - let mut rng = thread_rng(); - - let mixnodes = current_mixnodes(&mut rng, nym_apis).await?; - Ok(HardcodedTopologyProvider::new(NymTopology::new_unordered( - mixnodes, - combined_gateways, - ))) -}