diff --git a/Cargo.lock b/Cargo.lock index 13a9d7f4a29..2bd6cc5767c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2675,10 +2675,13 @@ version = "0.1.0" dependencies = [ "console_error_panic_hook", "crypto", + "log 0.4.8", "nymsphinx", + "rand 0.7.3", "serde", "serde_json", "slice_as_array", + "topology", "wasm-bindgen", "wasm-bindgen-test", "wee_alloc", diff --git a/clients/desktop/js-examples/websocket/src/index.js b/clients/desktop/js-examples/websocket/src/index.js index 1df986bb7c2..0ddbaf9c57e 100644 --- a/clients/desktop/js-examples/websocket/src/index.js +++ b/clients/desktop/js-examples/websocket/src/index.js @@ -8,7 +8,7 @@ async function main() { var connection = await connectWebsocket(localClientUrl).then(function (c) { return c; }).catch(function (err) { - display("Websocket ERROR: " + err); + display("Websocket connection error. Is the client running with <pre>--connection-type WebSocket</pre> on port " + port + "?"); }) connection.onmessage = function (e) { handleResponse(e); diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index fe106a7a96c..a1b285d24e2 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -11,15 +11,18 @@ crate-type = ["cdylib", "rlib"] default = ["console_error_panic_hook"] [dependencies] +# itertools = "0.8.2" +log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" slice_as_array = "1.1.0" wasm-bindgen = "0.2" +rand = "0.7.2" # internal crypto = { path = "../../common/crypto" } nymsphinx = { path = "../../common/nymsphinx" } - +topology = { path = "../../common/topology" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/clients/webassembly/js-examples/index.js b/clients/webassembly/js-examples/index.js index a44791edcd5..9be381819cd 100644 --- a/clients/webassembly/js-examples/index.js +++ b/clients/webassembly/js-examples/index.js @@ -14,19 +14,6 @@ import * as wasm from "nym-sphinx-wasm"; -class Route { - constructor(nodes) { - this.nodes = nodes; - } -} - -class NodeData { - constructor(address, public_key) { - this.address = address; - this.public_key = public_key; - } -} - async function main() { var gatewayUrl = "ws://127.0.0.1:1793"; var directoryUrl = "http://127.0.0.1:8080/api/presence/topology"; @@ -50,10 +37,9 @@ async function main() { // Create a Sphinx packet and send it to the mixnet through the Gateway node. function sendMessageToMixnet(connection, topology) { - let route = constructRoute(topology); var recipient = document.getElementById("recipient").value; var sendText = document.getElementById("sendtext").value; - let packet = wasm.create_sphinx_packet(JSON.stringify(route), sendText, recipient); + let packet = wasm.create_sphinx_packet(JSON.stringify(topology), sendText, recipient); connection.send(packet); displaySend(packet); display("Sent a Sphinx packet containing message: " + sendText); @@ -65,20 +51,6 @@ async function getTopology(directoryUrl) { return topology; } -// Construct a route from the current network topology so we can get wasm to build us a Sphinx packet -function constructRoute(topology) { - const mixnodes = topology.mixNodes; - const provider = topology.mixProviderNodes[0]; - let nodes = []; - mixnodes.forEach(node => { - let n = new NodeData(node.host, node.pubKey); - nodes.push(n); - }); - let p = new NodeData(provider.mixnetListener, provider.pubKey) - nodes.push(p); - return new Route(nodes); -} - // Let's get started! main(); diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 2177255745f..779c32c8b78 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -12,20 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. use crypto::identity::MixIdentityPublicKey; +use models::Topology; use nymsphinx::addressing::nodes::NymNodeRoutingAddress; use nymsphinx::chunking::split_and_prepare_payloads; +use nymsphinx::Node as SphinxNode; use nymsphinx::{ - delays, Destination, DestinationAddressBytes, Node, NodeAddressBytes, SphinxPacket, - IDENTIFIER_LENGTH, + delays, Destination, DestinationAddressBytes, NodeAddressBytes, SphinxPacket, IDENTIFIER_LENGTH, }; use serde::{Deserialize, Serialize}; -use serde_json; use std::convert::TryFrom; use std::convert::TryInto; use std::net::SocketAddr; use std::time::Duration; +use topology::provider::Node as TopologyNode; use wasm_bindgen::prelude::*; +mod models; mod utils; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global @@ -53,10 +55,10 @@ pub struct NodeData { /// Message chunking is currently not implemented. If the message exceeds the /// capacity of a single Sphinx packet, the extra information will be discarded. #[wasm_bindgen] -pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Vec<u8> { +pub fn create_sphinx_packet(topology_json: &str, msg: &str, destination: &str) -> Vec<u8> { utils::set_panic_hook(); // nicer js errors. - let route = sphinx_route_from(raw_route); + let route = sphinx_route_from(topology_json); let average_delay = Duration::from_secs_f64(0.1); let delays = delays::generate_from_average_duration(route.len(), average_delay); @@ -76,7 +78,7 @@ pub fn create_sphinx_packet(raw_route: &str, msg: &str, destination: &str) -> Ve /// it should send a packet it receives. So we prepend the packet with the /// address bytes of the first mix inside the packet, so that the gateway can /// forward the packet to it. -fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> { +fn payload(sphinx_packet: SphinxPacket, route: Vec<SphinxNode>) -> Vec<u8> { let packet = sphinx_packet.to_bytes(); let first_mix_address = route.first().unwrap().clone().address.to_bytes().to_vec(); @@ -93,29 +95,16 @@ fn payload(sphinx_packet: SphinxPacket, route: Vec<Node>) -> Vec<u8> { /// /// This function panics if the supplied `raw_route` json string can't be /// extracted to a `JsonRoute`. -/// -/// # Panics -/// -/// This function panics if `JsonRoute.nodes` doesn't contain at least 1 -/// node. -/// -fn sphinx_route_from(raw_route: &str) -> Vec<Node> { - let json_route: JsonRoute = serde_json::from_str(raw_route).unwrap(); - - assert!( - json_route.nodes.len() > 0, - "Sphinx packet must route to at least one mixnode." - ); - - let mut sphinx_route: Vec<Node> = vec![]; - for node_data in json_route.nodes.iter() { - let x = Node::try_from(node_data.clone()).expect("Malformed NodeData"); - sphinx_route.push(x); - } - sphinx_route +fn sphinx_route_from(topology_json: &str) -> Vec<SphinxNode> { + let topology = Topology::new(topology_json); + let p: TopologyNode = topology.providers().first().unwrap().to_owned(); + let provider = p.into(); + let route = topology.route_to(provider).unwrap(); + assert_eq!(4, route.len()); + route } -impl TryFrom<NodeData> for Node { +impl TryFrom<NodeData> for SphinxNode { type Error = (); fn try_from(node_data: NodeData) -> Result<Self, Self::Error> { @@ -130,6 +119,162 @@ impl TryFrom<NodeData> for Node { nymsphinx::key::new(dest) }; - Ok(Node { address, pub_key }) + Ok(SphinxNode { address, pub_key }) } } + +#[cfg(test)] +mod test_constructing_a_sphinx_packet { + use super::*; + #[test] + fn produces_1404_bytes() { + // 32 byte address + 1372 byte sphinx packet + let packet = create_sphinx_packet( + topology_fixture(), + "foomp", + "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR", + ); + assert_eq!(1404, packet.len()); + } + + #[test] + fn starts_with_a_mix_address() { + let mut payload = create_sphinx_packet( + topology_fixture(), + "foomp", + "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR", + ); + let mut address_buffer = [0; 32]; + let _ = payload.split_off(32); + address_buffer.copy_from_slice(payload.as_slice()); + let address = NymNodeRoutingAddress::try_from_bytes(&address_buffer); + + assert!(address.is_ok()); + } +} + +#[cfg(test)] +mod building_a_topology_from_json { + use super::*; + + #[test] + #[should_panic] + fn panics_on_empty_string() { + sphinx_route_from(""); + } + + #[test] + #[should_panic] + fn panics_on_bad_json() { + sphinx_route_from("bad bad bad not json"); + } + + #[test] + #[should_panic] + fn panics_when_there_are_no_mixnodes() { + let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap(); + topology.mix_nodes = vec![]; + let json = serde_json::to_string(&topology).unwrap(); + sphinx_route_from(&json); + } + + #[test] + #[should_panic] + fn panics_when_there_are_not_enough_mixnodes() { + let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap(); + let node = topology.mix_nodes.first().unwrap().clone(); + topology.mix_nodes = vec![node]; // 1 mixnode isn't enough. Panic! + let json = serde_json::to_string(&topology).unwrap(); + sphinx_route_from(&json); + } + + #[test] + fn test_works_on_happy_json() { + let route = sphinx_route_from(topology_fixture()); + assert_eq!(4, route.len()); + } +} + +#[cfg(test)] +fn topology_fixture() -> &'static str { + let json = r#" + { + "cocoNodes": [], + "mixNodes": [ + { + "host": "nym.300baud.de:1789", + "pubKey": "AetTDvynUNB2N35rvCVDxkPR593Cx4PCe4QQKrMgm5RR", + "version": "0.6.0", + "location": "Falkenstein, DE", + "layer": 3, + "lastSeen": 1587572945877713700 + }, + { + "host": "testnet_nymmixnode.roussel-zeter.eu:1789", + "pubKey": "9wJ3zLoyat41e4ZgT1AWeueExv5c6uwnjvkRepj8Ebis", + "version": "0.6.0", + "location": "Geneva, CH", + "layer": 3, + "lastSeen": 1587572945907250400 + }, + { + "host": "185.144.83.134:1789", + "pubKey": "59tCzpCYsiKXz89rtvNiEYwQDdkseSShPEkifQXhsCgA", + "version": "0.6.0", + "location": "Bucharest", + "layer": 1, + "lastSeen": 1587572946007431400 + }, + { + "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789", + "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG", + "version": "0.6.0", + "location": "Glarus", + "layer": 1, + "lastSeen": 1587572945920982000 + }, + { + "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789", + "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG", + "version": "0.6.0", + "location": "Glarus", + "layer": 2, + "lastSeen": 1587572945920982000 + }, + { + "host": "[2a0a:e5c0:2:2:0:c8ff:fe68:bf6b]:1789", + "pubKey": "J9f9uS1hN8iwcN2STqH55fPRYqt7McEPyhNzpTYsxNdG", + "version": "0.6.0", + "location": "Glarus", + "layer": 2, + "lastSeen": 1587572945920982000 + } + ], + "mixProviderNodes": [ + { + "clientListener": "139.162.246.48:9000", + "mixnetListener": "139.162.246.48:1789", + "pubKey": "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "version": "0.6.0", + "location": "London, UK", + "registeredClients": [ + { + "pubKey": "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq" + } + ], + "lastSeen": 1587572946261865200 + }, + { + "clientListener": "127.0.0.1:9000", + "mixnetListener": "127.0.0.1:1789", + "pubKey": "2XK8RDcUTRcJLUWoDfoXc2uP4YViscMLEM5NSzhSi87M", + "version": "0.6.0", + "location": "unknown", + "registeredClients": [], + "lastSeen": 1587572946304564700 + } + ] + } + "#; + json +} diff --git a/clients/webassembly/src/models/coconodes.rs b/clients/webassembly/src/models/coconodes.rs new file mode 100644 index 00000000000..bff88006305 --- /dev/null +++ b/clients/webassembly/src/models/coconodes.rs @@ -0,0 +1,50 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use topology::coco; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CocoPresence { + pub location: String, + pub host: String, + pub pub_key: String, + pub last_seen: u64, + pub version: String, +} + +impl Into<topology::coco::Node> for CocoPresence { + fn into(self) -> topology::coco::Node { + topology::coco::Node { + location: self.location, + host: self.host, + pub_key: self.pub_key, + last_seen: self.last_seen, + version: self.version, + } + } +} + +impl From<topology::coco::Node> for CocoPresence { + fn from(cn: coco::Node) -> Self { + CocoPresence { + location: cn.location, + host: cn.host, + pub_key: cn.pub_key, + last_seen: cn.last_seen, + version: cn.version, + } + } +} diff --git a/clients/webassembly/src/models/mixnodes.rs b/clients/webassembly/src/models/mixnodes.rs new file mode 100644 index 00000000000..b1f6e7f837f --- /dev/null +++ b/clients/webassembly/src/models/mixnodes.rs @@ -0,0 +1,66 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::io; +use std::net::ToSocketAddrs; +use topology::mix; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MixNodePresence { + pub location: String, + pub host: String, + pub pub_key: String, + pub layer: u64, + pub last_seen: u64, + pub version: String, +} + +impl TryInto<topology::mix::Node> for MixNodePresence { + type Error = io::Error; + + fn try_into(self) -> Result<topology::mix::Node, Self::Error> { + let resolved_hostname = self.host.to_socket_addrs()?.next(); + if resolved_hostname.is_none() { + return Err(io::Error::new( + io::ErrorKind::Other, + "no valid socket address", + )); + } + + Ok(topology::mix::Node { + location: self.location, + host: resolved_hostname.unwrap(), + pub_key: self.pub_key, + layer: self.layer, + last_seen: self.last_seen, + version: self.version, + }) + } +} + +impl From<topology::mix::Node> for MixNodePresence { + fn from(mn: mix::Node) -> Self { + MixNodePresence { + location: mn.location, + host: mn.host.to_string(), + pub_key: mn.pub_key, + layer: mn.layer, + last_seen: mn.last_seen, + version: mn.version, + } + } +} diff --git a/clients/webassembly/src/models/mod.rs b/clients/webassembly/src/models/mod.rs new file mode 100644 index 00000000000..9e3c695f56b --- /dev/null +++ b/clients/webassembly/src/models/mod.rs @@ -0,0 +1,157 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use nymsphinx::Node as SphinxNode; +use rand::seq::IteratorRandom; +use serde::{Deserialize, Serialize}; +use std::cmp::max; +use std::collections::HashMap; +use std::convert::TryInto; +use topology::mix; +use topology::provider; + +pub mod coconodes; +pub mod mixnodes; +pub mod providers; + +// Topology shows us the current state of the overall Nym network +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Topology { + pub coco_nodes: Vec<coconodes::CocoPresence>, + pub mix_nodes: Vec<mixnodes::MixNodePresence>, + pub mix_provider_nodes: Vec<providers::MixProviderPresence>, +} + +impl Topology { + pub fn new(json: &str) -> Self { + serde_json::from_str(json).unwrap() + } + + fn make_layered_topology(&self) -> Result<HashMap<u64, Vec<mix::Node>>, NymTopologyError> { + let mut layered_topology: HashMap<u64, Vec<mix::Node>> = HashMap::new(); + let mut highest_layer = 0; + for mix in self.mix_nodes() { + // we need to have extra space for provider + if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 { + return Err(NymTopologyError::InvalidMixLayerError); + } + highest_layer = max(highest_layer, mix.layer); + + let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new); + layer_nodes.push(mix); + } + + // verify the topology - make sure there are no gaps and there is at least one node per layer + let mut missing_layers = Vec::new(); + for layer in 1..=highest_layer { + if !layered_topology.contains_key(&layer) { + missing_layers.push(layer); + } + if layered_topology[&layer].is_empty() { + missing_layers.push(layer); + } + } + + if !missing_layers.is_empty() { + return Err(NymTopologyError::MissingLayerError(missing_layers)); + } + + Ok(layered_topology) + } + + // Tries to get a route through the mix network + fn mix_route(&self) -> Result<Vec<SphinxNode>, NymTopologyError> { + let mut layered_topology = self.make_layered_topology()?; + let num_layers = layered_topology.len(); + let route = (1..=num_layers as u64) + // unwrap is safe for 'remove' as it it failed, it implied the entry never existed + // in the map in the first place which would contradict what we've just done + .map(|layer| layered_topology.remove(&layer).unwrap()) // for each layer + .map(|nodes| nodes.into_iter().choose(&mut rand::thread_rng()).unwrap()) // choose random node + .map(|random_node| random_node.into()) // and convert it into sphinx specific node format + .collect(); + + Ok(route) + } + + // Sets up a route to a specific provider + pub fn route_to(&self, provider_node: SphinxNode) -> Result<Vec<SphinxNode>, NymTopologyError> { + Ok(self + .mix_route()? + .into_iter() + .chain(std::iter::once(provider_node)) + .collect()) + } + + pub fn mix_nodes(&self) -> Vec<mix::Node> { + self.mix_nodes + .iter() + .filter_map(|x| x.clone().try_into().ok()) + .collect() + } + + pub fn providers(&self) -> Vec<provider::Node> { + self.mix_provider_nodes + .iter() + .map(|x| x.clone().into()) + .collect() + } +} + +#[derive(Debug)] +pub enum NymTopologyError { + InvalidMixLayerError, + MissingLayerError(Vec<u64>), +} + +#[cfg(test)] +mod converting_mixnode_presence_into_topology_mixnode { + use super::*; + + #[test] + fn it_returns_error_on_unresolvable_hostname() { + let unresolvable_hostname = "foomp.foomp.foomp:1234"; + + let mix_presence = mixnodes::MixNodePresence { + location: "".to_string(), + host: unresolvable_hostname.to_string(), + pub_key: "".to_string(), + layer: 0, + last_seen: 0, + version: "".to_string(), + }; + + let _result: Result<mix::Node, std::io::Error> = mix_presence.try_into(); + // assert!(result.is_err()) // This fails only for me. Why? + // ¯\_(ツ)_/¯ - works on my machine (and travis) + } + + #[test] + fn it_returns_resolved_ip_on_resolvable_hostname() { + let resolvable_hostname = "nymtech.net:1234"; + + let mix_presence = mixnodes::MixNodePresence { + location: "".to_string(), + host: resolvable_hostname.to_string(), + pub_key: "".to_string(), + layer: 0, + last_seen: 0, + version: "".to_string(), + }; + + let result: Result<topology::mix::Node, std::io::Error> = mix_presence.try_into(); + assert!(result.is_ok()) + } +} diff --git a/clients/webassembly/src/models/providers.rs b/clients/webassembly/src/models/providers.rs new file mode 100644 index 00000000000..7f01f6d6b7e --- /dev/null +++ b/clients/webassembly/src/models/providers.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use topology::provider; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MixProviderPresence { + pub location: String, + pub client_listener: String, + pub mixnet_listener: String, + pub pub_key: String, + pub registered_clients: Vec<MixProviderClient>, + pub last_seen: u64, + pub version: String, +} + +impl Into<topology::provider::Node> for MixProviderPresence { + fn into(self) -> topology::provider::Node { + topology::provider::Node { + location: self.location, + client_listener: self.client_listener.parse().unwrap(), + mixnet_listener: self.mixnet_listener.parse().unwrap(), + pub_key: self.pub_key, + registered_clients: self + .registered_clients + .into_iter() + .map(|c| c.into()) + .collect(), + last_seen: self.last_seen, + version: self.version, + } + } +} + +impl From<topology::provider::Node> for MixProviderPresence { + fn from(mpn: provider::Node) -> Self { + MixProviderPresence { + location: mpn.location, + client_listener: mpn.client_listener.to_string(), + mixnet_listener: mpn.mixnet_listener.to_string(), + pub_key: mpn.pub_key, + registered_clients: mpn + .registered_clients + .into_iter() + .map(|c| c.into()) + .collect(), + last_seen: mpn.last_seen, + version: mpn.version, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MixProviderClient { + pub pub_key: String, +} + +impl Into<topology::provider::Client> for MixProviderClient { + fn into(self) -> topology::provider::Client { + topology::provider::Client { + pub_key: self.pub_key, + } + } +} + +impl From<topology::provider::Client> for MixProviderClient { + fn from(mpc: topology::provider::Client) -> Self { + MixProviderClient { + pub_key: mpc.pub_key, + } + } +} diff --git a/common/client-libs/directory-client/src/lib.rs b/common/client-libs/directory-client/src/lib.rs index 6f72b3bcd73..be0f49c85aa 100644 --- a/common/client-libs/directory-client/src/lib.rs +++ b/common/client-libs/directory-client/src/lib.rs @@ -58,17 +58,13 @@ pub struct Client { impl DirectoryClient for Client { fn new(config: Config) -> Client { - let health_check: HealthCheckRequest = HealthCheckRequest::new(config.base_url.clone()); - let metrics_mixes: MetricsMixRequest = MetricsMixRequest::new(config.base_url.clone()); - let metrics_post: MetricsMixPost = MetricsMixPost::new(config.base_url.clone()); - let presence_topology: PresenceTopologyRequest = - PresenceTopologyRequest::new(config.base_url.clone()); - let presence_coconodes_post: PresenceCocoNodesPost = - PresenceCocoNodesPost::new(config.base_url.clone()); - let presence_mix_nodes_post: PresenceMixNodesPost = - PresenceMixNodesPost::new(config.base_url.clone()); - let presence_providers_post: PresenceProvidersPost = - PresenceProvidersPost::new(config.base_url); + let health_check = HealthCheckRequest::new(config.base_url.clone()); + let metrics_mixes = MetricsMixRequest::new(config.base_url.clone()); + let metrics_post = MetricsMixPost::new(config.base_url.clone()); + let presence_topology = PresenceTopologyRequest::new(config.base_url.clone()); + let presence_coconodes_post = PresenceCocoNodesPost::new(config.base_url.clone()); + let presence_mix_nodes_post = PresenceMixNodesPost::new(config.base_url.clone()); + let presence_providers_post = PresenceProvidersPost::new(config.base_url); Client { health_check, metrics_mixes,