Compare commits

...

55 Commits

Author SHA1 Message Date
Zane Schepke 3142493cd1 Add Tauri windows service and admin installer
Add Tauri windows service and admin installer

Add windows service install and start on app start
Add windows service remove on app stop

Add system tray icon and prevent app close on UI close

Add system build installer with admin privileges

Add single instance allowed
Add app auto-start on startup
2023-11-13 06:57:17 -05:00
Pierre Dommerc 2934d24e53 feat(vpn-ui): scaffold rust backend (#4073)
* scaffold app

* feat: local storage for app data & config
2023-11-13 12:33:58 +01:00
Zane Schepke 11e01335c2 Update tauri.conf.json 2023-11-12 13:48:07 -05:00
Tommy Verrall 6d2d8ce149 Merge pull request #4118 from nymtech/feature/ts-sdk-fixes
Feature/ts sdk fixes
2023-11-09 12:12:06 +00:00
Bogdan-Ștefan Neacşu d38139be66 Add private ip assignment (#4089)
* Add private ip assignment

* Update wg IPs
2023-11-09 13:39:27 +02:00
Jon Häggblad 46d1ef7892 Disable poisson in ip-packet-router (#4123) 2023-11-09 12:16:33 +01:00
Jon Häggblad 72bad6bb38 Fix read packet buffer size (#4122) 2023-11-09 02:12:58 +01:00
Mark Sinclair 66fd484bd5 Update cd-docs.yml
Allow docs GH Action to run for all branches
2023-11-08 15:22:03 +00:00
mfahampshire a1328c96cf commit to trigger deploy 2023-11-08 16:02:02 +01:00
Jon Häggblad 18aa4707a4 wg: tun devices in wireguard and packet router are separate (#4121) 2023-11-08 15:16:00 +01:00
mx dadfc412f2 Merge pull request #4107 from nymtech/feature/websocket-client-usage-docs
Feature/websocket client usage docs
2023-11-08 08:48:12 +00:00
Jon Häggblad 3746975b14 Merge pull request #4110 from nymtech/jon/wireguard-changes
wireguard: config tweaks
2023-11-08 09:08:06 +01:00
Jon Häggblad 2c90229fce Set peer at runtime 2023-11-08 08:42:19 +01:00
Jon Häggblad b461645d3d Make MTU configurable at runtime 2023-11-08 08:42:19 +01:00
Jon Häggblad f7f8b9b898 wireguard: set MTU to 1420 2023-11-08 08:42:19 +01:00
Tommy Verrall 880d2d4edd Merge pull request #4119 from nymtech/tommy/publish-sdk-candidate
Update SDK - version bump to 1.2.4-rc.1
2023-11-07 17:37:03 +00:00
Tommy Verrall 7bc81a91c5 run the formatter 2023-11-07 17:47:43 +01:00
mfahampshire 4ce652af95 update mdbook admonish 2023-11-07 17:46:46 +01:00
Tommy Verrall 8142e5c84c update version bump to 1.2.4-rc.1
- includes fixes for parser body
- and errors associative with the encoded_payload_helper.rs
2023-11-07 17:38:46 +01:00
Lorexia 4efe712fc5 Merge branch 'develop' into feature/ts-sdk-fixes 2023-11-07 17:25:06 +01:00
Tommy Verrall 03f754cde4 Merge pull request #4102 from nymtech/chore/potential-fix-issue-3345
message checks around encoded payload helper
2023-11-07 15:37:49 +00:00
Jędrzej Stuczyński 8f53f095fb Change default http API timeout from 3s to 10s (#4117) 2023-11-07 15:35:02 +00:00
Tommy Verrall a0c667927c one last change 2023-11-07 16:21:59 +01:00
benedetta davico 0cc090038b Merge pull request #4116 from nymtech/master
Merge master to develop
2023-11-07 15:56:49 +01:00
benedetta davico af32fe4022 Merge pull request #4114 from nymtech/release/2023.4-galaxy
Release/2023.4 galaxy
2023-11-07 15:54:16 +01:00
mfahampshire eb2ac7630a first pass at ws client usage docs 2023-11-07 14:38:54 +01:00
Tommy Verrall 189fd0ece4 insert import 2023-11-07 13:20:25 +01:00
Tommy Verrall b6ccab79d2 pr comments
- update based on comments
2023-11-07 13:18:25 +01:00
Tommy Verrall 93cc281abc Merge pull request #4112 from nymtech/chore/fix-get-reader-errors
Fixing parseBody for Mixfetch
2023-11-07 11:51:27 +00:00
Tommy Verrall 1f83b6f4e8 Update request.go with PR comments 2023-11-07 12:38:06 +01:00
Lorexia 5a96ef4ffe Update mixnet docs 2023-11-07 12:20:35 +01:00
Lorexia dea3f7d4b3 Merge updates and update mixFetcha and mixnet examples 2023-11-07 11:54:01 +01:00
Tommy Verrall 40b2729a01 attempt fixing parseBody
- why?
when using the mixfetch SDK, i was encountering issues, when posting requests to specific endpoints. It was not parsing the response correctly with:

Error: panic:syscall/js: Value.Call: property getReader is not a function

By updating the above, i've tested this works on all variations of post and get request using mixfetch.

Locally I had to upgrade my version of go to 1.20
2023-11-07 11:19:57 +01:00
benedettadavico 91f383d5ac Bump mixnode version and update changelog 2023-11-07 07:58:27 +01:00
Jędrzej Stuczyński 268588daac Feature/tls mixnet client (#4103)
* adjusting ts mixnet client constructor

* added forceTls argument to 'ClientOptsSimple'

* more sdk types removed

* fixed import

* removed go debug code

* printing wasm blob version on load

* version bump

* temporarily removed 'nym/nym/wasm/full-nym-wasm'

* changed workspaces definition

* correctly setting initial rc.0 suffix

* updated crate versions

* reverted 'useWorkspaces' lerna option

* Fix up dependency versions

* Add dev mode toggle to SDK publish scripts

* Show location of WASM package

* Change dev mode and CI build order

* Bump package versions in SDK docs

* Remove two versions of `mix-fetch` from SDK docs and only use `-full-fat` version

* Remove old arguments for mixFetch and rename to bust cache

* Remove `nym-wasm-sdk` from linting

* Release v1.2.3 of Typescript SDK

* Force WSS on mixnet client

* Bump TS SDK to 1.2.4-rc.0

* Clean up lock file

* Update node-tester version to 1.2.3 in nym-wallet

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2023-11-06 19:05:11 +00:00
Tommy Verrall c0aff70b37 Merge pull request #4108 from nymtech/develop
Adding docs to the release
2023-11-06 15:55:58 +00:00
mfahampshire 410ef85165 updating websocket send and receives 2023-11-06 16:52:22 +01:00
mx 4af376cb33 Merge pull request #4106 from nymtech/serinko/docs/quick-patch
Quick addon on Exit Gateway setup
2023-11-06 15:24:04 +00:00
serinko 66f012c70e add exit policy to existing NR 2023-11-06 16:18:13 +01:00
mfahampshire 198739a126 added websocket client examples page 2023-11-06 15:48:59 +01:00
mfahampshire 85a0a3d8b5 * created 'examples' file
* added default port to configuration file
2023-11-06 15:48:20 +01:00
mfahampshire 789525c35b remove ref to previous single page setup 2023-11-06 14:02:41 +01:00
mfahampshire 7ad5ff7770 * cmdrun path fixes
* rename file to setup+run
2023-11-06 14:01:20 +01:00
mfahampshire bf56696adc smol tweaks to readability 2023-11-06 13:57:55 +01:00
mfahampshire 193ea34efc turned single ws client page into stub for expanded directory structure 2023-11-06 13:57:40 +01:00
mfahampshire 9c0ca32033 expanded pages for ws client 2023-11-06 13:57:12 +01:00
mfahampshire 76e49476a6 updated clients section format: expanding websocket client 2023-11-06 13:56:58 +01:00
mfahampshire 6d30ede01e updated mdbookadmonish 2023-11-06 13:56:33 +01:00
Lorexia 4c2c738bba Update versioning for sanity check 2023-11-03 14:04:24 +01:00
Tommy Verrall 41bbbed704 keep notes 2023-11-03 12:49:35 +01:00
Lorexia a7f9cb7db0 mimxnet client examples updates 2023-11-03 12:48:55 +01:00
Tommy Verrall 14961d231e - add checks around message lengths 2023-11-03 12:46:55 +01:00
Lorexia c1f2bf4f27 Update versioning and mixfetch docs 2023-11-03 11:02:07 +01:00
Lorexia d073442cfe Correct typo in docs 2023-11-02 14:45:44 +01:00
Lorexia 30165c10af Update contract client page 2023-11-02 13:05:34 +01:00
133 changed files with 10216 additions and 1889 deletions
-1
View File
@@ -3,7 +3,6 @@ name: cd-docs
on:
workflow_dispatch:
push:
branches: master
paths:
- 'documentation/docs/**'
+14
View File
@@ -4,6 +4,18 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2023.4-galaxy] (2023-11-07)
- DRY up client cli ([#4077])
- [mixnode] replace rocket with axum ([#4071])
- incorporate the nym node HTTP api into the mixnode ([#4070])
- replaced '--disable-sign-ext' with '--signext-lowering' when running wasm-opt ([#3896])
[#4077]: https://github.com/nymtech/nym/pull/4077
[#4071]: https://github.com/nymtech/nym/pull/4071
[#4070]: https://github.com/nymtech/nym/issues/4070
[#3896]: https://github.com/nymtech/nym/pull/3896
## [2023.3-kinder] (2023-10-31)
- suppress error output ([#4056])
@@ -15,6 +27,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- use saturating sub in case outfox is not enabled ([#3986])
- Fix sorting for mixnodes and gateways ([#3985])
- Gateway client registry and api routes ([#3955])
- Feature/configurable socks5 bind address ([#3992])
[#4056]: https://github.com/nymtech/nym/pull/4056
[#4042]: https://github.com/nymtech/nym/pull/4042
@@ -25,6 +38,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#3986]: https://github.com/nymtech/nym/pull/3986
[#3985]: https://github.com/nymtech/nym/pull/3985
[#3955]: https://github.com/nymtech/nym/pull/3955
[#3992]: https://github.com/nymtech/nym/pull/3992
## [2023.1-milka] (2023-09-24)
Generated
+19 -14
View File
@@ -2994,7 +2994,7 @@ dependencies = [
[[package]]
name = "extension-storage"
version = "1.2.1"
version = "1.2.4-rc.1"
dependencies = [
"bip39",
"console_error_panic_hook",
@@ -4282,6 +4282,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "ipnetwork"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8eca9f51da27bc908ef3dd85c21e1bbba794edaf94d7841e37356275b82d31e"
dependencies = [
"serde",
]
[[package]]
name = "ipnetwork"
version = "0.18.0"
@@ -5559,12 +5568,13 @@ dependencies = [
[[package]]
name = "mix-fetch-wasm"
version = "1.2.1"
version = "1.2.4-rc.1"
dependencies = [
"async-trait",
"futures",
"http-api-client",
"js-sys",
"nym-bin-common",
"nym-ordered-buffer",
"nym-service-providers-common",
"nym-socks5-requests",
@@ -6260,11 +6270,12 @@ dependencies = [
[[package]]
name = "nym-client-wasm"
version = "1.2.1"
version = "1.2.4-rc.1"
dependencies = [
"anyhow",
"futures",
"js-sys",
"nym-bin-common",
"nym-node-tester-utils",
"nym-node-tester-wasm",
"rand 0.7.3",
@@ -6532,6 +6543,7 @@ dependencies = [
"futures",
"humantime-serde",
"hyper",
"ipnetwork 0.16.0",
"lazy_static",
"log",
"nym-api-requests",
@@ -6666,6 +6678,7 @@ dependencies = [
"tap",
"thiserror",
"tokio",
"url",
]
[[package]]
@@ -6703,7 +6716,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.32"
version = "1.1.33"
dependencies = [
"anyhow",
"axum",
@@ -6898,6 +6911,7 @@ dependencies = [
"fastrand 2.0.1",
"hmac 0.12.1",
"hyper",
"ipnetwork 0.16.0",
"mime",
"nym-config",
"nym-crypto",
@@ -6958,7 +6972,7 @@ dependencies = [
[[package]]
name = "nym-node-tester-wasm"
version = "1.2.1"
version = "1.2.4-rc.1"
dependencies = [
"futures",
"js-sys",
@@ -7558,15 +7572,6 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "nym-wasm-sdk"
version = "1.2.1"
dependencies = [
"mix-fetch-wasm",
"nym-client-wasm",
"nym-node-tester-wasm",
]
[[package]]
name = "nym-wireguard"
version = "0.1.0"
+1 -1
View File
@@ -105,7 +105,7 @@ members = [
"tools/nym-nr-query",
"tools/ts-rs-cli",
"wasm/client",
"wasm/full-nym-wasm",
# "wasm/full-nym-wasm",
"wasm/mix-fetch",
"wasm/node-tester",
]
+2 -2
View File
@@ -104,7 +104,7 @@ sdk-wasm-build:
$(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/full-nym-wasm
#$(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
sdk-typescript-build:
@@ -114,7 +114,7 @@ sdk-typescript-build:
yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm nym-wasm-sdk
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm
sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
+1 -1
View File
@@ -11,7 +11,7 @@ use thiserror::Error;
use tracing::warn;
use url::Url;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
pub type PathSegments<'a> = &'a [&'a str];
pub type Params<'a, K, V> = &'a [(K, V)];
+16 -1
View File
@@ -6,6 +6,7 @@ use crate::PeerPublicKey;
use base64::{engine::general_purpose, Engine};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
@@ -17,11 +18,13 @@ use sha2::Sha256;
pub type GatewayClientRegistry = DashMap<PeerPublicKey, GatewayClient>;
pub type PendingRegistrations = DashMap<PeerPublicKey, Nonce>;
pub type PrivateIPs = DashMap<IpAddr, Free>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Free = bool;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
@@ -72,6 +75,9 @@ pub struct GatewayClient {
#[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))]
pub pub_key: PeerPublicKey,
/// Assigned private IP
pub private_ip: IpAddr,
/// Sha256 hmac on the data (alongside the prior nonce)
#[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))]
pub mac: ClientMac,
@@ -79,7 +85,12 @@ pub struct GatewayClient {
impl GatewayClient {
#[cfg(feature = "verify")]
pub fn new(local_secret: &PrivateKey, remote_public: PublicKey, nonce: u64) -> Self {
pub fn new(
local_secret: &PrivateKey,
remote_public: PublicKey,
private_ip: IpAddr,
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())
@@ -96,10 +107,12 @@ impl GatewayClient {
.expect("x25519 shared secret is always 32 bytes long");
mac.update(local_public.as_bytes());
mac.update(private_ip.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
GatewayClient {
pub_key: PeerPublicKey::new(local_public),
private_ip,
mac: ClientMac(mac.finalize().into_bytes().to_vec()),
}
}
@@ -121,6 +134,7 @@ impl GatewayClient {
.expect("x25519 shared secret is always 32 bytes long");
mac.update(self.pub_key.as_bytes());
mac.update(self.private_ip.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
mac.verify_slice(&self.mac)
@@ -209,6 +223,7 @@ mod tests {
let client = GatewayClient::new(
client_key_pair.private_key(),
*gateway_key_pair.public_key(),
"10.0.0.42".parse().unwrap(),
nonce,
);
assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok())
+6 -1
View File
@@ -44,7 +44,12 @@ pub async fn start_wireguard(
//let routing_mode = tun_device::RoutingMode::new_nat();
// Start the tun device that is used to relay traffic outbound
let (tun, tun_task_tx, tun_task_response_rx) = tun_device::TunDevice::new(routing_mode);
let config = tun_device::TunDeviceConfig {
base_name: setup::TUN_BASE_NAME.to_string(),
ip: setup::TUN_DEVICE_ADDRESS.parse().unwrap(),
netmask: setup::TUN_DEVICE_NETMASK.parse().unwrap(),
};
let (tun, tun_task_tx, tun_task_response_rx) = tun_device::TunDevice::new(routing_mode, config);
tun.start();
// We also index peers by a tag
@@ -10,7 +10,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{
event::Event,
setup::{TUN_BASE_NAME, TUN_DEVICE_ADDRESS, TUN_DEVICE_NETMASK},
tun_task_channel::{
tun_task_channel, tun_task_response_channel, TunTaskPayload, TunTaskResponseRx,
TunTaskResponseTx, TunTaskRx, TunTaskTx,
@@ -20,11 +19,16 @@ use crate::{
fn setup_tokio_tun_device(name: &str, address: Ipv4Addr, netmask: Ipv4Addr) -> tokio_tun::Tun {
log::info!("Creating TUN device with: address={address}, netmask={netmask}");
// Read MTU size from env variable NYM_MTU_SIZE, else default to 1420.
let mtu = std::env::var("NYM_MTU_SIZE")
.map(|mtu| mtu.parse().expect("NYM_MTU_SIZE must be a valid integer"))
.unwrap_or(1420);
log::info!("Using MTU size: {mtu}");
tokio_tun::Tun::builder()
.name(name)
.tap(false)
.packet_info(false)
.mtu(1350)
.mtu(mtu)
.up()
.address(address)
.netmask(netmask)
@@ -74,16 +78,25 @@ pub struct NatInner {
nat_table: HashMap<IpAddr, u64>,
}
pub struct TunDeviceConfig {
pub base_name: String,
pub ip: Ipv4Addr,
pub netmask: Ipv4Addr,
}
impl TunDevice {
pub fn new(
routing_mode: RoutingMode,
// peers_by_ip: Option<Arc<tokio::sync::Mutex<PeersByIp>>>,
config: TunDeviceConfig,
) -> (Self, TunTaskTx, TunTaskResponseRx) {
let tun = setup_tokio_tun_device(
format!("{TUN_BASE_NAME}%d").as_str(),
TUN_DEVICE_ADDRESS.parse().unwrap(),
TUN_DEVICE_NETMASK.parse().unwrap(),
);
let TunDeviceConfig {
base_name,
ip,
netmask,
} = config;
let name = format!("{base_name}%d");
let tun = setup_tokio_tun_device(&name, ip, netmask);
log::info!("Created TUN device: {}", tun.name());
// Channels to communicate with the other tasks
@@ -162,10 +175,10 @@ impl TunDevice {
}
}
// But we do it by consulting the NAT table.
// But we can also do it by consulting the NAT table.
RoutingMode::Nat(ref nat_table) => {
if let Some(tag) = nat_table.nat_table.get(&dst_addr) {
log::info!("Forward packet to wg tunnel with tag: {tag}");
log::info!("Forward packet with tag: {tag}");
self.tun_task_response_tx
.send((*tag, packet.to_vec()))
.await
@@ -180,7 +193,7 @@ impl TunDevice {
}
pub async fn run(mut self) {
let mut buf = [0u8; 1024];
let mut buf = [0u8; 65535];
loop {
tokio::select! {
+8 -8
View File
@@ -8,21 +8,17 @@ use log::info;
pub const WG_ADDRESS: &str = "0.0.0.0";
// The interface used to route traffic
pub const TUN_BASE_NAME: &str = "nymtun";
pub const TUN_DEVICE_ADDRESS: &str = "10.0.0.1";
pub const TUN_BASE_NAME: &str = "nymwg";
pub const TUN_DEVICE_ADDRESS: &str = "10.1.0.1";
pub const TUN_DEVICE_NETMASK: &str = "255.255.255.0";
// The private key of the listener
// Corresponding public key: "WM8s8bYegwMa0TJ+xIwhk+dImk2IpDUKslDBCZPizlE="
const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg=";
// The public keys of the registered peer (clients)
// Corresponding private key: "ILeN6gEh6vJ3Ju8RJ3HVswz+sPgkcKtAYTqzQRhTtlo="
const PEER: &str = "NCIhkgiqxFx1ckKl3Zuh595DzIFl8mxju1Vg995EZhI=";
// The AllowedIPs for the connected peer, which is one a single IP and the same as the IP that the
// peer has configured on their side.
const ALLOWED_IPS: &str = "10.0.0.2";
const ALLOWED_IPS: &str = "10.1.0.2";
fn decode_base64_key(base64_key: &str) -> [u8; 32] {
general_purpose::STANDARD
@@ -46,7 +42,11 @@ pub fn server_static_private_key() -> x25519::StaticSecret {
pub fn peer_static_public_key() -> x25519::PublicKey {
// A single static public key is used during development
let peer_static_public_bytes: [u8; 32] = decode_base64_key(PEER);
// Read from NYM_PEER_PUBLIC_KEY env variable
let peer = std::env::var("NYM_PEER_PUBLIC_KEY").expect("NYM_PEER_PUBLIC_KEY must be set");
let peer_static_public_bytes: [u8; 32] = decode_base64_key(&peer);
let peer_static_public = x25519::PublicKey::try_from(peer_static_public_bytes).unwrap();
info!(
"Adding wg peer public key: {}",
+8 -4
View File
@@ -4,7 +4,7 @@
# Architecture
- [Network Overview](architecture/network-overview.md)
- [Mixnet Traffic Flow](architecture/traffic-flow.md)
<!-- todo reintroduce this with themed images -->
<!-- TODO reintroduce this with themed images -->
<!-- - [Network Rewards](architecture/network-rewards.md) -->
# Binaries
@@ -22,9 +22,13 @@
# Clients
- [Clients Overview](clients/overview.md)
- [Websocket](clients/websocket-client.md)
- [Socks5](clients/socks5-client.md)
- [Webassembly](clients/webassembly-client.md)
- [Websocket Client](clients/websocket-client.md)
- [Setup & Run](clients/websocket/setup.md)
- [Configuration](clients/websocket/config.md)
- [Using Your Client](clients/websocket/usage.md)
- [Examples](clients/websocket/examples.md)
- [Socks5 Client](clients/socks5-client.md)
- [Webassembly Client](clients/webassembly-client.md)
- [Addressing System](clients/addressing-system.md)
# SDK
@@ -160,7 +160,7 @@ Create a service file for the socks5 client at `/etc/systemd/system/nym-socks5-c
```ini
[Unit]
Description=Nym Socks5 Client
Description=Nym Socks5 Client
StartLimitInterval=350
StartLimitBurst=10
@@ -2,13 +2,15 @@
The Nym webassembly client allows any webassembly-capable runtime to build and send Sphinx packets to the Nym network, for uses in edge computing and browser-based applications.
This is currently packaged and distributed for ease of use via the [Nym Typescript SDK library](../sdk/typescript.md).
This is currently packaged and distributed for ease of use via the [Nym Typescript SDK library](../sdk/typescript.md). **We imagine most developers will use this client via the SDK for ease.**
The webassembly client allows for the easy creation of Sphinx packets from within mobile apps and browser-based client-side apps (including Electron or similar).
## Building apps with nym-client-wasm
## Building apps with Webassembly Client
Check out the [examples section](../sdk/typescript.md#using-the-sdk) of the SDK docs for examples of simple application framework setups. There are also two example applications located in the `clients/webassembly` directory in the main Nym platform codebase. The `js-example` is a simple, bare-bones JavaScript app.
Check out the [Typescript SDK docs](https://sdk.nymtech.net) for examples of usage.
There are also example applications located in the `clients/webassembly` directory in the main Nym platform codebase.
## Think about what you're sending!
```admonish caution
@@ -7,208 +7,7 @@
<!-- cmdrun ../../../../target/release/nym-client --version | grep "Build Version" | cut -b 21-26 -->
```
## Client setup
### Viewing command help
You can run this client as a standalone process and pipe traffic into it to be sent through the mixnet. This is useful if you're building an application in a language other than Typescript or Rust and cannot utilise one of the SDKs.
You can check that your binaries are properly compiled with:
You can find the code for this client [here](https://github.com/nymtech/nym/tree/develop/clients/native).
```
./nym-client --help
```
~~~admonish example collapsible=true title="Console output"
```
<!-- cmdrun ../../../../target/release/nym-client --help -->
```
~~~
The two most important commands you will issue to the client are:
* `init` - initalise a new client instance.
* `run` - run a mixnet client process.
You can check the necessary parameters for the available commands by running:
```
./nym-client <command> --help
```
### Initialising your client
Before you can use the client, you need to initalise a new instance of it. Each instance of the client has its own public/private keypair, and connects to its own gateway node. Taken together, these 3 things (public/private keypair + gateway node identity key) make up an app's identity.
Initialising a new client instance can be done with the following command:
```
./nym-client init --id example-client
```
~~~admonish example collapsible=true title="Console output"
```
<!-- cmdrun ../../../../target/release/nym-client init --id example-client -->
```
~~~
The `--id` in the example above is a local identifier so that you can name your clients; it is **never** transmitted over the network.
There is an optional `--gateway` flag that you can use if you want to use a specific gateway. The supplied argument is the `Identity Key` of the gateway you wish to use, which can be found on the [mainnet Network Explorer](https://explorer.nymtech.net/network-components/gateways) or [Sandbox Testnet Explorer](https://sandbox-explorer.nymtech.net/network-components/gateways) depending on which network you are on.
Not passing this argument will randomly select a gateway for your client.
#### Choosing a Gateway
By default - as in the example above - your client will choose a random gateway to connect to.
However, there are several options for choosing a gateway, if you do not want one that is randomly assigned to your client:
* If you wish to connect to a specific gateway, you can specify this with the `--gateway` flag when running `init`.
* You can also choose a gateway based on its location relative to your client. This can be done by appending the `--latency-based-routing` flag to your `init` command. This command means that to select a gateway, your client will:
* fetch a list of all availiable gateways
* send few ping messages to all of them, and measure response times.
* create a weighted distribution to randomly choose one, favouring ones with lower latency.
> Note this doesn't mean that your client will pick the closest gateway to you, but it will be far more likely to connect to gateway with a 20ms ping rather than 200ms
### Running your client
You can run the initalised client by doing this:
```
./nym-client run --id example-client
```
When you run the client, it immediately starts generating (fake) cover traffic and sending it to the mixnet.
When the client is first started, it will reach out to the Nym network's validators, and get a list of available Nym nodes (gateways, mixnodes, and validators). We call this list of nodes the network _topology_. The client does this so that it knows how to connect, register itself with the network, and know which mixnodes it can route Sphinx packets through.
### Configuring your client
When you initalise a client instance, a configuration directory will be generated and stored in `$HOME_DIR/.nym/clients/<client-name>/`.
```
tree $HOME/<user>/.nym/clients/example-client
├── config
│   └── config.toml
└── data
├── ack_key.pem
├── gateway_shared.pem
├── private_encryption.pem
├── private_identity.pem
├── public_encryption.pem
└── public_identity.pem
```
The `config.toml` file contains client configuration options, while the two `pem` files contain client key information.
The generated files contain the client name, public/private keypairs, and gateway address. The name `<client_id>` in the example above is just a local identifier so that you can name your clients.
#### Configuring your client for Docker
By default, the native client listens to host `127.0.0.1`. However this can be an issue if you wish to run a client in a Dockerized environment, where it can be convenenient to listen on a different host such as `0.0.0.0`.
You can set this via the `--host` flag during either the `init` or `run` commands.
Alternatively, a custom host can be set in the `config.toml` file under the `socket` section. If you do this, remember to restart your client process.
## Using your client
### Connecting to the local websocket
The Nym native client exposes a websocket interface that your code connects to. To program your app, choose a websocket library for whatever language you're using. The **default** websocket port is `1977`, you can override that in the client config if you want.
The Nym monorepo includes websocket client example code for Rust, Go, Javacript, and Python, all of which can be found [here](https://github.com/nymtech/nym/tree/master/clients/native/examples).
> Rust users can run the examples with `cargo run --example <rust_file>.rs`, as the examples are not organised in the same way as the other examples, due to already being inside a Cargo project.
All of these code examples will do the following:
* connect to a running websocket client on port `1977`
* format a message to send in either JSON or Binary format. Nym messages have defined JSON formats.
* send the message into the websocket. The native client packages the message into a Sphinx packet and sends it to the mixnet
* wait for confirmation that the message hit the native client
* wait to receive messages from other Nym apps
By varying the message content, you can easily build sophisticated service provider apps. For example, instead of printing the response received from the mixnet, your service provider might take some action on behalf of the user - perhaps initiating a network request, a blockchain transaction, or writing to a local data store.
> You can find an example of building both frontend and service provider code with the websocket client in the [Simple Service Provider Tutorial](https://nymtech.net/developers/tutorials/simple-service-provider/simple-service-provider.html) in the Developer Portal.
### Message Types
There are a small number of messages that your application sends up the websocket to interact with the native client, as follows.
#### Sending text
If you want to send text information through the mixnet, format a message like this one and poke it into the websocket:
```json
{
"type": "send",
"message": "the message",
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
In some applications, e.g. where people are chatting with friends who they know, you might want to include unencrypted reply information in the message field. This provides an easy way for the receiving chat to then turn around and send a reply message:
```json
{
"type": "send",
"message": {
"sender": "198427b63ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm",
"chatMessage": "hi julia!"
},
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
If that fits your security model, good. However, will probably be the case that you want to send **anonymous replies using Single Use Reply Blocks (SURBs)**.
You can read more about SURBs [here](../architecture/traffic-flow.md#private-replies-using-surbs) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - without them having to know your nym address.
Your client will send along a number of `replySurbs` to the recipient of the message. These are pre-addressed Sphinx packets that the recipient can write to the payload of (i.e. write response data to), but not view the address. If the recipient is unable to fit the response data into the bucket of SURBs sent to it, it will use a SURB to request more SURBs be sent to it from your client.
```json
{
"type": "sendAnonymous",
"message": "something you want to keep secret"
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
"replySurbs": 100 // however many reply SURBs to send along with your message
}
```
Each bucket of replySURBs, when received as part of an incoming message, has a unique session identifier, which **only identifies the bucket of pre-addressed packets**. This is necessary to make sure that your app is replying to the correct people with the information meant for them! Constructing a reply with SURBs looks something like this (where `senderTag` was parsed from the incoming message)
```json
{
"type": "reply",
"message": "reply you also want to keep secret",
"senderTag": "the sender tag you parsed from the incoming message"
}
```
#### Sending binary data
You can also send bytes instead of JSON. For that you have to send a binary websocket frame containing a binary encoded
Nym [`ClientRequest`](https://github.com/nymtech/nym/blob/develop/clients/native/websocket-requests/src/requests.rs#L25) containing the same information.
As a response the `native-client` will send a `ServerResponse` to be decoded.
You can find examples of sending and receiving binary data in the Rust, Python and Go [code examples](https://github.com/nymtech/nym/tree/master/clients/native/examples), and an example project from the Nym community [BTC-BC](https://github.com/sgeisler/btcbc-rs/): Bitcoin transaction transmission via Nym, a client and service provider written in Rust.
#### Getting your own address
Sometimes, when you start your app, it can be convenient to ask the native client to tell you what your own address is (from the saved configuration files). To do this, send:
```json
{
"type": "selfAddress"
}
```
You'll get back:
```json
{
"type": "selfAddress",
"address": "the-address" // e.g. "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
#### Error messages
Errors from the app's client, or from the gateway, will be sent down the websocket to your code in the following format:
```json
{
"type": "error",
"message": "string message"
}
```
@@ -0,0 +1,47 @@
# Configuration
## Default listening port
The Nym native client exposes a websocket interface that your code connects to. To program your app, choose a websocket library for whatever language you're using. The **default** websocket port is `1977`, you can override that in the client config if you want.
You can either set this via the `--port` flag at `init` or `run`, or you can manually edit `~/.nym/clients/<CLIENT-ID>/config/config.toml`.
> Remember to restart your client if you change your listening port via editing your config file.
## Choosing a Gateway
By default your client will choose a random gateway to connect to.
However, there are several options for choosing a gateway, if you do not want one that is randomly assigned to your client:
* If you wish to connect to a specific gateway, you can specify this with the `--gateway` flag when running `init`.
* You can also choose a gateway based on its location relative to your client. This can be done by appending the `--latency-based-routing` flag to your `init` command. This command means that to select a gateway, your client will:
* fetch a list of all available gateways
* send few ping messages to all of them, and measure response times.
* create a weighted distribution to randomly choose one, favouring ones with lower latency.
> Note this doesn't mean that your client will pick the closest gateway to you, but it will be far more likely to connect to gateway with a 20ms ping rather than 200ms
## Configuring your client
When you initalise a client instance, a configuration directory will be generated and stored in `$HOME_DIR/.nym/clients/<client-name>/`.
```
tree $HOME/<user>/.nym/clients/example-client
├── config
│   └── config.toml
└── data
├── ack_key.pem
├── gateway_shared.pem
├── private_encryption.pem
├── private_identity.pem
├── public_encryption.pem
└── public_identity.pem
```
The `config.toml` file contains client configuration options, while the two `pem` files contain client key information.
The generated files contain the client name, public/private keypairs, and gateway address. The name `<client_id>` in the example above is just a local identifier so that you can name your clients.
### Configuring your client for Docker
By default, the native client listens to host `127.0.0.1`. However this can be an issue if you wish to run a client in a Dockerized environment, where it can be convenenient to listen on a different host such as `0.0.0.0`.
You can set this via the `--host` flag during either the `init` or `run` commands.
Alternatively, a custom host can be set in the `config.toml` file under the `socket` section. If you do this, remember to restart your client process.
@@ -0,0 +1,15 @@
# Examples
The Nym monorepo includes websocket client example code for Rust, Go, Javacript, and Python, all of which can be found [here](https://github.com/nymtech/nym/tree/master/clients/native/examples).
> Rust users can run the examples with `cargo run --example <rust_file>.rs`, as the examples are not organised in the same way as the other examples, due to already being inside a Cargo project.
All of these code examples will do the following:
* connect to a running websocket client on port `1977`
* format a message to send in either JSON or Binary format. Nym messages have defined JSON formats.
* send the message into the websocket. The native client packages the message into a Sphinx packet and sends it to the mixnet
* wait for confirmation that the message hit the native client
* wait to receive messages from other Nym apps
By varying the message content, you can easily build sophisticated service provider apps. For example, instead of printing the response received from the mixnet, your service provider might take some action on behalf of the user - perhaps initiating a network request, a blockchain transaction, or writing to a local data store.
> You can find an example of building both frontend and service provider code with the websocket client in the [Simple Service Provider Tutorial](https://nymtech.net/developers/tutorials/simple-service-provider/simple-service-provider.html) in the Developer Portal.
@@ -0,0 +1,59 @@
# Setup & Run
## Viewing command help
You can check that your binaries are properly compiled with:
```
./nym-client --help
```
~~~admonish example collapsible=true title="Console output"
```
<!-- cmdrun ../../../../../target/release/nym-client --help -->
```
~~~
The two most important commands you will issue to the client are:
* `init` - initalise a new client instance.
* `run` - run a mixnet client process.
You can check the necessary parameters for the available commands by running:
```
./nym-client <command> --help
```
## Initialising your client
Before you can use the client, you need to initalise a new instance of it. Each instance of the client has its own public/private keypair, and connects to its own gateway node. Taken together, these 3 things (public/private keypair + gateway node identity key) make up an app's identity.
Initialising a new client instance can be done with the following command:
```
./nym-client init --id example-client
```
~~~admonish example collapsible=true title="Console output"
```
<!-- cmdrun ../../../../../target/release/nym-client init --id example-client -->
```
~~~
The `--id` in the example above is a local identifier so that you can name your clients; it is **never** transmitted over the network.
There is an optional `--gateway` flag that you can use if you want to use a specific gateway. The supplied argument is the `Identity Key` of the gateway you wish to use, which can be found on the [mainnet Network Explorer](https://explorer.nymtech.net/network-components/gateways) or [Sandbox Testnet Explorer](https://sandbox-explorer.nymtech.net/network-components/gateways) depending on which network you are on.
Not passing this argument will randomly select a gateway for your client.
## Running your client
You can run the initalised client by doing this:
```
./nym-client run --id example-client
```
When you run the client, it immediately starts generating (fake) cover traffic and sending it to the mixnet.
When the client is first started, it will reach out to the Nym network's validators, and get a list of available Nym nodes (gateways, mixnodes, and validators). We call this list of nodes the network _topology_. The client does this so that it knows how to connect, register itself with the network, and know which mixnodes it can route Sphinx packets through.
@@ -0,0 +1,117 @@
# Using Your Client
The Nym native client exposes a websocket interface that your code connects to. The **default** websocket port is `1977`, you can override that in the client config if you want.
Once you have a websocket connection, interacting with the client involves piping messages down the socket and listening for incoming messages.
# Message Requests
There are a number of message types that you can send up the websocket as defined [here](https://github.com/nymtech/nym/blob/develop/clients/native/websocket-requests/src/requests.rs):
```rust,noplayground
{{#include ../../../../../clients/native/websocket-requests/src/requests.rs:55:97}}
```
## Getting your own address
When you start your app, it is best practice to ask the native client to tell you what your own address is (from the generated configuration files - see [here](../addressing-system.md) for more on Nym's addressing scheme). If you are running a service, you need to do this in order to know what address to give others. In a client-side piece of code you can also use this as a test to make sure your websocket connection is running smoothly. To do this, send:
```json
{
"type": "selfAddress"
}
```
You'll receive a response of the format:
```json
{
"type": "selfAddress",
"address": "your address" // e.g. "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
See [here](https://github.com/nymtech/nym/blob/93cc281abc2cc951023b51746fa6f2ead1f56c46/clients/native/examples/python-examples/websocket/textsend.py#L16C9-L16C9) for an example of this being used.
> Note that all the pieces of native client example code begin with printing the selfAddress. Examples exist for Rust, Go, Javascript, and Python.
## Sending text
If you want to send text information through the mixnet, format a message like this one and poke it into the websocket:
```json
{
"type": "send",
"message": "the message",
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
In some applications, e.g. where people are chatting with friends who they know, you might want to include unencrypted reply information in the message field. This provides an easy way for the receiving chat to then turn around and send a reply message:
```json
{
"type": "send",
"message": {
"sender": "198427b63ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm",
"chatMessage": "hi julia!"
},
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm"
}
```
**If that fits your security model, good. However, will probably be the case that you want to send anonymous replies using Single Use Reply Blocks (SURBs)**.
You can read more about SURBs [here](../../architecture/traffic-flow.md#private-replies-using-surbs) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - **without them having to know your client address**.
Your client will send along a number of `replySurbs` to the recipient of the message. These are pre-addressed Sphinx packets that the recipient can write to the payload of (i.e. write response data to), but not view the final destination of. If the recipient is unable to fit the response data into the bucket of SURBs sent to it, it will use a SURB to request more SURBs be sent to it from your client.
```json
{
"type": "sendAnonymous",
"message": "something you want to keep secret",
"recipient": "71od3ZAupdCdxeFNg8sdonqfZTnZZy1E86WYKEjxD4kj@FWYoUrnKuXryysptnCZgUYRTauHq4FnEFu2QGn5LZWbm",
"replySurbs": 20 // however many reply SURBs to send along with your message
}
```
See ['Replying to SURB Messages'](#replying-to-surb-messages) below for an example of how to deal with incoming messages that have SURBs attached.
Deciding on the amount of SURBs to generate and send along with outgoing messages depends on the expected size of the reply. You might want to send a lot of SURBs in order to make sure you get your response as quickly as possible (but accept the minor additional latency when sending, as your client has to generate and encrypt the packets), or you might just send a few (e.g. 20) and then if your response requires more SURBs, send them along, accepting the additional latency in getting your response.
## Sending binary data
You can also send bytes instead of JSON. For that you have to send a binary websocket frame containing a binary encoded
Nym [`ClientRequest`](https://github.com/nymtech/nym/blob/develop/clients/native/websocket-requests/src/requests.rs#L25) containing the same information.
> As a response the `native-client` will send a `ServerResponse` to be decoded. See [Message Responses](#message-responses) below for more.
You can find examples of sending and receiving binary data in the [code examples](https://github.com/nymtech/nym/tree/master/clients/native/examples), and an example project from the Nym community [BTC-BC](https://github.com/sgeisler/btcbc-rs/): Bitcoin transaction transmission via Nym, a client and service provider written in Rust.
## Replying to SURB messages
Each bucket of `replySURBs`, when received as part of an incoming message, has a unique session identifier, which **only identifies the bucket of pre-addressed packets**. This is necessary to make sure that your app is replying to the correct people with the information meant for them in a situation where multiple clients are sending requests to a single service.
Constructing a reply with SURBs looks something like this (where `senderTag` was parsed from the incoming message)
```json
{
"type": "reply",
"message": "reply you also want to keep secret",
"senderTag": "the sender tag you parsed from the incoming message"
}
```
## Error messages
Errors from the app's client, or from the gateway, will be sent down the websocket to your code in the following format:
```json
{
"type": "error",
"message": "string message"
}
```
## LaneQueueLength
This is currently only used in the [Socks Client](../socks5-client.md) to keep track of the number of Sphinx packets waiting to be sent to the mixnet via being slotted amongst cover traffic. As this value becomes larger, the client signals to the application it should slow down the speed with which it writes to the proxy. This is to stop situations arising whereby an app connected to the client appears as if it has sent (e.g.) a bunch of messages and is awaiting a reply, when they in fact have not been sent through the mixnet yet.
# Message Responses
Responses to your messages are defined [here](https://github.com/nymtech/nym/blob/develop/clients/native/websocket-requests/src/responses.rs):
```rust,noplayground
{{#include ../../../../../clients/native/websocket-requests/src/responses.rs:48:53}}
```
@@ -57,7 +57,7 @@ Before you start an Exit Gateway, read our [Operators Legal Forum](../legal/exit
```
```admonish info
There has been an ongoing development with dynamic upgrades. Follow the status of the Project Smoosh [changes](../faq/smoosh-faq.md#what-are-the-changes) and the progression state of Exit policy [implementation](../faq/smoosh-faq.html#how-will-the-exit-policy-be-implemented) to be up to date with the current design.
There has been an ongoing development with dynamic upgrades. Follow the status of the Project Smoosh [changes](../faq/smoosh-faq.md#what-are-the-changes) and the progression state of exit policy [implementation](../faq/smoosh-faq.html#how-will-the-exit-policy-be-implemented) to be up to date with the current design.
```
### Initialising Exit Gateway
@@ -126,11 +126,23 @@ enabled = true
Save, exit and restart your gateway. Now you are an operator of post-smooshed Exit gateway.
All information about network requester part of your exit gateway is in `/home/user/.nym/gateways/<ID>/config/network_requester_config.toml`.
#### Enable Nym exit policy to an existing Gateway with Network requester functionality
In case you already added Network Requester functionality to your Gateway as described above but haven't enabled the [exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) there is an easy tweak to do so and turn your node into [Nym Exit Gateway](../faq/smoosh-faq.md#what-are-the-changes).
Open the config file stored at `.nym/gateways/<ID>/config/network_requester_config.tom` and set:
```sh
use_deprecated_allow_list = false
```
Save, exit and restart your gateway. Now you are an operator of post-smooshed Exit gateway.
```admonish info
All information about network requester part of your Exit Gateway is in `/home/user/.nym/gateways/<ID>/config/network_requester_config.toml`.
```
For now you can run Gateway without Network requester or with and without the new exit policy. This will soon change as we inform in our [Project Smoosh FAQ](../faq/smoosh-faq.html#how-will-the-exit-policy-be-implemented).
To read more about the configuration like whitelisted outbound requesters in `allowed.list` and other useful information, see the page [*Network Requester Whitelist*](network-requester-setup.md#using-your-network-requester).
To read more about the configuration like whitelisted outbound requesters in `allowed.list` and other useful information, see the page [*Network requester whitelist*](network-requester-setup.md#using-your-network-requester).
#### Initialising Gateway without Network requester
+1
View File
@@ -27,6 +27,7 @@ dirs = "4.0"
dotenvy = { workspace = true }
futures = { workspace = true }
humantime-serde = "1.0.1"
ipnetwork = "0.16"
lazy_static = "1.4.0"
log = { workspace = true }
once_cell = "1.7.2"
+10 -2
View File
@@ -241,9 +241,17 @@ pub(crate) fn override_network_requester_config(
}
pub(crate) fn override_ip_packet_router_config(
cfg: nym_ip_packet_router::Config,
_opts: Option<OverrideIpPacketRouterConfig>,
mut cfg: nym_ip_packet_router::Config,
opts: Option<OverrideIpPacketRouterConfig>,
) -> nym_ip_packet_router::Config {
let Some(_opts) = opts else { return cfg };
// disable poisson rate in the BASE client if the IPR option is enabled
if cfg.ip_packet_router.disable_poisson_rate {
log::info!("Disabling poisson rate for ip packet router");
cfg.set_no_poisson_process();
}
cfg
}
+1
View File
@@ -128,6 +128,7 @@ impl From<ConfigV1_1_31> for Config {
enabled: value.wireguard.enabled,
bind_address: value.wireguard.bind_address,
announced_port: value.wireguard.announced_port,
private_network_prefix: Default::default(),
storage_paths: nym_node::config::persistence::WireguardPaths {
// no fields (yet)
},
+6
View File
@@ -134,6 +134,12 @@ pub(crate) enum GatewayError {
// 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),
#[error("there was an issue with wireguard IP network: {source}")]
IpNetworkError {
#[from]
source: ipnetwork::IpNetworkError,
},
}
impl From<ClientCoreError> for GatewayError {
+7
View File
@@ -4,6 +4,7 @@
use crate::config::Config;
use crate::error::GatewayError;
use crate::node::helpers::load_public_key;
use ipnetwork::IpNetwork;
use log::warn;
use nym_bin_common::bin_info_owned;
use nym_crypto::asymmetric::{encryption, identity};
@@ -16,6 +17,7 @@ use nym_node::http::router::WireguardAppState;
use nym_node::wireguard::types::GatewayClientRegistry;
use nym_sphinx::addressing::clients::Recipient;
use nym_task::TaskClient;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
fn load_gateway_details(
@@ -223,12 +225,17 @@ impl<'a> HttpApiBuilder<'a> {
}
}
let wireguard_private_network = IpNetwork::new(
IpAddr::from(Ipv4Addr::new(10, 1, 0, 0)),
self.gateway_config.wireguard.private_network_prefix,
)?;
let wg_state = self.client_registry.map(|client_registry| {
WireguardAppState::new(
self.sphinx_keypair,
client_registry,
Default::default(),
self.gateway_config.wireguard.bind_address.port(),
wireguard_private_network,
)
});
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.32"
version = "1.1.33"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "extension-storage"
version = "1.2.1"
version = "1.2.4-rc.1"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/nymtech/nym"
+2
View File
@@ -14,6 +14,8 @@ license.workspace = true
anyhow = { workspace = true }
bytes = "1.5.0"
colored = "2"
ipnetwork = "0.16"
rand = "0.7.3"
serde = { workspace = true, features = ["derive"] }
serde_yaml = "0.9.25"
serde_json = { workspace = true }
+6
View File
@@ -12,6 +12,7 @@ pub mod persistence;
mod serde_helpers;
pub const DEFAULT_WIREGUARD_PORT: u16 = WG_PORT;
pub const DEFAULT_WIREGUARD_PREFIX: u8 = 16;
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
@@ -75,6 +76,10 @@ pub struct Wireguard {
/// Useful in the instances where the node is behind a proxy.
pub announced_port: u16,
/// The prefix denoting the maximum number of the clients that can be connected via Wireguard.
/// The maximum value for IPv4 is 32 and for IPv6 is 128
pub private_network_prefix: u8,
/// Paths for wireguard keys, client registries, etc.
pub storage_paths: persistence::WireguardPaths,
}
@@ -88,6 +93,7 @@ impl Default for Wireguard {
DEFAULT_WIREGUARD_PORT,
),
announced_port: DEFAULT_WIREGUARD_PORT,
private_network_prefix: DEFAULT_WIREGUARD_PREFIX,
storage_paths: persistence::WireguardPaths {},
}
}
@@ -14,6 +14,7 @@ use nym_crypto::asymmetric::encryption::PublicKey;
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{
ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce, PeerPublicKey,
};
use rand::{prelude::IteratorRandom, thread_rng};
async fn process_final_message(
client: GatewayClient,
@@ -91,8 +92,23 @@ pub(crate) async fn register_client(
let remote_public = PublicKey::from_bytes(init.pub_key().as_bytes())
.map_err(|_| RequestError::new_status(StatusCode::BAD_REQUEST))?;
let nonce = process_init_message(init, state).await;
let gateway_data =
GatewayClient::new(state.dh_keypair.private_key(), remote_public, nonce);
let mut private_ip_ref = state
.free_private_network_ips
.iter_mut()
.filter(|r| **r)
.choose(&mut thread_rng())
.ok_or(RequestError::new(
"No more space in the network",
StatusCode::SERVICE_UNAVAILABLE,
))?;
// mark it as used, even though it's not final
*private_ip_ref = false;
let gateway_data = GatewayClient::new(
state.dh_keypair.private_key(),
remote_public,
*private_ip_ref.key(),
nonce,
);
let response = ClientRegistrationResponse::PendingRegistration {
nonce,
gateway_data,
@@ -7,8 +7,10 @@ use crate::http::api::v1::gateway::client_interfaces::wireguard::client_registry
use crate::wireguard::types::{GatewayClientRegistry, PendingRegistrations};
use axum::routing::{get, post};
use axum::Router;
use ipnetwork::IpNetwork;
use nym_crypto::asymmetric::encryption;
use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard;
use nym_wireguard_types::registration::PrivateIPs;
use std::sync::Arc;
pub(crate) mod client_registry;
@@ -26,6 +28,7 @@ impl WireguardAppState {
client_registry: Arc<GatewayClientRegistry>,
registration_in_progress: Arc<PendingRegistrations>,
binding_port: u16,
private_ip_network: IpNetwork,
) -> Self {
WireguardAppState {
inner: Some(WireguardAppStateInner {
@@ -33,6 +36,9 @@ impl WireguardAppState {
client_registry,
registration_in_progress,
binding_port,
free_private_network_ips: Arc::new(
private_ip_network.iter().map(|ip| (ip, true)).collect(),
),
}),
}
}
@@ -77,6 +83,7 @@ pub(crate) struct WireguardAppStateInner {
client_registry: Arc<GatewayClientRegistry>,
registration_in_progress: Arc<PendingRegistrations>,
binding_port: u16,
free_private_network_ips: Arc<PrivateIPs>,
}
pub(crate) fn routes<S>(initial_state: WireguardAppState) -> Router<S> {
@@ -98,6 +105,7 @@ mod test {
use axum::http::StatusCode;
use dashmap::DashMap;
use hmac::Mac;
use ipnetwork::IpNetwork;
use nym_crypto::asymmetric::encryption;
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage,
@@ -105,6 +113,8 @@ mod test {
};
use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard;
use nym_wireguard_types::registration::HmacSha256;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
use tower::Service;
use tower::ServiceExt;
@@ -136,6 +146,14 @@ mod test {
let registration_in_progress = Arc::new(DashMap::new());
let client_registry = Arc::new(DashMap::new());
let free_private_network_ips = Arc::new(
IpNetwork::from_str("10.1.0.0/24")
.unwrap()
.iter()
.map(|ip| (ip, true))
.collect(),
);
let client_private_ip = IpAddr::from_str("10.1.0.42").unwrap();
let state = WireguardAppState {
inner: Some(WireguardAppStateInner {
@@ -143,6 +161,7 @@ mod test {
dh_keypair: Arc::new(gateway_key_pair),
registration_in_progress: Arc::clone(&registration_in_progress),
binding_port: 8080,
free_private_network_ips,
}),
};
@@ -186,11 +205,13 @@ mod test {
let mut mac = HmacSha256::new_from_slice(client_dh.as_bytes()).unwrap();
mac.update(client_static_public.as_bytes());
mac.update(client_private_ip.to_string().as_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),
private_ip: client_private_ip,
mac: ClientMac::new(mac.as_slice().to_vec()),
});
+35
View File
@@ -25,6 +25,25 @@ yarn
yarn dev:app
```
or
```
cd src-tauri
cargo tauri dev
```
#### Logging
Rust logging (standard output) is controlled by the `RUST_LOG`
env variable
Example:
```
cd src-tauri
RUST_LOG=trace cargo tauri dev
```
## Dev in the browser
For convenience and better development experience, we can run the
@@ -43,6 +62,22 @@ When creating new tauri command, be sure to add the corresponding
mock definition into `nym-vpn/ui/src/dev/tauri-cmd-mocks/` and
update `nym-vpn/ui/src/dev/setup.ts` accordingly.
## Type bindings
[ts-rs](https://github.com/Aleph-Alpha/ts-rs) can be used to generate
TS type definitions from Rust types
To generate bindings, first
[annotate](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs)
Rust types, then run
```
cd src-tauri
cargo test
```
Generated TS types will be located in `src-tauri/bindings/`
## Build
To build as a **shared library**
+7 -4
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:app": "WEBKIT_DISABLE_COMPOSITING_MODE=1 tauri dev",
"dev:app": "RUST_LOG=nymvpn_ui=trace tauri dev",
"dev:browser": "vite --mode dev-browser",
"build": "tsc && vite build",
"build:app": "yarn build && cd src-tauri && cargo build --release --lib --features custom-protocol",
@@ -15,7 +15,9 @@
"fmt": "prettier --write --ignore-unknown \"**/*\"",
"fmt:check": "prettier --check --ignore-unknown \"**/*\"",
"typecheck": "tsc --noEmit",
"tauri": "tauri"
"tauri:build": "tauri build",
"tauri:build:x86": "tauri build --target i686-pc-windows-msvc",
"tauri:build:arm64": "tauri build --target aarch64-pc-windows-msvc"
},
"dependencies": {
"@mui/base": "^5.0.0-beta.20",
@@ -23,10 +25,11 @@
"@tauri-apps/api": "^1.5.0",
"clsx": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tauri-plugin-autostart-api": "https://github.com/tauri-apps/tauri-plugin-autostart#v1"
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.0",
"@tauri-apps/cli": "^1.5.6",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@typescript-eslint/eslint-plugin": "^6.8.0",
+1 -2
View File
@@ -1,4 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/bindings/
+1992 -109
View File
File diff suppressed because it is too large Load Diff
+30 -3
View File
@@ -1,5 +1,5 @@
[package]
name = "nymvpn-ui"
name = "nymvpn"
version = "0.0.0"
description = "Application UI for Nym VPN desktop clients"
authors = ["you"]
@@ -11,12 +11,39 @@ edition = "2021"
crate-type = ["cdylib"]
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
tauri-build = { version = "1.5.0", features = [] }
[dependencies]
tauri = { version = "1.5", features = ["shell-open"] }
#devtools is only to be used in dev with LLDB vscode
tauri = { version = "1.5.2", features = [ "system-tray", "shell-open"
# ,"devtools"
] }
tokio = { version = "1.33", features = ["rt", "sync", "time", "fs"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3.1", features = ["tracing-log", "env-filter"] }
anyhow = "1.0"
dotenvy = "0.15.7"
thiserror = "1.0"
ts-rs = { version = "7.0.0", features = ["chrono-impl"] }
once_cell = "1.18.0"
toml = "0.8.5"
log = "0.4.20"
hbb_common = { path = "libs/hbb_common" }
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
reqwest = { version = "0.11.22", features = ["blocking"] }
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[target.'cfg(target_os = "windows")'.dependencies]
winreg = "0.51.0"
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "processthreadsapi", "handleapi",
"wingdi", "minwinbase", "securitybaseapi", "shellapi", "ntdef", "windef", "winreg"] }
windows-service = { version = "0.6.0" }
windows-sys = "0.48.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
+35 -1
View File
@@ -1,3 +1,37 @@
fn main() {
tauri_build::build()
if cfg!(debug_assertions) {
println!("DEV BUILD");
tauri_build::build();
} else {
//this is so we can run installer and app as admin
//this only works for release builds
let mut windows = tauri_build::WindowsAttributes::new();
windows = windows.app_manifest(
r#"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
"#,
);
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
.expect("failed to run build script");
};
}
@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock
@@ -0,0 +1,59 @@
[package]
name = "hbb_common"
version = "0.1.0"
authors = ["open-trade <info@opentradesolutions.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
flexi_logger = { version = "0.25", features = ["async"] }
protobuf = { version = "3.2", features = ["with-bytes"] }
tokio = { version = "=1.28.1", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
bytes = { version = "1.4", features = ["serde"] }
log = "0.4"
env_logger = "0.10"
socket2 = { version = "0.3", features = ["reuseport"] }
zstd = "0.12"
quinn = {version = "0.9", optional = true }
anyhow = "1.0"
futures-util = "0.3"
directories-next = "2.0"
rand = "0.8"
serde_derive = "1.0"
serde = "1.0"
serde_json = "1.0"
lazy_static = "1.4"
confy = { git = "https://github.com/open-trade/confy" }
dirs-next = "2.0"
filetime = "0.2"
sodiumoxide = "0.2"
regex = "1.8"
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
chrono = "0.4"
backtrace = "0.3"
libc = "0.2"
dlopen = "0.1"
toml = "0.7"
uuid = { version = "1.3", features = ["v4"] }
sysinfo = "0.29"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
machine-uid = { git = "https://github.com/21pages/machine-uid" }
[features]
quic = []
flatpak = []
[build-dependencies]
protobuf-codegen = { version = "3.2" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[target.'cfg(target_os = "macos")'.dependencies]
osascript = "0.3"
@@ -0,0 +1,14 @@
fn main() {
let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap());
std::fs::create_dir_all(&out_dir).unwrap();
protobuf_codegen::Codegen::new()
.pure()
.out_dir(out_dir)
.inputs(["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
.customize(protobuf_codegen::Customize::default().tokio_bytes(true))
.run()
.expect("Codegen failed.");
}
@@ -0,0 +1,5 @@
extern crate hbb_common;
fn main() {
println!("{:?}", hbb_common::config::PeerConfig::load("455058072"));
}
@@ -0,0 +1,20 @@
extern crate hbb_common;
#[cfg(target_os = "linux")]
use hbb_common::platform::linux;
#[cfg(target_os = "macos")]
use hbb_common::platform::macos;
fn main() {
#[cfg(target_os = "linux")]
let res = linux::system_message("test title", "test message", true);
#[cfg(target_os = "macos")]
let res = macos::alert(
"System Preferences".to_owned(),
"warning".to_owned(),
"test title".to_owned(),
"test message".to_owned(),
["Ok".to_owned()].to_vec(),
);
#[cfg(any(target_os = "linux", target_os = "macos"))]
println!("result {:?}", &res);
}
@@ -0,0 +1,766 @@
syntax = "proto3";
package hbb;
message EncodedVideoFrame {
bytes data = 1;
bool key = 2;
int64 pts = 3;
}
message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
message RGB { bool compress = 1; }
// planes data send directly in binary for better use arraybuffer on web
message YUV {
bool compress = 1;
int32 stride = 2;
}
enum Chroma {
I420 = 0;
I444 = 1;
}
message VideoFrame {
oneof union {
EncodedVideoFrames vp9s = 6;
RGB rgb = 7;
YUV yuv = 8;
EncodedVideoFrames h264s = 10;
EncodedVideoFrames h265s = 11;
EncodedVideoFrames vp8s = 12;
EncodedVideoFrames av1s = 13;
}
int32 display = 14;
}
message IdPk {
string id = 1;
bytes pk = 2;
}
message DisplayInfo {
sint32 x = 1;
sint32 y = 2;
int32 width = 3;
int32 height = 4;
string name = 5;
bool online = 6;
bool cursor_embedded = 7;
Resolution original_resolution = 8;
}
message PortForward {
string host = 1;
int32 port = 2;
}
message FileTransfer {
string dir = 1;
bool show_hidden = 2;
}
message OSLogin {
string username = 1;
string password = 2;
}
message LoginRequest {
string username = 1;
bytes password = 2;
string my_id = 4;
string my_name = 5;
OptionMessage option = 6;
oneof union {
FileTransfer file_transfer = 7;
PortForward port_forward = 8;
}
bool video_ack_required = 9;
uint64 session_id = 10;
string version = 11;
OSLogin os_login = 12;
}
message ChatMessage { string text = 1; }
message Features {
bool privacy_mode = 1;
}
message CodecAbility {
bool vp8 = 1;
bool vp9 = 2;
bool av1 = 3;
bool h264 = 4;
bool h265 = 5;
}
message SupportedEncoding {
bool h264 = 1;
bool h265 = 2;
bool vp8 = 3;
bool av1 = 4;
CodecAbility i444 = 5;
}
message PeerInfo {
string username = 1;
string hostname = 2;
string platform = 3;
repeated DisplayInfo displays = 4;
int32 current_display = 5;
bool sas_enabled = 6;
string version = 7;
Features features = 9;
SupportedEncoding encoding = 10;
SupportedResolutions resolutions = 11;
// Use JSON's key-value format which is friendly for peer to handle.
// NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string.
string platform_additions = 12;
}
message LoginResponse {
oneof union {
string error = 1;
PeerInfo peer_info = 2;
}
}
message TouchScaleUpdate {
// The delta scale factor relative to the previous scale.
// delta * 1000
// 0 means scale end
int32 scale = 1;
}
message TouchPanStart {
int32 x = 1;
int32 y = 2;
}
message TouchPanUpdate {
// The delta x position relative to the previous position.
int32 x = 1;
// The delta y position relative to the previous position.
int32 y = 2;
}
message TouchPanEnd {
int32 x = 1;
int32 y = 2;
}
message TouchEvent {
oneof union {
TouchScaleUpdate scale_update = 1;
TouchPanStart pan_start = 2;
TouchPanUpdate pan_update = 3;
TouchPanEnd pan_end = 4;
}
}
message PointerDeviceEvent {
oneof union {
TouchEvent touch_event = 1;
}
repeated ControlKey modifiers = 2;
}
message MouseEvent {
int32 mask = 1;
sint32 x = 2;
sint32 y = 3;
repeated ControlKey modifiers = 4;
}
enum KeyboardMode{
Legacy = 0;
Map = 1;
Translate = 2;
Auto = 3;
}
enum ControlKey {
Unknown = 0;
Alt = 1;
Backspace = 2;
CapsLock = 3;
Control = 4;
Delete = 5;
DownArrow = 6;
End = 7;
Escape = 8;
F1 = 9;
F10 = 10;
F11 = 11;
F12 = 12;
F2 = 13;
F3 = 14;
F4 = 15;
F5 = 16;
F6 = 17;
F7 = 18;
F8 = 19;
F9 = 20;
Home = 21;
LeftArrow = 22;
/// meta key (also known as "windows"; "super"; and "command")
Meta = 23;
/// option key on macOS (alt key on Linux and Windows)
Option = 24; // deprecated, use Alt instead
PageDown = 25;
PageUp = 26;
Return = 27;
RightArrow = 28;
Shift = 29;
Space = 30;
Tab = 31;
UpArrow = 32;
Numpad0 = 33;
Numpad1 = 34;
Numpad2 = 35;
Numpad3 = 36;
Numpad4 = 37;
Numpad5 = 38;
Numpad6 = 39;
Numpad7 = 40;
Numpad8 = 41;
Numpad9 = 42;
Cancel = 43;
Clear = 44;
Menu = 45; // deprecated, use Alt instead
Pause = 46;
Kana = 47;
Hangul = 48;
Junja = 49;
Final = 50;
Hanja = 51;
Kanji = 52;
Convert = 53;
Select = 54;
Print = 55;
Execute = 56;
Snapshot = 57;
Insert = 58;
Help = 59;
Sleep = 60;
Separator = 61;
Scroll = 62;
NumLock = 63;
RWin = 64;
Apps = 65;
Multiply = 66;
Add = 67;
Subtract = 68;
Decimal = 69;
Divide = 70;
Equals = 71;
NumpadEnter = 72;
RShift = 73;
RControl = 74;
RAlt = 75;
CtrlAltDel = 100;
LockScreen = 101;
}
message KeyEvent {
bool down = 1;
bool press = 2;
oneof union {
ControlKey control_key = 3;
// position key code. win: scancode, linux: key code, macos: key code
uint32 chr = 4;
uint32 unicode = 5;
string seq = 6;
// high word. virtual keycode
// low word. unicode
uint32 win2win_hotkey = 7;
}
repeated ControlKey modifiers = 8;
KeyboardMode mode = 9;
}
message CursorData {
uint64 id = 1;
sint32 hotx = 2;
sint32 hoty = 3;
int32 width = 4;
int32 height = 5;
bytes colors = 6;
}
message CursorPosition {
sint32 x = 1;
sint32 y = 2;
}
message Hash {
string salt = 1;
string challenge = 2;
}
message Clipboard {
bool compress = 1;
bytes content = 2;
}
enum FileType {
Dir = 0;
DirLink = 2;
DirDrive = 3;
File = 4;
FileLink = 5;
}
message FileEntry {
FileType entry_type = 1;
string name = 2;
bool is_hidden = 3;
uint64 size = 4;
uint64 modified_time = 5;
}
message FileDirectory {
int32 id = 1;
string path = 2;
repeated FileEntry entries = 3;
}
message ReadDir {
string path = 1;
bool include_hidden = 2;
}
message ReadAllFiles {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
}
message FileAction {
oneof union {
ReadDir read_dir = 1;
FileTransferSendRequest send = 2;
FileTransferReceiveRequest receive = 3;
FileDirCreate create = 4;
FileRemoveDir remove_dir = 5;
FileRemoveFile remove_file = 6;
ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8;
FileTransferSendConfirmRequest send_confirm = 9;
}
}
message FileTransferCancel { int32 id = 1; }
message FileResponse {
oneof union {
FileDirectory dir = 1;
FileTransferBlock block = 2;
FileTransferError error = 3;
FileTransferDone done = 4;
FileTransferDigest digest = 5;
}
}
message FileTransferDigest {
int32 id = 1;
sint32 file_num = 2;
uint64 last_modified = 3;
uint64 file_size = 4;
bool is_upload = 5;
bool is_identical = 6;
}
message FileTransferBlock {
int32 id = 1;
sint32 file_num = 2;
bytes data = 3;
bool compressed = 4;
uint32 blk_id = 5;
}
message FileTransferError {
int32 id = 1;
string error = 2;
sint32 file_num = 3;
}
message FileTransferSendRequest {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
int32 file_num = 4;
}
message FileTransferSendConfirmRequest {
int32 id = 1;
sint32 file_num = 2;
oneof union {
bool skip = 3;
uint32 offset_blk = 4;
}
}
message FileTransferDone {
int32 id = 1;
sint32 file_num = 2;
}
message FileTransferReceiveRequest {
int32 id = 1;
string path = 2; // path written to
repeated FileEntry files = 3;
int32 file_num = 4;
uint64 total_size = 5;
}
message FileRemoveDir {
int32 id = 1;
string path = 2;
bool recursive = 3;
}
message FileRemoveFile {
int32 id = 1;
string path = 2;
sint32 file_num = 3;
}
message FileDirCreate {
int32 id = 1;
string path = 2;
}
// main logic from freeRDP
message CliprdrMonitorReady {
}
message CliprdrFormat {
int32 id = 2;
string format = 3;
}
message CliprdrServerFormatList {
repeated CliprdrFormat formats = 2;
}
message CliprdrServerFormatListResponse {
int32 msg_flags = 2;
}
message CliprdrServerFormatDataRequest {
int32 requested_format_id = 2;
}
message CliprdrServerFormatDataResponse {
int32 msg_flags = 2;
bytes format_data = 3;
}
message CliprdrFileContentsRequest {
int32 stream_id = 2;
int32 list_index = 3;
int32 dw_flags = 4;
int32 n_position_low = 5;
int32 n_position_high = 6;
int32 cb_requested = 7;
bool have_clip_data_id = 8;
int32 clip_data_id = 9;
}
message CliprdrFileContentsResponse {
int32 msg_flags = 3;
int32 stream_id = 4;
bytes requested_data = 5;
}
message Cliprdr {
oneof union {
CliprdrMonitorReady ready = 1;
CliprdrServerFormatList format_list = 2;
CliprdrServerFormatListResponse format_list_response = 3;
CliprdrServerFormatDataRequest format_data_request = 4;
CliprdrServerFormatDataResponse format_data_response = 5;
CliprdrFileContentsRequest file_contents_request = 6;
CliprdrFileContentsResponse file_contents_response = 7;
}
}
message Resolution {
int32 width = 1;
int32 height = 2;
}
message SupportedResolutions { repeated Resolution resolutions = 1; }
message SwitchDisplay {
int32 display = 1;
sint32 x = 2;
sint32 y = 3;
int32 width = 4;
int32 height = 5;
bool cursor_embedded = 6;
SupportedResolutions resolutions = 7;
// Do not care about the origin point for now.
Resolution original_resolution = 8;
}
message CaptureDisplays {
repeated int32 add = 1;
repeated int32 sub = 2;
repeated int32 set = 3;
}
message ToggleVirtualDisplay {
int32 display = 1;
bool on = 2;
}
message PermissionInfo {
enum Permission {
Keyboard = 0;
Clipboard = 2;
Audio = 3;
File = 4;
Restart = 5;
Recording = 6;
BlockInput = 7;
}
Permission permission = 1;
bool enabled = 2;
}
enum ImageQuality {
NotSet = 0;
Low = 2;
Balanced = 3;
Best = 4;
}
message SupportedDecoding {
enum PreferCodec {
Auto = 0;
VP9 = 1;
H264 = 2;
H265 = 3;
VP8 = 4;
AV1 = 5;
}
int32 ability_vp9 = 1;
int32 ability_h264 = 2;
int32 ability_h265 = 3;
PreferCodec prefer = 4;
int32 ability_vp8 = 5;
int32 ability_av1 = 6;
CodecAbility i444 = 7;
Chroma prefer_chroma = 8;
}
message OptionMessage {
enum BoolOption {
NotSet = 0;
No = 1;
Yes = 2;
}
ImageQuality image_quality = 1;
BoolOption lock_after_session_end = 2;
BoolOption show_remote_cursor = 3;
BoolOption privacy_mode = 4;
BoolOption block_input = 5;
int32 custom_image_quality = 6;
BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9;
SupportedDecoding supported_decoding = 10;
int32 custom_fps = 11;
BoolOption disable_keyboard = 12;
// Position 13 is used for Resolution. Remove later.
// Resolution custom_resolution = 13;
}
message TestDelay {
int64 time = 1;
bool from_client = 2;
uint32 last_delay = 3;
uint32 target_bitrate = 4;
}
message PublicKey {
bytes asymmetric_value = 1;
bytes symmetric_value = 2;
}
message SignedId { bytes id = 1; }
message AudioFormat {
uint32 sample_rate = 1;
uint32 channels = 2;
}
message AudioFrame {
bytes data = 1;
}
// Notify peer to show message box.
message MessageBox {
// Message type. Refer to flutter/lib/common.dart/msgBox().
string msgtype = 1;
string title = 2;
// English
string text = 3;
// If not empty, msgbox provides a button to following the link.
// The link here can't be directly http url.
// It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
string link = 4;
}
message BackNotification {
// no need to consider block input by someone else
enum BlockInputState {
BlkStateUnknown = 0;
BlkOnSucceeded = 2;
BlkOnFailed = 3;
BlkOffSucceeded = 4;
BlkOffFailed = 5;
}
enum PrivacyModeState {
PrvStateUnknown = 0;
// Privacy mode on by someone else
PrvOnByOther = 2;
// Privacy mode is not supported on the remote side
PrvNotSupported = 3;
// Privacy mode on by self
PrvOnSucceeded = 4;
// Privacy mode on by self, but denied
PrvOnFailedDenied = 5;
// Some plugins are not found
PrvOnFailedPlugin = 6;
// Privacy mode on by self, but failed
PrvOnFailed = 7;
// Privacy mode off by self
PrvOffSucceeded = 8;
// Ctrl + P
PrvOffByPeer = 9;
// Privacy mode off by self, but failed
PrvOffFailed = 10;
PrvOffUnknown = 11;
}
oneof union {
PrivacyModeState privacy_mode_state = 1;
BlockInputState block_input_state = 2;
}
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
string details = 3;
}
message ElevationRequestWithLogon {
string username = 1;
string password = 2;
}
message ElevationRequest {
oneof union {
bool direct = 1;
ElevationRequestWithLogon logon = 2;
}
}
message SwitchSidesRequest {
bytes uuid = 1;
}
message SwitchSidesResponse {
bytes uuid = 1;
LoginRequest lr = 2;
}
message SwitchBack {}
message PluginRequest {
string id = 1;
bytes content = 2;
}
message PluginFailure {
string id = 1;
string name = 2;
string msg = 3;
}
message Misc {
oneof union {
ChatMessage chat_message = 4;
SwitchDisplay switch_display = 5;
PermissionInfo permission_info = 6;
OptionMessage option = 7;
AudioFormat audio_format = 8;
string close_reason = 9;
bool refresh_video = 10;
bool video_received = 12;
BackNotification back_notification = 13;
bool restart_remote_device = 14;
bool uac = 15;
bool foreground_window_elevated = 16;
bool stop_service = 17;
ElevationRequest elevation_request = 18;
string elevation_response = 19;
bool portable_service_running = 20;
SwitchSidesRequest switch_sides_request = 21;
SwitchBack switch_back = 22;
Resolution change_resolution = 24;
PluginRequest plugin_request = 25;
PluginFailure plugin_failure = 26;
uint32 full_speed_fps = 27;
uint32 auto_adjust_fps = 28;
bool client_record_status = 29;
CaptureDisplays capture_displays = 30;
int32 refresh_video_display = 31;
ToggleVirtualDisplay toggle_virtual_display = 32;
}
}
message VoiceCallRequest {
int64 req_timestamp = 1;
// Indicates whether the request is a connect action or a disconnect action.
bool is_connect = 2;
}
message VoiceCallResponse {
bool accepted = 1;
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
int64 ack_timestamp = 3;
}
message Message {
oneof union {
SignedId signed_id = 3;
PublicKey public_key = 4;
TestDelay test_delay = 5;
VideoFrame video_frame = 6;
LoginRequest login_request = 7;
LoginResponse login_response = 8;
Hash hash = 9;
MouseEvent mouse_event = 10;
AudioFrame audio_frame = 11;
CursorData cursor_data = 12;
CursorPosition cursor_position = 13;
uint64 cursor_id = 14;
KeyEvent key_event = 15;
Clipboard clipboard = 16;
FileAction file_action = 17;
FileResponse file_response = 18;
Misc misc = 19;
Cliprdr cliprdr = 20;
MessageBox message_box = 21;
SwitchSidesResponse switch_sides_response = 22;
VoiceCallRequest voice_call_request = 23;
VoiceCallResponse voice_call_response = 24;
PeerInfo peer_info = 25;
PointerDeviceEvent pointer_device_event = 26;
}
}
@@ -0,0 +1,193 @@
syntax = "proto3";
package hbb;
message RegisterPeer {
string id = 1;
int32 serial = 2;
}
enum ConnType {
DEFAULT_CONN = 0;
FILE_TRANSFER = 1;
PORT_FORWARD = 2;
RDP = 3;
}
message RegisterPeerResponse { bool request_pk = 2; }
message PunchHoleRequest {
string id = 1;
NatType nat_type = 2;
string licence_key = 3;
ConnType conn_type = 4;
string token = 5;
}
message PunchHole {
bytes socket_addr = 1;
string relay_server = 2;
NatType nat_type = 3;
string request_region = 4;
}
message TestNatRequest {
int32 serial = 1;
}
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
message TestNatResponse {
int32 port = 1;
ConfigUpdate cu = 2; // for mobile
}
enum NatType {
UNKNOWN_NAT = 0;
ASYMMETRIC = 1;
SYMMETRIC = 2;
}
message PunchHoleSent {
bytes socket_addr = 1;
string id = 2;
string relay_server = 3;
NatType nat_type = 4;
string version = 5;
string request_region = 6;
}
message RegisterPk {
string id = 1;
bytes uuid = 2;
bytes pk = 3;
string old_id = 4;
}
message RegisterPkResponse {
enum Result {
OK = 0;
UUID_MISMATCH = 2;
ID_EXISTS = 3;
TOO_FREQUENT = 4;
INVALID_ID_FORMAT = 5;
NOT_SUPPORT = 6;
SERVER_ERROR = 7;
}
Result result = 1;
}
message PunchHoleResponse {
bytes socket_addr = 1;
bytes pk = 2;
enum Failure {
ID_NOT_EXIST = 0;
OFFLINE = 2;
LICENSE_MISMATCH = 3;
LICENSE_OVERUSE = 4;
}
Failure failure = 3;
string relay_server = 4;
oneof union {
NatType nat_type = 5;
bool is_local = 6;
}
string other_failure = 7;
}
message ConfigUpdate {
int32 serial = 1;
repeated string rendezvous_servers = 2;
}
message RequestRelay {
string id = 1;
string uuid = 2;
bytes socket_addr = 3;
string relay_server = 4;
bool secure = 5;
string licence_key = 6;
ConnType conn_type = 7;
string token = 8;
string request_region = 9;
}
message RelayResponse {
bytes socket_addr = 1;
string uuid = 2;
string relay_server = 3;
oneof union {
string id = 4;
bytes pk = 5;
}
string refuse_reason = 6;
string version = 7;
string request_region = 8;
}
message SoftwareUpdate { string url = 1; }
// if in same intranet, punch hole won't work both for udp and tcp,
// even some router has below connection error if we connect itself,
// { kind: Other, error: "could not resolve to any address" },
// so we request local address to connect.
message FetchLocalAddr {
bytes socket_addr = 1;
string relay_server = 2;
string request_region = 3;
}
message LocalAddr {
bytes socket_addr = 1;
bytes local_addr = 2;
string relay_server = 3;
string id = 4;
string version = 5;
string request_region = 6;
}
message PeerDiscovery {
string cmd = 1;
string mac = 2;
string id = 3;
string username = 4;
string hostname = 5;
string platform = 6;
string misc = 7;
}
message OnlineRequest {
string id = 1;
repeated string peers = 2;
}
message OnlineResponse {
bytes states = 1;
}
message KeyExchange {
repeated bytes keys = 1;
}
message RendezvousMessage {
oneof union {
RegisterPeer register_peer = 6;
RegisterPeerResponse register_peer_response = 7;
PunchHoleRequest punch_hole_request = 8;
PunchHole punch_hole = 9;
PunchHoleSent punch_hole_sent = 10;
PunchHoleResponse punch_hole_response = 11;
FetchLocalAddr fetch_local_addr = 12;
LocalAddr local_addr = 13;
ConfigUpdate configure_update = 14;
RegisterPk register_pk = 15;
RegisterPkResponse register_pk_response = 16;
SoftwareUpdate software_update = 17;
RequestRelay request_relay = 18;
RelayResponse relay_response = 19;
TestNatRequest test_nat_request = 20;
TestNatResponse test_nat_response = 21;
PeerDiscovery peer_discovery = 22;
OnlineRequest online_request = 23;
OnlineResponse online_response = 24;
KeyExchange key_exchange = 25;
}
}
@@ -0,0 +1,280 @@
use bytes::{Buf, BufMut, Bytes, BytesMut};
use std::io;
use tokio_util::codec::{Decoder, Encoder};
#[derive(Debug, Clone, Copy)]
pub struct BytesCodec {
state: DecodeState,
raw: bool,
max_packet_length: usize,
}
#[derive(Debug, Clone, Copy)]
enum DecodeState {
Head,
Data(usize),
}
impl Default for BytesCodec {
fn default() -> Self {
Self::new()
}
}
impl BytesCodec {
pub fn new() -> Self {
Self {
state: DecodeState::Head,
raw: false,
max_packet_length: usize::MAX,
}
}
pub fn set_raw(&mut self) {
self.raw = true;
}
pub fn set_max_packet_length(&mut self, n: usize) {
self.max_packet_length = n;
}
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
if src.is_empty() {
return Ok(None);
}
let head_len = ((src[0] & 0x3) + 1) as usize;
if src.len() < head_len {
return Ok(None);
}
let mut n = src[0] as usize;
if head_len > 1 {
n |= (src[1] as usize) << 8;
}
if head_len > 2 {
n |= (src[2] as usize) << 16;
}
if head_len > 3 {
n |= (src[3] as usize) << 24;
}
n >>= 2;
if n > self.max_packet_length {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet"));
}
src.advance(head_len);
src.reserve(n);
Ok(Some(n))
}
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
if src.len() < n {
return Ok(None);
}
Ok(Some(src.split_to(n)))
}
}
impl Decoder for BytesCodec {
type Item = BytesMut;
type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<BytesMut>, io::Error> {
if self.raw {
if !src.is_empty() {
let len = src.len();
return Ok(Some(src.split_to(len)));
} else {
return Ok(None);
}
}
let n = match self.state {
DecodeState::Head => match self.decode_head(src)? {
Some(n) => {
self.state = DecodeState::Data(n);
n
}
None => return Ok(None),
},
DecodeState::Data(n) => n,
};
match self.decode_data(n, src)? {
Some(data) => {
self.state = DecodeState::Head;
Ok(Some(data))
}
None => Ok(None),
}
}
}
impl Encoder<Bytes> for BytesCodec {
type Error = io::Error;
fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> {
if self.raw {
buf.reserve(data.len());
buf.put(data);
return Ok(());
}
if data.len() <= 0x3F {
buf.put_u8((data.len() << 2) as u8);
} else if data.len() <= 0x3FFF {
buf.put_u16_le((data.len() << 2) as u16 | 0x1);
} else if data.len() <= 0x3FFFFF {
let h = (data.len() << 2) as u32 | 0x2;
buf.put_u16_le((h & 0xFFFF) as u16);
buf.put_u8((h >> 16) as u8);
} else if data.len() <= 0x3FFFFFFF {
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
} else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
}
buf.extend(data);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec1() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F, 1);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3F + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
panic!();
}
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
panic!();
}
buf2.extend(&buf_saved[1..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
panic!();
}
}
#[test]
fn test_codec2() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
assert!(codec.encode("".into(), &mut buf).is_ok());
assert_eq!(buf.len(), 1);
bytes.resize(0x3F + 1, 2);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 2 + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0);
} else {
panic!();
}
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F + 1);
assert_eq!(res[0], 2);
} else {
panic!();
}
}
#[test]
fn test_codec3() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F - 1, 3);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 1 - 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F - 1);
assert_eq!(res[0], 3);
} else {
panic!();
}
}
#[test]
fn test_codec4() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFF, 4);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFF + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFF);
assert_eq!(res[0], 4);
} else {
panic!();
}
}
#[test]
fn test_codec5() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF, 5);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFFFF + 3);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF);
assert_eq!(res[0], 5);
} else {
panic!();
}
}
#[test]
fn test_codec6() {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF + 1, 6);
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
panic!();
}
buf2.extend(&buf_saved[1..6]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
panic!();
}
buf2.extend(&buf_saved[6..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
panic!();
}
}
}
@@ -0,0 +1,57 @@
use std::{cell::RefCell, io};
use zstd::bulk::{Compressor, Decompressor};
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
// which is currently 22. Levels >= 20
// Default level is ZSTD_CLEVEL_DEFAULT==3.
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
thread_local! {
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
static DECOMPRESSOR: RefCell<io::Result<Decompressor<'static>>> = RefCell::new(Decompressor::new());
}
pub fn compress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
COMPRESSOR.with(|c| {
if let Ok(mut c) = c.try_borrow_mut() {
match &mut *c {
Ok(c) => match c.compress(data) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to compress: {}", err);
}
},
Err(err) => {
crate::log::debug!("Failed to get compressor: {}", err);
}
}
}
});
out
}
pub fn decompress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
DECOMPRESSOR.with(|d| {
if let Ok(mut d) = d.try_borrow_mut() {
match &mut *d {
Ok(d) => {
const MAX: usize = 1024 * 1024 * 64;
const MIN: usize = 1024 * 1024;
let mut n = 30 * data.len();
n = n.clamp(MIN, MAX);
match d.decompress(data, n) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to decompress: {}", err);
}
}
}
Err(err) => {
crate::log::debug!("Failed to get decompressor: {}", err);
}
}
}
});
out
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,900 @@
#[cfg(windows)]
use std::os::windows::prelude::*;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use tokio::{fs::File, io::*};
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
use crate::{
compress::{compress, decompress},
config::Config,
};
pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType<FileDirectory> {
let mut dir = FileDirectory {
path: get_string(path),
..Default::default()
};
#[cfg(windows)]
if "/" == &get_string(path) {
let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() };
for i in 0..32 {
if drives & (1 << i) != 0 {
let name = format!(
"{}:",
std::char::from_u32('A' as u32 + i as u32).unwrap_or('A')
);
dir.entries.push(FileEntry {
name,
entry_type: FileType::DirDrive.into(),
..Default::default()
});
}
}
return Ok(dir);
}
for entry in path.read_dir()?.flatten() {
let p = entry.path();
let name = p
.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
if name.is_empty() {
continue;
}
let mut is_hidden = false;
let meta;
if let Ok(tmp) = std::fs::symlink_metadata(&p) {
meta = tmp;
} else {
continue;
}
// docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
#[cfg(windows)]
if meta.file_attributes() & 0x2 != 0 {
is_hidden = true;
}
#[cfg(not(windows))]
if name.find('.').unwrap_or(usize::MAX) == 0 {
is_hidden = true;
}
if is_hidden && !include_hidden {
continue;
}
let (entry_type, size) = {
if p.is_dir() {
if meta.file_type().is_symlink() {
(FileType::DirLink.into(), 0)
} else {
(FileType::Dir.into(), 0)
}
} else if meta.file_type().is_symlink() {
(FileType::FileLink.into(), 0)
} else {
(FileType::File.into(), meta.len())
}
};
let modified_time = meta
.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0);
dir.entries.push(FileEntry {
name: get_file_name(&p),
entry_type,
is_hidden,
size,
modified_time,
..Default::default()
});
}
Ok(dir)
}
#[inline]
pub fn get_file_name(p: &Path) -> String {
p.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned()
}
#[inline]
pub fn get_string(path: &Path) -> String {
path.to_str().unwrap_or("").to_owned()
}
#[inline]
pub fn get_path(path: &str) -> PathBuf {
Path::new(path).to_path_buf()
}
#[inline]
pub fn get_home_as_string() -> String {
get_string(&Config::get_home())
}
fn read_dir_recursive(
path: &PathBuf,
prefix: &Path,
include_hidden: bool,
) -> ResultType<Vec<FileEntry>> {
let mut files = Vec::new();
if path.is_dir() {
// to-do: symbol link handling, cp the link rather than the content
// to-do: file mode, for unix
let fd = read_dir(path, include_hidden)?;
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::File) => {
let mut entry = entry.clone();
entry.name = get_string(&prefix.join(entry.name));
files.push(entry);
}
Ok(FileType::Dir) => {
if let Ok(mut tmp) = read_dir_recursive(
&path.join(&entry.name),
&prefix.join(&entry.name),
include_hidden,
) {
for entry in tmp.drain(0..) {
files.push(entry);
}
}
}
_ => {}
}
}
Ok(files)
} else if path.is_file() {
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) {
(
meta.len(),
meta.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0),
)
} else {
(0, 0)
};
files.push(FileEntry {
entry_type: FileType::File.into(),
size,
modified_time,
..Default::default()
});
Ok(files)
} else {
bail!("Not exists");
}
}
pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<FileEntry>> {
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
#[inline]
pub fn is_file_exists(file_path: &str) -> bool {
return Path::new(file_path).exists();
}
#[inline]
pub fn can_enable_overwrite_detection(version: i64) -> bool {
version >= get_version_number("1.1.10")
}
#[derive(Default, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransferJob {
pub id: i32,
pub remote: String,
pub path: PathBuf,
pub show_hidden: bool,
pub is_remote: bool,
pub is_last_job: bool,
pub file_num: i32,
#[serde(skip_serializing)]
pub files: Vec<FileEntry>,
pub conn_id: i32, // server only
#[serde(skip_serializing)]
file: Option<File>,
pub total_size: u64,
finished_size: u64,
transferred: u64,
enable_overwrite_detection: bool,
file_confirmed: bool,
// indicating the last file is skipped
file_skipped: bool,
file_is_waiting: bool,
default_overwrite_strategy: Option<bool>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct TransferJobMeta {
#[serde(default)]
pub id: i32,
#[serde(default)]
pub remote: String,
#[serde(default)]
pub to: String,
#[serde(default)]
pub show_hidden: bool,
#[serde(default)]
pub file_num: i32,
#[serde(default)]
pub is_remote: bool,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct RemoveJobMeta {
#[serde(default)]
pub path: String,
#[serde(default)]
pub is_remote: bool,
#[serde(default)]
pub no_confirm: bool,
}
#[inline]
fn get_ext(name: &str) -> &str {
if let Some(i) = name.rfind('.') {
return &name[i + 1..];
}
""
}
#[inline]
fn is_compressed_file(name: &str) -> bool {
let ext = get_ext(name);
ext == "xz"
|| ext == "gz"
|| ext == "zip"
|| ext == "7z"
|| ext == "rar"
|| ext == "bz2"
|| ext == "tgz"
|| ext == "png"
|| ext == "jpg"
}
impl TransferJob {
#[allow(clippy::too_many_arguments)]
pub fn new_write(
id: i32,
remote: String,
path: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
files: Vec<FileEntry>,
enable_overwrite_detection: bool,
) -> Self {
log::info!("new write {}", path);
let total_size = files.iter().map(|x| x.size).sum();
Self {
id,
remote,
path: get_path(&path),
file_num,
show_hidden,
is_remote,
files,
total_size,
enable_overwrite_detection,
..Default::default()
}
}
pub fn new_read(
id: i32,
remote: String,
path: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
enable_overwrite_detection: bool,
) -> ResultType<Self> {
log::info!("new read {}", path);
let files = get_recursive_files(&path, show_hidden)?;
let total_size = files.iter().map(|x| x.size).sum();
Ok(Self {
id,
remote,
path: get_path(&path),
file_num,
show_hidden,
is_remote,
files,
total_size,
enable_overwrite_detection,
..Default::default()
})
}
#[inline]
pub fn files(&self) -> &Vec<FileEntry> {
&self.files
}
#[inline]
pub fn set_files(&mut self, files: Vec<FileEntry>) {
self.files = files;
}
#[inline]
pub fn id(&self) -> i32 {
self.id
}
#[inline]
pub fn total_size(&self) -> u64 {
self.total_size
}
#[inline]
pub fn finished_size(&self) -> u64 {
self.finished_size
}
#[inline]
pub fn transferred(&self) -> u64 {
self.transferred
}
#[inline]
pub fn file_num(&self) -> i32 {
self.file_num
}
pub fn modify_time(&self) {
let file_num = self.file_num as usize;
if file_num < self.files.len() {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::rename(download_path, &path).ok();
filetime::set_file_mtime(
&path,
filetime::FileTime::from_unix_time(entry.modified_time as _, 0),
)
.ok();
}
}
pub fn remove_download_file(&self) {
let file_num = self.file_num as usize;
if file_num < self.files.len() {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::remove_file(download_path).ok();
}
}
pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
if block.id != self.id {
bail!("Wrong id");
}
let file_num = block.file_num as usize;
if file_num >= self.files.len() {
bail!("Wrong file number");
}
if file_num != self.file_num as usize || self.file.is_none() {
self.modify_time();
if let Some(file) = self.file.as_mut() {
file.sync_all().await?;
}
self.file_num = block.file_num;
let entry = &self.files[file_num];
let path = self.join(&entry.name);
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).ok();
}
let path = format!("{}.download", get_string(&path));
self.file = Some(File::create(&path).await?);
}
if block.compressed {
let tmp = decompress(&block.data);
self.file
.as_mut()
.ok_or(anyhow!("file is None"))?
.write_all(&tmp)
.await?;
self.finished_size += tmp.len() as u64;
} else {
self.file
.as_mut()
.ok_or(anyhow!("file is None"))?
.write_all(&block.data)
.await?;
self.finished_size += block.data.len() as u64;
}
self.transferred += block.data.len() as u64;
Ok(())
}
#[inline]
pub fn join(&self, name: &str) -> PathBuf {
if name.is_empty() {
self.path.clone()
} else {
self.path.join(name)
}
}
pub async fn read(&mut self, stream: &mut Stream) -> ResultType<Option<FileTransferBlock>> {
let file_num = self.file_num as usize;
if file_num >= self.files.len() {
self.file.take();
return Ok(None);
}
let name = &self.files[file_num].name;
if self.file.is_none() {
match File::open(self.join(name)).await {
Ok(file) => {
self.file = Some(file);
self.file_confirmed = false;
self.file_is_waiting = false;
}
Err(err) => {
self.file_num += 1;
self.file_confirmed = false;
self.file_is_waiting = false;
return Err(err.into());
}
}
}
if self.enable_overwrite_detection && !self.file_confirmed() {
if !self.file_is_waiting() {
self.send_current_digest(stream).await?;
self.set_file_is_waiting(true);
}
return Ok(None);
}
const BUF_SIZE: usize = 128 * 1024;
let mut buf: Vec<u8> = vec![0; BUF_SIZE];
let mut compressed = false;
let mut offset: usize = 0;
loop {
match self
.file
.as_mut()
.ok_or(anyhow!("file is None"))?
.read(&mut buf[offset..])
.await
{
Err(err) => {
self.file_num += 1;
self.file = None;
self.file_confirmed = false;
self.file_is_waiting = false;
return Err(err.into());
}
Ok(n) => {
offset += n;
if n == 0 || offset == BUF_SIZE {
break;
}
}
}
}
unsafe { buf.set_len(offset) };
if offset == 0 {
self.file_num += 1;
self.file = None;
self.file_confirmed = false;
self.file_is_waiting = false;
} else {
self.finished_size += offset as u64;
if !is_compressed_file(name) {
let tmp = compress(&buf);
if tmp.len() < buf.len() {
buf = tmp;
compressed = true;
}
}
self.transferred += buf.len() as u64;
}
Ok(Some(FileTransferBlock {
id: self.id,
file_num: file_num as _,
data: buf.into(),
compressed,
..Default::default()
}))
}
async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
let mut msg = Message::new();
let mut resp = FileResponse::new();
let meta = self
.file
.as_ref()
.ok_or(anyhow!("file is None"))?
.metadata()
.await?;
let last_modified = meta
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs();
resp.set_digest(FileTransferDigest {
id: self.id,
file_num: self.file_num,
last_modified,
file_size: meta.len(),
..Default::default()
});
msg.set_file_response(resp);
stream.send(&msg).await?;
log::info!(
"id: {}, file_num:{}, digest message is sent. waiting for confirm. msg: {:?}",
self.id,
self.file_num,
msg
);
Ok(())
}
pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option<bool>) {
self.default_overwrite_strategy = overwrite_strategy;
}
pub fn default_overwrite_strategy(&self) -> Option<bool> {
self.default_overwrite_strategy
}
pub fn set_file_confirmed(&mut self, file_confirmed: bool) {
log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed);
self.file_confirmed = file_confirmed;
self.file_skipped = false;
}
pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) {
self.file_is_waiting = file_is_waiting;
}
#[inline]
pub fn file_is_waiting(&self) -> bool {
self.file_is_waiting
}
#[inline]
pub fn file_confirmed(&self) -> bool {
self.file_confirmed
}
/// Indicating whether the last file is skipped
#[inline]
pub fn file_skipped(&self) -> bool {
self.file_skipped
}
/// Indicating whether the whole task is skipped
#[inline]
pub fn job_skipped(&self) -> bool {
self.file_skipped() && self.files.len() == 1
}
/// Check whether the job is completed after `read` returns `None`
/// This is a helper function which gives additional lifecycle when the job reads `None`.
/// If returns `true`, it means we can delete the job automatically. `False` otherwise.
///
/// [`Note`]
/// Conditions:
/// 1. Files are not waiting for confirmation by peers.
#[inline]
pub fn job_completed(&self) -> bool {
// has no error, Condition 2
!self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting)
}
/// Get job error message, useful for getting status when job had finished
pub fn job_error(&self) -> Option<String> {
if self.job_skipped() {
return Some("skipped".to_string());
}
None
}
pub fn set_file_skipped(&mut self) -> bool {
log::debug!("skip file {} in job {}", self.file_num, self.id);
self.file.take();
self.set_file_confirmed(false);
self.set_file_is_waiting(false);
self.file_num += 1;
self.file_skipped = true;
true
}
pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool {
if self.file_num() != r.file_num {
log::info!("file num truncated, ignoring");
} else {
match r.union {
Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
if s {
self.set_file_skipped();
} else {
self.set_file_confirmed(true);
}
}
Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
self.set_file_confirmed(true);
}
_ => {}
}
}
true
}
#[inline]
pub fn gen_meta(&self) -> TransferJobMeta {
TransferJobMeta {
id: self.id,
remote: self.remote.to_string(),
to: self.path.to_string_lossy().to_string(),
file_num: self.file_num,
show_hidden: self.show_hidden,
is_remote: self.is_remote,
}
}
}
#[inline]
pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Message {
let mut resp = FileResponse::new();
resp.set_error(FileTransferError {
id,
error: err.to_string(),
file_num,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
let mut resp = FileResponse::new();
resp.set_dir(FileDirectory {
id,
path,
entries: files,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_block(block: FileTransferBlock) -> Message {
let mut resp = FileResponse::new();
resp.set_block(block);
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
let mut msg_out = Message::new();
let mut action = FileAction::new();
action.set_send_confirm(r);
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_receive(
id: i32,
path: String,
file_num: i32,
files: Vec<FileEntry>,
total_size: u64,
) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files,
file_num,
total_size,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message {
log::info!("new send: {},id : {}", path, id);
let mut action = FileAction::new();
action.set_send(FileTransferSendRequest {
id,
path,
include_hidden,
file_num,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_done(id: i32, file_num: i32) -> Message {
let mut resp = FileResponse::new();
resp.set_done(FileTransferDone {
id,
file_num,
..Default::default()
});
let mut msg_out = Message::new();
msg_out.set_file_response(resp);
msg_out
}
#[inline]
pub fn remove_job(id: i32, jobs: &mut Vec<TransferJob>) {
*jobs = jobs.drain(0..).filter(|x| x.id() != id).collect();
}
#[inline]
pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
jobs.iter_mut().find(|x| x.id() == id)
}
#[inline]
pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
jobs.iter().find(|x| x.id() == id)
}
pub async fn handle_read_jobs(
jobs: &mut Vec<TransferJob>,
stream: &mut crate::Stream,
) -> ResultType<String> {
let mut job_log = Default::default();
let mut finished = Vec::new();
for job in jobs.iter_mut() {
if job.is_last_job {
continue;
}
match job.read(stream).await {
Err(err) => {
stream
.send(&new_error(job.id(), err, job.file_num()))
.await?;
}
Ok(Some(block)) => {
stream.send(&new_block(block)).await?;
}
Ok(None) => {
if job.job_completed() {
job_log = serialize_transfer_job(job, true, false, "");
finished.push(job.id());
match job.job_error() {
Some(err) => {
job_log = serialize_transfer_job(job, false, false, &err);
stream
.send(&new_error(job.id(), err, job.file_num()))
.await?
}
None => stream.send(&new_done(job.id(), job.file_num())).await?,
}
} else {
// waiting confirmation.
}
}
}
}
for id in finished {
remove_job(id, jobs);
}
Ok(job_log)
}
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
let fd = read_dir(path, true)?;
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::Dir) => {
remove_all_empty_dir(&path.join(&entry.name)).ok();
}
Ok(FileType::DirLink) | Ok(FileType::FileLink) => {
std::fs::remove_file(path.join(&entry.name)).ok();
}
_ => {}
}
}
std::fs::remove_dir(path).ok();
Ok(())
}
#[inline]
pub fn remove_file(file: &str) -> ResultType<()> {
std::fs::remove_file(get_path(file))?;
Ok(())
}
#[inline]
pub fn create_dir(dir: &str) -> ResultType<()> {
std::fs::create_dir_all(get_path(dir))?;
Ok(())
}
#[inline]
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
for entry in entries {
entry.name = entry.name.replace('\\', "/");
}
}
pub enum DigestCheckResult {
IsSame,
NeedConfirm(FileTransferDigest),
NoSuchFile,
}
#[inline]
pub fn is_write_need_confirmation(
file_path: &str,
digest: &FileTransferDigest,
) -> ResultType<DigestCheckResult> {
let path = Path::new(file_path);
if path.exists() && path.is_file() {
let metadata = std::fs::metadata(path)?;
let modified_time = metadata.modified()?;
let remote_mt = Duration::from_secs(digest.last_modified);
let local_mt = modified_time.duration_since(UNIX_EPOCH)?;
// [Note]
// We decide to give the decision whether to override the existing file to users,
// which obey the behavior of the file manager in our system.
let mut is_identical = false;
if remote_mt == local_mt && digest.file_size == metadata.len() {
is_identical = true;
}
Ok(DigestCheckResult::NeedConfirm(FileTransferDigest {
id: digest.id,
file_num: digest.file_num,
last_modified: local_mt.as_secs(),
file_size: metadata.len(),
is_identical,
..Default::default()
}))
} else {
Ok(DigestCheckResult::NoSuchFile)
}
}
pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
let mut v = vec![];
for job in jobs {
let value = serde_json::to_value(job).unwrap_or_default();
v.push(value);
}
serde_json::to_string(&v).unwrap_or_default()
}
pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
let mut value = serde_json::to_value(job).unwrap_or_default();
value["done"] = json!(done);
value["cancel"] = json!(cancel);
value["error"] = json!(error);
serde_json::to_string(&value).unwrap_or_default()
}
@@ -0,0 +1,39 @@
use std::{fmt, slice::Iter, str::FromStr};
use crate::protos::message::KeyboardMode;
impl fmt::Display for KeyboardMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
KeyboardMode::Legacy => write!(f, "legacy"),
KeyboardMode::Map => write!(f, "map"),
KeyboardMode::Translate => write!(f, "translate"),
KeyboardMode::Auto => write!(f, "auto"),
}
}
}
impl FromStr for KeyboardMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"legacy" => Ok(KeyboardMode::Legacy),
"map" => Ok(KeyboardMode::Map),
"translate" => Ok(KeyboardMode::Translate),
"auto" => Ok(KeyboardMode::Auto),
_ => Err(()),
}
}
}
impl KeyboardMode {
pub fn iter() -> Iter<'static, KeyboardMode> {
static KEYBOARD_MODES: [KeyboardMode; 4] = [
KeyboardMode::Legacy,
KeyboardMode::Map,
KeyboardMode::Translate,
KeyboardMode::Auto,
];
KEYBOARD_MODES.iter()
}
}
@@ -0,0 +1,464 @@
pub mod compress;
pub mod platform;
pub mod protos;
pub use bytes;
use config::Config;
pub use futures;
pub use protobuf;
pub use protos::message as message_proto;
pub use protos::rendezvous as rendezvous_proto;
use std::{
fs::File,
io::{self, BufRead},
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
path::Path,
time::{self, SystemTime, UNIX_EPOCH},
};
pub use tokio;
pub use tokio_util;
pub mod socket_client;
pub mod tcp;
pub mod udp;
pub use env_logger;
pub use log;
pub mod bytes_codec;
#[cfg(feature = "quic")]
pub mod quic;
pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address;
pub use rand;
pub use regex;
pub use sodiumoxide;
pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
pub mod password_security;
pub use chrono;
pub use directories_next;
pub use libc;
pub mod keyboard;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use dlopen;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use machine_uid;
pub use sysinfo;
pub use toml;
pub use uuid;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
#[cfg(not(feature = "quic"))]
pub type Stream = tcp::FramedStream;
pub type SessionID = uuid::Uuid;
#[inline]
pub async fn sleep(sec: f32) {
tokio::time::sleep(time::Duration::from_secs_f32(sec)).await;
}
#[macro_export]
macro_rules! allow_err {
($e:expr) => {
if let Err(err) = $e {
log::debug!(
"{:?}, {}:{}:{}:{}",
err,
module_path!(),
file!(),
line!(),
column!()
);
} else {
}
};
($e:expr, $($arg:tt)*) => {
if let Err(err) = $e {
log::debug!(
"{:?}, {}, {}:{}:{}:{}",
err,
format_args!($($arg)*),
module_path!(),
file!(),
line!(),
column!()
);
} else {
}
};
}
#[inline]
pub fn timeout<T: std::future::Future>(ms: u64, future: T) -> tokio::time::Timeout<T> {
tokio::time::timeout(std::time::Duration::from_millis(ms), future)
}
pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
/// Certain router and firewalls scan the packet and if they
/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address
pub struct AddrMangle();
#[inline]
pub fn try_into_v4(addr: SocketAddr) -> SocketAddr {
match addr {
SocketAddr::V6(v6) if !addr.ip().is_loopback() => {
if let Some(v4) = v6.ip().to_ipv4() {
SocketAddr::new(IpAddr::V4(v4), addr.port())
} else {
addr
}
}
_ => addr,
}
}
impl AddrMangle {
pub fn encode(addr: SocketAddr) -> Vec<u8> {
// not work with [:1]:<port>
let addr = try_into_v4(addr);
match addr {
SocketAddr::V4(addr_v4) => {
let tm = (SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO)
.as_micros() as u32) as u128;
let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
let port = addr.port() as u128;
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
let bytes = v.to_le_bytes();
let mut n_padding = 0;
for i in bytes.iter().rev() {
if i == &0u8 {
n_padding += 1;
} else {
break;
}
}
bytes[..(16 - n_padding)].to_vec()
}
SocketAddr::V6(addr_v6) => {
let mut x = addr_v6.ip().octets().to_vec();
let port: [u8; 2] = addr_v6.port().to_le_bytes();
x.push(port[0]);
x.push(port[1]);
x
}
}
}
pub fn decode(bytes: &[u8]) -> SocketAddr {
use std::convert::TryInto;
if bytes.len() > 16 {
if bytes.len() != 18 {
return Config::get_any_listen_addr(false);
}
let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default();
let port = u16::from_le_bytes(tmp);
let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default();
let ip = std::net::Ipv6Addr::from(tmp);
return SocketAddr::new(IpAddr::V6(ip), port);
}
let mut padded = [0u8; 16];
padded[..bytes.len()].copy_from_slice(bytes);
let number = u128::from_le_bytes(padded);
let tm = (number >> 17) & (u32::max_value() as u128);
let ip = (((number >> 49) - tm) as u32).to_le_bytes();
let port = (number & 0xFFFFFF) - (tm & 0xFFFF);
SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
port as u16,
))
}
}
pub fn get_version_from_url(url: &str) -> String {
let n = url.chars().count();
let a = url.chars().rev().position(|x| x == '-');
if let Some(a) = a {
let b = url.chars().rev().position(|x| x == '.');
if let Some(b) = b {
if a > b {
if url
.chars()
.skip(n - b)
.collect::<String>()
.parse::<i32>()
.is_ok()
{
return url.chars().skip(n - a).collect();
} else {
return url.chars().skip(n - a).take(a - b - 1).collect();
}
} else {
return url.chars().skip(n - a).collect();
}
}
}
"".to_owned()
}
pub fn gen_version() {
println!("cargo:rerun-if-changed=Cargo.toml");
use std::io::prelude::*;
let mut file = File::create("./src/version.rs").unwrap();
for line in read_lines("Cargo.toml").unwrap().flatten() {
let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect();
if ab.len() == 2 && ab[0] == "version" {
file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
.ok();
break;
}
}
// generate build date
let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
file.write_all(
format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(),
)
.ok();
file.sync_all().ok();
}
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
pub fn is_valid_custom_id(id: &str) -> bool {
regex::Regex::new(r"^[a-zA-Z]\w{5,15}$")
.unwrap()
.is_match(id)
}
pub fn get_version_number(v: &str) -> i64 {
let mut n = 0;
for x in v.split('.') {
n = n * 1000 + x.parse::<i64>().unwrap_or(0);
}
n
}
pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
std::fs::metadata(path)
.map(|m| m.modified().unwrap_or(UNIX_EPOCH))
.unwrap_or(UNIX_EPOCH)
}
pub fn get_created_time(path: &std::path::Path) -> SystemTime {
std::fs::metadata(path)
.map(|m| m.created().unwrap_or(UNIX_EPOCH))
.unwrap_or(UNIX_EPOCH)
}
pub fn get_exe_time() -> SystemTime {
std::env::current_exe().map_or(UNIX_EPOCH, |path| {
let m = get_modified_time(&path);
let c = get_created_time(&path);
if m > c {
m
} else {
c
}
})
}
pub fn get_uuid() -> Vec<u8> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(id) = machine_uid::get() {
return id.into();
}
Config::get_key_pair().1
}
#[inline]
pub fn get_time() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0) as _
}
#[inline]
pub fn is_ipv4_str(id: &str) -> bool {
if let Ok(reg) = regex::Regex::new(
r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$",
) {
reg.is_match(id)
} else {
false
}
}
#[inline]
pub fn is_ipv6_str(id: &str) -> bool {
if let Ok(reg) = regex::Regex::new(
r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$",
) {
reg.is_match(id)
} else {
false
}
}
#[inline]
pub fn is_ip_str(id: &str) -> bool {
is_ipv4_str(id) || is_ipv6_str(id)
}
#[inline]
pub fn is_domain_port_str(id: &str) -> bool {
// modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname.
// according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700,
// there is no digits in TLD, and length is 2~63.
if let Ok(reg) = regex::Regex::new(
r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$",
) {
reg.is_match(id)
} else {
false
}
}
pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHandle> {
#[cfg(debug_assertions)]
{
use env_logger::*;
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
None
}
#[cfg(not(debug_assertions))]
{
// https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write
// though async logger more efficient, but it also causes more problems, disable it for now
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
let mut path = config::Config::log_path();
if !_name.is_empty() {
path.push(_name);
}
use flexi_logger::*;
if let Ok(x) = Logger::try_with_env_or_str("debug") {
logger_holder = x
.log_to_file(FileSpec::default().directory(path))
.write_mode(if _is_async {
WriteMode::Async
} else {
WriteMode::Direct
})
.format(opt_format)
.rotate(
Criterion::Age(Age::Day),
Naming::Timestamps,
Cleanup::KeepLogFiles(6),
)
.start()
.ok();
}
logger_holder
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_mangle() {
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
let addr = "[2001:db8::1]:8080".parse::<SocketAddr>().unwrap();
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
let addr = "[2001:db8:ff::1111]:80".parse::<SocketAddr>().unwrap();
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
}
#[test]
fn test_allow_err() {
allow_err!(Err("test err") as Result<(), &str>);
allow_err!(
Err("test err with msg") as Result<(), &str>,
"prompt {}",
"failed"
);
}
#[test]
fn test_ipv6() {
assert!(is_ipv6_str("1:2:3"));
assert!(is_ipv6_str("[ab:2:3]:12"));
assert!(is_ipv6_str("[ABEF:2a:3]:12"));
assert!(!is_ipv6_str("[ABEG:2a:3]:12"));
assert!(!is_ipv6_str("1[ab:2:3]:12"));
assert!(!is_ipv6_str("1.1.1.1"));
assert!(is_ip_str("1.1.1.1"));
assert!(!is_ipv6_str("1:2:"));
assert!(is_ipv6_str("1:2::0"));
assert!(is_ipv6_str("[1:2::0]:1"));
assert!(!is_ipv6_str("[1:2::0]:"));
assert!(!is_ipv6_str("1:2::0]:1"));
}
#[test]
fn test_ipv4() {
assert!(is_ipv4_str("1.2.3.4"));
assert!(is_ipv4_str("1.2.3.4:90"));
assert!(is_ipv4_str("192.168.0.1"));
assert!(is_ipv4_str("0.0.0.0"));
assert!(is_ipv4_str("255.255.255.255"));
assert!(!is_ipv4_str("256.0.0.0"));
assert!(!is_ipv4_str("256.256.256.256"));
assert!(!is_ipv4_str("1:2:"));
assert!(!is_ipv4_str("192.168.0.256"));
assert!(!is_ipv4_str("192.168.0.1/24"));
assert!(!is_ipv4_str("192.168.0."));
assert!(!is_ipv4_str("192.168..1"));
}
#[test]
fn test_hostname_port() {
assert!(!is_domain_port_str("a:12"));
assert!(!is_domain_port_str("a.b.c:12"));
assert!(is_domain_port_str("test.com:12"));
assert!(is_domain_port_str("test-UPPER.com:12"));
assert!(is_domain_port_str("some-other.domain.com:12"));
assert!(!is_domain_port_str("under_score:12"));
assert!(!is_domain_port_str("a@bc:12"));
assert!(!is_domain_port_str("1.1.1.1:12"));
assert!(!is_domain_port_str("1.2.3:12"));
assert!(!is_domain_port_str("1.2.3.45:12"));
assert!(!is_domain_port_str("a.b.c:123456"));
assert!(!is_domain_port_str("---:12"));
assert!(!is_domain_port_str(".:12"));
// todo: should we also check for these edge cases?
// out-of-range port
assert!(is_domain_port_str("test.com:0"));
assert!(is_domain_port_str("test.com:98989"));
}
#[test]
fn test_mangle2() {
let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap();
let addr_v4 = "127.0.0.1:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4);
assert_eq!(
AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())),
addr_v4
);
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4);
let addr_v6 = "[ef::fe]:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
let addr_v6 = "[::1]:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
}
}
@@ -0,0 +1,295 @@
use crate::config::Config;
use sodiumoxide::base64;
use std::sync::{Arc, RwLock};
lazy_static::lazy_static! {
pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VerificationMethod {
OnlyUseTemporaryPassword,
OnlyUsePermanentPassword,
UseBothPasswords,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApproveMode {
Both,
Password,
Click,
}
// Should only be called in server
pub fn update_temporary_password() {
*TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
}
// Should only be called in server
pub fn temporary_password() -> String {
TEMPORARY_PASSWORD.read().unwrap().clone()
}
fn verification_method() -> VerificationMethod {
let method = Config::get_option("verification-method");
if method == "use-temporary-password" {
VerificationMethod::OnlyUseTemporaryPassword
} else if method == "use-permanent-password" {
VerificationMethod::OnlyUsePermanentPassword
} else {
VerificationMethod::UseBothPasswords // default
}
}
pub fn temporary_password_length() -> usize {
let length = Config::get_option("temporary-password-length");
if length == "8" {
8
} else if length == "10" {
10
} else {
6 // default
}
}
pub fn temporary_enabled() -> bool {
verification_method() != VerificationMethod::OnlyUsePermanentPassword
}
pub fn permanent_enabled() -> bool {
verification_method() != VerificationMethod::OnlyUseTemporaryPassword
}
pub fn has_valid_password() -> bool {
temporary_enabled() && !temporary_password().is_empty()
|| permanent_enabled() && !Config::get_permanent_password().is_empty()
}
pub fn approve_mode() -> ApproveMode {
let mode = Config::get_option("approve-mode");
if mode == "password" {
ApproveMode::Password
} else if mode == "click" {
ApproveMode::Click
} else {
ApproveMode::Both
}
}
pub fn hide_cm() -> bool {
approve_mode() == ApproveMode::Password
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
&& !Config::get_option("allow-hide-cm").is_empty()
}
const VERSION_LEN: usize = 2;
pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String {
if decrypt_str_or_original(s, version).1 {
log::error!("Duplicate encryption!");
return s.to_owned();
}
if s.bytes().len() > max_len {
return String::default();
}
if version == "00" {
if let Ok(s) = encrypt(s.as_bytes(), max_len) {
return version.to_owned() + &s;
}
}
s.to_owned()
}
// String: password
// bool: whether decryption is successful
// bool: whether should store to re-encrypt when load
// note: s.len() return length in bytes, s.chars().count() return char count
// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
if s.len() > VERSION_LEN {
if s.starts_with("00") {
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
return (
String::from_utf8_lossy(&v).to_string(),
true,
"00" != current_version,
);
}
}
}
(s.to_owned(), false, !s.is_empty())
}
pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u8> {
if decrypt_vec_or_original(v, version).1 {
log::error!("Duplicate encryption!");
return v.to_owned();
}
if v.len() > max_len {
return vec![];
}
if version == "00" {
if let Ok(s) = encrypt(v, max_len) {
let mut version = version.to_owned().into_bytes();
version.append(&mut s.into_bytes());
return version;
}
}
v.to_owned()
}
// Vec<u8>: password
// bool: whether decryption is successful
// bool: whether should store to re-encrypt when load
pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, bool, bool) {
if v.len() > VERSION_LEN {
let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
if version == "00" {
if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
return (v, true, version != current_version);
}
}
}
(v.to_owned(), false, !v.is_empty())
}
fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> {
if !v.is_empty() && v.len() <= max_len {
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
} else {
Err(())
}
}
fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
if !v.is_empty() {
base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
} else {
Err(())
}
}
pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
use sodiumoxide::crypto::secretbox;
use std::convert::TryInto;
let mut keybuf = crate::get_uuid();
keybuf.resize(secretbox::KEYBYTES, 0);
let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
if encrypt {
Ok(secretbox::seal(data, &nonce, &key))
} else {
secretbox::open(data, &nonce, &key)
}
}
mod test {
#[test]
fn test() {
use super::*;
use rand::{thread_rng, Rng};
use std::time::Instant;
let version = "00";
let max_len = 128;
println!("test str");
let data = "1ü1111";
let encrypted = encrypt_str_or_original(data, version, max_len);
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
println!("data: {data}");
println!("encrypted: {encrypted}");
println!("decrypted: {decrypted}");
assert_eq!(data, decrypted);
assert_eq!(version, &encrypted[..2]);
assert!(succ);
assert!(!store);
let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
assert!(store);
assert!(!decrypt_str_or_original(&decrypted, version).1);
assert_eq!(
encrypt_str_or_original(&encrypted, version, max_len),
encrypted
);
println!("test vec");
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
let encrypted = encrypt_vec_or_original(&data, version, max_len);
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
println!("data: {data:?}");
println!("encrypted: {encrypted:?}");
println!("decrypted: {decrypted:?}");
assert_eq!(data, decrypted);
assert_eq!(version.as_bytes(), &encrypted[..2]);
assert!(!store);
assert!(succ);
let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
assert!(store);
assert!(!decrypt_vec_or_original(&decrypted, version).1);
assert_eq!(
encrypt_vec_or_original(&encrypted, version, max_len),
encrypted
);
println!("test original");
let data = version.to_string() + "Hello World";
let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
assert_eq!(data, decrypted);
assert!(store);
assert!(!succ);
let verbytes = version.as_bytes();
let data: Vec<u8> = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6];
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
assert_eq!(data, decrypted);
assert!(store);
assert!(!succ);
let (_, succ, store) = decrypt_str_or_original("", version);
assert!(!store);
assert!(!succ);
let (_, succ, store) = decrypt_vec_or_original(&[], version);
assert!(!store);
assert!(!succ);
let data = "1ü1111";
assert_eq!(decrypt_str_or_original(data, version).0, data);
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
assert_eq!(decrypt_vec_or_original(&data, version).0, data);
println!("test speed");
let test_speed = |len: usize, name: &str| {
let mut data: Vec<u8> = vec![];
let mut rng = thread_rng();
for _ in 0..len {
data.push(rng.gen_range(0..255));
}
let start: Instant = Instant::now();
let encrypted = encrypt_vec_or_original(&data, version, len);
assert_ne!(data, decrypted);
let t1 = start.elapsed();
let start = Instant::now();
let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version);
let t2 = start.elapsed();
assert_eq!(data, decrypted);
println!("{name}");
println!("encrypt:{:?}, decrypt:{:?}", t1, t2);
let start: Instant = Instant::now();
let encrypted = base64::encode(&data, base64::Variant::Original);
let t1 = start.elapsed();
let start = Instant::now();
let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap();
let t2 = start.elapsed();
assert_eq!(data, decrypted);
println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,);
};
test_speed(128, "128");
test_speed(1024, "1k");
test_speed(1024 * 1024, "1M");
test_speed(10 * 1024 * 1024, "10M");
test_speed(100 * 1024 * 1024, "100M");
}
}
@@ -0,0 +1,259 @@
use crate::ResultType;
use std::{collections::HashMap, process::Command};
lazy_static::lazy_static! {
pub static ref DISTRO: Distro = Distro::new();
}
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
pub const DISPLAY_SERVER_X11: &str = "x11";
pub struct Distro {
pub name: String,
pub version_id: String,
}
impl Distro {
fn new() -> Self {
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string();
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string();
Self { name, version_id }
}
}
#[inline]
pub fn is_gdm_user(username: &str) -> bool {
username == "gdm"
// || username == "lightgdm"
}
#[inline]
pub fn is_desktop_wayland() -> bool {
get_display_server() == DISPLAY_SERVER_WAYLAND
}
#[inline]
pub fn is_x11_or_headless() -> bool {
!is_desktop_wayland()
}
// -1
const INVALID_SESSION: &str = "4294967295";
pub fn get_display_server() -> String {
let mut session = get_values_of_seat0(&[0])[0].clone();
if session.is_empty() {
// loginctl has not given the expected output. try something else.
if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
// could also execute "cat /proc/self/sessionid"
session = sid;
}
if session.is_empty() {
session = run_cmds("cat /proc/self/sessionid").unwrap_or_default();
if session == INVALID_SESSION {
session = "".to_owned();
}
}
}
if session.is_empty() {
"".to_owned()
} else {
get_display_server_of_session(&session)
}
}
pub fn get_display_server_of_session(session: &str) -> String {
let mut display_server = if let Ok(output) =
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
// Check session type of the session
{
let display_server = String::from_utf8_lossy(&output.stdout)
.replace("Type=", "")
.trim_end()
.into();
if display_server == "tty" {
// If the type is tty...
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
// Get the tty number
{
let tty: String = String::from_utf8_lossy(&output.stdout)
.replace("TTY=", "")
.trim_end()
.into();
if let Ok(xorg_results) = run_cmds(&format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
// And check if Xorg is running on that tty
{
if xorg_results.trim_end() != "" {
// If it is, manually return "x11", otherwise return tty
return "x11".to_owned();
}
}
}
}
display_server
} else {
"".to_owned()
};
if display_server.is_empty() || display_server == "tty" {
// loginctl has not given the expected output. try something else.
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
display_server = sestype;
}
}
if display_server == "" {
display_server = "x11".to_owned();
}
display_server.to_lowercase()
}
#[inline]
fn line_values(indices: &[usize], line: &str) -> Vec<String> {
indices
.into_iter()
.map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned())
.collect::<Vec<String>>()
}
#[inline]
pub fn get_values_of_seat0(indices: &[usize]) -> Vec<String> {
_get_values_of_seat0(indices, true)
}
#[inline]
pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
_get_values_of_seat0(indices, false)
}
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
if let Ok(output) = run_loginctl(None) {
for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains("seat0") {
if let Some(sid) = line.split_whitespace().next() {
if is_active(sid) {
if ignore_gdm_wayland {
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
&& get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND
{
continue;
}
}
return line_values(indices, line);
}
}
}
}
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some(sid) = line.split_whitespace().next() {
if is_active(sid) {
let d = get_display_server_of_session(sid);
if ignore_gdm_wayland {
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
&& d == DISPLAY_SERVER_WAYLAND
{
continue;
}
}
if d == "tty" {
continue;
}
return line_values(indices, line);
}
}
}
}
line_values(indices, "")
}
pub fn is_active(sid: &str) -> bool {
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
String::from_utf8_lossy(&output.stdout).contains("active")
} else {
false
}
}
pub fn is_active_and_seat0(sid: &str) -> bool {
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
String::from_utf8_lossy(&output.stdout).contains("State=active")
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
} else {
false
}
}
pub fn run_cmds(cmds: &str) -> ResultType<String> {
let output = std::process::Command::new("sh")
.args(vec!["-c", cmds])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(not(feature = "flatpak"))]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
let mut cmd = std::process::Command::new("loginctl");
if let Some(a) = args {
return cmd.args(a).output();
}
cmd.output()
}
#[cfg(feature = "flatpak")]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
let mut l_args = String::from("loginctl");
if let Some(a) = args {
l_args = format!("{} {}", l_args, a.join(" "));
}
std::process::Command::new("flatpak-spawn")
.args(vec![String::from("--host"), l_args])
.output()
}
/// forever: may not work
#[cfg(target_os = "linux")]
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
let cmds: HashMap<&str, Vec<&str>> = HashMap::from([
("notify-send", [title, msg].to_vec()),
(
"zenity",
[
"--info",
"--timeout",
if forever { "0" } else { "3" },
"--title",
title,
"--text",
msg,
]
.to_vec(),
),
("kdialog", ["--title", title, "--msgbox", msg].to_vec()),
(
"xmessage",
[
"-center",
"-timeout",
if forever { "0" } else { "3" },
title,
msg,
]
.to_vec(),
),
]);
for (k, v) in cmds {
if Command::new(k).args(v).spawn().is_ok() {
return Ok(());
}
}
crate::bail!("failed to post system message");
}
@@ -0,0 +1,55 @@
use crate::ResultType;
use osascript;
use serde_derive::{Deserialize, Serialize};
#[derive(Serialize)]
struct AlertParams {
title: String,
message: String,
alert_type: String,
buttons: Vec<String>,
}
#[derive(Deserialize)]
struct AlertResult {
#[serde(rename = "buttonReturned")]
button: String,
}
/// Firstly run the specified app, then alert a dialog. Return the clicked button value.
///
/// # Arguments
///
/// * `app` - The app to execute the script.
/// * `alert_type` - Alert type. . informational, warning, critical
/// * `title` - The alert title.
/// * `message` - The alert message.
/// * `buttons` - The buttons to show.
pub fn alert(
app: String,
alert_type: String,
title: String,
message: String,
buttons: Vec<String>,
) -> ResultType<String> {
let script = osascript::JavaScript::new(&format!(
"
var App = Application('{}');
App.includeStandardAdditions = true;
return App.displayAlert($params.title, {{
message: $params.message,
'as': $params.alert_type,
buttons: $params.buttons,
}});
",
app
));
let result: AlertResult = script.execute_with_params(AlertParams {
title,
message,
alert_type,
buttons,
})?;
Ok(result.button)
}
@@ -0,0 +1,81 @@
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(not(debug_assertions))]
use crate::{config::Config, log};
#[cfg(not(debug_assertions))]
use std::process::exit;
#[cfg(not(debug_assertions))]
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
#[cfg(not(debug_assertions))]
extern "C" fn breakdown_signal_handler(sig: i32) {
let mut stack = vec![];
backtrace::trace(|frame| {
backtrace::resolve_frame(frame, |symbol| {
if let Some(name) = symbol.name() {
stack.push(name.to_string());
}
});
true // keep going to the next frame
});
let mut info = String::default();
if stack.iter().any(|s| {
s.contains(&"nouveau_pushbuf_kick")
|| s.to_lowercase().contains("nvidia")
|| s.contains("gdk_window_end_draw_frame")
|| s.contains("glGetString")
}) {
Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
info = "Always use software rendering will be set.".to_string();
log::info!("{}", info);
}
if stack.iter().any(|s| {
s.to_lowercase().contains("nvidia")
|| s.to_lowercase().contains("amf")
|| s.to_lowercase().contains("mfx")
|| s.contains("cuProfilerStop")
}) {
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
log::info!("{}", info);
}
log::error!(
"Got signal {} and exit. stack:\n{}",
sig,
stack.join("\n").to_string()
);
if !info.is_empty() {
#[cfg(target_os = "linux")]
linux::system_message(
"RustDesk",
&format!("Got signal {} and exit.{}", sig, info),
true,
)
.ok();
}
unsafe {
if let Some(callback) = &GLOBAL_CALLBACK {
callback()
}
}
exit(0);
}
#[cfg(not(debug_assertions))]
pub fn register_breakdown_handler<T>(callback: T)
where
T: Fn() + 'static,
{
unsafe {
GLOBAL_CALLBACK = Some(Box::new(callback));
libc::signal(libc::SIGSEGV, breakdown_signal_handler as _);
}
}
@@ -0,0 +1,154 @@
use std::{
collections::VecDeque,
os::windows::raw::HANDLE,
sync::{Arc, Mutex},
time::Instant,
};
use winapi::{
shared::minwindef::{DWORD, FALSE},
um::{
handleapi::CloseHandle,
pdh::{
PdhAddCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx,
PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE,
PDH_HCOUNTER, PDH_HQUERY,
},
synchapi::{CreateEventA, WaitForSingleObject},
winbase::{INFINITE, WAIT_OBJECT_0},
},
};
lazy_static::lazy_static! {
static ref CPU_USAGE_ONE_MINUTE: Arc<Mutex<Option<(f64, Instant)>>> = Arc::new(Mutex::new(None));
}
// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
#[repr(transparent)]
pub struct RAIIHandle(pub HANDLE);
impl Drop for RAIIHandle {
fn drop(&mut self) {
// This never gives problem except when running under a debugger.
unsafe { CloseHandle(self.0) };
}
}
#[repr(transparent)]
pub(self) struct RAIIPDHQuery(pub PDH_HQUERY);
impl Drop for RAIIPDHQuery {
fn drop(&mut self) {
unsafe { PdhCloseQuery(self.0) };
}
}
pub fn start_cpu_performance_monitor() {
// Code from:
// https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data
// https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex
// Why value lower than taskManager:
// https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43
// Therefore we should compare with Precess Explorer rather than taskManager
let f = || unsafe {
// load avg or cpu usage, test with prime95.
// Prefer cpu usage because we can get accurate value from Precess Explorer.
// const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0";
const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0";
const SAMPLE_INTERVAL: DWORD = 2; // 2 second
let mut ret;
let mut query: PDH_HQUERY = std::mem::zeroed();
ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query);
if ret != 0 {
log::error!("PdhOpenQueryA failed: 0x{:X}", ret);
return;
}
let _query = RAIIPDHQuery(query);
let mut counter: PDH_HCOUNTER = std::mem::zeroed();
ret = PdhAddCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter);
if ret != 0 {
log::error!("PdhAddCounterA failed: 0x{:X}", ret);
return;
}
ret = PdhCollectQueryData(query);
if ret != 0 {
log::error!("PdhCollectQueryData failed: 0x{:X}", ret);
return;
}
let mut _counter_type: DWORD = 0;
let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed();
let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _);
if event.is_null() {
log::error!("CreateEventA failed");
return;
}
let _event: RAIIHandle = RAIIHandle(event);
ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event);
if ret != 0 {
log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret);
return;
}
let mut queue: VecDeque<f64> = VecDeque::new();
let mut recent_valid: VecDeque<bool> = VecDeque::new();
loop {
// latest one minute
if queue.len() == 31 {
queue.pop_front();
}
if recent_valid.len() == 31 {
recent_valid.pop_front();
}
// allow get value within one minute
if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 {
let sum: f64 = queue.iter().map(|f| f.to_owned()).sum();
let avg = sum / (queue.len() as f64);
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now()));
} else {
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = None;
}
if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) {
recent_valid.push_back(false);
continue;
}
if PdhGetFormattedCounterValue(
counter,
PDH_FMT_DOUBLE,
&mut _counter_type,
&mut counter_value,
) != 0
|| counter_value.CStatus != 0
{
recent_valid.push_back(false);
continue;
}
queue.push_back(counter_value.u.doubleValue().clone());
recent_valid.push_back(true);
}
};
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
std::thread::spawn(f);
});
}
pub fn cpu_uage_one_minute() -> Option<f64> {
let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone();
if let Some((v, instant)) = v {
if instant.elapsed().as_secs() < 30 {
return Some(v);
}
}
None
}
pub fn sync_cpu_usage(cpu_usage: Option<f64>) {
let v = match cpu_usage {
Some(cpu_usage) => Some((cpu_usage, Instant::now())),
None => None,
};
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = v;
log::info!("cpu usage synced: {:?}", cpu_usage);
}
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
@@ -0,0 +1,135 @@
use crate::{allow_err, anyhow::anyhow, ResultType};
use protobuf::Message;
use std::{net::SocketAddr, sync::Arc};
use tokio::{self, stream::StreamExt, sync::mpsc};
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
const SERVER_NAME: &str = "hbb";
type Sender = mpsc::UnboundedSender<Value>;
type Receiver = mpsc::UnboundedReceiver<Value>;
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
let mut transport_config = quinn::TransportConfig::default();
transport_config.stream_window_uni(0);
let mut server_config = quinn::ServerConfig::default();
server_config.transport = Arc::new(transport_config);
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
server_config.protocols(QUIC_HBB);
// server_config.enable_keylog();
// server_config.use_stateless_retry(true);
let mut endpoint = quinn::Endpoint::builder();
endpoint.listen(server_config.build());
let (end, incoming) = endpoint.with_socket(socket)?;
Ok((Server { incoming }, end.local_addr()?))
}
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
let mut endpoint = quinn::Endpoint::builder();
let mut client_config = quinn::ClientConfigBuilder::default();
client_config.protocols(QUIC_HBB);
//client_config.enable_keylog();
endpoint.default_client_config(client_config.build());
let (endpoint, _) = endpoint.bind(local_addr)?;
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
Connection::new_for_client(new_conn.connection).await
}
pub struct Server {
incoming: quinn::Incoming,
}
impl Server {
#[inline]
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
Connection::new_for_server(&mut self.incoming).await
}
}
pub struct Connection {
conn: quinn::Connection,
tx: quinn::SendStream,
rx: Receiver,
}
type Value = ResultType<Vec<u8>>;
impl Connection {
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
if let Some(conn) = incoming.next().await {
let quinn::NewConnection {
connection: conn,
// uni_streams,
mut bi_streams,
..
} = conn.await?;
let (tx, rx) = mpsc::unbounded_channel::<Value>();
tokio::spawn(async move {
loop {
let stream = bi_streams.next().await;
if let Some(stream) = stream {
let stream = match stream {
Err(e) => {
tx.send(Err(e.into())).ok();
break;
}
Ok(s) => s,
};
let cloned = tx.clone();
tokio::spawn(async move {
allow_err!(handle_request(stream.1, cloned).await);
});
} else {
tx.send(Err(anyhow!("Reset by the peer"))).ok();
break;
}
}
log::info!("Exit connection outer loop");
});
let tx = conn.open_uni().await?;
Ok(Some(Self { conn, tx, rx }))
} else {
Ok(None)
}
}
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
let (tx, rx_quic) = conn.open_bi().await?;
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
tokio::spawn(async move {
allow_err!(handle_request(rx_quic, tx_mpsc).await);
});
Ok(Self { conn, tx, rx })
}
#[inline]
pub async fn next(&mut self) -> Option<Value> {
// None is returned when all Sender halves have dropped,
// indicating that no further values can be sent on the channel.
self.rx.recv().await
}
#[inline]
pub fn remote_address(&self) -> SocketAddr {
self.conn.remote_address()
}
#[inline]
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
self.tx.write_all(bytes).await?;
Ok(())
}
#[inline]
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
match msg.write_to_bytes() {
Ok(bytes) => self.send_raw(&bytes).await?,
err => allow_err!(err),
}
Ok(())
}
}
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
Ok(())
}
@@ -0,0 +1,281 @@
use crate::{
config::{Config, NetworkType},
tcp::FramedStream,
udp::FramedSocket,
ResultType,
};
use anyhow::Context;
use std::net::SocketAddr;
use tokio::net::ToSocketAddrs;
use tokio_socks::{IntoTargetAddr, TargetAddr};
#[inline]
pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
if host.starts_with('[') {
return host;
}
return format!("[{host}]:{port}");
}
if !host.contains(':') {
return format!("{host}:{port}");
}
host
}
#[inline]
pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
if host.starts_with('[') {
let tmp: Vec<&str> = host.split("]:").collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
if port > 0 {
return format!("{}]:{}", tmp[0], port + offset);
}
}
}
} else if host.contains(':') {
let tmp: Vec<&str> = host.split(':').collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
if port > 0 {
return format!("{}:{}", tmp[0], port + offset);
}
}
}
host
}
pub fn test_if_valid_server(host: &str) -> String {
let host = check_port(host, 0);
use std::net::ToSocketAddrs;
match Config::get_network_type() {
NetworkType::Direct => match host.to_socket_addrs() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
NetworkType::ProxySocks => match &host.into_target_addr() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
}
}
pub trait IsResolvedSocketAddr {
fn resolve(&self) -> Option<&SocketAddr>;
}
impl IsResolvedSocketAddr for SocketAddr {
fn resolve(&self) -> Option<&SocketAddr> {
Some(self)
}
}
impl IsResolvedSocketAddr for String {
fn resolve(&self) -> Option<&SocketAddr> {
None
}
}
impl IsResolvedSocketAddr for &str {
fn resolve(&self) -> Option<&SocketAddr> {
None
}
}
#[inline]
pub async fn connect_tcp<
't,
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
>(
target: T,
ms_timeout: u64,
) -> ResultType<FramedStream> {
connect_tcp_local(target, None, ms_timeout).await
}
pub async fn connect_tcp_local<
't,
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
>(
target: T,
local: Option<SocketAddr>,
ms_timeout: u64,
) -> ResultType<FramedStream> {
if let Some(conf) = Config::get_socks() {
return FramedStream::connect(
conf.proxy.as_str(),
target,
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
.await;
}
if let Some(target) = target.resolve() {
if let Some(local) = local {
if local.is_ipv6() && target.is_ipv4() {
let target = query_nip_io(target).await?;
return FramedStream::new(target, Some(local), ms_timeout).await;
}
}
}
FramedStream::new(target, local, ms_timeout).await
}
#[inline]
pub fn is_ipv4(target: &TargetAddr<'_>) -> bool {
match target {
TargetAddr::Ip(addr) => addr.is_ipv4(),
_ => true,
}
}
#[inline]
pub async fn query_nip_io(addr: &SocketAddr) -> ResultType<SocketAddr> {
tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port()))
.await?
.find(|x| x.is_ipv6())
.context("Failed to get ipv6 from nip.io")
}
#[inline]
pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
if !ipv4 && crate::is_ipv4_str(&addr) {
if let Some(ip) = addr.split(':').next() {
return addr.replace(ip, &format!("{ip}.nip.io"));
}
}
addr
}
async fn test_target(target: &str) -> ResultType<SocketAddr> {
if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await {
if let Ok(addr) = s.peer_addr() {
return Ok(addr);
}
}
tokio::net::lookup_host(target)
.await?
.next()
.context(format!("Failed to look up host for {target}"))
}
#[inline]
pub async fn new_udp_for(
target: &str,
ms_timeout: u64,
) -> ResultType<(FramedSocket, TargetAddr<'static>)> {
let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() {
let addr = test_target(target).await?;
(addr.is_ipv4(), addr.into_target_addr()?)
} else {
(true, target.into_target_addr()?)
};
Ok((
new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?,
target.to_owned(),
))
}
async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
match Config::get_socks() {
None => Ok(FramedSocket::new(local).await?),
Some(conf) => {
let socket = FramedSocket::new_proxy(
conf.proxy.as_str(),
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
.await?;
Ok(socket)
}
}
}
pub async fn rebind_udp_for(
target: &str,
) -> ResultType<Option<(FramedSocket, TargetAddr<'static>)>> {
if Config::get_network_type() != NetworkType::Direct {
return Ok(None);
}
let addr = test_target(target).await?;
let v4 = addr.is_ipv4();
Ok(Some((
FramedSocket::new(Config::get_any_listen_addr(v4)).await?,
addr.into_target_addr()?.to_owned(),
)))
}
#[cfg(test)]
mod tests {
use std::net::ToSocketAddrs;
use super::*;
#[test]
fn test_nat64() {
test_nat64_async();
}
#[tokio::main(flavor = "current_thread")]
async fn test_nat64_async() {
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1");
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io");
assert_eq!(
ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false),
"1.1.1.1.nip.io:8080"
);
assert_eq!(
ipv4_to_ipv6("rustdesk.com".to_owned(), false),
"rustdesk.com"
);
if ("rustdesk.com:80")
.to_socket_addrs()
.unwrap()
.next()
.unwrap()
.is_ipv6()
{
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap())
.await
.unwrap()
.is_ipv6());
return;
}
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err());
}
#[test]
fn test_test_if_valid_server() {
assert!(!test_if_valid_server("a").is_empty());
// on Linux, "1" is resolved to "0.0.0.1"
assert!(test_if_valid_server("1.1.1.1").is_empty());
assert!(test_if_valid_server("1.1.1.1:1").is_empty());
}
#[test]
fn test_check_port() {
assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12");
assert_eq!(check_port("1:2", 32), "[1:2]:32");
assert_eq!(check_port("z1:2", 32), "z1:2");
assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32");
assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32");
assert_eq!(check_port("test.com:32", 0), "test.com:32");
assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13");
assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13");
assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4");
assert_eq!(increase_port("test.com", 1), "test.com");
assert_eq!(increase_port("test.com:13", 4), "test.com:17");
assert_eq!(increase_port("1:13", 4), "1:13");
assert_eq!(increase_port("22:1:13", 4), "22:1:13");
assert_eq!(increase_port("z1:2", 1), "z1:3");
}
}
@@ -0,0 +1,372 @@
use crate::{bail, bytes_codec::BytesCodec, ResultType};
use anyhow::Context as AnyhowCtx;
use bytes::{BufMut, Bytes, BytesMut};
use futures::{SinkExt, StreamExt};
use protobuf::Message;
use sodiumoxide::crypto::{
box_,
secretbox::{self, Key, Nonce},
};
use std::{
io::{self, Error, ErrorKind},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
ops::{Deref, DerefMut},
pin::Pin,
task::{Context, Poll},
};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
};
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
use tokio_util::codec::Framed;
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>);
#[derive(Clone)]
pub struct Encrypt(Key, u64, u64);
pub struct FramedStream(
Framed<DynTcpStream, BytesCodec>,
SocketAddr,
Option<Encrypt>,
u64,
);
impl Deref for FramedStream {
type Target = Framed<DynTcpStream, BytesCodec>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FramedStream {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Deref for DynTcpStream {
type Target = Box<dyn TcpStreamTrait + Send + Sync>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DynTcpStream {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
let socket = match addr {
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
};
if reuse {
// windows has no reuse_port, but it's reuse_address
// almost equals to unix's reuse_port + reuse_address,
// though may introduce nondeterministic behavior
#[cfg(unix)]
socket.set_reuseport(true).ok();
socket.set_reuseaddr(true).ok();
}
socket.bind(addr)?;
Ok(socket)
}
impl FramedStream {
pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
remote_addr: T,
local_addr: Option<SocketAddr>,
ms_timeout: u64,
) -> ResultType<Self> {
for remote_addr in lookup_host(&remote_addr).await? {
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4())
};
if let Ok(socket) = new_socket(local, true) {
if let Ok(Ok(stream)) =
super::timeout(ms_timeout, socket.connect(remote_addr)).await
{
stream.set_nodelay(true).ok();
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
}
}
bail!(format!("Failed to connect to {remote_addr}"));
}
pub async fn connect<'a, 't, P, T>(
proxy: P,
target: T,
local_addr: Option<SocketAddr>,
username: &'a str,
password: &'a str,
ms_timeout: u64,
) -> ResultType<Self>
where
P: ToProxyAddrs,
T: IntoTargetAddr<'t>,
{
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
let stream =
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
stream.set_nodelay(true).ok();
let stream = if username.trim().is_empty() {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
} else {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream, target, username, password,
),
)
.await??
};
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
bail!("could not resolve to any address");
}
pub fn local_addr(&self) -> SocketAddr {
self.1
}
pub fn set_send_timeout(&mut self, ms: u64) {
self.3 = ms;
}
pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self {
Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
)
}
pub fn set_raw(&mut self) {
self.0.codec_mut().set_raw();
self.2 = None;
}
pub fn is_secured(&self) -> bool {
self.2.is_some()
}
#[inline]
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
self.send_raw(msg.write_to_bytes()?).await
}
#[inline]
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
let mut msg = msg;
if let Some(key) = self.2.as_mut() {
msg = key.enc(&msg);
}
self.send_bytes(bytes::Bytes::from(msg)).await?;
Ok(())
}
#[inline]
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
if self.3 > 0 {
super::timeout(self.3, self.0.send(bytes)).await??;
} else {
self.0.send(bytes).await?;
}
Ok(())
}
#[inline]
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
let mut res = self.0.next().await;
if let Some(Ok(bytes)) = res.as_mut() {
if let Some(key) = self.2.as_mut() {
if let Err(err) = key.dec(bytes) {
return Some(Err(err));
}
}
}
res
}
#[inline]
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
if let Ok(res) = super::timeout(ms, self.next()).await {
res
} else {
None
}
}
pub fn set_key(&mut self, key: Key) {
self.2 = Some(Encrypt::new(key));
}
fn get_nonce(seqnum: u64) -> Nonce {
let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]);
nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes());
nonce
}
}
const DEFAULT_BACKLOG: u32 = 128;
pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<TcpListener> {
if !reuse {
Ok(TcpListener::bind(addr).await?)
} else {
let addr = lookup_host(&addr)
.await?
.next()
.context("could not resolve to any address")?;
new_socket(addr, true)?
.listen(DEFAULT_BACKLOG)
.map_err(anyhow::Error::msg)
}
}
pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
if let Ok(mut socket) = TcpSocket::new_v6() {
#[cfg(unix)]
{
socket.set_reuseport(true).ok();
socket.set_reuseaddr(true).ok();
use std::os::unix::io::{FromRawFd, IntoRawFd};
let raw_fd = socket.into_raw_fd();
let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) };
sock2.set_only_v6(false).ok();
socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) };
}
#[cfg(windows)]
{
use std::os::windows::prelude::{FromRawSocket, IntoRawSocket};
let raw_socket = socket.into_raw_socket();
let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) };
sock2.set_only_v6(false).ok();
socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) };
}
if socket
.bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
.is_ok()
{
if let Ok(l) = socket.listen(DEFAULT_BACKLOG) {
return Ok(l);
}
}
}
Ok(new_socket(
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
true,
)?
.listen(DEFAULT_BACKLOG)?)
}
impl Unpin for DynTcpStream {}
impl AsyncRead for DynTcpStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
}
}
impl AsyncWrite for DynTcpStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
}
}
impl<R: AsyncRead + AsyncWrite + Unpin> TcpStreamTrait for R {}
impl Encrypt {
pub fn new(key: Key) -> Self {
Self(key, 0, 0)
}
pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> {
self.2 += 1;
let nonce = FramedStream::get_nonce(self.2);
match secretbox::open(bytes, &nonce, &self.0) {
Ok(res) => {
bytes.clear();
bytes.put_slice(&res);
Ok(())
}
Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")),
}
}
pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
self.1 += 1;
let nonce = FramedStream::get_nonce(self.1);
secretbox::seal(&data, &nonce, &self.0)
}
pub fn decode(
symmetric_data: &[u8],
their_pk_b: &[u8],
our_sk_b: &box_::SecretKey,
) -> ResultType<Key> {
if their_pk_b.len() != box_::PUBLICKEYBYTES {
anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len());
}
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
pk_[..].copy_from_slice(their_pk_b);
let their_pk_b = box_::PublicKey(pk_);
let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b)
.map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?;
if symmetric_key.len() != secretbox::KEYBYTES {
anyhow::bail!("Handshake failed: invalid secret key length from peer");
}
let mut key = [0u8; secretbox::KEYBYTES];
key[..].copy_from_slice(&symmetric_key);
Ok(Key(key))
}
}
@@ -0,0 +1,170 @@
use crate::ResultType;
use anyhow::{anyhow, Context};
use bytes::{Bytes, BytesMut};
use futures::{SinkExt, StreamExt};
use protobuf::Message;
use socket2::{Domain, Socket, Type};
use std::net::SocketAddr;
use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket};
use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
pub enum FramedSocket {
Direct(UdpFramed<BytesCodec>),
ProxySocks(Socks5UdpFramed),
}
fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket, std::io::Error> {
let socket = match addr {
SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None),
SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None),
}?;
if reuse {
// windows has no reuse_port, but it's reuse_address
// almost equals to unix's reuse_port + reuse_address,
// though may introduce nondeterministic behavior
#[cfg(unix)]
socket.set_reuse_port(true).ok();
socket.set_reuse_address(true).ok();
}
// only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full
socket.set_nonblocking(true)?;
if buf_size > 0 {
socket.set_recv_buffer_size(buf_size).ok();
}
log::debug!(
"Receive buf size of udp {}: {:?}",
addr,
socket.recv_buffer_size()
);
if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 {
socket.set_only_v6(false).ok();
}
socket.bind(&addr.into())?;
Ok(socket)
}
impl FramedSocket {
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
Self::new_reuse(addr, false, 0).await
}
pub async fn new_reuse<T: ToSocketAddrs>(
addr: T,
reuse: bool,
buf_size: usize,
) -> ResultType<Self> {
let addr = lookup_host(&addr)
.await?
.next()
.context("could not resolve to any address")?;
Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?,
BytesCodec::new(),
)))
}
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
proxy: P,
local: T,
username: &'a str,
password: &'a str,
ms_timeout: u64,
) -> ResultType<Self> {
let framed = if username.trim().is_empty() {
super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await??
} else {
super::timeout(
ms_timeout,
Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password),
)
.await??
};
log::trace!(
"Socks5 udp connected, local addr: {:?}, target addr: {}",
framed.local_addr(),
framed.socks_addr()
);
Ok(Self::ProxySocks(framed))
}
#[inline]
pub async fn send(
&mut self,
msg: &impl Message,
addr: impl IntoTargetAddr<'_>,
) -> ResultType<()> {
let addr = addr.into_target_addr()?.to_owned();
let send_data = Bytes::from(msg.write_to_bytes()?);
match self {
Self::Direct(f) => {
if let TargetAddr::Ip(addr) = addr {
f.send((send_data, addr)).await?
}
}
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
};
Ok(())
}
// https://stackoverflow.com/a/68733302/1926020
#[inline]
pub async fn send_raw(
&mut self,
msg: &'static [u8],
addr: impl IntoTargetAddr<'static>,
) -> ResultType<()> {
let addr = addr.into_target_addr()?.to_owned();
match self {
Self::Direct(f) => {
if let TargetAddr::Ip(addr) = addr {
f.send((Bytes::from(msg), addr)).await?
}
}
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
};
Ok(())
}
#[inline]
pub async fn next(&mut self) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
match self {
Self::Direct(f) => match f.next().await {
Some(Ok((data, addr))) => {
Some(Ok((data, addr.into_target_addr().ok()?.to_owned())))
}
Some(Err(e)) => Some(Err(anyhow!(e))),
None => None,
},
Self::ProxySocks(f) => match f.next().await {
Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))),
Some(Err(e)) => Some(Err(anyhow!(e))),
None => None,
},
}
}
#[inline]
pub async fn next_timeout(
&mut self,
ms: u64,
) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
if let Ok(res) =
tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await
{
res
} else {
None
}
}
pub fn local_addr(&self) -> Option<SocketAddr> {
if let FramedSocket::Direct(x) = self {
if let Ok(v) = x.get_ref().local_addr() {
return Some(v);
}
}
None
}
}
@@ -0,0 +1,61 @@
use std::time::Duration;
use tauri::State;
use tokio::time::sleep;
use tracing::{debug, instrument, trace};
use crate::{
error::CommandError,
states::{app::ConnectionState, SharedAppState},
};
#[instrument]
#[tauri::command]
pub async fn get_connection_state(
state: State<'_, SharedAppState>,
) -> Result<ConnectionState, CommandError> {
debug!("get_connection_state");
let app_state = state.lock().await;
Ok(app_state.state)
}
#[tauri::command]
pub async fn connect(state: State<'_, SharedAppState>) -> Result<ConnectionState, CommandError> {
debug!("connect");
let mut app_state = state.lock().await;
let ConnectionState::Disconnected = app_state.state else {
return Err(CommandError::CallerError(format!(
"cannot connect from state {:?}",
app_state.state
)));
};
// TODO fake some delay to establish connection
let app_state_cloned = state.inner().clone();
let task = tokio::spawn(async move {
sleep(Duration::from_secs(2)).await;
trace!("connected");
app_state_cloned.lock().await.state = ConnectionState::Connected;
});
let _ = task.await;
app_state.state = ConnectionState::Connecting;
Ok(app_state.state)
}
#[instrument]
#[tauri::command]
pub async fn disconnect(state: State<'_, SharedAppState>) -> Result<ConnectionState, CommandError> {
debug!("disconnect");
let mut app_state = state.lock().await;
let ConnectionState::Connected = app_state.state else {
return Err(CommandError::CallerError(format!(
"cannot disconnect from state {:?}",
app_state.state
)));
};
app_state.state = ConnectionState::Disconnecting;
Ok(app_state.state)
}
+11
View File
@@ -0,0 +1,11 @@
use tracing::{debug, instrument};
pub mod connection;
pub mod settings;
#[instrument]
#[tauri::command]
pub fn greet(name: &str) -> String {
debug!("greet");
format!("Hello, {}! You've been greeted from Rust!", name)
}
@@ -0,0 +1,32 @@
use tauri::State;
use tracing::{debug, instrument};
use crate::{error::CommandError, fs::data::AppData, states::SharedAppData};
#[instrument]
#[tauri::command]
pub async fn save_user_settings(state: State<'_, SharedAppData>) -> Result<(), CommandError> {
debug!("save_user_settings");
let app_data = state.lock().await;
app_data
.write()
.map_err(|e| CommandError::InternalError(e.to_string()))?;
Ok(())
}
#[instrument]
#[tauri::command]
pub async fn set_user_settings(
state: State<'_, SharedAppData>,
settings: AppData,
) -> Result<(), CommandError> {
debug!("set_user_settings");
let mut app_data = state.lock().await;
app_data.data = settings;
app_data
.write()
.map_err(|e| CommandError::InternalError(e.to_string()))?;
Ok(())
}
+12
View File
@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum CommandError {
#[error("internal error: `{0}`")]
InternalError(String),
#[error("caller error: `{0}`")]
CallerError(String),
#[error("unknown error")]
Unknown,
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppCache {
// TODO
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppConfig {
// TODO
}
+13
View File
@@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
use crate::states::app::{NodeConfig, PrivacyMode};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppData {
pub monitoring: Option<bool>,
pub autoconnect: Option<bool>,
pub killswitch: Option<bool>,
pub privacy_mode: Option<PrivacyMode>,
pub entry_node: Option<NodeConfig>,
pub exit_node: Option<NodeConfig>,
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct AppLog {
// TODO
}
+4
View File
@@ -0,0 +1,4 @@
pub mod config;
pub mod data;
pub mod log;
pub mod storage;
+84
View File
@@ -0,0 +1,84 @@
use anyhow::{anyhow, Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt, fs, path::PathBuf, str};
use tauri::api::path::data_dir;
use tracing::{debug, error, instrument};
#[derive(Debug, Clone)]
pub struct AppStorage<T>
where
T: Serialize + DeserializeOwned + Default + fmt::Debug,
{
pub data: T,
pub dir_path: PathBuf,
pub filename: String,
pub full_path: PathBuf,
}
fn create_directory_path(path: &PathBuf) -> Result<()> {
let mut data_dir = data_dir().ok_or(anyhow!(
"Failed to retrieve data directory {:?}",
path.display()
))?;
data_dir.push(path);
fs::create_dir_all(&data_dir).context(format!(
"Failed to create data directory {}",
data_dir.display()
))
}
impl<T> AppStorage<T>
where
T: Serialize + DeserializeOwned + Default + fmt::Debug,
{
pub fn new(dir_path: PathBuf, filename: &str, data: Option<T>) -> Self {
let mut full_path = dir_path.clone();
full_path.push(filename);
Self {
data: data.unwrap_or_default(),
dir_path,
filename: filename.to_owned(),
full_path,
}
}
#[instrument]
pub fn read(&self) -> Result<T> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("reading stored data from {}", self.full_path.display());
let content = fs::read(&self.full_path).context(format!(
"Failed to read data from {}",
self.full_path.display()
))?;
toml::from_str::<T>(str::from_utf8(&content)?).map_err(|e| {
error!("{e}");
anyhow!("{e}")
})
}
#[instrument]
pub fn write(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("writing data to {}", self.full_path.display());
let toml = toml::to_string(&self.data)?;
fs::write(&self.full_path, toml)?;
Ok(())
}
#[instrument]
pub fn clear(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path(&self.dir_path)?;
debug!("clearing data {}", self.full_path.display());
fs::write(&self.full_path, vec![])?;
Ok(())
}
}
+1 -1
View File
@@ -13,4 +13,4 @@ pub extern "C" fn run_tauri() {
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}
+455 -8
View File
@@ -1,15 +1,462 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
//commented out for dev
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::Result;
use tauri::api::path::{config_dir, data_dir};
use tokio::sync::Mutex;
use tracing::info;
use std::time::{Duration, Instant};
use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem};
use tauri::Manager;
use windows_service::service::{ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, ServiceInfo, ServiceStartType, ServiceStatus};
use windows_sys::Win32::Foundation::ERROR_SERVICE_DOES_NOT_EXIST;
use std::sync::mpsc;
use hbb_common::{allow_err, anyhow::anyhow, bail, config::{Config}, log, ResultType, sleep, tokio};
use winapi::{
um::{
processthreadsapi::{
GetCurrentProcessId,
}
},
};
use windows_service::{define_windows_service, service::{
ServiceState,
ServiceType,
}, service_control_handler};
use std::ffi::OsString;
use std::io::Write;
use std::path::PathBuf;
use tauri_plugin_autostart::MacosLauncher;
use windows_service::service_control_handler::ServiceControlHandlerResult;
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
mod commands;
mod error;
mod fs;
mod states;
use commands::*;
use states::app::AppState;
use crate::fs::config::AppConfig;
use crate::fs::data::AppData;
use crate::fs::storage::AppStorage;
const APP_DIR: &str = "nymvpn";
const APP_DATA_FILE: &str = "app-data.toml";
const APP_CONFIG_FILE: &str = "config.toml";
const VPN_SERVICE_NAME: &'static str = "nym_vpn_service";
const VPN_EXE: &'static str = "nymvpn.exe";
const SERVICE_DISPLAY_NAME: &'static str = "NymVPN Manager";
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
cwd: String,
}
fn main() {
fn main() -> Result<()> {
dotenvy::dotenv()?;
// uses RUST_LOG value for logging level
// eg. RUST_LOG=tauri=debug,nymvpn_ui=trace
tracing_subscriber::fmt::init();
let mut app_data_path = data_dir().ok_or(anyhow!("Failed to retrieve data directory"))?;
app_data_path.push(APP_DIR);
let app_data_store = AppStorage::<AppData>::new(app_data_path, APP_DATA_FILE, None);
let mut app_config_path = config_dir().ok_or(anyhow!("Failed to retrieve config directory"))?;
app_config_path.push(APP_DIR);
let app_config_store = AppStorage::<AppConfig>::new(app_config_path, APP_CONFIG_FILE, None);
println!("Installing the service");
if let Err(_err) = install_service() {
eprintln!("Installing error: {_err}");
}
println!("Registering the service");
start_os_service();
println!("Starting service");
if let Err(_err) = start_service() {
eprintln!("Start error: {_err}");
}
println!("Service should be started");
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let open = CustomMenuItem::new("open".to_string(), "Open");
let tray_menu = SystemTrayMenu::new()
.add_item(open)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(quit);
info!("Starting tauri app");
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--flag1", "--flag2"])))
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
println!("{}, {argv:?}, {cwd}", app.package_info().name);
app.emit_all("single-instance", Payload { args: argv, cwd }).unwrap();
}))
.manage(Arc::new(Mutex::new(AppState::default())))
.manage(Arc::new(Mutex::new(app_data_store)))
.manage(Arc::new(Mutex::new(app_config_store)))
.on_window_event(|event| match event.event() {
tauri::WindowEvent::CloseRequested { api, .. } => {
event.window().hide().unwrap();
api.prevent_close();
}
_ => {}
})
.system_tray(SystemTray::new().with_menu(tray_menu))
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::MenuItemClick { id, .. } => {
match id.as_str() {
"quit" => {
let did_uninstall = uninstall_service();
if did_uninstall {
println!("Uninstalled service");
} else {
println!("Failed to uninstall service")
}
}
"open" => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
}
_ => {}
}
}
_ => {}
})
.setup(|_app| {
info!("app setup");
Ok(())
})
.invoke_handler(tauri::generate_handler![
greet,
connection::get_connection_state,
connection::connect,
connection::disconnect,
settings::save_user_settings,
settings::set_user_settings,
])
.build(tauri::generate_context!()).expect("error while building app")
.run(|_app_handle, event| match event {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
_ => {}
});
Ok(())
}
fn start_service() -> windows_service::Result<()> {
let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::START;
let service = service_manager.open_service(VPN_SERVICE_NAME, service_access)?;
println!("Opened connection to service");
if service.query_status()?.current_state == ServiceState::Stopped {
println!("Service is stopped, starting.");
service.start(&["Starting service"])?;
}
drop(service);
Ok(())
}
fn stop_service() -> windows_service::Result<()> {
let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP;
let service = service_manager.open_service(VPN_SERVICE_NAME, service_access)?;
println!("Opened connection to service");
if service.query_status()?.current_state == ServiceState::Running {
println!("Service is running, stopping.");
let status = service.stop()?;
if status.current_state == ServiceState::Running {
println!("Failed to stop service")
}
}
drop(service);
Ok(())
}
#[cfg(windows)]
fn install_service() -> windows_service::Result<()> {
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
let service_binary_path = std::env::current_exe()
.unwrap()
.with_file_name(VPN_EXE);
let service_info = ServiceInfo {
name: OsString::from(VPN_SERVICE_NAME),
display_name: OsString::from(SERVICE_DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: service_binary_path,
launch_arguments: vec![],
dependencies: vec![],
account_name: None, // run as System
account_password: None,
};
let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)?;
service.set_description("The service to manage NymVPN")?;
drop(service);
println!("Set description");
Ok(())
}
//this doesn't work that great, uninstall_service works better
#[cfg(windows)]
async fn remove_service() -> windows_service::Result<()> {
let manager_access = ServiceManagerAccess::CONNECT;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = service_manager.open_service(VPN_SERVICE_NAME, service_access)?;
// The service will be marked for deletion as long as this function call succeeds.
// However, it will not be deleted from the database until it is stopped and all open handles to it are closed.
service.delete()?;
// Our handle to it is not closed yet. So we can still query it.
if service.query_status()?.current_state != ServiceState::Stopped {
// If the service cannot be stopped, it will be deleted when the system restarts.
service.stop()?;
}
// Explicitly close our open handle to the service. This is automatically called when `service` goes out of scope.
drop(service);
// Win32 API does not give us a way to wait for service deletion.
// To check if the service is deleted from the database, we have to poll it ourselves.
let start = Instant::now();
let timeout = Duration::from_secs(5);
while start.elapsed() < timeout {
if let Err(windows_service::Error::Winapi(e)) =
service_manager.open_service(VPN_SERVICE_NAME, ServiceAccess::QUERY_STATUS)
{
if e.raw_os_error() == Some(ERROR_SERVICE_DOES_NOT_EXIST as i32) {
println!("service is deleted.");
return Ok(());
}
}
sleep(1f32).await;
}
println!("service is marked for deletion.");
Ok(())
}
fn get_current_pid() -> u32 {
unsafe { GetCurrentProcessId() }
}
pub fn uninstall_service() -> bool {
log::info!("Uninstalling service...");
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
Config::set_option("stop-service".into(), "Y".into());
let cmds = format!(
"
chcp 65001
sc stop {app_name}
sc delete {app_name}
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
taskkill /F /IM {broker_exe}
taskkill /F /IM {app_name}.exe{filter}
",
app_name = VPN_SERVICE_NAME,
broker_exe = VPN_EXE
);
if let Err(err) = run_cmds(cmds, false, "uninstall") {
Config::set_option("stop-service".into(), "".into());
log::debug!("{err}");
return true;
}
std::process::exit(0);
}
fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
let tmp = write_cmds(cmds, "bat", tip)?;
let tmp2 = get_undone_file(&tmp)?;
let tmp_fn = tmp.to_str().unwrap_or("");
let res = runas::Command::new("cmd")
.args(&["/C", &tmp_fn])
.show(show)
.force_prompt(true)
.status();
if !show {
allow_err!(std::fs::remove_file(tmp));
}
let _ = res?;
if tmp2.exists() {
allow_err!(std::fs::remove_file(tmp2));
bail!("{} failed", tip);
}
Ok(())
}
fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<PathBuf> {
let mut cmds = cmds;
let mut tmp = std::env::temp_dir();
// When dir contains these characters, the bat file will not execute in elevated mode.
if vec!["&", "@", "^"]
.drain(..)
.any(|s| tmp.to_string_lossy().to_string().contains(s))
{
if let Ok(dir) = user_accessible_folder() {
tmp = dir;
}
}
tmp.push(format!("{}_{}.{}", VPN_SERVICE_NAME, tip, ext));
let mut file = std::fs::File::create(&tmp)?;
if ext == "bat" {
let tmp2 = get_undone_file(&tmp)?;
std::fs::File::create(&tmp2).ok();
cmds = format!(
"
{cmds}
if exist \"{path}\" del /f /q \"{path}\"
",
path = tmp2.to_string_lossy()
);
}
// in case cmds mixed with \r\n and \n, make sure all ending with \r\n
// in some windows, \r\n required for cmd file to run
cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n");
if ext == "vbs" {
let mut v: Vec<u16> = cmds.encode_utf16().collect();
// utf8 -> utf16le which vbs support it only
file.write_all(to_le(&mut v))?;
} else {
file.write_all(cmds.as_bytes())?;
}
file.sync_all()?;
return Ok(tmp);
}
fn to_le(v: &mut [u16]) -> &[u8] {
for b in v.iter_mut() {
*b = b.to_le()
}
unsafe { v.align_to().1 }
}
pub fn user_accessible_folder() -> ResultType<PathBuf> {
let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string());
let dir1 = PathBuf::from(format!("{}\\ProgramData", disk));
// NOTICE: "C:\Windows\Temp" requires permanent authorization.
let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk));
let dir;
if dir1.exists() {
dir = dir1;
} else if dir2.exists() {
dir = dir2;
} else {
bail!("no vaild user accessible folder");
}
Ok(dir)
}
fn get_undone_file(tmp: &PathBuf) -> ResultType<PathBuf> {
let mut tmp1 = tmp.clone();
tmp1.set_file_name(format!(
"{}.undone",
tmp.file_name()
.ok_or(anyhow!("Failed to get filename of {:?}", tmp))?
.to_string_lossy()
));
Ok(tmp1)
}
define_windows_service!(ffi_service_main, service_main);
fn service_main(arguments: Vec<OsString>) {
if let Err(e) = run_service(arguments) {
log::error!("run_service failed: {}", e);
}
}
pub fn start_os_service() {
if let Err(e) =
windows_service::service_dispatcher::start(VPN_SERVICE_NAME, ffi_service_main)
{
log::error!("start_service failed: {}", e);
}
}
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
#[tokio::main(flavor = "current_thread")]
async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
let (send, recv) = mpsc::channel();
let event_handler = move |control_event| -> ServiceControlHandlerResult {
log::info!("Got service control event: {:?}", control_event);
match control_event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop => {
//I can never get this to work
//for now, just uninstall the service if we need to stop it
println!("Received stop command");
if let Err(_err) = send.send(()) {
eprintln!("Failed to send stop to sleep: {_err}");
}
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
// Register system service event handler
let status_handle = service_control_handler::register(VPN_SERVICE_NAME, event_handler)?;
let next_status = ServiceStatus {
// Should match the one from system service registry
service_type: SERVICE_TYPE,
// The new state
current_state: ServiceState::Running,
// Accept stop events when running
controls_accepted: ServiceControlAccept::STOP,
// Used to report an error when starting or stopping only, otherwise must be zero
exit_code: ServiceExitCode::Win32(0),
// Only used for pending states, otherwise must be zero
checkpoint: 0,
// Only used for pending states, otherwise must be zero
wait_hint: Duration::default(),
process_id: None,
};
// Tell the system that the service is running now
status_handle.set_service_status(next_status)?;
//TODO do work here, loop for now
loop {
//request to local server for testing
let resp = reqwest::blocking::get("http://localhost:8080/")?.text()?;
println!("{:#?}", resp);
if let Ok(_) = recv.recv_timeout(Duration::from_secs(2)) {
// Sleep was interrupted
break;
}
}
println!("This should only happen after work");
//after work, update status of service
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
})?;
Ok(())
}
+44
View File
@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NodeConfig {
pub id: String,
pub country: String,
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum ConnectionState {
Connected,
#[default]
Disconnected,
Connecting,
Disconnecting,
Error,
}
#[derive(Default, Debug, Serialize, Deserialize, TS, Clone)]
#[ts(export)]
pub enum PrivacyMode {
High,
Medium,
#[default]
Low,
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TunnelConfig {
pub id: String,
pub name: String,
}
#[derive(Debug, Default)]
pub struct AppState {
pub state: ConnectionState,
pub privacy_mode: PrivacyMode,
pub entry_node: Option<NodeConfig>,
pub exit_node: Option<NodeConfig>,
pub tunnel: Option<TunnelConfig>,
}
+11
View File
@@ -0,0 +1,11 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::fs::{config::AppConfig, data::AppData, storage::AppStorage};
pub mod app;
pub type SharedAppState = Arc<Mutex<app::AppState>>;
pub type SharedAppData = Arc<Mutex<AppStorage<AppData>>>;
pub type SharedAppConfig = Arc<Mutex<AppStorage<AppConfig>>>;
+14 -9
View File
@@ -7,21 +7,26 @@
"withGlobalTauri": false
},
"package": {
"productName": "nymvpn-ui",
"version": "0.0.0"
"productName": "nymvpn",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
}
"updater": {
"active": true,
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDUxMjFCMDhFOTczQzE5MjUKUldRbEdUeVhqckFoVVljRDZNZkRQZzIyYTBSZUVmSk1SVUlaTC9OeTk0NDFYUVl1blhWV2VTQi8K"
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"identifier": "net.nymtech.vpn",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
+7 -1
View File
@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import { Button as BaseButton, ButtonProps } from '@mui/base/Button';
import clsx from 'clsx';
import { enable, isEnabled } from 'tauri-plugin-autostart-api';
// eslint-disable-next-line react/display-name
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
@@ -24,6 +25,11 @@ function App() {
const [greetMsg, setGreetMsg] = useState('');
const [name, setName] = useState('');
useEffect(() => {
enable();
console.log(`registered for autostart? ${isEnabled()}`);
}, []);
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
setGreetMsg(await invoke('greet', { name }));
+61 -55
View File
@@ -459,76 +459,76 @@
resolved "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz"
integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==
"@tauri-apps/api@^1.5.0":
"@tauri-apps/api@1.5.1", "@tauri-apps/api@^1.5.0":
version "1.5.1"
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.1.tgz"
integrity sha512-6unsZDOdlXTmauU3NhWhn+Cx0rODV+rvNvTdvolE5Kls5ybA6cqndQENDt1+FS0tF7ozCP66jwWoH6a5h90BrA==
"@tauri-apps/cli-darwin-arm64@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.5.tgz#394fbc2920bc84524c8adb0021b1f788495a643f"
integrity sha512-CmKc/PjlI1+oD88VtR1Nr0pmrf/cUU1XFRazU+FB9ChWO3ZPp4MeA+eSemiln0F1XJR9fMJw/QS58IPH4GydLw==
"@tauri-apps/cli-darwin-arm64@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.6.tgz#0d68eaf6fa1b35197a9d4991ac5377e6085a569a"
integrity sha512-NNvG3XLtciCMsBahbDNUEvq184VZmOveTGOuy0So2R33b/6FDkuWaSgWZsR1mISpOuP034htQYW0VITCLelfqg==
"@tauri-apps/cli-darwin-x64@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.5.tgz#76f91cdb629d3f2996fe41a9401080baaabd956c"
integrity sha512-d7l/4FB5uWGkMHM08UI6+qk45PAeBYMSC19l0Sz47WrRHQDMIX4V591ydnUg8AffWK/I3r1DJtQmd6C89g7JwQ==
"@tauri-apps/cli-darwin-x64@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.6.tgz#a48e1f66b12c33cf6c9c216865f2e9a3ff18a923"
integrity sha512-nkiqmtUQw3N1j4WoVjv81q6zWuZFhBLya/RNGUL94oafORloOZoSY0uTZJAoeieb3Y1YK0rCHSDl02MyV2Fi4A==
"@tauri-apps/cli-linux-arm-gnueabihf@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.5.tgz#94cad5de0ce908271768aae931ed455352516b44"
integrity sha512-avFw/BvW01qhXPbzfVPy/KU/FYJ/SUoCe9DP8oA/eSh49VzE9JvlH62iqjtGtA8XzxfTJRezXdCQbrq7OkQHKQ==
"@tauri-apps/cli-linux-arm-gnueabihf@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.6.tgz#385cd8054be7722cc07acc2d6d86f8e20c6265ef"
integrity sha512-z6SPx+axZexmWXTIVPNs4Tg7FtvdJl9EKxYN6JPjOmDZcqA13iyqWBQal2DA/GMZ1Xqo3vyJf6EoEaKaliymPQ==
"@tauri-apps/cli-linux-arm64-gnu@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.5.tgz#9601a48572080796f30621a3723a2bbab5109e29"
integrity sha512-j7yvbZ/IG+W5QtEqK9nSz33lJtaZEFvNnFs0Bxz8r2TjF80m8SdlfxL38R/OVl7xM7ctJWRyM6ws9mBWT0uHNA==
"@tauri-apps/cli-linux-arm64-gnu@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.6.tgz#bd3f8f61637a1073909cb2d663aa0c6e8e036337"
integrity sha512-QuQjMQmpsCbzBrmtQiG4uhnfAbdFx3nzm+9LtqjuZlurc12+Mj5MTgqQ3AOwQedH3f7C+KlvbqD2AdXpwTg7VA==
"@tauri-apps/cli-linux-arm64-musl@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.5.tgz#c99c3612ffe65f4cc603baa821f7d8dcc1afd226"
integrity sha512-neLu3FEYE2IixnqtX10+jsvkJx26kxmh5ekktzjolu5HqV73nquCj7VK/V5uyRMyMQeGEPyhbT09A36DUl+zDA==
"@tauri-apps/cli-linux-arm64-musl@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.6.tgz#77fb4a804b77447bffba725a4b8b62df45f9ff97"
integrity sha512-8j5dH3odweFeom7bRGlfzDApWVOT4jIq8/214Wl+JeiNVehouIBo9lZGeghZBH3XKFRwEvU23i7sRVjuh2s8mg==
"@tauri-apps/cli-linux-x64-gnu@1.5.5":
version "1.5.5"
resolved "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.5.tgz"
integrity sha512-zZlfklupFaV6RxPze9kQytp1N/K4q/QuYUsgQ5GB/7/OX4EWTUkOpNCeVEocmHag4+9UCQkb1HxdTkXiEVcXEQ==
"@tauri-apps/cli-linux-x64-gnu@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.6.tgz#e70a6c9dd243c2a3fa1fec69de9ebc9abf4dc795"
integrity sha512-gbFHYHfdEGW0ffk8SigDsoXks6USpilF6wR0nqB/JbWzbzFR/sBuLVNQlJl1RKNakyJHu+lsFxGy0fcTdoX8xA==
"@tauri-apps/cli-linux-x64-musl@1.5.5":
version "1.5.5"
resolved "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.5.tgz"
integrity sha512-2VByWblZnSgLZJyhRxggy528ahcYFH8jboZZ2BUaYT/P5WeJ1lOoQwQj9ssEUrGauGPNS3PmmfBCF7u5oaMRJA==
"@tauri-apps/cli-linux-x64-musl@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.6.tgz#f25dad778b2da6ab7b2b81f81c0805026d742c33"
integrity sha512-9v688ogoLkeFYQNgqiSErfhTreLUd8B3prIBSYUt+x4+5Kcw91zWvIh+VSxL1n3KCGGsM7cuXhkGPaxwlEh1ug==
"@tauri-apps/cli-win32-arm64-msvc@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.5.tgz#44b6fcf0966a3bb9fc163d19ace83e98ee03cdce"
integrity sha512-4UZFHMIJaqgPGT+PHfDDp63OgJsXwLd+0u8x1+2hFMT25dEYj+KzKOVwktYgN6UT9F7rEyzNTTZe7ZZpAkGT5Q==
"@tauri-apps/cli-win32-arm64-msvc@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.6.tgz#47d0f359e04d93c1fd0b527e18a56606f3df9bed"
integrity sha512-DRNDXFNZb6y5IZrw+lhTTA9l4wbzO4TNRBAlHAiXUrH+pRFZ/ZJtv5WEuAj9ocVSahVw2NaK5Yaold4NPAxHog==
"@tauri-apps/cli-win32-ia32-msvc@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.5.tgz#04c7c7eba376a82e2f13c6e3c640cf34c988502c"
integrity sha512-t4XbmMyDtX7kW+wQrlWO4tZus+w77w+Hz5/NBQsjRNnO3lbuYMYaF4IZpt0tZG6lQ0uyvH+o2v5dbZhUTpVT0Q==
"@tauri-apps/cli-win32-ia32-msvc@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.6.tgz#54c7ec152065e8167068411b82748a25a483d948"
integrity sha512-oUYKNR/IZjF4fsOzRpw0xesl2lOjhsQEyWlgbpT25T83EU113Xgck9UjtI7xemNI/OPCv1tPiaM1e7/ABdg5iA==
"@tauri-apps/cli-win32-x64-msvc@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.5.tgz#cc4f5336e958e5d754a551e5c5df5e32680b89ec"
integrity sha512-7OiUfBmYjQ9LGTvl0Zs567JQIQuxpTCDraca3cpJFV/6TsRLEZAvXo3sgqEFOJopImrCWTpUT4FyzsGC76KlIg==
"@tauri-apps/cli-win32-x64-msvc@1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.6.tgz#963280a4f7001c27d4e4745f302728297c007deb"
integrity sha512-RmEf1os9C8//uq2hbjXi7Vgz9ne7798ZxqemAZdUwo1pv3oLVZSz1/IvZmUHPdy2e6zSeySqWu1D0Y3QRNN+dg==
"@tauri-apps/cli@^1.5.0":
version "1.5.5"
resolved "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.5.tgz"
integrity sha512-AUFqiA5vbriMd6xWDLWwxzW2FtEhSmL0KcMktkQQGzM+QKFnFbQsubvvd95YDAIX2Q4L1eygGv7ebNX0QVA7sg==
"@tauri-apps/cli@^1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.5.6.tgz#5011c9dd3a36dc89073fed7d4cb429d38b37862b"
integrity sha512-k4Y19oVCnt7WZb2TnDzLqfs7o98Jq0tUoVMv+JQSzuRDJqaVu2xMBZ8dYplEn+EccdR5SOMyzaLBJWu38TVK1A==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "1.5.5"
"@tauri-apps/cli-darwin-x64" "1.5.5"
"@tauri-apps/cli-linux-arm-gnueabihf" "1.5.5"
"@tauri-apps/cli-linux-arm64-gnu" "1.5.5"
"@tauri-apps/cli-linux-arm64-musl" "1.5.5"
"@tauri-apps/cli-linux-x64-gnu" "1.5.5"
"@tauri-apps/cli-linux-x64-musl" "1.5.5"
"@tauri-apps/cli-win32-arm64-msvc" "1.5.5"
"@tauri-apps/cli-win32-ia32-msvc" "1.5.5"
"@tauri-apps/cli-win32-x64-msvc" "1.5.5"
"@tauri-apps/cli-darwin-arm64" "1.5.6"
"@tauri-apps/cli-darwin-x64" "1.5.6"
"@tauri-apps/cli-linux-arm-gnueabihf" "1.5.6"
"@tauri-apps/cli-linux-arm64-gnu" "1.5.6"
"@tauri-apps/cli-linux-arm64-musl" "1.5.6"
"@tauri-apps/cli-linux-x64-gnu" "1.5.6"
"@tauri-apps/cli-linux-x64-musl" "1.5.6"
"@tauri-apps/cli-win32-arm64-msvc" "1.5.6"
"@tauri-apps/cli-win32-ia32-msvc" "1.5.6"
"@tauri-apps/cli-win32-x64-msvc" "1.5.6"
"@types/json-schema@^7.0.12":
version "7.0.14"
@@ -2455,6 +2455,12 @@ tailwindcss@^3.3.3:
resolve "^1.22.2"
sucrase "^3.32.0"
"tauri-plugin-autostart-api@https://github.com/tauri-apps/tauri-plugin-autostart#v1":
version "0.0.0"
resolved "https://github.com/tauri-apps/tauri-plugin-autostart#072ebc2914c19eb7022c90add7ea328d437d5eb2"
dependencies:
"@tauri-apps/api" "1.5.1"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
+15 -14
View File
@@ -1,24 +1,24 @@
{
"name": "@nymproject/nym-wallet-app",
"version": "1.2.10",
"main": "index.js",
"version": "1.2.12-rc.1",
"license": "MIT",
"main": "index.js",
"scripts": {
"prewebpack:dev": "yarn --cwd .. build",
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
"webpack:prod": "yarn webpack --progress --config webpack.prod.js",
"tauri:dev": "yarn tauri dev",
"tauri:build": "yarn tauri build",
"tsc": "tsc --noEmit true",
"tsc:watch": "tsc --noEmit true --watch",
"dev": "run-p tauri:dev webpack:dev",
"prebuild": "yarn --cwd .. build",
"build": "run-s webpack:prod tauri:build",
"dev": "run-p tauri:dev webpack:dev",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"prebuild": "yarn --cwd .. build",
"prestorybook": "yarn --cwd .. build",
"prewebpack:dev": "yarn --cwd .. build",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
"storybook:build": "build-storybook",
"tauri:build": "yarn tauri build",
"tauri:dev": "yarn tauri dev",
"tsc": "tsc --noEmit true",
"tsc:watch": "tsc --noEmit true --watch",
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
"webpack:prod": "yarn webpack --progress --config webpack.prod.js"
},
"dependencies": {
"@emotion/react": "^11.7.0",
@@ -29,7 +29,7 @@
"@mui/styles": "^5.2.2",
"@mui/utils": "^5.7.0",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/node-tester": "^1.0.0",
"@nymproject/node-tester": "^1.2.3",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@storybook/react": "^6.5.15",
@@ -123,5 +123,6 @@
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
}
},
"private": false
}
+1 -8
View File
@@ -10,13 +10,6 @@
"sdk/typescript/packages/mui-theme",
"sdk/typescript/packages/react-components",
"sdk/typescript/packages/validator-client",
"sdk/typescript/packages/mix-fetch-node",
"sdk/typescript/packages/nodejs-client",
"sdk/typescript/packages/node-tester",
"sdk/typescript/packages/sdk-react",
"sdk/typescript/packages/mix-fetch",
"sdk/typescript/packages/sdk",
"sdk/typescript/codegen/contract-clients",
"ts-packages/*",
"nym-wallet",
"nym-connect/**",
@@ -60,4 +53,4 @@
"@npmcli/node-gyp": "^3.0.0",
"node-gyp": "^9.3.1"
}
}
}
@@ -1,6 +1,6 @@
{
"name": "@nymproject/contract-clients",
"version": "1.2.1",
"version": "1.2.4-rc.1",
"description": "A client for all Nym smart contracts",
"license": "Apache-2.0",
"author": "Nym Technologies SA",
@@ -27,4 +27,4 @@
},
"private": false,
"types": "./dist/index.d.ts"
}
}
@@ -8,16 +8,21 @@ import Box from '@mui/material/Box';
import { mixFetch } from '@nymproject/mix-fetch-full-fat';
import Stack from '@mui/material/Stack';
import Paper from '@mui/material/Paper';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
const defaultUrl = 'https://nymtech.net/favicon.svg';
const args = { mode: 'unsafe-ignore-cors' };
const mixFetchOptions = {
preferredGateway: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM',
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM', // with WSS
preferredNetworkRequester:
'GiRjFWrMxt58pEMuusm4yT3RxoMD1MMPrR9M2N4VWRJP.3CNZBPq4vg7v7qozjGjdPMXcvDmkbWPCgbGCjQVw9n6Z@2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW',
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra: {},
};
export const MixFetch = () => {
@@ -77,4 +82,5 @@ export const MixFetch = () => {
</div>
);
};
```
@@ -25,6 +25,7 @@ export const Traffic = () => {
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
client?.events.subscribeToConnected((e) => {
@@ -7,23 +7,11 @@ import Box from '@mui/material/Box';
import { mixFetch } from '@nymproject/mix-fetch-full-fat';
import Stack from '@mui/material/Stack';
import Paper from '@mui/material/Paper';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
const defaultUrl = 'https://nymtech.net/favicon.svg';
const args = { mode: 'unsafe-ignore-cors' };
const extra = {
hiddenGateways: [
{
owner: 'n1kymvkx6vsq7pvn6hfurkpg06h3j4gxj4em7tlg',
host: 'gateway1.nymtech.net',
explicitIp: '213.219.38.119',
identityKey: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM',
sphinxKey: 'CYcrjoJ8GT7Dp54zViUyyRUfegeRCyPifWQZHRgMZrfX',
},
],
};
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM', // with WSS
preferredNetworkRequester:
@@ -32,7 +20,6 @@ const mixFetchOptions: SetupMixFetchOps = {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra, // manually set the gateway details for WSS so certificates will work for hostname
};
export const MixFetch = () => {
@@ -26,6 +26,7 @@ export const Traffic = () => {
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
// check when is connected and set the self address
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@nymproject/ts-sdk-docs",
"version": "1.2.1",
"version": "1.2.4-rc.1",
"description": "Nym Typescript SDK Docs",
"license": "Apache-2.0",
"author": "Nym Technologies SA",
@@ -28,10 +28,9 @@
"@mui/icons-material": "^5.14.9",
"@mui/lab": "^5.0.0-alpha.145",
"@mui/material": "^5.14.8",
"@nymproject/contract-clients": ">=1.2.1-rc.0 || ^1",
"@nymproject/mix-fetch": ">=1.2.1-rc.0 || ^1",
"@nymproject/mix-fetch-full-fat": ">=1.2.1-rc.0 || ^1",
"@nymproject/sdk-full-fat": ">=1.2.1-rc.0 || ^1",
"@nymproject/contract-clients": ">=1.2.4-rc.1 || ^1",
"@nymproject/mix-fetch-full-fat": ">=1.2.4-rc.1 || ^1",
"@nymproject/sdk-full-fat": ">=1.2.4-rc.1 || ^1",
"chain-registry": "^1.19.0",
"cosmjs-types": "^0.8.0",
"next": "^13.4.19",
@@ -29,18 +29,6 @@ Read [this article](https://blog.nymtech.net/mixfetch-like-the-fetch-api-but-via
// For mainnet
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
const extra = {
hiddenGateways: [
{
owner: 'n1kymvkx6vsq7pvn6hfurkpg06h3j4gxj4em7tlg',
host: 'gateway1.nymtech.net',
explicitIp: '213.219.38.119',
identityKey: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM',
sphinxKey: 'CYcrjoJ8GT7Dp54zViUyyRUfegeRCyPifWQZHRgMZrfX',
},
],
};
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM', // with WSS
preferredNetworkRequester:
@@ -49,7 +37,7 @@ const mixFetchOptions: SetupMixFetchOps = {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra, // manually set the gateway details for WSS so certificates will work for hostname
extra: {},
};
```
@@ -84,10 +72,6 @@ import { mixFetch } from "@nymproject/mix-fetch-full-fat";
##### Example: using the `mixFetch` client:
<Callout type="info" emoji="️">
Again, for this example, we will be using the `full-fat` version of the ESM SDK.
</Callout>
`Get` and `Post` outputs will be observable from your console.
```ts
@@ -95,18 +79,6 @@ import "./App.css";
import { mixFetch, SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
import React from 'react';
const extra = {
hiddenGateways: [
{
owner: 'n1kymvkx6vsq7pvn6hfurkpg06h3j4gxj4em7tlg',
host: 'gateway1.nymtech.net',
explicitIp: '213.219.38.119',
identityKey: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM',
sphinxKey: 'CYcrjoJ8GT7Dp54zViUyyRUfegeRCyPifWQZHRgMZrfX',
},
],
};
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM', // with WSS
preferredNetworkRequester:
@@ -115,7 +87,7 @@ const mixFetchOptions: SetupMixFetchOps = {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra
extra: {},
};
@@ -70,6 +70,7 @@ export function MixnetClient() {
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
// Check when is connected and set the self address
@@ -9,7 +9,7 @@ Lists of the different available clients and methods from the `Contract Clients`
| Client name | Functionality| Methods list |
| :-------------: | :----------: | :----------: |
| Coconut Bandwidth Client| Manages the depositing and release of funds. Tracks double spending. | [Coconut Bandwidth](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/CoconutBandwidth.client.ts) |
| Coconut DKG Client | Allows signers partcipating in issuing Coconut credentials to derive keys to be used. | [Coconut DKG](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/CoconutDkg.client.ts) |
| Coconut DKG Client | Allows signers participating in issuing Coconut credentials to derive keys to be used. | [Coconut DKG](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/CoconutDkg.client.ts) |
| Cw3FlexMultisig Client | Used by the Coconut APIs to issue credentials. [This](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw3-flex-multisig) is a multisig contract that is backed by the cw4 (group) contract, which independently maintains the voter set. | [Cw3Flex Multisig](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Cw3FlexMultisig.client.ts) |
| Cw4Group Client | Used by the Coconut APIs to issue credentials. [Cw4 Group](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group) stores a set of members along with an admin, and allows the admin to update the state. | [Cw4Group](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Cw4Group.client.ts) |
| Mixnet Client | Manages the network topology of the mixnet, tracking delegations and rewards. | [Mixnet](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Mixnet.client.ts) |
@@ -18,11 +18,6 @@ Lists of the different available clients and methods from the `Contract Clients`
| Vesting Client | Manages NYM token vesting functionality. | [Vesting](https://github.com/nymtech/nym/blob/develop/sdk/typescript/codegen/contract-clients/src/Vesting.client.ts) |
Depending on your app or project's architecture, this could be any of the ESM or CJS versions of the `Contract Clients`.
<Callout type="info" emoji="️">
This and the following examples will use the ESbuild bundler.
If you'd like to use another one, we will document different bundlers and polyfills in the [bundling](https://sdk.nymtech.net/bundling) page.
</Callout>
##### Environment Setup
Begin by creating a directory and configuring your application environment:
@@ -1,6 +1,6 @@
# mixFetch
import { MixFetch } from '../../components/mixfetch'
import { MixFetch } from '../../components/mix-fetch'
import Box from '@mui/material/Box';
import FormattedMixFetchExampleCode from '../../code-examples/mixfetch-example-code.mdx';
@@ -4,9 +4,6 @@ import { Traffic } from '../../components/traffic';
import Box from '@mui/material/Box';
import FormattedTrafficExampleCode from '../../code-examples/traffic-example-code.mdx';
<Callout type="info" emoji="⚠️">
Currently, due to SSL-related issues, the mixnet client is functional exclusively in local development environments. Unless you clone this repository or create your own build, you may encounter limitations when attempting to test this example.
</Callout>
Use this tool to experiment with the mixnet: send and receive messages!
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-plain-html-parcel",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM and plain HTML bundled with Parcel v2",
"license": "Apache-2.0",
"scripts": {
@@ -15,7 +15,7 @@
"tsc:watch": "tsc --watch"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"@types/jest": "^27.0.1",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-plain-html",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM and plain HTML",
"license": "Apache-2.0",
"scripts": {
@@ -16,7 +16,7 @@
"tsc:watch": "tsc --watch"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-react-webpack-wasm",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM, React, Webpack, Typescript and the Nym theme + components library",
"license": "Apache-2.0",
"scripts": {
@@ -20,7 +20,7 @@
"@mui/lab": "^5.0.0-alpha.72",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1",
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-chrome-extension",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "This is an example of how Nym can be used within the context of a Chrome extension.",
"license": "ISC",
"author": "",
@@ -9,7 +9,7 @@
"build": "webpack"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-firefox-extension",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "This is an example of how Nym can be used within the context of a Firefox extension.",
"license": "ISC",
"author": "",
@@ -9,7 +9,7 @@
"build": "yarn webpack"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch-example-parcel",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"license": "Apache-2.0",
"scripts": {
"build": "parcel build --no-cache --no-content-hash",
@@ -8,7 +8,7 @@
"start": "parcel --no-cache"
},
"dependencies": {
"@nymproject/mix-fetch": ">=1.2.1-rc.0 || ^1",
"@nymproject/mix-fetch": ">=1.2.2-rc.0 || ^1",
"parcel": "^2.9.3"
},
"private": false,
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-node-tester-plain-html-parcel",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM and plain HTML bundled with Parcel v2",
"license": "Apache-2.0",
"scripts": {
@@ -15,7 +15,7 @@
"tsc:watch": "tsc --watch"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"@types/jest": "^27.0.1",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-node-tester-plain-html",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM node tester and plain HTML",
"license": "Apache-2.0",
"scripts": {
@@ -16,7 +16,7 @@
"tsc:watch": "tsc --watch"
},
"dependencies": {
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1"
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
@@ -1,6 +1,6 @@
{
"name": "@nymproject/sdk-example-node-tester-react",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"description": "An example project that uses WASM node tester and React",
"license": "Apache-2.0",
"scripts": {
@@ -11,7 +11,7 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.0",
"@nymproject/sdk": ">=1.2.1-rc.0 || ^1",
"@nymproject/sdk": ">=1.2.2-rc.0 || ^1",
"parcel": "^2.9.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch-node",
"version": "1.2.1",
"version": "1.2.4-rc.1",
"description": "This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.",
"license": "Apache-2.0",
"author": "Nym Technologies SA",
@@ -28,7 +28,7 @@
"tsc": "tsc --noEmit true"
},
"dependencies": {
"@nymproject/mix-fetch-wasm-node": ">=1.2.1-rc.0 || ^1",
"@nymproject/mix-fetch-wasm-node": ">=1.2.4-rc.1 || ^1",
"comlink": "^4.3.1",
"fake-indexeddb": "^5.0.0",
"node-fetch": "^3.3.2",
@@ -68,4 +68,4 @@
},
"private": false,
"types": "./dist/cjs/index.d.ts"
}
}
@@ -1,6 +1,6 @@
{
"name": "@nymproject/mix-fetch-tester-webpack",
"version": "1.0.1",
"version": "1.0.4-rc.1",
"license": "Apache-2.0",
"scripts": {
"build": "webpack build --progress --config webpack.prod.js",
@@ -8,7 +8,7 @@
"start": "webpack serve --progress --port 3000"
},
"dependencies": {
"@nymproject/mix-fetch": ">=1.2.1-rc.0 || ^1"
"@nymproject/mix-fetch": ">=1.2.2-rc.0 || ^1"
},
"devDependencies": {
"@babel/core": "^7.22.10",

Some files were not shown because too many files have changed in this diff Show More