Merge pull request #465 from nworbnhoj/sigchat

Link sigchat to existing Signal account
This commit is contained in:
bunnie 2024-01-08 17:23:09 +08:00 committed by GitHub
commit f196bcf469
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1380 additions and 157 deletions

View file

@ -36,6 +36,7 @@ rkyv = {version = "0.4.3", default-features = false, features = ["const_generics
rustls = { version = "0.21.7" }
serde = { version = "1.0", features = [ "derive" ] }
tls = { path = "../../libs/tls" }
tungstenite = "0.20.1"
ureq = { version = "2.7.1", features = ["json"] }
url = "2.2.2"

View file

@ -1,4 +1,4 @@
# mtxchat
# sigchat
This is the skeleton of a Signal chat application.

View file

@ -3,10 +3,30 @@
"en": "Failed to setup Signal Account",
"en-tts": "Failed to setup Signal Account"
},
"sigchat.account.abort": {
"en": "Signal Account setup aborted",
"en-tts": "Signal Account setup aborted"
},
"sigchat.account.host.name": {
"en": "Please provide the Signal host server name",
"en-tts": "Please provide the Signal host server name"
},
"sigchat.account.link": {
"en": "Link to an existing Signal Account",
"en-tts": "Link to an existing Signal Account"
},
"sigchat.account.link.name": {
"en": "Please provide a name for this device",
"en-tts": "Please provide a name for this device"
},
"sigchat.account.link.scan": {
"en": "1. Open Signal on your phone\n2. Tap into settings, then tap `Linked Devices`\n3. Tap ⊕ or `Link New Device` and scan",
"en-tts": "1. Open Signal on your phone\n2. Tap into settings, then tap `Linked Devices`\n3. Tap ⊕ or `Link New Device` and scan"
},
"sigchat.account.offline": {
"en": "Read Signal offline",
"en-tts": "Read Signal offline"
},
"sigchat.account.register": {
"en": "Register a new Signal Account",
"en-tts": "Register a new Signal Account"
@ -15,6 +35,10 @@
"en": "Setup Signal on this device.",
"en-tts": "Setup Signal on this device."
},
"sigchat.host.invalid": {
"en": "Invalid host format",
"en-tts": "Invalid host format"
},
"sigchat.menu.close": {
"en": "Close menu",
"en-tts": "Close menu"
@ -42,6 +66,26 @@
"ja": "WARNING: pddb not mounted *EN*",
"zh": "WARNING: pddb not mounted *EN*"
},
"sigchat.status.connecting": {
"en": "connecting",
"en-tts": "connecting"
},
"sigchat.status.initializing": {
"en": "initializing Signal account in pddb",
"en-tts": "initializing Signal account in pddb"
},
"sigchat.status.offline": {
"en": "Signal offline",
"en-tts": "Signal offline"
},
"sigchat.status.online": {
"en": "Signal online",
"en-tts": "Signal online"
},
"sigchat.status.probing": {
"en": "probing host for trusted tls",
"en-tts": "probing host for trusted tls"
},
"sigchat.wifi.connected": {
"en": "WiFi connected",
"en-tts": "WiFi connected",

View file

@ -1,57 +1,341 @@
mod service_environment;
use crate::manager::libsignal::{DeviceNameUtil, ProvisionMessage, SignalServiceAddress};
use pddb::Pddb;
use service_environment::ServiceEnvironment;
pub use service_environment::ServiceEnvironment;
use std::io::{Error, ErrorKind, Read, Write};
use std::str::FromStr;
use url::Host;
/// The Account struct is architected as a cache over a pddb dictionary.
///
/// * Creating a new Account inherently involves writing to pddb
/// * Each field has a 1:1 relationship with a pddb.key.
/// * Field values are able to be set individually
///
/// Steps to ensure consistency:
/// * the default values in a new Account are first written to pddb, and then read back into the struct.
/// * all fields must be successfully read from pddb or read fails with Error
/// * setting a value requires a successful writes to pddb before updating the field
///
#[allow(dead_code)]
pub struct Account {
pddb: Pddb,
pddb_dict: String,
service_environment: ServiceEnvironment,
aci_identity_private: Option<String>,
aci_identity_public: Option<String>,
aci_service_id: Option<String>,
device_id: u32,
encrypted_device_name: Option<String>,
host: Host,
is_multi_device: bool,
number: Option<String>,
password: Option<String>,
pin_master_key: Option<String>,
pni_identity_private: Option<String>,
pni_identity_public: Option<String>,
pni_service_id: Option<String>,
profile_key: Option<String>,
registered: bool,
service_environment: ServiceEnvironment,
storage_key: Option<String>,
store_last_receive_timestamp: i64,
store_manifest_version: i64,
store_manifest: Option<String>,
}
const SERVICE_ENVIRONMENT_KEY: &str = "service_environment";
pub const DEFAULT_HOST: &str = "signal.org";
const ACI_IDENTITY_PRIVATE_KEY: &str = "aci.identity.private";
const ACI_IDENTITY_PUBLIC_KEY: &str = "aci.identity.public";
const ACI_SERVICE_ID_KEY: &str = "aci.service_id";
const DEVICE_ID_KEY: &str = "device_id";
const ENCRYPTED_DEVICE_NAME_KEY: &str = "encrypted_device_name";
const HOST_KEY: &str = "host";
const IS_MULTI_DEVICE_KEY: &str = "is_multi_device";
const NUMBER_KEY: &str = "number";
const PASSWORD_KEY: &str = "password";
const PIN_MASTER_KEY_KEY: &str = "pin_master_key";
const PNI_IDENTITY_PRIVATE_KEY: &str = "pni.identity.private";
const PNI_IDENTITY_PUBLIC_KEY: &str = "pni.identity.public";
const PNI_SERVICE_ID_KEY: &str = "pni.service_id";
const PROFILE_KEY_KEY: &str = "profile_key";
const REGISTERED_KEY: &str = "registered";
const SERVICE_ENVIRONMENT_KEY: &str = "service_environment";
const STORAGE_KEY_KEY: &str = "storage_key";
const STORE_LAST_RECEIVE_TIMESTAMP_KEY: &str = "store_last_receive_timestamp";
const STORE_MANIFEST_VERSION_KEY: &str = "store_manifest_version";
const STORE_MANIFEST_KEY: &str = "store_manifest";
impl Account {
pub fn new(pddb_dict: &str) -> Result<Account, Error> {
/// Create a new Account stored in pddb with default values
///
/// This function saves default values for each field in the pddb
/// and then calls read() to load the values into the Account struct
///
/// # Arguments
/// * `pddb_dict` - pddb dictionary name to hold the Account
/// * `host` - Signal host server (immutable)
/// * `service_environment` - Signal service-environment (immutable)
///
/// # Returns
///
/// a new Account with default values
///
pub fn new(
pddb_dict: &str,
host: &Host,
service_environment: &ServiceEnvironment,
) -> Result<Account, Error> {
let pddb = pddb::Pddb::new();
pddb.try_mount();
set(&pddb, pddb_dict, ACI_IDENTITY_PRIVATE_KEY, None)?;
set(&pddb, pddb_dict, ACI_IDENTITY_PUBLIC_KEY, None)?;
set(&pddb, pddb_dict, ACI_SERVICE_ID_KEY, None)?;
set(&pddb, pddb_dict, DEVICE_ID_KEY, Some("0"))?;
set(&pddb, pddb_dict, ENCRYPTED_DEVICE_NAME_KEY, None)?;
set(&pddb, pddb_dict, HOST_KEY, Some(&host.to_string()))?;
set(
&pddb,
pddb_dict,
IS_MULTI_DEVICE_KEY,
Some(&false.to_string()),
)?;
set(&pddb, pddb_dict, NUMBER_KEY, None)?;
set(&pddb, pddb_dict, PASSWORD_KEY, None)?;
set(&pddb, pddb_dict, PIN_MASTER_KEY_KEY, None)?;
set(&pddb, pddb_dict, PNI_IDENTITY_PRIVATE_KEY, None)?;
set(&pddb, pddb_dict, PNI_IDENTITY_PUBLIC_KEY, None)?;
set(&pddb, pddb_dict, PNI_SERVICE_ID_KEY, None)?;
set(&pddb, pddb_dict, PROFILE_KEY_KEY, None)?;
set(&pddb, pddb_dict, REGISTERED_KEY, Some(&false.to_string()))?;
set(
&pddb,
pddb_dict,
SERVICE_ENVIRONMENT_KEY,
Some(&ServiceEnvironment::Staging.to_string()),
Some(&service_environment.to_string()),
)?;
set(&pddb, pddb_dict, REGISTERED_KEY, Some(&false.to_string()))?;
set(&pddb, pddb_dict, STORAGE_KEY_KEY, None)?;
set(
&pddb,
pddb_dict,
STORE_LAST_RECEIVE_TIMESTAMP_KEY,
Some("0"),
)?;
set(&pddb, pddb_dict, STORE_MANIFEST_VERSION_KEY, Some("-1"))?;
set(&pddb, pddb_dict, STORE_MANIFEST_KEY, None)?;
Account::read(pddb_dict)
}
// retrieves an existing Account from the pddb
//
// # Arguments
// * `pddb_dict` - the pddb dictionary name holding the Account
//
// # Returns
//
// a Account with values read from pddb_dict
//
pub fn read(pddb_dict: &str) -> Result<Account, Error> {
let pddb = pddb::Pddb::new();
pddb.try_mount();
match (
get(&pddb, pddb_dict, SERVICE_ENVIRONMENT_KEY),
get(&pddb, pddb_dict, ACI_IDENTITY_PRIVATE_KEY),
get(&pddb, pddb_dict, ACI_IDENTITY_PUBLIC_KEY),
get(&pddb, pddb_dict, ACI_SERVICE_ID_KEY),
get(&pddb, pddb_dict, DEVICE_ID_KEY),
get(&pddb, pddb_dict, ENCRYPTED_DEVICE_NAME_KEY),
get(&pddb, pddb_dict, HOST_KEY),
get(&pddb, pddb_dict, IS_MULTI_DEVICE_KEY),
get(&pddb, pddb_dict, NUMBER_KEY),
get(&pddb, pddb_dict, PASSWORD_KEY),
get(&pddb, pddb_dict, PIN_MASTER_KEY_KEY),
get(&pddb, pddb_dict, PNI_IDENTITY_PRIVATE_KEY),
get(&pddb, pddb_dict, PNI_IDENTITY_PUBLIC_KEY),
get(&pddb, pddb_dict, PNI_SERVICE_ID_KEY),
get(&pddb, pddb_dict, PROFILE_KEY_KEY),
get(&pddb, pddb_dict, REGISTERED_KEY),
get(&pddb, pddb_dict, SERVICE_ENVIRONMENT_KEY),
get(&pddb, pddb_dict, STORAGE_KEY_KEY),
get(&pddb, pddb_dict, STORE_LAST_RECEIVE_TIMESTAMP_KEY),
get(&pddb, pddb_dict, STORE_MANIFEST_VERSION_KEY),
get(&pddb, pddb_dict, STORE_MANIFEST_KEY),
) {
(Ok(Some(service_environment)), Ok(Some(registered))) => Ok(Account {
(
Ok(aci_identity_private),
Ok(aci_identity_public),
Ok(aci_service_id),
Ok(Some(device_id)),
Ok(encrypted_device_name),
Ok(Some(host)),
Ok(Some(is_multi_device)),
Ok(number),
Ok(password),
Ok(pin_master_key),
Ok(pni_identity_private),
Ok(pni_identity_public),
Ok(pni_service_id),
Ok(profile_key),
Ok(Some(registered)),
Ok(Some(service_environment)),
Ok(storage_key),
Ok(Some(store_last_receive_timestamp)),
Ok(Some(store_manifest_version)),
Ok(store_manifest),
) => Ok(Account {
pddb: pddb,
pddb_dict: pddb_dict.to_string(),
service_environment: ServiceEnvironment::from_str(&service_environment).unwrap(),
number: None,
aci_identity_private: aci_identity_private,
aci_identity_public: aci_identity_public,
aci_service_id: aci_service_id,
device_id: device_id.parse().unwrap(),
encrypted_device_name: encrypted_device_name,
host: Host::parse(&host).unwrap(),
is_multi_device: is_multi_device.parse().unwrap(),
number: number,
password: password,
pin_master_key: pin_master_key,
pni_identity_private: pni_identity_private,
pni_identity_public: pni_identity_public,
pni_service_id: pni_service_id,
profile_key: profile_key,
registered: registered.parse().unwrap(),
service_environment: ServiceEnvironment::from_str(&service_environment).unwrap(),
storage_key: storage_key,
store_last_receive_timestamp: store_last_receive_timestamp.parse().unwrap(),
store_manifest_version: store_manifest_version.parse().unwrap(),
store_manifest: store_manifest,
}),
(Err(e), _) => Err(e),
(_, Err(e)) => Err(e),
(_, _) => Err(Error::from(ErrorKind::InvalidData)),
(Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _, _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e), _) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, Err(e)) => Err(e),
(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => {
Err(Error::from(ErrorKind::InvalidData))
}
}
}
/// Delete this Account key/value from the pddb
///
/// While this Account struct will persist in memory, a subsequent Account.read() will fail
///
pub fn delete(pddb_dict: &str) -> Result<(), Error> {
let pddb = pddb::Pddb::new();
pddb.try_mount();
pddb.delete_dict(pddb_dict, None)?;
log::info!("deleted Signal Account from pddb");
Ok(())
}
/// link to an existing Signal Account as a secondary device
///
/// Confirm that the state of the Signal Account is OK before linking
/// https://github.com/AsamK/signal-cli/blob/375bdb79485ec90beb9a154112821a4657740b7a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java#L200-L239
///
/// # Arguments
///
/// * `device_name` - name to describe this new device
/// * `provisioning_msg` - obtained from the Signal server
///
/// # Returns
///
/// true on success
///
pub fn link(
&mut self,
device_name: &str,
provisioning_msg: ProvisionMessage,
) -> Result<bool, Error> {
// Check if this device can be relinked
if self.is_primary_device() {
log::warn!("failed to link device as already registered as primary");
return Ok(false);
}
// TODO complete link pre-checks
// } else if self.is_registered()
// && self.service_environment() != provisioning_msg.service_environment{
// log::warn!("failed to link device as already registered in different Service Environment");
// return Ok(false);
// }
let aci = provisioning_msg.aci;
self.set(ACI_IDENTITY_PRIVATE_KEY, Some(&aci.djb_private_key.key))?;
self.set(ACI_IDENTITY_PUBLIC_KEY, Some(&aci.djb_identity_key.key))?;
self.set(ACI_SERVICE_ID_KEY, Some(&aci.service_id))?;
self.set(DEVICE_ID_KEY, Some("0"))?;
self.set(
ENCRYPTED_DEVICE_NAME_KEY,
Some(&DeviceNameUtil::encrypt_device_name(
device_name,
aci.djb_private_key,
)),
)?;
self.set(IS_MULTI_DEVICE_KEY, Some(&true.to_string()))?;
self.set(NUMBER_KEY, Some(&provisioning_msg.number))?;
self.set(PASSWORD_KEY, Some("STUB 32 bytes"))?;
self.set(PIN_MASTER_KEY_KEY, Some(&provisioning_msg.master_key))?;
let pni = provisioning_msg.pni;
self.set(PNI_IDENTITY_PRIVATE_KEY, Some(&pni.djb_private_key.key))?;
self.set(PNI_IDENTITY_PUBLIC_KEY, Some(&pni.djb_identity_key.key))?;
self.set(PNI_SERVICE_ID_KEY, Some(&aci.service_id))?;
self.set(
PROFILE_KEY_KEY,
Some(
&provisioning_msg
.profile_key
.unwrap_or_else(|| "STUB 32 bytes".to_string()),
),
)?;
self.set(REGISTERED_KEY, Some(&false.to_string()))?;
self.set(STORAGE_KEY_KEY, None)?;
self.set(STORE_LAST_RECEIVE_TIMESTAMP_KEY, Some("0"))?;
self.set(STORE_MANIFEST_VERSION_KEY, Some("-1"))?;
self.set(STORE_MANIFEST_KEY, None)?;
// TODO complete registration setup
// https://github.com/AsamK/signal-cli/blob/375bdb79485ec90beb9a154112821a4657740b7a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java#L270-L306
// getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
// getSenderKeyStore().deleteAll();
// trustSelfIdentity(ServiceIdType.ACI);
// trustSelfIdentity(ServiceIdType.PNI);
// aciAccountData.getSessionStore().archiveAllSessions();
// pniAccountData.getSessionStore().archiveAllSessions();
// clearAllPreKeys();
// getKeyValueStore().storeEntry(lastRecipientsRefresh, null);
Ok(true)
}
pub fn host(&self) -> &Host {
&self.host
}
pub fn is_primary_device(&self) -> bool {
self.device_id == SignalServiceAddress::DEFAULT_DEVICE_ID
}
pub fn is_registered(&self) -> bool {
self.registered
}
#[allow(dead_code)]
pub fn number(&self) -> Option<&str> {
match &self.number {
@ -60,6 +344,10 @@ impl Account {
}
}
pub fn service_environment(&self) -> &ServiceEnvironment {
&self.service_environment
}
#[allow(dead_code)]
pub fn set_number(&mut self, value: &str) -> Result<(), Error> {
match self.set(NUMBER_KEY, Some(value)) {
@ -74,9 +362,49 @@ impl Account {
get(&self.pddb, &self.pddb_dict, key)
}
#[allow(dead_code)]
fn set(&self, key: &str, value: Option<&str>) -> Result<(), Error> {
set(&self.pddb, &self.pddb_dict, key, value)
// Sets the value of a pddb_key / field in the Account
//
// To guarantee consistency, the value is saved to the pddb and,
// on success, set to the corresponding field in the Account struct.
//
// # Arguments
// * `key` - the pddb_key corresponding to the Account field
// * `value` - the value to save into the Account field (and pddb)
//
// # Returns
//
// Ok()
//
fn set(&mut self, key: &str, value: Option<&str>) -> Result<(), Error> {
let owned_value = value.map(str::to_string);
match set(&self.pddb, &self.pddb_dict, key, value) {
Ok(()) => match key {
ACI_IDENTITY_PRIVATE_KEY => Ok(self.aci_identity_private = owned_value),
ACI_IDENTITY_PUBLIC_KEY => Ok(self.aci_identity_public = owned_value),
ACI_SERVICE_ID_KEY => Ok(self.aci_service_id = owned_value),
DEVICE_ID_KEY => Ok(self.device_id = owned_value.unwrap().parse().unwrap()),
IS_MULTI_DEVICE_KEY => {
Ok(self.is_multi_device = owned_value.unwrap().parse().unwrap())
}
NUMBER_KEY => Ok(self.number = owned_value),
PASSWORD_KEY => Ok(self.password = owned_value),
PIN_MASTER_KEY_KEY => Ok(self.pin_master_key = owned_value),
PNI_IDENTITY_PRIVATE_KEY => Ok(self.pni_identity_private = owned_value),
PNI_IDENTITY_PUBLIC_KEY => Ok(self.pni_identity_public = owned_value),
PNI_SERVICE_ID_KEY => Ok(self.pni_service_id = owned_value),
PROFILE_KEY_KEY => Ok(self.profile_key = owned_value),
REGISTERED_KEY => Ok(self.registered = owned_value.unwrap().parse().unwrap()),
SERVICE_ENVIRONMENT_KEY => Ok(self.service_environment =
ServiceEnvironment::from_str(&value.unwrap()).unwrap()),
STORAGE_KEY_KEY => Ok(self.storage_key = owned_value),
_ => {
log::warn!("invalid key: {key}");
let _ = &self.pddb.delete_key(&self.pddb_dict, &key, None);
Err(Error::from(ErrorKind::NotFound))
}
},
Err(e) => Err(e),
}
}
}

View file

@ -1,9 +1,8 @@
use rkyv::{Archive, Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
// The server environment to use:
#[derive(Archive, Serialize, Deserialize, Debug)]
#[derive(Clone, Debug)]
pub enum ServiceEnvironment {
Live,
Staging,

View file

@ -1,5 +1,6 @@
#[allow(dead_code)]
pub(crate) const SERVER_NAME_SIGCHAT: &str = "_Signal chat_";
pub(crate) const DEFAULT_DEVICE_NAME: &str = "xous";
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub enum SigchatOp {
@ -17,9 +18,7 @@ pub enum SigchatOp {
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub enum MenuOp {
Link,
Noop,
Register,
}
#[allow(dead_code)]

View file

@ -2,13 +2,15 @@ mod account;
pub mod api;
mod manager;
use crate::account::Account;
use crate::manager::{Manager, TrustMode};
use crate::account::{Account, ServiceEnvironment, DEFAULT_HOST};
use crate::manager::{Config, Manager, TrustMode};
pub use api::*;
use chat::Chat;
use locales::t;
use modals::Modals;
use std::io::{Error, ErrorKind};
use tls::Tls;
use url::{Host, Url};
/// PDDB Dict for sigchat keys
const SIGCHAT_ACCOUNT: &str = "sigchat.account";
@ -47,47 +49,179 @@ impl<'a> SigChat<'a> {
/// Connect to the Signal servers
///
pub fn connect(&mut self) -> bool {
/// The process first waits for an active WiFi connection, and then
/// initiates a Signal Account Manager with the Signal Account struct stored
/// in the pddb (or kicks off the Account setup process otherwise). The
/// Account Manager orchestrates the interaction with the Signal host server.
///
/// # Returns
/// true on a successful connection to a Signal Account/Server
///
pub fn connect(&mut self) -> Result<bool, Error> {
log::info!("Attempting connect to Signal server");
if self.wifi() {
if self.manager.is_none() {
self.account_setup();
log::info!("Setting up Signal Account Manager");
let account = match Account::read(SIGCHAT_ACCOUNT) {
Ok(account) => account,
Err(_) => self.account_setup()?,
};
self.chat
.set_status_text(t!("sigchat.status.connecting", locales::LANG));
self.chat.set_busy_state(true);
self.manager = Some(Manager::new(account, TrustMode::OnFirstUse));
self.chat.set_busy_state(false);
}
if let Some(_manager) = &self.manager {
log::info!("Signal manager OK" );
if self.manager.is_some() {
log::info!("Signal Account Manager OK");
self.chat
.set_status_text(t!("sigchat.status.online", locales::LANG));
Ok(true)
} else {
self.modals
.show_notification(t!("sigchat.account.failed", locales::LANG), None)
.expect("notification failed");
};
log::info!("failed to setup Signal Account Manager");
self.chat
.set_status_text(t!("sigchat.status.offline", locales::LANG));
Ok(false)
}
} else {
self.modals
.show_notification(t!("sigchat.wifi.warning", locales::LANG), None)
.expect("notification failed");
Ok(false)
}
self.dialogue_set(None);
false
}
/// Setup a Signal Account via Registration or Linking
/// Setup a Signal Account via Registration or Linking,
/// or abort setup and read existing chat Dialogues in pddb
///
fn account_setup(&mut self) {
/// The user can choose to:
/// 1. Link to an existing Signal Account
/// 2. Register a new Signal Account
/// 3. Abort account setup to read existing Signal Dialogues stored in the pddb
/// The online options involve nominating the Signal host server,
/// and probing the host for trusted tlls Certificate Authorities.
///
/// # Returns
/// Account struct stored in pddb
///
fn account_setup(&mut self) -> Result<Account, Error> {
log::info!("Attempting to setup a Signal Account");
let service_environment = ServiceEnvironment::Staging;
self.modals
.add_list_item(t!("sigchat.account.link", locales::LANG))
.expect("failed add list item");
self.modals
.add_list_item(t!("sigchat.account.register", locales::LANG))
.expect("failed add list item");
self.modals
.add_list_item(t!("sigchat.account.offline", locales::LANG))
.expect("failed add list item");
self.modals
.get_radiobutton(t!("sigchat.account.title", locales::LANG))
.expect("failed radiobutton modal");
match self.modals.get_radio_index() {
Ok(index) => match index {
0 => self.account_link(),
1 => self.account_register(),
_ => log::warn!("invalid index"),
0 => {
let host = self.host_modal();
let config = Config::new(host, service_environment);
match self.probe_host(config.url()) {
true => Ok(self.account_link(&config)?),
false => Err(Error::new(
ErrorKind::Other,
"failed to trust host certificate",
)),
}
}
1 => {
let host = self.host_modal();
let config = Config::new(host, service_environment);
match self.probe_host(config.url()) {
true => Ok(self.account_register(&config)?),
false => Err(Error::new(
ErrorKind::Other,
"failed to trust host certificate",
)),
}
}
2 => {
log::info!("account setup aborted");
Err(Error::new(ErrorKind::Other, "account setup aborted"))
}
_ => {
log::warn!("invalid index");
Err(Error::new(ErrorKind::Other, "invalid radio index"))
}
},
Err(e) => log::warn!("failed to select register/link {:?}", e),
Err(e) => {
log::warn!("failed to present account setup radio buttons: {:?}", e);
Err(Error::new(
ErrorKind::Other,
"failed to present account setup radio buttons",
))
}
}
}
/// Prompt for a host name from the user
///
/// # Returns
///
/// the host provided by the user
///
fn host_modal(&self) -> Host {
let mut host = None;
while host.is_none() {
host = match self
.modals
.alert_builder(t!("sigchat.account.host.name", locales::LANG))
.field(Some(DEFAULT_HOST.to_string()), None)
.build()
{
Ok(text) => match Host::parse(&text.content()[0].content.to_string()) {
Ok(host) => match host {
Host::Domain(..) => Some(host),
_ => {
self.modals
.show_notification(t!("sigchat.host.invalid", locales::LANG), None)
.expect("notification failed");
None
}
},
Err(_) => {
self.modals
.show_notification(t!("sigchat.host.invalid", locales::LANG), None)
.expect("notification failed");
None
}
},
_ => Host::parse(DEFAULT_HOST).ok(),
}
}
host.unwrap()
}
/// Probe host for tls Certificate Authority chain of trust
///
/// # Arguments
/// * `host` - the dns name or ip address of a Signal server
///
/// # Returns
/// true if at least 1 Certificate Authority is trusted
///
fn probe_host(&self, url: &Url) -> bool {
self.chat
.set_status_text(t!("sigchat.status.probing", locales::LANG));
self.chat.set_busy_state(true);
let tls = Tls::new();
if tls.accessible(url.host_str().unwrap(), true) {
self.chat.set_busy_state(false);
true
} else {
self.modals
.show_notification(t!("sigchat.account.abort", locales::LANG), None)
.expect("abort failed");
self.chat.set_busy_state(false);
false
}
}
@ -96,10 +230,95 @@ impl<'a> SigChat<'a> {
/// Signal allows to link additional devices to your primary device (e.g. Signal-Android).
/// Note that currently Signal allows up to three linked devices per primary.
///
pub fn account_link(&self) {
self.modals
.show_notification(&"sorry - linking is not implemented yet", None)
.expect("notification failed");
/// The user must provide a name for the current device before the Link process
/// commences - culminating by presenting a qr-code to be scanned by the primary device.
///
/// # Arguments
/// * `config` - Signal configuration - host server and Live/Staging environment
///
/// # Returns
/// Account struct stored in pddb
///
pub fn account_link(&mut self, config: &Config) -> Result<Account, Error> {
log::info!("Attempting to Link to an existing Signal Account");
self.chat
.set_status_text(t!("sigchat.status.initializing", locales::LANG));
self.chat.set_busy_state(true);
match Account::new(SIGCHAT_ACCOUNT, config.host(), config.service_environment()) {
Ok(account) => {
let mut manager = Manager::new(account, TrustMode::OnFirstUse);
let name = self.name_modal(
DEFAULT_DEVICE_NAME,
t!("sigchat.account.link.name", locales::LANG),
);
self.chat
.set_status_text(t!("sigchat.status.connecting", locales::LANG));
self.chat.set_busy_state(true);
match manager.link(&name) {
Ok(true) => {
log::info!("Linked Signal Account");
self.chat.set_busy_state(false);
Ok(Account::read(SIGCHAT_ACCOUNT)?)
}
Ok(false) => {
log::info!("failed to link Signal Account");
self.chat.set_busy_state(false);
Err(Error::new(
ErrorKind::Other,
"failed to link Signal Account",
))
}
Err(e) => {
log::warn!("error while linking Signal Account: {e}");
Account::delete(SIGCHAT_ACCOUNT).unwrap_or_else(|e| {
log::warn!("failed to delete unregistered account from pddb: {e}")
});
self.chat.set_busy_state(false);
self.modals
.show_notification(&format!("{}", e), None)
.expect("notification failed");
Err(Error::new(
ErrorKind::Other,
"error while linking Signal Account",
))
}
}
}
Err(e) => {
log::warn!(
"failed to create new Account in pddb:{} : {e}",
SIGCHAT_ACCOUNT
);
self.modals
.show_notification(t!("sigchat.account.failed", locales::LANG), None)
.expect("notification failed");
Err(Error::new(
ErrorKind::Other,
"failed to create new Account in pddb",
))
}
}
}
/// Prompt a name from the user
///
/// # Arguments
/// * `default_name` - A name suggested to the user
/// * `prompt` - A prompt to explain what is being requested
///
/// # Returns
/// the name provided by the user
///
fn name_modal(&self, default_name: &str, prompt: &str) -> String {
match self
.modals
.alert_builder(prompt)
.field(Some(default_name.to_string()), None)
.build()
{
Ok(text) => text.content()[0].content.to_string(),
_ => default_name.to_string(),
}
}
/// Register a new Signal Account with this as the primary device.
@ -108,27 +327,31 @@ impl<'a> SigChat<'a> {
/// The phone number must include the country calling code, i.e. the number must start with a "+" sign.
/// Warning: this will disable the authentication of your phone as a primary device.
///
pub fn account_register(&mut self) {
/// # Arguments
///
/// * `config` - Signal configuration - host server and Live/Staging environment
///
pub fn account_register(&mut self, config: &Config) -> Result<Account, Error> {
log::info!("Attempting to Register a new Signal Account");
self.modals
.show_notification(&"sorry - registration is not implemented yet", None)
.expect("notification failed");
match self.number_modal() {
Ok(number) => {
log::info!("registration phone number = {:?}", number);
match Account::new(SIGCHAT_ACCOUNT) {
Ok(mut account) => {
match account.set_number(&number.to_string()) {
Ok(_number) => {
self.manager = Some(Manager::new(account, TrustMode::OnFirstUse));
}
Err(e) => log::warn!("failed to set Account number: {e}"),
match Account::new(SIGCHAT_ACCOUNT, config.host(), config.service_environment()) {
Ok(mut account) => match account.set_number(&number.to_string()) {
Ok(_number) => {
self.manager = Some(Manager::new(account, TrustMode::OnFirstUse));
}
}
Err(e) => log::warn!("failed to set Account number: {e}"),
},
Err(e) => log::warn!("failed to create new Account: {e}"),
}
}
Err(e) => log::warn!("failed to get phone number: {e}"),
}
Err(Error::new(ErrorKind::Other, "not implmented"))
}
/// Attempts to obtain a phone number from the user.
@ -173,6 +396,9 @@ impl<'a> SigChat<'a> {
/// If wifi is not connected then a modal offers to "Connect to wifi?"
/// and tries for 10 seconds before representing.
///
/// # Returns
/// true when wifi is connected
///
pub fn wifi(&self) -> bool {
if HOSTED_MODE {
return true;

View file

@ -61,22 +61,6 @@ fn wrapped_main() -> ! {
);
let cid = xous::connect(sid).unwrap();
chat.menu_add(MenuItem {
name: xous_ipc::String::from_str(t!("sigchat.register", locales::LANG)),
action_conn: Some(cid),
action_opcode: SigchatOp::Menu as u32,
action_payload: MenuPayload::Scalar([MenuOp::Register as u32, 0, 0, 0]),
close_on_select: true,
})
.expect("failed add menu");
chat.menu_add(MenuItem {
name: xous_ipc::String::from_str(t!("sigchat.link", locales::LANG)),
action_conn: Some(cid),
action_opcode: SigchatOp::Menu as u32,
action_payload: MenuPayload::Scalar([MenuOp::Link as u32, 0, 0, 0]),
close_on_select: true,
})
.expect("failed add menu");
chat.menu_add(MenuItem {
name: xous_ipc::String::from_str(t!("sigchat.menu.close", locales::LANG)),
action_conn: Some(cid),
@ -100,7 +84,13 @@ fn wrapped_main() -> ! {
Some(Event::Focus) => {
if first_focus {
first_focus = false;
sigchat.connect();
match sigchat.connect() {
Ok(true) => log::info!("connected to Signal Account"),
Ok(false) => log::info!("not connected to Signal Account"),
Err(e) => {
log::warn!("error while connecting to Signal Account: {e}")
}
}
}
sigchat.redraw();
}
@ -112,9 +102,7 @@ fn wrapped_main() -> ! {
log::info!("got Chat Menu Click");
xous::msg_scalar_unpack!(msg, menu_code, _, _, _, {
match FromPrimitive::from_usize(menu_code) {
Some(MenuOp::Link) => sigchat.account_link(),
Some(MenuOp::Noop) => {}
Some(MenuOp::Register) => sigchat.account_register(),
_ => (),
}
});

View file

@ -1,12 +1,21 @@
mod config;
mod group_permission;
pub mod libsignal; // stub
mod link_state;
mod signal_ws;
mod trust_mode;
use crate::Account;
pub use config::Config;
use group_permission::GroupPermission;
use link_state::LinkState;
use std::io::Error;
use locales::t;
use modals::Modals;
use signal_ws::SignalWS;
use std::io::{Error, ErrorKind};
use std::time::Duration;
pub use trust_mode::TrustMode;
use tungstenite::Message;
// Structure modeled on signal-cli by AsamK <asamk@gmx.de> and contributors - https://github.com/AsamK/signal-cli.
// signal-cli is a commandline interface for libsignal-service-java. It supports registering, verifying, sending and receiving messages.
@ -14,8 +23,10 @@ pub use trust_mode::TrustMode;
#[allow(dead_code)]
pub struct Manager {
account:Account,
account: Account,
config: Config,
trust_mode: TrustMode,
websocket: Option<SignalWS>,
log_verbose: bool,
log_scrub: bool,
log_send: bool,
@ -29,9 +40,15 @@ impl Manager {
/// * `account` - Specify your phone number, that will be your identifier. The phone number must include the country calling code, i.e. the number must start with a "+" sign.
///
pub fn new(account: Account, trust_mode: TrustMode) -> Manager {
let config = Config::new(
account.host().clone(),
account.service_environment().clone(),
);
Manager {
account,
config,
trust_mode,
websocket: None,
log_verbose: false,
log_scrub: false,
log_send: false,
@ -122,12 +139,98 @@ impl Manager {
/// Link to an existing device, instead of registering a new number. This shows a "sgnl://linkdevice?uuid=…​" URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
///
/// Linking sigchat to an existing Signal Account involves:
/// 1. establishing a tls connection to the Signal Provisioning websocket server
/// 2. obtaining a uuid via the websocket
/// 3. generating a key-pair
/// 4. displaying a uri as a qr-code (containing the uuid and pub_key)
/// 5. scanning the qr-code with the primary Signal device
/// 6. obtaining device registration via the websocket
/// 7. checking and saving account details
///
/// # Arguments
/// * `name` - Optionally specify a name to describe this new device. By default "cli" will be used.
/// * `name` - name to describe this new device
/// * `config` - Signal configuration - host server and Live/Staging environment
/// * `service_environment` - service_environment to connect to on host
///
#[allow(dead_code)]
pub fn link(_name: &str) -> Result<(), Error> {
todo!();
pub fn link(&mut self, name: &str) -> Result<bool, Error> {
let link_err = Error::new(ErrorKind::Other, "failed to link device");
let mut host = self.config.url().clone();
match SignalWS::new_provision(&mut host) {
Ok(mut ws) => {
log::info!("provisioning websocket established to {host}");
let result = match ws.read(Some(Duration::from_millis(5000))) {
Ok(Message::Binary(uuid)) => {
log::info!("received Provisioning UUID message from host");
let uuid = libsignal::ProvisioningUuid::decode(uuid).id.clone();
let identity_key_pair = libsignal::generate_identity_key_pair();
let pub_key = identity_key_pair.djb_identity_key.key.clone();
match url::Url::parse_with_params(
"sgnl://linkdevice",
&[("uuid", &uuid.as_str()), ("pub_key", &pub_key.as_str())],
) {
Ok(device_link_uri) => {
log::info!("device_link_uri: {device_link_uri}");
let xns = xous_names::XousNames::new().unwrap();
let modals =
Modals::new(&xns).expect("can't connect to Modals server");
modals
.show_notification(
t!("sigchat.account.link.scan", locales::LANG),
Some(device_link_uri.as_str()),
)
.expect("qrcode failed");
match ws.read(Some(Duration::from_millis(5000))) {
Ok(Message::Binary(registration)) => {
log::info!("Registration message received from host");
match self.account.link(
name,
libsignal::ProvisionMessage::decode(
identity_key_pair,
registration,
),
) {
Ok(result) => Ok(result),
Err(e) => {
log::warn!("linking error: {e}");
Ok(false)
}
}
}
Ok(_) => {
log::warn!("unexpected Provisioning msg");
Err(link_err)
}
Err(e) => {
log::warn!("{e}");
Err(link_err)
}
}
}
Err(e) => {
log::info!("{}", format!("{e}"));
Err(link_err)
}
}
}
Ok(_) => {
log::warn!("unexpected Provisioning msg.");
Err(link_err)
}
Err(e) => {
log::warn!("{e}");
Err(link_err)
}
};
ws.close();
result
}
Err(e) => {
log::info!("failed to connect to server: {}", e);
Err(e)
}
}
}
/// Link another device to this device. Only works, if this is the primary device.

View file

@ -0,0 +1,9 @@
The structure of the Signal interface is modelled on [signal-cli](https://github.com/AsamK/signal-cli) by AsamK and contributors. signal-cli is a command-line interface for libsignal-service-java. It supports registering, verifying, sending and receiving messages. This development provides a welcome road-map of the various common transactions between a Signal client and server - see the signal-cli [Manual Page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc). This road-map is sketched out in apps/sigchat/src/manager.rs, but will be subject to refinement as sigchat development progresses.
Manager::methods() will likely make calls on the official [libsignal](https://github.com/signalapp/libsignal#overview) library.
> libsignal contains platform-agnostic APIs used by the official Signal clients and servers, exposed as a Java, Swift, or TypeScript library. The underlying implementations are written in Rust.
> This repository is used by the Signal client apps ([Android](https://github.com/signalapp/Signal-Android), [iOS](https://github.com/signalapp/Signal-iOS), and [Desktop](https://github.com/signalapp/Signal-Desktop)) as well as server-side. Use outside of Signal is unsupported.
libsignal is released under the AGPLv3 licence, and therefore not directly compatible with xous released under the MIT license. Therefore, libsignal must be wrapped in a suitable API, and included as a binary in the hardware image.

View file

@ -0,0 +1,38 @@
use crate::ServiceEnvironment;
use url::{Host, Url};
pub struct Config {
host: Host,
service_environment: ServiceEnvironment,
url: Url,
}
impl Config {
pub fn new(host: Host, service_environment: ServiceEnvironment) -> Self {
let host_base = host.to_string();
match service_environment {
ServiceEnvironment::Live => Self {
host: host,
service_environment: service_environment,
url: Url::parse(&format!("https://chat.{}", host_base)).unwrap(),
},
ServiceEnvironment::Staging => Self {
host: host,
service_environment: service_environment,
url: Url::parse(&format!("https://chat.staging.{}", host_base)).unwrap(),
},
}
}
pub fn host(&self) -> &Host {
&self.host
}
pub fn service_environment(&self) -> &ServiceEnvironment {
&self.service_environment
}
pub fn url(&self) -> &Url {
&self.url
}
}

View file

@ -0,0 +1,147 @@
// this is a stub for a wrapped libsignal
// use org.whispersystems.signalservice.api.push.SignalServiceAddress;
pub struct SignalServiceAddress {}
impl SignalServiceAddress {
pub const DEFAULT_DEVICE_ID: u32 = 0;
}
////////////////////////////////////////////////////////
// use org.whispersystems.signalservice.api.util.DeviceNameUtil;
pub struct DeviceNameUtil {}
impl DeviceNameUtil {
pub fn encrypt_device_name(_device_name: &str, _aci_private_key: IdentityKey) -> String {
"STUB".to_string()
}
}
////////////////////////////////////////////////////////
// use org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher;
pub struct PrimaryProvisioningCipher {}
impl PrimaryProvisioningCipher {
pub fn new(_stub: Option<String>) -> PrimaryProvisioningCipher {
PrimaryProvisioningCipher {}
}
pub fn decrypt(&self, _temp_identity: IdentityKeyPair, bytes: Vec<u8>) -> ProvisionMessage {
//log::info!("temp_identity: {:?}", temp_identity);
log::info!("raw uuid Protocol Buffer: {:?}", bytes);
ProvisionMessage {
number: "STUB number".to_string(),
aci: IdentityKeyPair {
service_id: "STUB".to_string(),
djb_identity_key: IdentityKey {
key: "STUB".to_string(),
},
djb_private_key: IdentityKey {
key: "STUB".to_string(),
},
},
pni: IdentityKeyPair {
service_id: "STUB".to_string(),
djb_identity_key: IdentityKey {
key: "STUB".to_string(),
},
djb_private_key: IdentityKey {
key: "STUB".to_string(),
},
},
master_key: "STUB number".to_string(),
profile_key: Some("STUB number".to_string()),
}
}
}
pub struct ProvisionMessage {
pub number: String,
pub aci: IdentityKeyPair,
pub pni: IdentityKeyPair,
pub master_key: String,
pub profile_key: Option<String>,
}
impl ProvisionMessage {
pub fn decode(temp_identity: IdentityKeyPair, bytes: Vec<u8>) -> ProvisionMessage {
let primary_provisioning_cipher = PrimaryProvisioningCipher::new(None);
primary_provisioning_cipher.decrypt(temp_identity, bytes)
}
}
////////////////////////////////////////////////////////
// https://github.com/signalapp/Signal-Android/blob/d2053d2db7b1b930b7058ce5506dd6037ac3b808/libsignal-service/src/main/protowire/Provisioning.proto#L13C9-L15
//
// message ProvisioningUuid {
// optional string uuid = 1;
// }
pub struct ProvisioningUuid {
pub id: String,
}
impl ProvisioningUuid {
pub fn decode(bytes: Vec<u8>) -> ProvisioningUuid {
log::info!("raw uuid Protocol Buffer: {:?}", bytes);
ProvisioningUuid {
id: "TODO decode uuid Protocol Buffer".to_string(),
}
}
}
//////////////////////////////////////////////////////////
// use org.signal.libsignal.protocol.IdentityKey;
pub struct IdentityKey {
pub key: String,
}
impl IdentityKey {
pub fn new(key: String) -> Self {
IdentityKey { key }
}
pub fn clone(&self) -> IdentityKey {
IdentityKey::new(self.key.clone())
}
}
// use org.signal.libsignal.protocol.IdentityKeyPair;
pub struct IdentityKeyPair {
pub service_id: String,
pub djb_identity_key: IdentityKey,
pub djb_private_key: IdentityKey,
}
// use org.signal.libsignal.protocol.ecc.Curve;
pub struct Curve {}
impl Curve {
pub fn generate_key_pair() -> DjbKeyPair {
DjbKeyPair {
djb_private_key: IdentityKey::new("STUB privateIdentityKey".to_string()),
djb_public_key: IdentityKey::new("STUB publicIdentityKey".to_string()),
}
}
}
pub struct DjbKeyPair {
djb_private_key: IdentityKey,
djb_public_key: IdentityKey,
}
impl DjbKeyPair {
pub fn get_private_key(&self) -> IdentityKey {
self.djb_private_key.clone()
}
pub fn get_public_key(&self) -> IdentityKey {
self.djb_public_key.clone()
}
}
// https://github.com/AsamK/signal-cli/blob/375bdb79485ec90beb9a154112821a4657740b7a/lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java#L45-L51
pub fn generate_identity_key_pair() -> IdentityKeyPair {
let djb_key_pair = Curve::generate_key_pair();
let djb_identity_key = IdentityKey::new(djb_key_pair.get_public_key().key);
let djb_private_key = djb_key_pair.get_private_key();
IdentityKeyPair {
service_id: "STUB".to_string(),
djb_identity_key,
djb_private_key,
}
}

View file

@ -0,0 +1,180 @@
use rustls::{ClientConnection, StreamOwned};
use std::io::{Error, ErrorKind};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tls::Tls;
use tungstenite::{Message, WebSocket};
use url::Url;
const PROVISIONING_PATH: [&str; 4] = ["v1", "websocket", "provisioning", ""];
const REGISTRATION_PATH: [&str; 3] = ["v1", "registration", ""];
#[allow(dead_code)]
pub struct SignalWS {
ws: Arc<Mutex<WebSocket<StreamOwned<ClientConnection, TcpStream>>>>,
}
impl SignalWS {
#[allow(dead_code)]
pub fn new_message(_host: &str) -> Result<Self, Error> {
todo!();
}
fn new(url: &Url) -> Result<Self, Error> {
match SignalWS::connect(url) {
Ok(ws) => Ok(Self {
ws: Arc::new(Mutex::new(ws)),
}),
Err(e) => Err(e),
}
}
pub fn new_provision(url: &mut Url) -> Result<Self, Error> {
url.set_scheme("wss").expect("failed to set scheme");
url.path_segments_mut()
.expect("failed to add path")
.extend(&PROVISIONING_PATH);
Ok(Self::new(&url)?)
}
#[allow(dead_code)]
pub fn new_register(url: &mut Url) -> Result<Self, Error> {
url.set_scheme("wss").expect("failed to set scheme");
url.path_segments_mut()
.expect("failed to add path")
.extend(&REGISTRATION_PATH);
Ok(Self::new(&url)?)
}
pub fn close(&mut self) {
log::info!("attempting to close websocket connection");
let ws = self.ws.clone();
thread::spawn(move || loop {
if let Ok(mut ws) = ws.lock() {
ws.close(None)
.unwrap_or_else(|e| log::warn!("failed to close websocket: {e}"));
loop {
match ws.flush() {
Ok(()) => (),
Err(
tungstenite::Error::ConnectionClosed
| tungstenite::Error::AlreadyClosed,
) => {
log::info!("websocket connection closed");
break;
}
Err(e) => {
log::warn!("{e}");
break;
}
}
}
};
});
}
/// Reads a msg from the websocket with optional timeout
///
/// Hint: timeout = None is more efficient than Some(Duration(1)
///
/// # Arguments
///
/// * `timeout` - a duration before the read operation times-out and returns
///
/// # Returns
/// a message read from the websocket or ErrorKind::TimedOut
///
pub fn read(&mut self, timeout: Option<Duration>) -> Result<Message, Error> {
match timeout {
Some(duration) => {
let (tx, rx) = std::sync::mpsc::channel();
let ws = self.ws.clone();
thread::spawn(move || {
if let Ok(mut ws) = ws.lock() {
tx.send(ws.read())
.unwrap_or_else(|e| log::warn!("failed to forward ws msg: {e}"));
}
});
match rx.recv_timeout(duration) {
Ok(rx_msg) => match rx_msg {
Ok(msg) => Ok(msg),
Err(e) => {
log::warn!("{e}");
Err(Error::new(ErrorKind::Other, "error on reading ws"))
}
},
Err(e) => {
log::warn!("{e}");
Err(Error::new(ErrorKind::TimedOut, e))
}
}
}
None => {
let msg = if let Ok(mut ws) = self.ws.lock() {
match ws.read() {
Ok(msg) => Ok(msg),
Err(e) => {
log::warn!("{e}");
Err(Error::new(ErrorKind::Other, "error on reading ws"))
}
}
} else {
Err(Error::new(
ErrorKind::Other,
"failed to get lock on websocket",
))
};
msg
}
}
}
#[allow(dead_code)]
pub fn send(&mut self, _message: Message) -> Result<(), Error> {
todo!()
}
/// Make a websocket connection to host server
///
/// # Arguments
/// * `url` - url of Signal server
///
/// # Returns
///
fn connect(url: &Url) -> Result<WebSocket<StreamOwned<ClientConnection, TcpStream>>, Error> {
log::info!("attempting websocket connection to {}", url.as_str());
let host = url.host_str().expect("failed to extract host from url");
match TcpStream::connect((host, 443)) {
Ok(sock) => {
log::info!("tcp connected to {host}");
let xtls = Tls::new();
match xtls.stream_owned(host, sock) {
Ok(tls_stream) => {
log::info!("tls configured");
match tungstenite::client(url, tls_stream) {
Ok((socket, response)) => {
log::info!("Websocket connected to: {}", url.as_str());
log::info!("Response HTTP code: {}", response.status());
Ok(socket)
}
Err(e) => {
log::info!("failed to connect websocket: {}", e);
Err(Error::from(ErrorKind::ConnectionRefused))
}
}
}
Err(e) => {
log::warn!("failed to configure tls: {e}");
Err(e)
}
}
}
Err(e) => {
log::warn!("failed to connect tcp: {e}");
Err(e)
}
}
}
}

View file

@ -1,11 +1,11 @@
{
"tls.check_trust_prompt": {
"en": "Do you trust any of these certificate authorities?",
"en-tts": "Do you trust any of these certificate authorities?"
"en": "Please select trusted certificate authorities.",
"en-tts": "Please select trusted certificate authorities."
},
"tls.cmd_help": {
"en": "Establishing an encrypted tls connection requires the host to provide a signed Certificate of identity. Each Certificate is signed for authenticity by a Certificate Authority. The CA's Certificate will in-turn be signed by yet another CA. These signed Certificates link into a chain of trust back to a trusted Root CA.\n\nTypically, this all happens automagically because many OS's & browsers incorporate a long list of trusted Root CA Certiicates. And this is OK because you trust the CA's that your OS trusts, right!\n\nOn Precursor, you must explicitly trust one or more root CA Certificates. You can:\nnet tls probe <host>\n\tto trust a specific CA Certificate, or\nnet tls mozilla\n\tto trust all of the CA Certificates incporporated in the Firefox browser.",
"en-tts": "Establishing an encrypted tls connection requires the host to provide a signed Certificate of identity. Each Certificate is signed for authenticity by a Certificate Authority. The CA's Certificate will in-turn be signed by yet another CA. These signed Certificates link into a chain of trust back to a trusted Root CA.\n\nTypically, this all happens automagically because many OS's & browsers incorporate a long list of trusted Root CA Certiicates. And this is OK because you trust the CA's that your OS trusts, right!\n\nOn Precursor, you must explicitly trust one or more root CA Certificates. You can:\nnet tls probe <host>\n\tto trust a specific CA Certificate, or\nnet tls mozilla\n\tto trust all of the CA Certificates incporporated in the Firefox browser."
"en": "Establishing an encrypted tls connection requires the host to provide a signed Certificate of identity. Each Certificate is signed for authenticity by a Certificate Authority. The CA's Certificate will in-turn be signed by yet another CA. These signed Certificates link into a chain of trust back to a trusted Root CA.\n\nTypically, this all happens automagically because many OS's & browsers incorporate a long list of trusted Root CA Certiicates. And this is OK because you trust the CA's that your OS trusts, right!\n\nOn Precursor, you must explicitly trust one or more root CA Certificates. You can:\nnet tls inspect <host>\n\tto trust a specific CA Certificate, or\nnet tls mozilla\n\tto trust all of the CA Certificates incporporated in the Firefox browser.",
"en-tts": "Establishing an encrypted tls connection requires the host to provide a signed Certificate of identity. Each Certificate is signed for authenticity by a Certificate Authority. The CA's Certificate will in-turn be signed by yet another CA. These signed Certificates link into a chain of trust back to a trusted Root CA.\n\nTypically, this all happens automagically because many OS's & browsers incorporate a long list of trusted Root CA Certiicates. And this is OK because you trust the CA's that your OS trusts, right!\n\nOn Precursor, you must explicitly trust one or more root CA Certificates. You can:\nnet tls inspect <host>\n\tto trust a specific CA Certificate, or\nnet tls mozilla\n\tto trust all of the CA Certificates incporporated in the Firefox browser."
},
"tls.cmd": {
"en": "net tls <sub-command>",
@ -35,11 +35,11 @@
"en": "trusted Root CA's",
"en-tts": "trusted Root CA's"
},
"tls.probe_cmd": {
"tls.inspect_cmd": {
"en": "save host CA'a if trusted",
"en-tts": "save host CA'a if trusted"
},
"tls.probe_fail_servername": {
"tls.inspect_fail_servername": {
"en": "failed to create server name from",
"en-tts": "failed to create server name from"
},
@ -47,15 +47,11 @@
"en": "Invalid certificate : ",
"en-tts": "Invalid certificate : "
},
"tls.probe_help_unknown_issuer": {
"en": "you may choose to trust a known CA",
"en-tts": "you may choose to trust a known CA"
},
"tls.probe_help_not_valid_yet": {
"en": "error maybe caused by an improperly set clock",
"en-tts": "error maybe caused by an improperly set clock"
},
"tls.probe_done": {
"tls.inspect_done": {
"en": "trusted CA's",
"en-tts": "trusted CA's"
},

View file

@ -21,7 +21,7 @@ pub fn shellchat<'a>(
Some("deleteall") => {
log::info!("starting TLS delete certificates");
let tls = Tls::new();
let count = tls.del_all_cert().unwrap();
let count = tls.del_all_rota().unwrap();
write!(ret, "{} {}", count, t!("tls.deleteall_done", locales::LANG)).ok();
log::info!("finished TLS delete certificates");
}
@ -62,30 +62,32 @@ pub fn shellchat<'a>(
count = 0;
let tls = Tls::new();
for rota in rotas {
tls.save_cert(&rota).unwrap_or_else(|e| log::warn!("{e}"));
tls.save_rota(&rota).unwrap_or_else(|e| log::warn!("{e}"));
modals.update_progress(count).expect("no progress");
count += 1;
}
modals.finish_progress().expect("finish progress");
write!(ret, "{} {}", count, t!("tls.mozilla_done", locales::LANG)).ok();
}
// probe establishes a tls connection to the supplied host, extracts the
// inspect establishes a tls connection to the supplied host, extracts the
// certificates offered and immediately closes the connection.
// The certificates are presented by modal to the user, and saved to the
// pddb if trusted.
Some("probe") => {
Some("inspect") => {
log::set_max_level(log::LevelFilter::Info);
let target = match tokens.next() {
Some(target) => target,
None => "betrusted.io",
};
let tls = Tls::new();
match tls.probe(target) {
Ok(count) => write!(ret, "{} {}", count, t!("tls.probe_done", locales::LANG)).ok(),
match tls.inspect(target) {
Ok(count) => {
write!(ret, "{} {}", count, t!("tls.inspect_done", locales::LANG)).ok()
}
Err(_) => write!(
ret,
"{} {target}",
t!("tls.probe_fail_servername", locales::LANG)
t!("tls.inspect_fail_servername", locales::LANG)
)
.ok(),
};
@ -170,8 +172,8 @@ pub fn shellchat<'a>(
write!(ret, "\tmozilla\t{}\n", t!("tls.mozilla_cmd", locales::LANG)).ok();
write!(
ret,
"\tprobe <host>\t{}\n",
t!("tls.probe_cmd", locales::LANG)
"\tinspect <host>\t{}\n",
t!("tls.inspect_cmd", locales::LANG)
)
.ok();
write!(

View file

@ -34,26 +34,32 @@ impl rustls::client::ServerCertVerifier for StifledCertificateVerification {
Err(Error::InvalidCertificate(e)) => {
let xns = XousNames::new().unwrap();
let modals = Modals::new(&xns).unwrap();
modals
.show_notification(
format!("{}\n{:?}", t!("tls.probe_invalid_certificate", locales::LANG), e).as_str(),
None,
)
.expect("modal failed");
match e {
CertificateError::UnknownIssuer => {
modals
.show_notification(t!("tls.probe_help_unknown_issuer", locales::LANG), None)
.expect("modal failed");
Ok(ServerCertVerified::assertion())
}
CertificateError::UnknownIssuer => Ok(ServerCertVerified::assertion()),
CertificateError::NotValidYet => {
modals
.show_notification(t!("tls.probe_help_not_valid_yet", locales::LANG), None)
.show_notification(
t!("tls.probe_help_not_valid_yet", locales::LANG),
None,
)
.expect("modal failed");
Err(Error::InvalidCertificate(e))
}
_ => Err(Error::InvalidCertificate(e)),
_ => {
modals
.show_notification(
format!(
"{}\n{:?}",
t!("tls.probe_invalid_certificate", locales::LANG),
e
)
.as_str(),
None,
)
.expect("modal failed");
Err(Error::InvalidCertificate(e))
}
}
}
Err(e) => Err(e),

View file

@ -11,7 +11,7 @@ use rkyv::{
ser::{serializers::WriteSerializer, Serializer},
Deserialize,
};
use rustls::{Certificate, ClientConfig, RootCertStore};
use rustls::{Certificate, ClientConfig, ClientConnection, RootCertStore};
use sha2::Digest;
use std::convert::{Into, TryFrom, TryInto};
use std::io::{Error, ErrorKind, Read, Write};
@ -45,7 +45,7 @@ impl Tls {
///
/// a count of trusted certificates
///
pub fn check_trust(&self, certificates: &[Certificate]) -> usize {
pub fn trust_modal(&self, certificates: Vec<Certificate>) -> usize {
let xns = XousNames::new().unwrap();
let modals = Modals::new(&xns).unwrap();
@ -84,7 +84,7 @@ impl Tls {
.map(|i| &certificates[*i].1)
.map(|x509| RustlsOwnedTrustAnchor::from(x509))
.for_each(|rota| {
self.save_cert(&rota).unwrap_or_else(|e| {
self.save_rota(&rota).unwrap_or_else(|e| {
log::warn!("failed to save cert: {e}");
modals
.show_notification(
@ -109,7 +109,7 @@ impl Tls {
///
/// the number of trust-anchors deleted
///
pub fn del_all_cert(&self) -> Result<usize, Error> {
pub fn del_all_rota(&self) -> Result<usize, Error> {
let count = match self.pddb.list_keys(TLS_TRUSTED_DICT, None) {
Ok(list) => list.len(),
Err(_) => 0,
@ -137,7 +137,7 @@ impl Tls {
///
/// * `key` - the pddb-key containing the unwanted trust-anchor
///
pub fn del_cert(&self, key: &str) -> Result<(), Error> {
pub fn del_rota(&self, key: &str) -> Result<(), Error> {
match self.pddb.delete_key(TLS_TRUSTED_DICT, key, None) {
Ok(_) => {
log::info!("Deleted {}:{}\n", TLS_TRUSTED_DICT, key);
@ -157,7 +157,7 @@ impl Tls {
///
/// * `ta` - a trusted trust-anchor
///
pub fn save_cert(&self, ta: &RustlsOwnedTrustAnchor) -> Result<(), Error> {
pub fn save_rota(&self, ta: &RustlsOwnedTrustAnchor) -> Result<(), Error> {
let key = ta.pddb_key();
match self.pddb.get(
TLS_TRUSTED_DICT,
@ -206,7 +206,7 @@ impl Tls {
///
/// * `key` - pddb key holding the trust-anchor
///
pub fn get_cert(&self, key: &str) -> Option<RustlsOwnedTrustAnchor> {
pub fn get_rota(&self, key: &str) -> Option<RustlsOwnedTrustAnchor> {
match self.pddb.get(
TLS_TRUSTED_DICT,
key,
@ -227,8 +227,8 @@ impl Tls {
let archive =
unsafe { rkyv::archived_value::<RustlsOwnedTrustAnchor>(&bytes, pos) };
let ta = archive.deserialize(&mut AllocDeserializer {}).ok();
log::info!("get {}", key);
log::trace!("get '{}' = '{:?}'", key, &ta);
log::info!("get trust anchor {}", key);
log::trace!("get trust anchor'{}' = '{:?}'", key, &ta);
ta
}
Err(e) => {
@ -250,7 +250,7 @@ impl Tls {
match self.pddb.list_keys(TLS_TRUSTED_DICT, None) {
Ok(list) => list
.iter()
.map(|key| self.get_cert(&key))
.map(|key| self.get_rota(&key))
.filter_map(|rota| rota)
.collect::<Vec<RustlsOwnedTrustAnchor>>(),
Err(e) => {
@ -260,6 +260,59 @@ impl Tls {
}
}
/// Checks if the rustls Certificate provided is trusted (saved in pddb)
///
/// # Arguments
///
/// * `cert` - an rustls Certificate to be checked
///
/// # Returns
///
/// true if the certificate is saved in the TLS_TRUSTED_DICT in the pddb
///
pub fn is_trusted_cert(&self, cert: Certificate) -> bool {
match X509Certificate::from_der(cert.as_ref()) {
Ok(result) => self.is_trusted_x509(&result.1),
Err(e) => {
log::warn!("failed to get x509 from Certificate: {e}");
false
}
}
}
/// Checks if the x509 Certificate provided is trusted (saved in pddb)
///
/// $ Arguments
///
/// * `x509` - an x509 Certificate to be checked
///
/// # Returns
///
/// true if the certificate is saved in the TLS_TRUSTED_DICT in the pddb
///
pub fn is_trusted_x509(&self, x509: &X509Certificate) -> bool {
let ta = RustlsOwnedTrustAnchor::from(x509);
let key = ta.pddb_key();
match self.pddb.get(
TLS_TRUSTED_DICT,
&key,
None,
false,
false,
None,
None::<fn()>,
) {
Ok(_) => {
log::info!("trusted: {key}");
true
}
Err(_) => {
log::info!("UNtrusted: {key}");
false
}
}
}
/// Returns a RootCertStore containing all trusted trust-anchors
///
pub fn root_store(&self) -> RootCertStore {
@ -268,7 +321,7 @@ impl Tls {
Ok(list) => {
let rota = list
.iter()
.map(|key| self.get_cert(&key))
.map(|key| self.get_rota(&key))
.filter_map(|rota| rota)
.map(|t| Into::<rustls::OwnedTrustAnchor>::into(t));
root_store.add_trust_anchors(rota);
@ -278,12 +331,10 @@ impl Tls {
root_store
}
/// Probes the target and returns a count of newly trusted 1 certificates
/// Probes the host and returns the TLS chain of trust
///
/// Establishes a tls connection to the target host, extracts the
/// Establishes a tls connection to the host, extracts the
/// certificates offered and immediately closes the connection.
/// The certificates are presented by modal to the user, and saved to the
/// pddb if trusted.
///
/// By default, rustls only provides access to a trusted certificate chain.
/// Probe briefly stifles the certificate validation (ie trusts everything)
@ -292,9 +343,13 @@ impl Tls {
///
/// # Arguments
///
/// * `target` - the target tls site (i.e. betrusted.io)
/// * `host` - the target tls site (i.e. betrusted.io)
///
pub fn probe(&self, target: &str) -> Result<usize, Error> {
/// # Returns
///
/// The TLS chain of trust for the host
///
pub fn probe(&self, host: &str) -> Result<Vec<Certificate>, Error> {
log::info!("starting TLS probe");
// Attempt to open the tls connection with an empty root_store
let root_store = rustls::RootCertStore::empty();
@ -306,12 +361,12 @@ impl Tls {
.with_safe_defaults()
.with_custom_certificate_verifier(stifled_verifier)
.with_no_client_auth();
match target.try_into() {
match host.try_into() {
Ok(server_name) => {
let mut conn =
rustls::ClientConnection::new(Arc::new(config), server_name).unwrap();
log::info!("connect TCPstream to {}", target);
match TcpStream::connect((target, 443)) {
log::info!("connect TCPstream to {}", host);
match TcpStream::connect((host, 443)) {
Ok(mut sock) => {
match conn.complete_io(&mut sock) {
Ok(_) => log::info!("handshake complete"),
@ -319,8 +374,8 @@ impl Tls {
}
conn.send_close_notify();
match conn.peer_certificates() {
Some(certificates) => Ok(self.check_trust(certificates)),
None => Ok(0),
Some(certificates) => Ok(certificates.to_vec()),
None => Ok(vec![]),
}
}
Err(e) => {
@ -330,18 +385,101 @@ impl Tls {
}
}
Err(e) => {
log::warn!("failed to create sever_name from {target}: {e}");
log::warn!("failed to create sever_name from {host}: {e}");
Err(Error::from(ErrorKind::InvalidInput))
}
}
}
/// Inspect and optionally trust Certificates offered by the host.
///
/// Probes the host and presents the certificates offered in a modal.
/// The user can optionally save trusted certificates to the pddb.
///
/// # Arguments
///
/// * `host` - the target tls site (i.e. betrusted.io)
///
/// # Returns
///
/// the number of trusted Certificates offered by the host
///
pub fn inspect(&self, host: &str) -> Result<usize, Error> {
match self.probe(host) {
Ok(certs) => Ok(self.trust_modal(certs)),
Err(e) => {
log::warn!("failed to probe {host}: {e}");
Ok(0)
}
}
}
/// Check if host offers a trusted Certificate, and optionally inspect if none trusted.
///
/// Probes the host and checks if any of the Certificates are trusted. If none are
/// trusted, and inspect==true, then the offered certificates are presented in a modal.
/// The user can optionally save trusted certificates to the pddb.
///
/// # Arguments
///
/// * `host` - the target tls site (i.e. betrusted.io)
/// * `inspect` - if no trusted certificates then inspect those offered
///
/// # Returns
///
/// true if the user trusts at least one of the Certificates offered by the host.
///
pub fn accessible(&self, host: &str, inspect: bool) -> bool {
match self.probe(host) {
Ok(certs) => {
match certs
.iter()
.find(|&cert| self.is_trusted_cert(cert.clone()))
{
Some(_) => true,
None => inspect && (self.trust_modal(certs) > 0),
}
}
Err(e) => {
log::warn!("failed to probe {host}: {e}");
false
}
}
}
pub fn client_config(&self) -> ClientConfig {
rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(self.root_store())
.with_no_client_auth()
}
/// Construct a tls-stream on the tcp-stream provided
///
/// # Arguments
///
/// * `host` - the host end-point of the stream
/// * `sock` - a tcp-stream connected to host
///
/// # Returns
///
/// an owned rusttls stream on the tcp-stream provided
pub fn stream_owned(
&self,
host: &str,
sock: TcpStream,
) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, Error> {
match rustls::ClientConnection::new(
Arc::new(self.client_config()),
host.try_into().expect("failed url host_str"),
) {
Ok(conn) => Ok(rustls::StreamOwned::new(conn, sock)),
Err(_) => Err(Error::new(
ErrorKind::Other,
"failed to configure client connection",
)),
}
}
}
// https://stackoverflow.com/questions/57029974/how-to-split-string-into-chunks-in-rust-to-insert-spaces

View file

@ -142,9 +142,7 @@ fn add_der_header(tag: Tag, naked: &Vec<u8>) -> Result<Vec<u8>, Error> {
Ok(header) => {
let mut buff: [u8; 32] = [0u8; 32];
match header.encode_to_slice(&mut buff) {
Ok(der) => {
Ok([der, naked].concat())
}
Ok(der) => Ok([der, naked].concat()),
Err(_) => Err(Error::new(
ErrorKind::InvalidData,
"der parse failed: encode",

View file

@ -37,8 +37,8 @@ impl ureq::TlsConnector for TlsConnector {
if let Some(inner) = e.get_ref() {
if let Some(rustls_error) = inner.downcast_ref::<rustls::Error>() {
if let rustls::Error::InvalidCertificate(_) = rustls_error {
if let Ok(n) = tls.probe(dns_name) {
if n > 0 {
if let Ok(certs) = tls.probe(dns_name) {
if certs.len() > 0 {
log::info!("try again with new trusted certs");
continue;
}
@ -54,8 +54,8 @@ impl ureq::TlsConnector for TlsConnector {
}
// errors generated early in the tls handshake
Err(rustls::Error::InvalidCertificate(_)) => {
if let Ok(n) = tls.probe(dns_name) {
if n > 0 {
if let Ok(certs) = tls.probe(dns_name) {
if certs.len() > 0 {
log::info!("try again with new trusted certs");
continue;
}

View file

@ -85,7 +85,7 @@ url = {version = "2.2.2", optional = true}
ring = {version = "0.16.20", optional = true}
# for websocket testing
tungstenite = {version = "0.20.0", optional = true, features = ["rustls-tls-webpki-roots"]}
tungstenite = {version = "0.20.0", optional = true}
# for performance testing
perflib = {path="../../tools/perflib", optional = true}

View file

@ -2,8 +2,7 @@ use crate::{CommonEnv, ShellCmdApi};
use com::api::NET_MTU;
use net::NetPingCallback;
use num_traits::*;
use std::io::Read;
use std::io::Write;
use std::io::{Read, Write};
use std::net::{IpAddr, TcpListener, TcpStream};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
@ -22,7 +21,10 @@ use net::XousServerId;
use perflib::*;
#[cfg(feature = "websocket")]
use tungstenite::{stream::MaybeTlsStream, WebSocket};
use {
tls::Tls,
tungstenite::{stream::MaybeTlsStream, WebSocket},
};
pub struct NetCmd {
callback_id: Option<u32>,
@ -415,21 +417,40 @@ impl<'a> ShellCmdApi<'a> for NetCmd {
#[cfg(feature = "websocket")]
"ws" => {
if self.ws.is_none() {
let url = url::Url::parse("wss://awake.noskills.club/ws").expect("Can't parse");
match tungstenite::connect(url) {
Ok((socket, response)) => {
log::info!("Connected to the server");
log::info!("Response HTTP code: {}", response.status());
log::info!("Response contains the following headers:");
for (ref header, _value) in response.headers() {
log::info!("* {}", header);
let server = "wss://awake.noskills.club/ws";
let url = url::Url::parse(server).expect("Can't parse");
log::info!("attempting websocket connection to {}", url.as_str());
let host = url.host_str().expect("failed to extract host from url");
match TcpStream::connect((host, 443)) {
Ok(sock) => {
log::info!("tcp connected to {host}");
let xtls = Tls::new();
match xtls.stream_owned(host, sock) {
Ok(tls_stream) => {
log::info!("tls configured");
match tungstenite::client(url, tls_stream) {
Ok((_socket, response)) => {
log::info!("Websocket connected to: {}", server);
log::info!(
"Response HTTP code: {}",
response.status()
);
log::info!(
"Response contains the following headers:"
);
for (ref header, _value) in response.headers() {
log::info!("* {}", header);
}
}
Err(e) => {
log::info!("failed to connect websocket: {}", e)
}
}
}
Err(e) => log::warn!("failed to configure tls: {e}"),
}
self.ws = Some(socket);
}
Err(e) => {
log::warn!("failed to connect to ws server: {e}");
write!(ret, "failed to connect to ws server: {e}").ok();
}
Err(e) => log::warn!("failed to connect tcp: {e}"),
}
}
let mut err = false;