Compare commits
127 Commits
Author | SHA1 | Date |
---|---|---|
onryo | 00fe0ea59d | 1 month ago |
onryo | 32ba8b8f9b | 1 month ago |
onryo | ddad184bb9 | 1 month ago |
onryo | e1ef74c83c | 1 month ago |
onryo | 70237c2d84 | 1 month ago |
onryo | d2ceecaffa | 1 month ago |
onryo | c2d7161ea2 | 3 months ago |
onryo | b917e39071 | 3 months ago |
onryo | 35482c2ab1 | 3 months ago |
onryo | 0c142c7b23 | 3 months ago |
onryo | a5d7beb19b | 3 months ago |
duke | c73ac543f4 | 3 months ago |
lucretius | e94df062a8 | 3 months ago |
lucretius | 068a3935e2 | 3 months ago |
lucretius | 26182290f5 | 3 months ago |
Duke | bd272dacb7 | 3 months ago |
Duke | a32146470b | 3 months ago |
lucretius | da09dc0ae2 | 3 months ago |
onryo | b8dc50f903 | 3 months ago |
onryo | 2ababaef70 | 3 months ago |
onryo | 5a338c7b55 | 3 months ago |
onryo | 002fba353e | 3 months ago |
onryo | 16e194d644 | 3 months ago |
Duke | 0c10cf1243 | 3 months ago |
Duke | 775135cc44 | 3 months ago |
duke | e299fe33e9 | 3 months ago |
lucretius | 49d587cd42 | 3 months ago |
lucretius | 4f0229a823 | 3 months ago |
lucretius | 683718008c | 3 months ago |
lucretius | 58f59661af | 3 months ago |
Duke | 4b95013d15 | 3 months ago |
duke | 4d2a32b6b2 | 3 months ago |
lucretius | 8da49166a8 | 3 months ago |
lucretius | d0b8ab074e | 3 months ago |
lucretius | 366f6e24bc | 3 months ago |
lucretius | 95090a90ad | 3 months ago |
lucretius | 24d262dcb9 | 3 months ago |
lucretius | 6f7fd863f0 | 3 months ago |
lucretius | 8348e61e2e | 3 months ago |
Duke | 68d9388c1b | 3 months ago |
Duke | 3b79e5fcd0 | 3 months ago |
duke | e7a974ec47 | 3 months ago |
lucretius | 156b1a6def | 3 months ago |
lucretius | 063303413c | 3 months ago |
lucretius | d8b88dcb3b | 3 months ago |
Duke | 5199f9487c | 3 months ago |
duke | 370058fa95 | 4 months ago |
jahway603 | 1fb344a8c2 | 4 months ago |
lucretius | 1a5ab786bc | 4 months ago |
Duke | 2fcb4f8358 | 4 months ago |
Duke | 6bbd2ac358 | 4 months ago |
Duke | 45e5091208 | 4 months ago |
Duke | f15a28f3ec | 4 months ago |
Duke | fd5eec230e | 4 months ago |
Deniod | 618625bc00 | 4 months ago |
Deniod | 130e0560d5 | 4 months ago |
Deniod | 0d6b84ec8a | 4 months ago |
Duke | df67f779f7 | 4 months ago |
Duke | 0f8f028d7d | 4 months ago |
Deniod | 261b3ad643 | 4 months ago |
Deniod | c6e8268450 | 4 months ago |
Deniod | c802a55bac | 4 months ago |
Deniod | fb1626d11d | 4 months ago |
Deniod | 84196cda87 | 4 months ago |
Deniod | 2b46484f90 | 4 months ago |
Deniod | 9276519c7b | 4 months ago |
Deniod | 98c21693e9 | 4 months ago |
onryo | 4e074b20fd | 4 months ago |
onryo | 391af1c75a | 4 months ago |
onryo | 16b6d43786 | 4 months ago |
onryo | 3c2414028b | 4 months ago |
onryo | 4a7dd7f959 | 4 months ago |
onryo | 07c16a6009 | 4 months ago |
onryo | c01e7ac728 | 4 months ago |
onryo | 6dca2fafa5 | 4 months ago |
Duke | cb0de2c3f6 | 4 months ago |
Deniod | 1f31adc30c | 5 months ago |
Deniod | 7863d6ffb2 | 5 months ago |
jahway603 | 5df4d75a43 | 5 months ago |
lucretius | 7dd665131e | 5 months ago |
jahway603 | 6183c244e5 | 5 months ago |
jahway603 | 5114b76be2 | 5 months ago |
Duke | 973ec2e823 | 5 months ago |
duke | eb7a083d41 | 5 months ago |
Deniod | 3962b42e30 | 5 months ago |
duke | aadb7e7c9e | 6 months ago |
onryo | 9a7e87fa1d | 6 months ago |
onryo | 2519acdd7a | 11 months ago |
onryo | aa58e3a4ce | 11 months ago |
onryo | 50964ce8b7 | 11 months ago |
onryo | 2e23886d86 | 11 months ago |
onryo | d8ff0de15d | 11 months ago |
onryo | b8c18aa38b | 11 months ago |
onryo | 1f31c40172 | 11 months ago |
onryo | eca9c53ff7 | 11 months ago |
Duke | 828a912d85 | 1 year ago |
Duke | bfdda1f1af | 1 year ago |
Duke | 5b33cb3638 | 1 year ago |
Duke | a3987fca13 | 1 year ago |
Duke | d05d8271ec | 1 year ago |
Duke | 69cf815ed2 | 1 year ago |
Duke | f46e1b4a57 | 1 year ago |
Duke | 77ac1f99ae | 1 year ago |
Duke | 7364e21f99 | 1 year ago |
Duke | 51fe4d6cde | 1 year ago |
Duke | a080d0ca80 | 1 year ago |
Duke | b84828604f | 1 year ago |
Duke | 7e54360b72 | 1 year ago |
Duke | 4116de6a69 | 1 year ago |
Duke | 9befa3450f | 1 year ago |
fekt | 5508050fcc | 1 year ago |
Duke | c7e0f0fae6 | 1 year ago |
Duke | ad5b294d95 | 1 year ago |
Duke | 89bf5b0f7c | 1 year ago |
Duke | 51483843ac | 1 year ago |
Duke | 17fcb84a89 | 1 year ago |
Duke | f22f97463b | 1 year ago |
Duke | f7787fe9e9 | 1 year ago |
Duke | 0b72d01f4a | 1 year ago |
Duke | 557e10e5e8 | 1 year ago |
Duke | 3f8ae1f9d7 | 1 year ago |
Duke | 3b6da338c9 | 1 year ago |
Duke | 430a7ab474 | 1 year ago |
Duke | 5d5447aced | 1 year ago |
Duke | 1e6e77055b | 1 year ago |
Duke | 6165733e03 | 1 year ago |
duke | 6590a49bb5 | 1 year ago |
135 changed files with 1331 additions and 1989 deletions
@ -0,0 +1,11 @@ |
|||
Install build tools: |
|||
|
|||
``` |
|||
sudo apt install dh-make |
|||
``` |
|||
|
|||
To build the package from source run the following: |
|||
|
|||
``` |
|||
dpkg-buildpackage -rfakeroot -b -uc -us |
|||
``` |
@ -0,0 +1,5 @@ |
|||
silentdragonlite (2.0.1) stable; urgency=medium |
|||
|
|||
* 2.0.1.1 release. |
|||
|
|||
-- onryo <onryo@hush.land> Sat, 06 Jan 2024 10:20:30 +0200 |
@ -0,0 +1 @@ |
|||
9 |
@ -0,0 +1,13 @@ |
|||
Source: silentdragonlite |
|||
Section: utils |
|||
Priority: optional |
|||
Maintainer: onryo <onryo@hush.land> |
|||
Standards-Version: 4.6.0 |
|||
Homepage: https://hush.is |
|||
Vcs-Browser: https://git.hush.is/hush/SilentDragonLite |
|||
Vcs-Git: https://git.hush.is/hush/SilentDragonLite.git |
|||
|
|||
Package: silentdragonlite |
|||
Architecture: amd64 arm64 |
|||
Depends: ${shlibs:Depends}, ${misc:Depends} |
|||
Description: SilentDragonLite is a lightwallet for HUSH which does not require you to download the full blockchain. |
@ -0,0 +1,5 @@ |
|||
Files: * |
|||
Copyright: 2019-2024, The Hush developers |
|||
2018-2019, The Zcash developers |
|||
License: GPLv3 |
|||
Comment: https://hush.is/developers |
@ -0,0 +1,20 @@ |
|||
#!/usr/bin/make -f |
|||
# See debhelper(7) (uncomment to enable) |
|||
# output every command that modifies files on the build system. |
|||
#export DH_VERBOSE = 1 |
|||
|
|||
# see FEATURE AREAS in dpkg-buildflags(1) |
|||
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all |
|||
# see ENVIRONMENT in dpkg-buildflags(1) |
|||
# package maintainers to append CFLAGS |
|||
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic |
|||
# package maintainers to append LDFLAGS |
|||
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed |
|||
|
|||
%: |
|||
dh $@ |
|||
|
|||
# dh_make generated override targets |
|||
# This is example for Cmake (See https://bugs.debian.org/641051 ) |
|||
#override_dh_auto_configure: |
|||
# dh_auto_configure -- # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) |
@ -0,0 +1,62 @@ |
|||
# Developer Docs for SDL |
|||
|
|||
Random stuff that is useful for devs. |
|||
|
|||
# Checking return values from litelib |
|||
|
|||
There are 3 functions written in Rust that live in lib/src/lib.rs : |
|||
|
|||
* `litelib_initialize_new` |
|||
* create a new client/connection and brand new wallet |
|||
* `litelib_initialize_new_from_phrase` |
|||
* create a new client/connection from a seedphrase (restoring from seedphrase) |
|||
* `litelib_initialize_existing` |
|||
* create a new client/connection with an already existing wallet |
|||
|
|||
The Rust code calls it a "LightClient" while the C++ of SDL calls it a "Connection". |
|||
|
|||
When `litelib_initialize_existing` or `litelib_initialize_new_from_phrase` return successfully, they return the string "OK" (which is not JSON). |
|||
|
|||
When `litelib_initialize_new` returns successfully it returns JSON that looks like : |
|||
|
|||
``` |
|||
{"seed":"seed","birthday":birthday} |
|||
``` |
|||
|
|||
where "seed" is a 24 word seed and birthday is an integer block height. |
|||
|
|||
So when calling these 3 functions, which looks almost the same in the calling code, the code which checks if they worked will be different, depending on what each returns on success. |
|||
|
|||
When checking the return value of `litelib_initialize_existing` or `litelib_initialize_new_from_phrase` it should look like : |
|||
|
|||
``` |
|||
QString reply = ""; |
|||
try { |
|||
char* resp = litelib_initialize_new_from_phrase(...); |
|||
reply = litelib_process_response(resp); |
|||
} catch { |
|||
... |
|||
} |
|||
if (reply.isEmpty())) { |
|||
// litelib_initialize_new_from_phrase failed |
|||
... |
|||
} |
|||
``` |
|||
|
|||
Yes, `isEmpty()` is not a very strict check, we could actually check for valid-looking JSON (starts with a { and ends with a }) as well as making sure the keys "seed" and "birthday" exist. Please implement this. |
|||
|
|||
When checking the return value of `litelib_initialize_new` it should look like : |
|||
|
|||
``` |
|||
QString reply = ""; |
|||
try { |
|||
char* resp = litelib_initialize_new(...); |
|||
reply = litelib_process_response(resp); |
|||
} catch { |
|||
... |
|||
} |
|||
if (reply.toUpper().trimmed() != "OK") { |
|||
// litelib_initialize_new failed |
|||
... |
|||
} |
|||
``` |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@ |
|||
#!/bin/bash |
|||
# Copyright 2019-2023 The Hush Developers |
|||
# Copyright 2019-2024 The Hush Developers |
|||
|
|||
./build.sh && ./SilentDragonLite |
|||
|
@ -0,0 +1,51 @@ |
|||
#include "NoteCountDataStore.h" |
|||
|
|||
NoteCountDataStore* NoteCountDataStore::instance = nullptr; |
|||
bool NoteCountDataStore::instanced = false; |
|||
|
|||
NoteCountDataStore* NoteCountDataStore::getInstance() { |
|||
if (!instanced) { |
|||
instanced = true; |
|||
instance = new NoteCountDataStore(); |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
void NoteCountDataStore::clear() { |
|||
data.clear(); |
|||
} |
|||
|
|||
void NoteCountDataStore::setData(const QString& key, const QString& value) { |
|||
data[key] = value; |
|||
} |
|||
|
|||
QString NoteCountDataStore::getData(const QString& key) { |
|||
return data.value(key); |
|||
} |
|||
|
|||
QString NoteCountDataStore::dump() { |
|||
QString result; |
|||
for (const auto& key : data.keys()) { |
|||
result += key + ": " + data[key] + "\n"; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
void NoteCountDataStore::setSpendableNotesCount(int count) { |
|||
spendableNotesCount = count; |
|||
} |
|||
|
|||
int NoteCountDataStore::getSpendableNotesCount() const { |
|||
return spendableNotesCount; |
|||
} |
|||
|
|||
void NoteCountDataStore::setAddressWithMaxValue(const QString& address, int value) { |
|||
if (value > maxValue) { |
|||
maxValue = value; |
|||
addressWithMaxValue = address; |
|||
} |
|||
} |
|||
|
|||
QString NoteCountDataStore::getAddressWithMaxValue() const { |
|||
return addressWithMaxValue; |
|||
} |
@ -0,0 +1,36 @@ |
|||
#ifndef NOTECOUNTDATASTORE_H |
|||
#define NOTECOUNTDATASTORE_H |
|||
|
|||
#include <QString> |
|||
#include <QMap> |
|||
|
|||
class NoteCountDataStore { |
|||
private: |
|||
static NoteCountDataStore* instance; |
|||
static bool instanced; |
|||
QMap<QString, QString> data; |
|||
int spendableNotesCount; |
|||
QString addressWithMaxValue; |
|||
int maxValue; // Hinzugefügt, um den maximalen Wert zu speichern
|
|||
|
|||
NoteCountDataStore() : spendableNotesCount(0), maxValue(0) {} // Initialisiere maxValue
|
|||
|
|||
public: |
|||
static NoteCountDataStore* getInstance(); |
|||
void clear(); |
|||
void setData(const QString& key, const QString& value); |
|||
QString getData(const QString& key); |
|||
QString dump(); |
|||
|
|||
void setSpendableNotesCount(int count); |
|||
int getSpendableNotesCount() const; |
|||
void setAddressWithMaxValue(const QString& address, int value); |
|||
QString getAddressWithMaxValue() const; |
|||
|
|||
~NoteCountDataStore() { |
|||
instanced = false; |
|||
instance = nullptr; |
|||
} |
|||
}; |
|||
|
|||
#endif // NOTECOUNTDATASTORE_H
|
@ -1,3 +1,3 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Copyright 2019-2024 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#include "ContactRequestChatItem.h" |
|||
|
@ -1,16 +0,0 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#include "mobileappconnector.h" |
|||
#include "ui_mobileappconnector.h" |
|||
|
|||
MobileAppConnector::MobileAppConnector(QWidget *parent) : |
|||
QDialog(parent), |
|||
ui(new Ui::MobileAppConnector) |
|||
{ |
|||
ui->setupUi(this); |
|||
} |
|||
|
|||
MobileAppConnector::~MobileAppConnector() |
|||
{ |
|||
delete ui; |
|||
} |
@ -1,24 +0,0 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#ifndef MOBILEAPPCONNECTOR_H |
|||
#define MOBILEAPPCONNECTOR_H |
|||
|
|||
#include <QDialog> |
|||
|
|||
namespace Ui { |
|||
class MobileAppConnector; |
|||
} |
|||
|
|||
class MobileAppConnector : public QDialog |
|||
{ |
|||
Q_OBJECT |
|||
|
|||
public: |
|||
explicit MobileAppConnector(QWidget *parent = nullptr); |
|||
~MobileAppConnector(); |
|||
|
|||
private: |
|||
Ui::MobileAppConnector *ui; |
|||
}; |
|||
|
|||
#endif // MOBILEAPPCONNECTOR_H
|
@ -1,214 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<ui version="4.0"> |
|||
<class>MobileAppConnector</class> |
|||
<widget class="QDialog" name="MobileAppConnector"> |
|||
<property name="geometry"> |
|||
<rect> |
|||
<x>0</x> |
|||
<y>0</y> |
|||
<width>800</width> |
|||
<height>530</height> |
|||
</rect> |
|||
</property> |
|||
<property name="windowTitle"> |
|||
<string>Mobile Connector App</string> |
|||
</property> |
|||
<layout class="QGridLayout" name="gridLayout"> |
|||
<item row="4" column="1" colspan="2"> |
|||
<widget class="QDialogButtonBox" name="buttonBox"> |
|||
<property name="orientation"> |
|||
<enum>Qt::Horizontal</enum> |
|||
</property> |
|||
<property name="standardButtons"> |
|||
<set>QDialogButtonBox::Close</set> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="0" colspan="3"> |
|||
<widget class="QLabel" name="label"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Scan this QRCode from your silentdragon companion app to connect your phone</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="0" colspan="2"> |
|||
<widget class="QGroupBox" name="groupBox_2"> |
|||
<property name="title"> |
|||
<string>QR Code</string> |
|||
</property> |
|||
<layout class="QGridLayout" name="gridLayout_3"> |
|||
<item row="1" column="0"> |
|||
<widget class="QLabel" name="label_2"> |
|||
<property name="text"> |
|||
<string>Connection String</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="0"> |
|||
<widget class="QLineEdit" name="txtConnStr"> |
|||
<property name="font"> |
|||
<font> |
|||
<pointsize>9</pointsize> |
|||
</font> |
|||
</property> |
|||
<property name="readOnly"> |
|||
<bool>true</bool> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="0"> |
|||
<widget class="QRCodeLabel" name="qrcode"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">background-color: #fff</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="3" column="0"> |
|||
<widget class="QCheckBox" name="chkInternetConn"> |
|||
<property name="text"> |
|||
<string>Allow connections over the internet via silentdragon wormhole</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="2" rowspan="2"> |
|||
<widget class="QGroupBox" name="groupBox"> |
|||
<property name="title"> |
|||
<string>silentdragon Companion App</string> |
|||
</property> |
|||
<layout class="QGridLayout" name="gridLayout_2"> |
|||
<item row="5" column="0"> |
|||
<widget class="QPushButton" name="btnDisconnect"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Disconnect</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="6" column="0"> |
|||
<spacer name="verticalSpacer"> |
|||
<property name="orientation"> |
|||
<enum>Qt::Vertical</enum> |
|||
</property> |
|||
<property name="sizeHint" stdset="0"> |
|||
<size> |
|||
<width>20</width> |
|||
<height>40</height> |
|||
</size> |
|||
</property> |
|||
</spacer> |
|||
</item> |
|||
<item row="2" column="0"> |
|||
<widget class="QLabel" name="lblLastSeen"> |
|||
<property name="text"> |
|||
<string>TextLabel</string> |
|||
</property> |
|||
<property name="alignment"> |
|||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="0"> |
|||
<widget class="QLabel" name="label_3"> |
|||
<property name="text"> |
|||
<string>Last seen:</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="0"> |
|||
<widget class="QLabel" name="lblRemoteName"> |
|||
<property name="text"> |
|||
<string notr="true">TextLabel</string> |
|||
</property> |
|||
<property name="alignment"> |
|||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="3" column="0"> |
|||
<widget class="QLabel" name="label_4"> |
|||
<property name="text"> |
|||
<string>Connection type:</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="4" column="0"> |
|||
<widget class="QLabel" name="lblConnectionType"> |
|||
<property name="text"> |
|||
<string>TextLabel</string> |
|||
</property> |
|||
<property name="alignment"> |
|||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
<customwidgets> |
|||
<customwidget> |
|||
<class>QRCodeLabel</class> |
|||
<extends>QLabel</extends> |
|||
<header>qrcodelabel.h</header> |
|||
</customwidget> |
|||
</customwidgets> |
|||
<resources/> |
|||
<connections> |
|||
<connection> |
|||
<sender>buttonBox</sender> |
|||
<signal>accepted()</signal> |
|||
<receiver>MobileAppConnector</receiver> |
|||
<slot>accept()</slot> |
|||
<hints> |
|||
<hint type="sourcelabel"> |
|||
<x>248</x> |
|||
<y>254</y> |
|||
</hint> |
|||
<hint type="destinationlabel"> |
|||
<x>157</x> |
|||
<y>274</y> |
|||
</hint> |
|||
</hints> |
|||
</connection> |
|||
<connection> |
|||
<sender>buttonBox</sender> |
|||
<signal>rejected()</signal> |
|||
<receiver>MobileAppConnector</receiver> |
|||
<slot>reject()</slot> |
|||
<hints> |
|||
<hint type="sourcelabel"> |
|||
<x>316</x> |
|||
<y>260</y> |
|||
</hint> |
|||
<hint type="destinationlabel"> |
|||
<x>286</x> |
|||
<y>274</y> |
|||
</hint> |
|||
</hints> |
|||
</connection> |
|||
</connections> |
|||
</ui> |
@ -0,0 +1 @@ |
|||
#define DEBUG(x) (qDebug() << QString(__func__) << ": " << x) |
@ -1,3 +1 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#define APP_VERSION "1.5.3" |
|||
#define APP_VERSION "2.0.2" |
|||
|
@ -1,940 +0,0 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#include "websockets.h" |
|||
#include "controller.h" |
|||
#include "settings.h" |
|||
#include "ui_mobileappconnector.h" |
|||
#include "version.h" |
|||
|
|||
// Weap the sendTextMessage to check if the connection is valid and that the parent WebServer didn't close this connection
|
|||
// for some reason.
|
|||
void ClientWebSocket::sendTextMessage(QString m) { |
|||
if (client) { |
|||
if (server && !server->isValidConnection(client)) { |
|||
return; |
|||
} |
|||
|
|||
if (client->isValid()) |
|||
client->sendTextMessage(m); |
|||
} |
|||
} |
|||
|
|||
WSServer::WSServer(quint16 port, bool debug, QObject *parent) : |
|||
QObject(parent), |
|||
m_pWebSocketServer(new QWebSocketServer(QStringLiteral("Direct Connection Server"), |
|||
QWebSocketServer::NonSecureMode, this)), |
|||
m_debug(debug) |
|||
{ |
|||
m_mainWindow = (MainWindow *) parent; |
|||
if (m_pWebSocketServer->listen(QHostAddress::AnyIPv4, port)) { |
|||
if (m_debug) |
|||
qDebug() << "Echoserver listening on port" << port; |
|||
connect(m_pWebSocketServer, &QWebSocketServer::newConnection, |
|||
this, &WSServer::onNewConnection); |
|||
connect(m_pWebSocketServer, &QWebSocketServer::closed, this, &WSServer::closed); |
|||
} |
|||
} |
|||
|
|||
WSServer::~WSServer() |
|||
{ |
|||
qDebug() << "Closing websocket server"; |
|||
m_pWebSocketServer->close(); |
|||
qDeleteAll(m_clients.begin(), m_clients.end()); |
|||
qDebug() << "Deleted all websocket clients"; |
|||
} |
|||
|
|||
void WSServer::onNewConnection() |
|||
{ |
|||
qDebug() << "Websocket server: new connection"; |
|||
QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection(); |
|||
|
|||
connect(pSocket, &QWebSocket::textMessageReceived, this, &WSServer::processTextMessage); |
|||
connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WSServer::processBinaryMessage); |
|||
connect(pSocket, &QWebSocket::disconnected, this, &WSServer::socketDisconnected); |
|||
|
|||
m_clients << pSocket; |
|||
} |
|||
|
|||
void WSServer::processTextMessage(QString message) |
|||
{ |
|||
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender()); |
|||
if (m_debug) |
|||
qDebug() << "Message received:" << message; |
|||
|
|||
if (pClient) { |
|||
std::shared_ptr<ClientWebSocket> client = std::make_shared<ClientWebSocket>(pClient, this); |
|||
AppDataServer::getInstance()->processMessage(message, m_mainWindow, client, AppConnectionType::DIRECT); |
|||
} |
|||
} |
|||
|
|||
void WSServer::processBinaryMessage(QByteArray message) |
|||
{ |
|||
//QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
|
|||
if (m_debug) |
|||
qDebug() << "Binary Message received:" << message; |
|||
|
|||
} |
|||
|
|||
void WSServer::socketDisconnected() |
|||
{ |
|||
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender()); |
|||
if (m_debug) |
|||
qDebug() << "socketDisconnected:" << pClient; |
|||
if (pClient) { |
|||
m_clients.removeAll(pClient); |
|||
pClient->deleteLater(); |
|||
} |
|||
} |
|||
|
|||
//===============================
|
|||
// WormholeClient
|
|||
//===============================
|
|||
WormholeClient::WormholeClient(MainWindow* p, QString wormholeCode) { |
|||
this->parent = p; |
|||
this->code = wormholeCode; |
|||
connect(); |
|||
qDebug() << "New wormhole client after connect()"; |
|||
} |
|||
|
|||
WormholeClient::~WormholeClient() { |
|||
qDebug() << "WormholeClient destructor"; |
|||
shuttingDown = true; |
|||
|
|||
if (m_webSocket && m_webSocket->isValid()) { |
|||
qDebug() << "Wormhole closing!"; |
|||
m_webSocket->close(); |
|||
|
|||
} |
|||
|
|||
if (timer) { |
|||
qDebug() << "Wormhole timer stopping"; |
|||
timer->stop(); |
|||
} |
|||
|
|||
delete timer; |
|||
qDebug() << "Wormhole timer deleted"; |
|||
} |
|||
|
|||
void WormholeClient::connect() { |
|||
qDebug() << "Wormhole::connect"; |
|||
delete m_webSocket; |
|||
m_webSocket = new QWebSocket(); |
|||
|
|||
if (m_webSocket) { |
|||
QObject::connect(m_webSocket, &QWebSocket::connected, this, &WormholeClient::onConnected); |
|||
QObject::connect(m_webSocket, &QWebSocket::disconnected, this, &WormholeClient::closed); |
|||
} else { |
|||
qDebug() << "Invalid websocket object!"; |
|||
} |
|||
|
|||
m_webSocket->open(QUrl("wss://wormhole.hush.is:443")); |
|||
//m_webSocket->open(QUrl("ws://127.0.0.1:7070"));
|
|||
} |
|||
|
|||
void WormholeClient::retryConnect() { |
|||
QTimer::singleShot(5 * 1000 * pow(2, retryCount), [=]() { |
|||
if (retryCount < 10) { |
|||
qDebug() << "Retrying websocket connection"; |
|||
this->retryCount++; |
|||
connect(); |
|||
} else { |
|||
qDebug() << "Retry count exceeded, will not attempt retry any more"; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Called when the websocket is closed. If this was closed without our explicitly closing it,
|
|||
// then we need to try and reconnect
|
|||
void WormholeClient::closed() { |
|||
if (!shuttingDown) { |
|||
retryConnect(); |
|||
} |
|||
} |
|||
|
|||
void WormholeClient::onConnected() |
|||
{ |
|||
qDebug() << "WebSocket connected"; |
|||
retryCount = 0; |
|||
qDebug() << "WebSocket connected, retryCount=" << retryCount; |
|||
QObject::connect(m_webSocket, &QWebSocket::textMessageReceived, |
|||
this, &WormholeClient::onTextMessageReceived); |
|||
|
|||
auto payload = QJsonDocument( QJsonObject { |
|||
{"register", code} |
|||
}).toJson(); |
|||
|
|||
m_webSocket->sendTextMessage(payload); |
|||
|
|||
// On connected, we'll also create a timer to ping it every 4 minutes, since the websocket
|
|||
// will timeout after 5 minutes
|
|||
if (m_webSocket && m_webSocket->isValid()) { |
|||
m_webSocket->sendTextMessage(payload); |
|||
qDebug() << "Sent registration message with code=" << code; |
|||
|
|||
// On connected, we'll also create a timer to ping it every 4 minutes, since the websocket
|
|||
// will timeout after 5 minutes
|
|||
timer = new QTimer(parent); |
|||
qDebug() << "Created QTimer"; |
|||
QObject::connect(timer, &QTimer::timeout, [=]() { |
|||
qDebug() << "Timer timeout!"; |
|||
if (!shuttingDown && m_webSocket && m_webSocket->isValid()) { |
|||
auto payload = QJsonDocument(QJsonObject { {"ping", "ping"} }).toJson(); |
|||
qint64 bytes = m_webSocket->sendTextMessage(payload); |
|||
qDebug() << "Sent ping, " << bytes << " bytes"; |
|||
} |
|||
}); |
|||
unsigned int interval = 4*60*1000; |
|||
timer->start(interval); // 4 minutes
|
|||
qDebug() << "Started timer with interval=" << interval; |
|||
} else { |
|||
qDebug() << "Invalid websocket object onConnected!"; |
|||
} |
|||
} |
|||
|
|||
void WormholeClient::onTextMessageReceived(QString message) |
|||
{ |
|||
AppDataServer::getInstance()->processMessage(message, parent, std::make_shared<ClientWebSocket>(m_webSocket), AppConnectionType::INTERNET); |
|||
qDebug() << "Destroyed tempWormholeClient and ui"; |
|||
} |
|||
|
|||
|
|||
// ==============================
|
|||
// AppDataServer
|
|||
// ==============================
|
|||
AppDataServer* AppDataServer::instance = nullptr; |
|||
|
|||
QString AppDataServer::getWormholeCode(QString secretHex) { |
|||
unsigned char* secret = new unsigned char[crypto_secretbox_KEYBYTES]; |
|||
sodium_hex2bin(secret, crypto_secretbox_KEYBYTES, secretHex.toStdString().c_str(), crypto_secretbox_KEYBYTES*2, |
|||
NULL, NULL, NULL); |
|||
|
|||
unsigned char* out1 = new unsigned char[crypto_hash_sha256_BYTES]; |
|||
crypto_hash_sha256(out1, secret, crypto_secretbox_KEYBYTES); |
|||
|
|||
unsigned char* out2 = new unsigned char[crypto_hash_sha256_BYTES]; |
|||
crypto_hash_sha256(out2, out1, crypto_hash_sha256_BYTES); |
|||
|
|||
char* wmcode = new char[crypto_hash_sha256_BYTES*2 + 1]; |
|||
sodium_bin2hex(wmcode, crypto_hash_sha256_BYTES*2 + 1, out2, crypto_hash_sha256_BYTES); |
|||
|
|||
QString wmcodehex(wmcode); |
|||
|
|||
delete[] wmcode; |
|||
delete[] out2; |
|||
delete[] out1; |
|||
delete[] secret; |
|||
|
|||
return wmcodehex; |
|||
} |
|||
|
|||
QString AppDataServer::getSecretHex() { |
|||
QSettings s; |
|||
|
|||
return s.value("mobileapp/secret", "").toString(); |
|||
} |
|||
|
|||
void AppDataServer::saveNewSecret(QString secretHex) { |
|||
QSettings().setValue("mobileapp/secret", secretHex); |
|||
|
|||
if (secretHex.isEmpty()) |
|||
setAllowInternetConnection(false); |
|||
} |
|||
|
|||
bool AppDataServer::getAllowInternetConnection() { |
|||
return QSettings().value("mobileapp/allowinternet", false).toBool(); |
|||
} |
|||
|
|||
void AppDataServer::setAllowInternetConnection(bool allow) { |
|||
QSettings().setValue("mobileapp/allowinternet", allow); |
|||
} |
|||
|
|||
void AppDataServer::saveLastConnectedOver(AppConnectionType type) { |
|||
QSettings().setValue("mobileapp/lastconnectedover", type); |
|||
} |
|||
|
|||
AppConnectionType AppDataServer::getLastConnectionType() { |
|||
return (AppConnectionType) QSettings().value("mobileapp/lastconnectedover", AppConnectionType::DIRECT).toInt(); |
|||
} |
|||
|
|||
void AppDataServer::saveLastSeenTime() { |
|||
QSettings().setValue("mobileapp/lastseentime", QDateTime::currentSecsSinceEpoch()); |
|||
} |
|||
|
|||
QDateTime AppDataServer::getLastSeenTime() { |
|||
return QDateTime::fromSecsSinceEpoch(QSettings().value("mobileapp/lastseentime", 0).toLongLong()); |
|||
} |
|||
|
|||
void AppDataServer::setConnectedName(QString name) { |
|||
QSettings().setValue("mobileapp/connectedname", name); |
|||
} |
|||
|
|||
QString AppDataServer::getConnectedName() { |
|||
return QSettings().value("mobileapp/connectedname", "").toString(); |
|||
} |
|||
|
|||
bool AppDataServer::isAppConnected() { |
|||
return !getConnectedName().isEmpty() && |
|||
getLastSeenTime().daysTo(QDateTime::currentDateTime()) < 14; |
|||
} |
|||
|
|||
void AppDataServer::connectAppDialog(MainWindow* parent) { |
|||
QDialog d(parent); |
|||
ui = new Ui_MobileAppConnector(); |
|||
ui->setupUi(&d); |
|||
Settings::saveRestore(&d); |
|||
|
|||
updateUIWithNewQRCode(parent); |
|||
updateConnectedUI(); |
|||
|
|||
QObject::connect(ui->btnDisconnect, &QPushButton::clicked, [=] () { |
|||
QSettings().setValue("mobileapp/connectedname", ""); |
|||
saveNewSecret(""); |
|||
|
|||
updateConnectedUI(); |
|||
}); |
|||
|
|||
QObject::connect(ui->txtConnStr, &QLineEdit::cursorPositionChanged, [=](int, int) { |
|||
ui->txtConnStr->selectAll(); |
|||
}); |
|||
|
|||
QObject::connect(ui->chkInternetConn, &QCheckBox::stateChanged, [=] (int state) { |
|||
if (state == Qt::Checked) { |
|||
|
|||
} |
|||
updateUIWithNewQRCode(parent); |
|||
}); |
|||
|
|||
// If we're not listening for the app, then start the websockets
|
|||
if (!parent->isWebsocketListening()) { |
|||
QString wormholecode = ""; |
|||
if (getAllowInternetConnection()) |
|||
wormholecode = AppDataServer::getInstance()->getWormholeCode(AppDataServer::getInstance()->getSecretHex()); |
|||
|
|||
parent->createWebsocket(wormholecode); |
|||
} |
|||
|
|||
d.exec(); |
|||
|
|||
// If there is nothing connected when the dialog exits, then shutdown the websockets
|
|||
if (!isAppConnected()) { |
|||
parent->stopWebsocket(); |
|||
} |
|||
|
|||
// Cleanup
|
|||
tempSecret = ""; |
|||
|
|||
delete tempWormholeClient; |
|||
tempWormholeClient = nullptr; |
|||
|
|||
delete ui; |
|||
ui = nullptr; |
|||
} |
|||
|
|||
void AppDataServer::updateUIWithNewQRCode(MainWindow* mainwindow) { |
|||
// Get the address of the localhost
|
|||
auto addrList = QNetworkInterface::allAddresses(); |
|||
|
|||
// Find a suitable address
|
|||
QString ipv4Addr; |
|||
for (auto addr : addrList) { |
|||
if (addr.isLoopback() || addr.protocol() == QAbstractSocket::IPv6Protocol) |
|||
continue; |
|||
|
|||
ipv4Addr = addr.toString(); |
|||
break; |
|||
} |
|||
|
|||
if (ipv4Addr.isEmpty()) |
|||
return; |
|||
|
|||
QString uri = "ws://" + ipv4Addr + ":8777"; |
|||
qDebug() << "Websocket URI: " << uri; |
|||
|
|||
// Get a new secret
|
|||
unsigned char* secretBin = new unsigned char[crypto_secretbox_KEYBYTES]; |
|||
randombytes_buf(secretBin, crypto_secretbox_KEYBYTES); |
|||
char* secretHex = new char[crypto_secretbox_KEYBYTES*2 + 1]; |
|||
sodium_bin2hex(secretHex, crypto_secretbox_KEYBYTES*2+1, secretBin, crypto_secretbox_KEYBYTES); |
|||
|
|||
QString secretStr(secretHex); |
|||
QString codeStr = uri + "," + secretStr; |
|||
|
|||
if (ui->chkInternetConn->isChecked()) { |
|||
codeStr = codeStr + ",1"; |
|||
} |
|||
|
|||
registerNewTempSecret(secretStr, ui->chkInternetConn->isChecked(), mainwindow); |
|||
|
|||
ui->qrcode->setQrcodeString(codeStr); |
|||
ui->txtConnStr->setText(codeStr); |
|||
qDebug() << "New QR="<<codeStr; |
|||
} |
|||
|
|||
void AppDataServer::registerNewTempSecret(QString tmpSecretHex, bool allowInternet, MainWindow* main) { |
|||
qDebug() << "Registering new tempSecret, allowInternet=" << allowInternet; |
|||
tempSecret = tmpSecretHex; |
|||
|
|||
delete tempWormholeClient; |
|||
tempWormholeClient = nullptr; |
|||
|
|||
if (allowInternet) |
|||
tempWormholeClient = new WormholeClient(main, getWormholeCode(tempSecret)); |
|||
qDebug() << "Created new wormhole client"; |
|||
} |
|||
|
|||
|
|||
QString AppDataServer::connDesc(AppConnectionType t) { |
|||
if (t == AppConnectionType::DIRECT) { |
|||
return QObject::tr("Connected directly"); |
|||
} |
|||
else { |
|||
return QObject::tr("Connected over the internet via silentdragon wormhole service"); |
|||
} |
|||
} |
|||
|
|||
void AppDataServer::updateConnectedUI() { |
|||
if (ui == nullptr) |
|||
return; |
|||
|
|||
auto remoteName = getConnectedName(); |
|||
|
|||
ui->lblRemoteName->setText(remoteName.isEmpty() ? "(Not connected to any device)" : remoteName); |
|||
ui->lblLastSeen->setText(remoteName.isEmpty() ? "" : getLastSeenTime().toString(Qt::SystemLocaleLongDate)); |
|||
ui->lblConnectionType->setText(remoteName.isEmpty() ? "" : connDesc(getLastConnectionType())); |
|||
|
|||
ui->btnDisconnect->setEnabled(!remoteName.isEmpty()); |
|||
} |
|||
|
|||
QString AppDataServer::getNonceHex(NonceType nt) { |
|||
QSettings s; |
|||
QString hex; |
|||
if (nt == NonceType::LOCAL) { |
|||
// The default local nonce starts from 1, to always keep it odd
|
|||
auto defaultLocalNonce = "01" + QString("00").repeated(crypto_secretbox_NONCEBYTES-1); |
|||
hex = s.value("mobileapp/localnoncehex", defaultLocalNonce).toString(); |
|||
} |
|||
else { |
|||
hex = s.value("mobileapp/remotenoncehex", QString("00").repeated(crypto_secretbox_NONCEBYTES)).toString(); |
|||
} |
|||
return hex; |
|||
} |
|||
|
|||
void AppDataServer::saveNonceHex(NonceType nt, QString noncehex) { |
|||
QSettings s; |
|||
assert(noncehex.length() == crypto_secretbox_NONCEBYTES * 2); |
|||
if (nt == NonceType::LOCAL) { |
|||
s.setValue("mobileapp/localnoncehex", noncehex); |
|||
} |
|||
else { |
|||
s.setValue("mobileapp/remotenoncehex", noncehex); |
|||
} |
|||
s.sync(); |
|||
} |
|||
|
|||
// Encrypt an outgoing message with the stored secret key.
|
|||
QString AppDataServer::encryptOutgoing(QString msg) { |
|||
int padding = 16*1024; |
|||
qDebug() << "Encrypt msg(pad="<<padding<<") prepad len=" << msg.length(); |
|||
if (msg.length() % padding > 0) { |
|||
msg = msg + QString(" ").repeated(padding - (msg.length() % padding)); |
|||
} |
|||
qDebug() << "Encrypt msg postpad len=" << msg.length(); |
|||
|
|||
QString localNonceHex = getNonceHex(NonceType::LOCAL); |
|||
|
|||
unsigned char* noncebin = new unsigned char[crypto_secretbox_NONCEBYTES]; |
|||
sodium_hex2bin(noncebin, crypto_secretbox_NONCEBYTES, localNonceHex.toStdString().c_str(), localNonceHex.length(), |
|||
NULL, NULL, NULL); |
|||
|
|||
// Increment the nonce +2 and save
|
|||
sodium_increment(noncebin, crypto_secretbox_NONCEBYTES); |
|||
sodium_increment(noncebin, crypto_secretbox_NONCEBYTES); |
|||
|
|||
char* newLocalNonce = new char[crypto_secretbox_NONCEBYTES*2 + 1]; |
|||
sodium_memzero(newLocalNonce, crypto_secretbox_NONCEBYTES*2 + 1); |
|||
sodium_bin2hex(newLocalNonce, crypto_secretbox_NONCEBYTES*2+1, noncebin, crypto_box_NONCEBYTES); |
|||
|
|||
saveNonceHex(NonceType::LOCAL, QString(newLocalNonce)); |
|||
|
|||
unsigned char* secret = new unsigned char[crypto_secretbox_KEYBYTES]; |
|||
sodium_hex2bin(secret, crypto_secretbox_KEYBYTES, getSecretHex().toStdString().c_str(), crypto_secretbox_KEYBYTES*2, |
|||
NULL, NULL, NULL); |
|||
|
|||
int msgSize = strlen(msg.toStdString().c_str()); |
|||
unsigned char* encrpyted = new unsigned char[ msgSize + crypto_secretbox_MACBYTES]; |
|||
|
|||
crypto_secretbox_easy(encrpyted, (const unsigned char *)msg.toStdString().c_str(), msgSize, noncebin, secret); |
|||
|
|||
int encryptedHexSize = (msgSize + crypto_secretbox_MACBYTES) * 2 + 1; |
|||
char * encryptedHex = new char[encryptedHexSize]; |
|||
sodium_memzero(encryptedHex, encryptedHexSize); |
|||
sodium_bin2hex(encryptedHex, encryptedHexSize, encrpyted, msgSize + crypto_secretbox_MACBYTES); |
|||
|
|||
auto json = QJsonDocument(QJsonObject{ |
|||
{"nonce", QString(newLocalNonce)}, |
|||
{"payload", QString(encryptedHex)}, |
|||
{"to", getWormholeCode(getSecretHex())} |
|||
}); |
|||
|
|||
delete[] noncebin; |
|||
delete[] newLocalNonce; |
|||
delete[] secret; |
|||
delete[] encrpyted; |
|||
delete[] encryptedHex; |
|||
|
|||
return json.toJson(); |
|||
} |
|||
|
|||
/**
|
|||
Attempt to decrypt a message. If the decryption fails, it returns the string "error", the decrypted message otherwise. |
|||
It will use the given secret to attempt decryption. In addition, it will enforce that the nonce is greater than the last seen nonce, |
|||
unless the skipNonceCheck = true, which is used when attempting decrtption with a temp secret key. |
|||
*/ |
|||
QString AppDataServer::decryptMessage(QJsonDocument msg, QString secretHex, QString lastRemoteNonceHex) { |
|||
// Decrypt and then process
|
|||
QString noncehex = msg.object().value("nonce").toString(); |
|||
QString encryptedhex = msg.object().value("payload").toString(); |
|||
|
|||
// Enforce limits on the size of the message
|
|||
if (noncehex.length() > ((int)crypto_secretbox_NONCEBYTES * 2) || |
|||
encryptedhex.length() > 2 * 50 * 1024 /*50kb*/) { |
|||
return "error"; |
|||
} |
|||
|
|||
// Check to make sure that the nonce is greater than the last known remote nonce
|
|||
unsigned char* lastRemoteBin = new unsigned char[crypto_secretbox_NONCEBYTES]; |
|||
sodium_hex2bin(lastRemoteBin, crypto_secretbox_NONCEBYTES, lastRemoteNonceHex.toStdString().c_str(), lastRemoteNonceHex.length(), |
|||
NULL, NULL, NULL); |
|||
|
|||
unsigned char* noncebin = new unsigned char[crypto_secretbox_NONCEBYTES]; |
|||
sodium_hex2bin(noncebin, crypto_secretbox_NONCEBYTES, noncehex.toStdString().c_str(), noncehex.length(), |
|||
NULL, NULL, NULL); |
|||
|
|||
assert(crypto_secretbox_KEYBYTES == crypto_hash_sha256_BYTES); |
|||
if (sodium_compare(lastRemoteBin, noncebin, crypto_secretbox_NONCEBYTES) != -1) { |
|||
// Refuse to accept a lower nonce, return an error
|
|||
delete[] lastRemoteBin; |
|||
delete[] noncebin; |
|||
return "error"; |
|||
} |
|||
|
|||
unsigned char* secret = new unsigned char[crypto_secretbox_KEYBYTES]; |
|||
sodium_hex2bin(secret, crypto_secretbox_KEYBYTES, secretHex.toStdString().c_str(), crypto_secretbox_KEYBYTES*2, |
|||
NULL, NULL, NULL); |
|||
|
|||
unsigned char* encrypted = new unsigned char[encryptedhex.length() / 2]; |
|||
sodium_hex2bin(encrypted, encryptedhex.length() / 2, encryptedhex.toStdString().c_str(), encryptedhex.length(), |
|||
NULL, NULL, NULL); |
|||
|
|||
int decryptedLen = encryptedhex.length() / 2 - crypto_secretbox_MACBYTES; |
|||
unsigned char* decrypted = new unsigned char[decryptedLen]; |
|||
int result = crypto_secretbox_open_easy(decrypted, encrypted, encryptedhex.length() / 2, noncebin, secret); |
|||
|
|||
QString payload; |
|||
if (result == -1) { |
|||
payload = "error"; |
|||
} else { |
|||
// Update the last seen remote hex
|
|||
saveNonceHex(NonceType::REMOTE, noncehex); |
|||
saveLastSeenTime(); |
|||
|
|||
char* decryptedStr = new char[decryptedLen + 1]; |
|||
sodium_memzero(decryptedStr, decryptedLen + 1); |
|||
memcpy(decryptedStr, decrypted, decryptedLen); |
|||
|
|||
payload = QString(decryptedStr); |
|||
|
|||
delete[] decryptedStr; |
|||
} |
|||
|
|||
delete[] secret; |
|||
delete[] lastRemoteBin; |
|||
delete[] noncebin; |
|||
delete[] encrypted; |
|||
delete[] decrypted; |
|||
|
|||
qDebug() << "Returning decrypted payload="<<payload; |
|||
return payload; |
|||
} |
|||
|
|||
// Process an incoming text message. The message has to be encrypted with the secret key (or the temporary secret key)
|
|||
void AppDataServer::processMessage(QString message, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient, AppConnectionType connType) { |
|||
qDebug() << "processMessage message"; |
|||
//qDebug() << "processMessage message=" << message; // this can log sensitive info
|
|||
auto replyWithError = [=]() { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"error", "Encryption error"}, |
|||
{"to", getWormholeCode(getSecretHex())} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(r); |
|||
return; |
|||
}; |
|||
|
|||
// First, extract the command from the message
|
|||
auto msg = QJsonDocument::fromJson(message.toUtf8()); |
|||
|
|||
// Check if we got an error from the websocket
|
|||
if (msg.object().contains("error")) { |
|||
qDebug() << "Error:" << msg.toJson(); |
|||
return; |
|||
} |
|||
|
|||
// If the message is a ping, just ignore it
|
|||
if (msg.object().contains("ping")) { |
|||
return; |
|||
} |
|||
|
|||
// Then, check if the message is encrpted
|
|||
if (!msg.object().contains("nonce")) { |
|||
replyWithError(); |
|||
return; |
|||
} |
|||
|
|||
auto decrypted = decryptMessage(msg, getSecretHex(), getNonceHex(NonceType::REMOTE)); |
|||
|
|||
// If the decryption failed, maybe this is a new connection, so see if the dialog is open and a
|
|||
// temp secret is in place
|
|||
if (decrypted == "error") { |
|||
// If the dialog is open, then there might be a temporary, new secret key. Attempt to decrypt
|
|||
// with that.
|
|||
if (!tempSecret.isEmpty()) { |
|||
// Since this is a temp secret, the last seen nonce will be "0", so basically we'll accept any nonce
|
|||
QString zeroNonce = QString("00").repeated(crypto_secretbox_NONCEBYTES); |
|||
decrypted = decryptMessage(msg, tempSecret, zeroNonce); |
|||
if (decrypted == "error") { |
|||
// Oh, well. Just return an error
|
|||
replyWithError(); |
|||
return; |
|||
} |
|||
else { |
|||
// This is a new connection. So, update the the secret. Note the last seen remote nonce has already been updated by
|
|||
// decryptMessage()
|
|||
saveNewSecret(tempSecret); |
|||
setAllowInternetConnection(tempWormholeClient != nullptr); |
|||
|
|||
// Swap out the wormhole connection
|
|||
mainWindow->replaceWormholeClient(tempWormholeClient); |
|||
tempWormholeClient = nullptr; |
|||
|
|||
saveLastConnectedOver(connType); |
|||
processDecryptedMessage(decrypted, mainWindow, pClient); |
|||
|
|||
// If the Connection UI is showing, we have to update the UI as well
|
|||
if (ui != nullptr) { |
|||
// Update the connected phone information
|
|||
updateConnectedUI(); |
|||
|
|||
// Update with a new QR Code for safety, so this secret isn't used by anyone else
|
|||
updateUIWithNewQRCode(mainWindow); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
} |
|||
else { |
|||
replyWithError(); |
|||
return; |
|||
} |
|||
} else { |
|||
saveLastConnectedOver(connType); |
|||
processDecryptedMessage(decrypted, mainWindow, pClient); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// Decrypted method will be executed here.
|
|||
void AppDataServer::processDecryptedMessage(QString message, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient) { |
|||
//qDebug() << "processDecryptedMessage message=" << message;
|
|||
// First, extract the command from the message
|
|||
auto msg = QJsonDocument::fromJson(message.toUtf8()); |
|||
|
|||
if (!msg.object().contains("command")) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"errorCode", -1}, |
|||
{"errorMessage", "Unknown JSON format"} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
return; |
|||
} |
|||
|
|||
if (msg.object()["command"] == "getInfo") { |
|||
processGetInfo(msg.object(), mainWindow, pClient); |
|||
} |
|||
else if (msg.object()["command"] == "getTransactions") { |
|||
processGetTransactions(mainWindow, pClient); |
|||
} |
|||
else if (msg.object()["command"] == "sendTx") { |
|||
processSendTx(msg.object()["tx"].toObject(), mainWindow, pClient); |
|||
} |
|||
else if (msg.object()["command"] == "sendmanyTx") { |
|||
processSendManyTx(msg.object()["tx"].toObject(), mainWindow, pClient); |
|||
} |
|||
else { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"errorCode", -1}, |
|||
{"errorMessage", "Command not found:" + msg.object()["command"].toString()} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
} |
|||
|
|||
// "sendTx" command. This method will actually send money, so be careful with everything
|
|||
void AppDataServer::processSendTx(QJsonObject sendTx, MainWindow* mainwindow, std::shared_ptr<ClientWebSocket> pClient) { |
|||
qDebug() << "processSendTx with to=" << sendTx["to"].toString(); |
|||
auto error = [=](QString reason) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"errorCode", -1}, |
|||
{"errorMessage", "Couldn't send Tx:" + reason} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
return; |
|||
}; |
|||
|
|||
// Refuse to send if the node is still syncing
|
|||
if (Settings::getInstance()->isSyncing()) { |
|||
error(QObject::tr("Node is still syncing.")); |
|||
return; |
|||
} |
|||
|
|||
// Create a Tx Object
|
|||
Tx tx; |
|||
tx.fee = Settings::getMinerFee(); |
|||
|
|||
// Find a from address that has at least the sending amout
|
|||
CAmount amt = CAmount::fromDecimalString(sendTx["amount"].toString()); |
|||
auto allBalances = mainwindow->getRPC()->getModel()->getAllBalances(); |
|||
QList<QPair<QString, CAmount>> bals; |
|||
for (auto i : allBalances.keys()) { |
|||
// Filter out balances that don't have the requisite amount
|
|||
if (allBalances.value(i) < amt) |
|||
continue; |
|||
|
|||
bals.append(QPair<QString, CAmount>(i, allBalances.value(i))); |
|||
} |
|||
|
|||
if (bals.isEmpty()) { |
|||
error(QObject::tr("No sapling or transparent addresses with enough balance to spend.")); |
|||
return; |
|||
} |
|||
|
|||
std::sort(bals.begin(), bals.end(), [=](const QPair<QString, CAmount>a, const QPair<QString, CAmount> b) -> bool { |
|||
// Sort z addresses first
|
|||
return a.first > b.first; |
|||
}); |
|||
|
|||
tx.fromAddr = bals[0].first; |
|||
tx.toAddrs = { ToFields{ sendTx["to"].toString(), amt, sendTx["memo"].toString()} }; |
|||
|
|||
// TODO: Respect the autoshield change setting
|
|||
|
|||
QString validation = mainwindow->doSendTxValidations(tx); |
|||
if (!validation.isEmpty()) { |
|||
error(validation); |
|||
return; |
|||
} |
|||
|
|||
json params = json::array(); |
|||
mainwindow->getRPC()->fillTxJsonParams(params, tx); |
|||
std::cout << std::setw(2) << params << std::endl; |
|||
|
|||
// And send the Tx
|
|||
mainwindow->getRPC()->executeTransaction(tx, |
|||
[=] (QString txid) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTxSubmitted"}, |
|||
{"txid", txid} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
}, |
|||
// Errored while submitting Tx
|
|||
[=] (QString, QString errStr) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTxFailed"}, |
|||
{"err", errStr} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
); |
|||
|
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTx"}, |
|||
{"result", "success"} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
|
|||
// "sendmanyTx" command. This method will actually send money, so be careful with everything
|
|||
void AppDataServer::processSendManyTx(QJsonObject sendmanyTx, MainWindow* mainwindow, std::shared_ptr<ClientWebSocket> pClient) { |
|||
qDebug() << "processSendManyTx with to=" << sendmanyTx["to"].toString(); |
|||
auto error = [=](QString reason) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"errorCode", -1}, |
|||
{"errorMessage", "Couldn't send Tx:" + reason} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
return; |
|||
}; |
|||
|
|||
// Refuse to send if the node is still syncing
|
|||
if (Settings::getInstance()->isSyncing()) { |
|||
error(QObject::tr("Node is still syncing.")); |
|||
return; |
|||
} |
|||
|
|||
// Create a Tx Object
|
|||
Tx tx; |
|||
tx.fee = Settings::getMinerFee(); |
|||
|
|||
// Find a from address that has at least the sending amout
|
|||
CAmount amt = CAmount::fromDecimalString(sendmanyTx["amount"].toString()); |
|||
auto allBalances = mainwindow->getRPC()->getModel()->getAllBalances(); |
|||
QList<QPair<QString, CAmount>> bals; |
|||
for (auto i : allBalances.keys()) { |
|||
// Filter out balances that don't have the requisite amount
|
|||
if (allBalances.value(i) < amt) |
|||
continue; |
|||
|
|||
bals.append(QPair<QString, CAmount>(i, allBalances.value(i))); |
|||
} |
|||
|
|||
if (bals.isEmpty()) { |
|||
error(QObject::tr("No sapling or transparent addresses with enough balance to spend.")); |
|||
return; |
|||
} |
|||
|
|||
std::sort(bals.begin(), bals.end(), [=](const QPair<QString, CAmount>a, const QPair<QString, CAmount> b) -> bool { |
|||
// Sort z addresses first
|
|||
return a.first > b.first; |
|||
}); |
|||
|
|||
//send to more then one Receipent
|
|||
|
|||
int totalSendManyItems = sendmanyTx.size(); |
|||
for (int i=0; i < totalSendManyItems; i++) { |
|||
|
|||
amt = CAmount::fromDecimalString(sendmanyTx["amount"].toString() % QString::number(i+1)); |
|||
QString addr = sendmanyTx["to"].toString() % QString::number(i+1); |
|||
QString memo = sendmanyTx["memo"].toString() % QString::number(i+1); |
|||
|
|||
tx.fromAddr = bals[0].first; |
|||
tx.toAddrs = { ToFields{ addr, amt, memo} }; |
|||
} |
|||
// TODO: Respect the autoshield change setting
|
|||
|
|||
QString validation = mainwindow->doSendTxValidations(tx); |
|||
if (!validation.isEmpty()) { |
|||
error(validation); |
|||
return; |
|||
} |
|||
|
|||
json params = json::array(); |
|||
mainwindow->getRPC()->fillTxJsonParams(params, tx); |
|||
std::cout << std::setw(2) << params << std::endl; |
|||
|
|||
// And send the Tx
|
|||
mainwindow->getRPC()->executeTransaction(tx, |
|||
[=] (QString txid) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTxSubmitted"}, |
|||
{"txid", txid} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
}, |
|||
// Errored while submitting Tx
|
|||
[=] (QString, QString errStr) { |
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTxFailed"}, |
|||
{"err", errStr} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
); |
|||
|
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "sendTx"}, |
|||
{"result", "success"} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
|
|||
|
|||
// "getInfo" command
|
|||
void AppDataServer::processGetInfo(QJsonObject jobj, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient) { |
|||
auto connectedName = jobj["name"].toString(); |
|||
|
|||
if (mainWindow == nullptr || mainWindow->getRPC() == nullptr) { |
|||
pClient->close(QWebSocketProtocol::CloseCodeNormal, "Not yet ready"); |
|||
return; |
|||
} |
|||
|
|||
// Max spendable safely from a z address and from any address
|
|||
CAmount maxZSpendable; |
|||
CAmount maxSpendable; |
|||
for (auto a : mainWindow->getRPC()->getModel()->getAllBalances().keys()) { |
|||
if (Settings::getInstance()->isSaplingAddress(a)) { |
|||
if (mainWindow->getRPC()->getModel()->getAllBalances().value(a) > maxZSpendable) { |
|||
maxZSpendable = mainWindow->getRPC()->getModel()->getAllBalances().value(a); |
|||
} |
|||
} |
|||
if (mainWindow->getRPC()->getModel()->getAllBalances().value(a) > maxSpendable) { |
|||
maxSpendable = mainWindow->getRPC()->getModel()->getAllBalances().value(a); |
|||
} |
|||
} |
|||
|
|||
setConnectedName(connectedName); |
|||
|
|||
auto r = QJsonDocument(QJsonObject { |
|||
{"version", 1.0}, |
|||
{"command", "getInfo"}, |
|||
{"saplingAddress", mainWindow->getRPC()->getDefaultSaplingAddress()}, |
|||
{"tAddress", mainWindow->getRPC()->getDefaultTAddress()}, |
|||
{"balance", AppDataModel::getInstance()->getTotalBalance().toDecimalDouble()}, |
|||
{"maxspendable", maxSpendable.toDecimalDouble()}, |
|||
{"maxzspendable", maxZSpendable.toDecimalDouble()}, |
|||
{"tokenName", Settings::getTokenName()}, |
|||
// changing this to hushprice is a backward incompatible change that requires
|
|||
// changing SDL, litewalletd and SDA in unison, and would break older clients
|
|||
// so we just leave it for now
|
|||
{"zecprice", Settings::getInstance()->getHUSHPrice()}, |
|||
{"serverversion", QString(APP_VERSION)} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
|
|||
void AppDataServer::processGetTransactions(MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient) { |
|||
QJsonArray txns; |
|||
auto model = mainWindow->getRPC()->getTransactionsModel(); |
|||
qDebug() << "processGetTransactions"; |
|||
|
|||
|
|||
// Add transactions
|
|||
for (int i = 0; i < model->rowCount(QModelIndex()) && i < Settings::getMaxMobileAppTxns(); i++) { |
|||
txns.append(QJsonObject{ |
|||
{"type", model->getType(i)}, |
|||
{"datetime", model->getDate(i)}, |
|||
{"amount", model->getAmt(i)}, |
|||
{"txid", model->getTxId(i)}, |
|||
{"address", model->getAddr(i)}, |
|||
// {"memo", model->getMemo(i)},
|
|||
{"confirmations", model->getConfirmations(i)} |
|||
}); |
|||
} |
|||
|
|||
auto r = QJsonDocument(QJsonObject{ |
|||
{"version", 1.0}, |
|||
{"command", "getTransactions"}, |
|||
{"transactions", txns} |
|||
}).toJson(); |
|||
pClient->sendTextMessage(encryptOutgoing(r)); |
|||
} |
|||
|
|||
// ==============================
|
|||
// AppDataModel
|
|||
// ==============================
|
|||
AppDataModel* AppDataModel::instance = nullptr; |
@ -1,179 +0,0 @@ |
|||
// Copyright 2019-2023 The Hush developers
|
|||
// Released under the GPLv3
|
|||
#ifndef WEBSOCKETS_H |
|||
#define WEBSOCKETS_H |
|||
|
|||
#include "precompiled.h" |
|||
#include "camount.h" |
|||
#include "mainwindow.h" |
|||
#include "ui_mobileappconnector.h" |
|||
|
|||
QT_FORWARD_DECLARE_CLASS(QWebSocketServer) |
|||
QT_FORWARD_DECLARE_CLASS(QWebSocket) |
|||
|
|||
class WSServer; |
|||
|
|||
// We're going to wrap the websocket in this class, because the underlying QWebSocket might get closed
|
|||
// or deleted while a callback is waiting to get the data back. Therefore, we write a custom "sendTextMessage"
|
|||
// class that checks all this before sending.
|
|||
class ClientWebSocket { |
|||
public: |
|||
ClientWebSocket(QWebSocket* c, WSServer* s = nullptr) { client = c; server = s; } |
|||
|
|||
void sendTextMessage(QString m); |
|||
void close(QWebSocketProtocol::CloseCode code, const QString& msg) { client->close(code, msg); } |
|||
private: |
|||
QWebSocket* client; |
|||
WSServer* server; |
|||
}; |
|||
|
|||
class WSServer : public QObject |
|||
{ |
|||
Q_OBJECT |
|||
public: |
|||
explicit WSServer(quint16 port, bool debug = false, QObject *parent = nullptr); |
|||
bool isValidConnection(QWebSocket* c) { return m_clients.contains(c); } |
|||
~WSServer(); |
|||
|
|||
Q_SIGNALS: |
|||
void closed(); |
|||
|
|||
private Q_SLOTS: |
|||
void onNewConnection(); |
|||
void processTextMessage(QString message); |
|||
void processBinaryMessage(QByteArray message); |
|||
void socketDisconnected(); |
|||
|
|||
private: |
|||
QWebSocketServer *m_pWebSocketServer; |
|||
MainWindow *m_mainWindow; |
|||
QList<QWebSocket *> m_clients; |
|||
bool m_debug; |
|||
}; |
|||
|
|||
class WormholeClient : public QObject { |
|||
Q_OBJECT |
|||
|
|||
private Q_SLOTS: |
|||
void onConnected(); |
|||
void onTextMessageReceived(QString message); |
|||
void closed(); |
|||
|
|||
public: |
|||
WormholeClient(MainWindow* parent, QString wormholeCode); |
|||
~WormholeClient(); |
|||
|
|||
void connect(); |
|||
void retryConnect(); |
|||
|
|||
private: |
|||
MainWindow* parent = nullptr; |
|||
QWebSocket* m_webSocket = nullptr; |
|||
|
|||
QTimer* timer = nullptr; |
|||
|
|||
QString code; |
|||
int retryCount = 0; |
|||
bool shuttingDown = false; |
|||
}; |
|||
|
|||
enum NonceType { |
|||
LOCAL = 1, |
|||
REMOTE |
|||
}; |
|||
|
|||
enum AppConnectionType { |
|||
DIRECT = 1, |
|||
INTERNET |
|||
}; |
|||
|
|||
class AppDataServer { |
|||
public: |
|||
static AppDataServer* getInstance() { |
|||
if (instance == nullptr) { |
|||
instance = new AppDataServer(); |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
void connectAppDialog(MainWindow* parent); |
|||
void updateConnectedUI(); |
|||
void updateUIWithNewQRCode(MainWindow* mainwindow); |
|||
|
|||
void processSendTx(QJsonObject sendTx, MainWindow* mainwindow, std::shared_ptr<ClientWebSocket> pClient); |
|||
void processSendManyTx(QJsonObject sendmanyTx, MainWindow* mainwindow, std::shared_ptr<ClientWebSocket> pClient); |
|||
void processMessage(QString message, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient, AppConnectionType connType); |
|||
void processGetInfo(QJsonObject jobj, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient); |
|||
void processDecryptedMessage(QString message, MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient); |
|||
void processGetTransactions(MainWindow* mainWindow, std::shared_ptr<ClientWebSocket> pClient); |
|||
|
|||
QString decryptMessage(QJsonDocument msg, QString secretHex, QString lastRemoteNonceHex); |
|||
QString encryptOutgoing(QString msg); |
|||
|
|||
QString getWormholeCode(QString secretHex); |
|||
QString getSecretHex(); |
|||
void saveNewSecret(QString secretHex); |
|||
|
|||
void registerNewTempSecret(QString tmpSecretHex, bool allowInternet, MainWindow* main); |
|||
|
|||
QString getNonceHex(NonceType nt); |
|||
void saveNonceHex(NonceType nt, QString noncehex); |
|||
|
|||
bool getAllowInternetConnection(); |
|||
void setAllowInternetConnection(bool allow); |
|||
|
|||
void saveLastSeenTime(); |
|||
QDateTime getLastSeenTime(); |
|||
|
|||
void setConnectedName(QString name); |
|||
QString getConnectedName(); |
|||
bool isAppConnected(); |
|||
|
|||
QString connDesc(AppConnectionType t); |
|||
|
|||
void saveLastConnectedOver(AppConnectionType type); |
|||
AppConnectionType getLastConnectionType(); |
|||
|
|||
private: |
|||
AppDataServer() = default; |
|||
|
|||
static AppDataServer* instance; |
|||
Ui_MobileAppConnector* ui; |
|||
|
|||
QString tempSecret; |
|||
WormholeClient* tempWormholeClient = nullptr; |
|||
}; |
|||
|
|||
class AppDataModel { |
|||
public: |
|||
static AppDataModel* getInstance() { |
|||
if (instance == NULL) |
|||
instance = new AppDataModel(); |
|||
|
|||
return instance; |
|||
} |
|||
|
|||
CAmount getTBalance() { return balTransparent; } |
|||
CAmount getZBalance() { return balShielded; } |
|||
CAmount getTotalBalance() { return balTotal; } |
|||
|
|||
void setBalances(CAmount transparent, CAmount shielded) { |
|||
balTransparent = transparent; |
|||
balShielded = shielded; |
|||
balTotal = balTransparent + balShielded; |
|||
} |
|||
|
|||
private: |
|||
AppDataModel() = default; // Private, for singleton
|
|||
|
|||
CAmount balTransparent; |
|||
CAmount balShielded; |
|||
CAmount balTotal; |
|||
|
|||
QString saplingAddress; |
|||
|
|||
static AppDataModel* instance; |
|||
}; |
|||
|
|||
|
|||
#endif // WEBSOCKETS_H
|
@ -1,12 +1,10 @@ |
|||
#!/bin/bash |
|||
# Copyright 2019-2023 The Hush Developers |
|||
# Copyright 2019-2024 The Hush Developers |
|||
|
|||
cd ../ && ./build.sh linguist && ./build.sh |
|||
# should be better |
|||
|
|||
username=$(id -un) |
|||
|
|||
cd util/ && sed -i "s|\/home\/.*\/SilentDragonLite\/|\/home\/$username\/SilentDragonLite\/|g" SilentDragonLite.desktop |
|||
|
|||
cp SilentDragonLite.desktop ~/.local/share/applications |
|||
# might be /usr/share/applications/ that requires sudo |
Loading…
Reference in new issue