Use Electron for desktop app (#473)

* Add codesigning for macos app

* Add dock icon and fix fullscreen on mac

* Revert "Add dock icon and fix fullscreen on mac"

This reverts commit 7598cab644.

* Use electron for mac

* Download from GH, splash screen

* fixes

* Fix reopening after closing window

* Screen based default size

* Fix quit issue

* Fix camera access

* Add file hash verification

* Add auto generate of hash for macos release

* Only download for mac

* Preferencs and HWIBriidge mode

* Bugfixes

* Fix certificate errors

* Windows support

* Add tor support

* set version js script

* Fix windows release script

* Update docs and delete old app

* Update main.js

* Add error handling

* update unix build

* update windows binary

* Update build-osx.sh

* Update .gitlab-ci.yml

* delete accidental import

* switch to electron-builder

* kick

* kick

* kick

* kick

* remove docker info thing

* pip -> pip3

* fix pip again

* disable snap

* fix

* remove specterd-binaries (and hotwallet . typo), fix build

* test windows build

* Bugfix: move update_pending_psbt into try/except block

* Add specterd specific settings

* Show tor URL in HWIBridge mode (if --tor specified)

* Finish CI

* Build specterd only windows ci

Co-authored-by: Kim Neunert <k9ert@gmx.de>
Co-authored-by: Stepan Snigirev <snigirev.stepan@gmail.com>
This commit is contained in:
benk10 2020-10-26 13:10:13 +02:00 committed by GitHub
parent 6a1a72a5fb
commit 7fca3333e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 4355 additions and 833 deletions

5
.gitignore vendored
View file

@ -13,5 +13,10 @@ key.pem
*.dmg
pyinstaller/specterd
pyinstaller/release
pyinstaller/release-linux
pyinstaller/release-win
pyinstaller/version.txt
pyinstaller/electron/version-data.json
pyinstaller/electron/node_modules
pyinstaller/electron/dist
.DS_Store

View file

@ -22,19 +22,24 @@ before_script:
- python -V # Print out python version for debugging
- apt update
- apt install -y libusb-1.0-0-dev libudev-dev # usb-support in hidapi
- pip install virtualenv
- pip3 install virtualenv
- virtualenv --python=python3 .env
- source .env/bin/activate
test:
stage: testing
script:
- pip3 install -r requirements.txt
- pip3 install -e .
- pip3 install -r test_requirements.txt
# pytest --docker not working? Uncomment this for better debugging:
# - python3 tests/conftest.py
- pytest --docker
- echo hello
#test:
# stage: testing
# script:
# - pip3 install -r requirements.txt
# - pip3 install -e .
# - pip3 install -r test_requirements.txt
# # pytest --docker not working? Uncomment this for better debugging:
# # - python3 tests/conftest.py
# - pytest --docker
release_pip:
stage: releasing
@ -55,25 +60,6 @@ release_pip:
- sha256sum dist/cryptoadvance.specter-*.tar.gz > ./dist/SHA256SUMS.txt
- echo $GH_BIN_UPLOAD_PW | github-binary-upload -u gitlab_upload_release_binaries cryptoadvance/specter-desktop $CI_COMMIT_TAG ./dist/SHA256SUMS.txt ./dist/cryptoadvance.specter-*.tar.gz
release_binary_linux:
image: registry.gitlab.com/cryptoadvance/specter-desktop/bionic-build:latest
stage: releasing
only:
- tags
before_script:
- python -V
- pip install virtualenv
- virtualenv --python=python3 .env
- source .env/bin/activate
script:
# Make sure the version-number is compatibe to the scheme
- if ! [[ $CI_COMMIT_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then exit 1; fi
- pip3 install github-binary-upload
- cd pyinstaller && ./build-unix.sh $CI_COMMIT_TAG
- echo $GH_BIN_UPLOAD_PW | github-binary-upload -u gitlab_upload_release_binaries cryptoadvance/specter-desktop $CI_COMMIT_TAG ./release/specterd-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz ./release/specter_desktop-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz
release_binary_windows:
stage: releasing
only:
@ -82,7 +68,7 @@ release_binary_windows:
- windows
before_script:
- python -V
- pip install virtualenv
- pip3 install virtualenv
- virtualenv --python=python3 .env
- .\.env\Scripts\activate
script:
@ -91,11 +77,26 @@ release_binary_windows:
- whoami
- pip3 install github-binary-upload
- cd pyinstaller
- .\build-win.bat $CI_COMMIT_TAG
- (Get-Content .\specter-desktop.iss).replace('#define MyAppVersion "x.y.z"',"#define MyAppVersion `"$Env:CI_COMMIT_TAG`"") | Set-Content .\specter-desktop.iss
- docker run --rm -i -v ${pwd}:/work k9ert/innosetup iscc /work/specter-desktop.iss
- Get-FileHash .\release\specter_desktop_setup.exe -Algorithm SHA256 > .\release\specter_desktop_setup.exe.SHA256
- echo $Env:GH_BIN_UPLOAD_PW | docker run -i -v ${pwd}:/work k9ert/github-binary-upload:latest -u gitlab_upload_release_binaries cryptoadvance/specter-desktop $Env:CI_COMMIT_TAG ./release/specter_desktop_setup.exe ./release/specter_desktop_setup.exe.SHA256
- .\build-win-ci.bat $CI_COMMIT_TAG
- echo $Env:GH_BIN_UPLOAD_PW | docker run -i -v ${pwd}:/work k9ert/github-binary-upload:latest -u gitlab_upload_release_binaries cryptoadvance/specter-desktop $Env:CI_COMMIT_TAG ./release/specterd-$CI_COMMIT_TAG-win64.zip
release_electron_linux_windows:
image: registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest
stage: releasing
only:
- tags
before_script:
- python -V # Print out python version for debugging
- apt update
- apt install -y unzip libusb-1.0-0-dev libudev-dev # usb-support in hidapi
- pip3 install virtualenv
# Only difference to default befor_script: (ToDo fix this)
- python3 -m virtualenv --python=python3 .env
- source .env/bin/activate
script:
- pip3 install github-binary-upload
- cd pyinstaller && ./build-ci.sh $CI_COMMIT_TAG
- echo $GH_BIN_UPLOAD_PW | github-binary-upload -u gitlab_upload_release_binaries cryptoadvance/specter-desktop $CI_COMMIT_TAG ./release-linux/specterd-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz ./release-linux/specter_desktop-${CI_COMMIT_TAG}-x86_64-linux-gnu.tar.gz ./release-win/Specter-Setup-${CI_COMMIT_TAG}.exe

View file

@ -0,0 +1,3 @@
FROM electronuserland/builder:wine
RUN apt-get update && apt-get install -y python3-pip zip unzip apt libusb-1.0-0-dev libudev-dev

View file

@ -0,0 +1,20 @@
Used for building the electron-app. In short it's the /pyinstaller/build-unix.sh script which is running in this image.
manually do it something like this (copied from [here](https://www.electron.build/multi-platform-build#build-electron-app-using-docker-on-a-local-machine)):
```
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD##*/}-node-modules:/project/node_modules \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine
```
build the image like:
```
docker build -t registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest .
docker push registry.gitlab.com/cryptoadvance/specter-desktop/electron-builder:latest
```

View file

@ -4,7 +4,8 @@ Run `build-<your-os> <version_number>` file to build everything.
For example, `build-osx.sh 1.2.3` will create `SpecterDesktop-1.2.3.dmg` and `specterd-1.2.3-osx.zip` in the `release` folder.
On Windows `release` folder is empty, but `dist` folder contains a `specter_desktop` folder for inno setup and `specterd.exe` binary.
If you're making a real release, you should append `"make hash"` at the end of your command calling the build script.
This will update the file hash and version name the Specter Desktop app expects to download from GitHub.
# Pyinstaller build
@ -26,53 +27,42 @@ And for HWIBridge, run:
pyinstaller hwibridge.spec
```
# Building Specter launcher (tray app)
## Code signing the macOS app for Apple GateKeeper
## Creating a Windows setup file
*Note: for this, you'll need to have an active Apple Developer account*
From Powershell:
If this is the first time you go through this process, you'll need to first set up the following:
1. Build `specterd` in onedir mode:
### Apple Developer Certificate for Code-Signing
1. Go to the Apple Developer website: https://developer.apple.com
2. Click `Account` -> `Certificates, Identifiers & Profiles`
3. Click the `+` icon to create a new certificate. Select `Developer ID Application` and click `Continue`
4. You'll need now to create a certificate signing request, which you can do by following these instructions: https://help.apple.com/developer-account/#/devbfa00fef7, After that you should be able to generate and download the certificate.
5. Download the certificate, then double-click the downloaded certificate to install it in your keychain.
### App Specific Password for authenticating to iTunesConnect for notarization
1. Sign into you Apple ID account: https://appleid.apple.com
2. Go to `Security` -> `App Specific Passwords` and click `Generate Password…`, you'll be asked to enter a label and click `Create`, then you'll receive a new password.
3. Copy the password generated, then open the Terminal and run:
```bash
pyinstaller specterd_onedir.spec
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "<your-apple-id>" -p "<the-generated-password>"
```
You should get a `specterd` directory in the `dist` folder.
2. Copy `specterd` folder from `dist` folder to this directory.
3. Run `pyinstaller specter_desktop.spec` - this should create a `specter_desktop` folder in the `dist` directory. Check that it works by running `dist\specter_desktop\specter_desktop.exe`
4. Create an installer using [InnoSetup](https://jrsoftware.org/isdl.php#stable), select `dist\specter_desktop\specter_desktop.exe` as main executable, add `dist\specter_desktop` folder to the setup wizard as well.
## Creating a DMG for macOS
*Note*: pyinstaller doesn't fully support python3.8 at the moment, use python3.7.
1. Build `specterd` in onedir mode:
After having these set up, you can use the automated script to sign by passing it 2 extra parameters:
- Your certificate name, which you can see on the Keychain app going to the sidebar -> `My Certificates` and copying the name of the certificate you've created in step 1.
- Your Apple ID.
With these two, you can run the command like so:
```bash
pyinstaller specterd_onedir.spec
./build-osx.sh <version_number> "<certificate_name>" "<apple_id>" "make-hash"
```
*Note: "make-hash" is optional and will automatically calculate hash of specterd generated for the macOS app. Should be used only for real release.*
You should get a `specterd` directory in the `dist` folder.
2. Copy `specterd` folder from `dist` folder to this directory: `cp -r dist/specterd/ ./specterd`
3. Now in the terminal, run `pyinstaller specter_desktop.spec` (you might need to use sudo). This should create a new Specter and Specter.app files.
4. The `Specter.app` file is the executable macOS app we will need to package now as a `.dmg` for distribution.
5. Make sure you have [`NPM`](https://www.npmjs.com/get-npm) installed, and run `npm install --global create-dmg`.
6. Now run `create-dmg 'dist/Specter.app'`. This should generate a new `Specter 0.0.0.dmg`.
7. The `.dmg` should now be ready to use! Note: You can rename the `.dmg` file to have the proper version (or just say `Specter`).
## Creating a binary for Linux
1. Build `specterd` in onedir mode:
This should take 10 minutes, during which you should receive an email from Apple notifying whatever the notarization was successful.
If for some reason the notarization failed, you'll be able to get the reason by copying the `Request Identifier` (you should be able to find this in the email and in the logs).
Then run the following command:
```bash
pyinstaller specterd_onedir.spec
xcrun altool --verbose --notarization-info <request_identifier> -u "<apple_id>" -p "@keychain:AC_PASSWORD"
```
You should get a `specterd` directory in the `dist` folder.
2. Copy `specterd` folder from `dist` folder to this directory: `cp -r dist/specterd/ ./specterd`
3. Run `pyinstaller specter_desktop.spec`. This should create a Specter executable.
This will output a long message, at the end of which you should have be able to find the `LogFileURL:`.
This URL should contain a JSON with the issues found by Apple and which you'll need to fix to be able to pass Apple's notarization.

43
pyinstaller/build-ci.sh Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# pass version number as an argument
echo $1 > version.txt
pip3 install -r requirements.txt --require-hashes
pip3 install -e ..
rm -rf build/ dist/ release/ electron/release/ electron/dist release-linux/ release-win/
pyinstaller specterd.spec
cd electron
npm ci
node ./set-version $1 ../dist/specterd
# build electron app
npm i
npm run dist -- --linux
cd ..
# copy everything to release folder
mkdir release-linux
cd dist
cp -r ../../udev ./udev
echo "Don't forget to set up udev rules! Check out udev folder for instructions." > README.md
zip -r ../release-linux/specterd-$1-`arch`-linux-gnu.zip specterd udev README.md
cp ../electron/dist/Specter-* ./
tar -czvf ../release-linux/specter_desktop-$1-`arch`-linux-gnu.tar.gz Specter-* udev README.md
cd ..
rm -rf dist
mkdir dist
cd dist
wget https://github.com/cryptoadvance/specter-desktop/releases/download/$1/specterd-$1-win64.zip -O ./specterd.zip
unzip specterd.zip
cd ../electron
rm -rf dist/
npm ci
node ./set-version $1 ../dist/specterd.exe
npm run dist -- --win
cd ..
mkdir release-win
cp electron/dist/Specter\ Setup\ *.exe release-win/Specter\ Setup\ $1.exe

View file

@ -1,17 +1,44 @@
#!/usr/bin/env bash
# pass version number as an argument
echo $1 > version.txt
pip install -r requirements.txt --require-hashes
pip install -e ..
rm -rf build/ dist/ release/
pip3 install -r requirements.txt --require-hashes
pip3 install -e ..
rm -rf build/ dist/ release/ electron/release/ electron/dist
rm *.dmg
pyinstaller specter_desktop.spec
pyinstaller specterd.spec
cd electron
npm ci
if [[ "$4" == 'make-hash' ]]
then
node ./set-version $1 ../dist/specterd
else
node ./set-version $1
fi
npm i
if [[ "$2" == '' ]]
then
echo "`jq '.build.mac.identity=null' package.json`" > package.json
else
echo "`jq '.build.mac.identity="'"$2"'"' package.json`" > package.json
fi
npm run dist
if [[ "$2" != '' ]]
then
echo 'Attempting to code sign...'
ditto -c -k --keepParent "dist/mac/Specter.app" dist/Specter.zip
xcrun altool --notarize-app -t osx -f dist/Specter.zip --primary-bundle-id "solutions.specter.desktop" -u "$3" --password "@keychain:AC_PASSWORD"
sleep 180
xcrun stapler staple "dist/mac/Specter.app"
fi
cd ..
mkdir release
create-dmg 'dist/Specter.app'
mv "Specter 0.0.0.dmg" release/SpecterDesktop-$1.dmg
zip release/specterd-$1-osx.zip dist/specterd
create-dmg 'electron/dist/mac/Specter.app' --identity="Developer ID Application: $2"
mv "Specter ${1:1}.dmg" release/SpecterDesktop-$1.dmg
cd dist
zip ../release/specterd-$1-osx.zip specterd
cd ..

View file

@ -3,20 +3,34 @@
# pass version number as an argument
echo $1 > version.txt
pip install -r requirements.txt --require-hashes
pip install -e ..
rm -rf build/ dist/ release/
pyinstaller specter_desktop.spec
pip3 install -r requirements.txt --require-hashes
pip3 install -e ..
rm -rf build/ dist/ release/ electron/release/ electron/dist
pyinstaller specterd.spec
cd electron
npm ci
# calculate the hash of the binary for download
if [[ "$2" == 'make-hash' ]]
then
node ./set-version $1 ../dist/specterd
else
node ./set-version $1
fi
# build electron app
npm i
npm run dist
cd ..
# copy everything to release folder
mkdir release
cd dist
cp -r ../../udev ./udev
echo "Don't forget to set up udev rules! Check out udev folder for instructions." > README.md
zip -r ../release/specterd-$1-`arch`-linux-gnu.zip specterd udev README.md
mkdir release/specter_desktop-$1-`arch`-linux-gnu
cp dist/Specter release/specter_desktop-$1-`arch`-linux-gnu/
cp -r ../udev release/specter_desktop-$1-`arch`-linux-gnu/udev
tar -czvf release/specter_desktop-$1-`arch`-linux-gnu.tar.gz release/specter_desktop-$1-`arch`-linux-gnu
cp ../electron/dist/Specter-* ./
tar -czvf ../release/specter_desktop-$1-`arch`-linux-gnu.tar.gz Specter-* udev README.md
mkdir release/specterd-$1-`arch`-linux-gnu
cp dist/specterd release/specterd-$1-`arch`-linux-gnu/
cp -r ../udev release/specterd-$1-`arch`-linux-gnu/udev
tar -czvf release/specterd-$1-`arch`-linux-gnu.tar.gz release/specterd-$1-`arch`-linux-gnu
cd ..

View file

@ -0,0 +1,13 @@
@ECHO OFF
echo %1% > version.txt
pip3 install -r requirements.txt --require-hashes
pip3 install -e ..
rmdir /s /q .\dist\
rmdir /s /q .\build\
rmdir /s /q .\release\
rmdir /s /q .\electron\dist\
pyinstaller.exe specterd.spec
mkdir release
powershell Compress-Archive -Path dist\specterd.exe release\specterd-%1%-win64.zip

View file

@ -1,13 +1,27 @@
@ECHO OFF
echo %1 > version.txt
pip install -r requirements.txt --require-hashes
pip install -e ..
echo %1% > version.txt
pip3 install -r requirements.txt --require-hashes
pip3 install -e ..
rmdir /s /q .\dist\
rmdir /s /q .\build\
rmdir /s /q .\release\
pyinstaller.exe specter_desktop.spec
rmdir /s /q .\electron\dist\
pyinstaller.exe specterd.spec
cd electron
call npm ci
if "%2%"=="make-hash" (
call node ./set-version "%1%" "../dist/specterd.exe"
) else (
node ./set-version "%1%"
)
call npm i
call npm run dist
cd ..
mkdir release
SET EXE_PATH="electron\dist\Specter Setup *.exe"
SET EXE_RELEASE_PATH="release\Specter Setup %1%.exe"
echo f | xcopy /s/y %EXE_PATH% %EXE_RELEASE_PATH%
echo We've built everything we could, now zip specterd and run inno-setup for specter-desktop
powershell Compress-Archive -Path dist\specterd.exe release\specterd-%1%-win64.zip

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,46 @@
const fs = require('fs')
const path = require('path')
let versionData
try {
versionData = require('./version-data.json')
} catch {
console.log('Could not find default version data configurations...')
versionData = {
"version": "",
"sha256": ""
}
}
const appSettingsPath = path.resolve(require('os').homedir(), '.specter/app_settings.json')
function getAppSettings() {
let defaultSettings = {
mode: 'specterd',
specterURL: 'http://localhost:25441',
tor: false,
proxyURL: "socks5://127.0.0.1:9050",
specterdVersion: versionData.version,
specterdHash: versionData.sha256,
specterdCLIArgs: ""
}
try {
fs.writeFileSync(appSettingsPath, JSON.stringify(defaultSettings), { flag: 'wx' });
} catch {
// settings file already exists
}
// Make sure to add missing settings in case the format changed or new settings were added
let appSettings = require(appSettingsPath)
for (let key of Object.keys(defaultSettings)) {
if (!appSettings.hasOwnProperty(key)) {
appSettings[key] = defaultSettings[key]
}
}
return appSettings
}
module.exports = {
appSettingsPath: appSettingsPath,
getAppSettings: getAppSettings
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

View file

@ -0,0 +1,286 @@
// Modules to control application life and create native browser window
const { app, BrowserWindow, Menu, screen, shell, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
const request = require('request')
const extract = require('extract-zip')
const crypto = require('crypto')
const defaultMenu = require('electron-default-menu');
const { spawn, exec } = require('child_process');
const console = require('console')
const getAppSettings = require('./helpers').getAppSettings
let appSettings = getAppSettings()
let dimensions = { widIth: 1500, height: 1000 };
const download = (uri, filename, callback) => {
request.head(uri, (err, res, body) => {
console.log('content-type:', res.headers['content-type'])
console.log('content-length:', res.headers['content-length'])
request(uri).pipe(fs.createWriteStream(filename)).on('close', callback)
})
}
let specterdProcess
let mainWindow
let webPreferences = {
worldSafeExecuteJavaScript: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
app.commandLine.appendSwitch('ignore-certificate-errors');
let platformName = ''
switch (process.platform) {
case 'darwin':
platformName = 'osx'
break
case 'win32':
platformName = 'win64'
break
case 'linux':
platformName = 'x86_64-linux-gnu'
break
}
function createWindow (specterURL) {
if (!mainWindow) {
mainWindow = new BrowserWindow({
width: parseInt(dimensions.width * 0.8),
height: parseInt(dimensions.height * 0.8),
webPreferences
})
}
mainWindow.webContents.on("did-fail-load", function() {
mainWindow.loadURL(`file://${__dirname}/splash.html`);
updatingLoaderMsg(`Failed to load: ${specterURL}<br>Please make sure the URL is entered correctly in the Preferences and try again...`)
});
// Create the browser window.
if (appSettings.tor) {
mainWindow.webContents.session.setProxy({ proxyRules: appSettings.proxyURL });
}
mainWindow.loadURL(specterURL)
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
dimensions = screen.getPrimaryDisplay().size;
// create a new `splash`-Window
mainWindow = new BrowserWindow({
width: parseInt(dimensions.width * 0.8),
height: parseInt(dimensions.height * 0.8),
webPreferences
})
setMainMenu();
mainWindow.loadURL(`file://${__dirname}/splash.html`);
const specterdDirPath = path.resolve(require('os').homedir(), '.specter/specterd-binaries')
if (!fs.existsSync(specterdDirPath)){
fs.mkdirSync(specterdDirPath, { recursive: true });
}
const specterdPath = specterdDirPath + '/specterd'
if (fs.existsSync(specterdPath + (platformName == 'win64' ? '.exe' : ''))) {
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
if (appSettings.specterdHash.toLowerCase() == specterdHash || appSettings.specterdHash == "") {
startSpecterd(specterdPath)
} else if (appSettings.specterdVersion != "") {
updatingLoaderMsg('Specterd version could not be validated.<br>Retrying fetching specterd...')
downloadSpecterd(specterdPath)
} else {
updatingLoaderMsg('Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...')
}
})
} else {
if (appSettings.specterdVersion) {
downloadSpecterd(specterdPath)
} else {
updatingLoaderMsg('Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...')
}
}
})
function downloadSpecterd(specterdPath) {
updatingLoaderMsg('Fetching the Specter binary...')
console.log("Using version ", appSettings.specterdVersion);
console.log(`https://github.com/cryptoadvance/specter-desktop/releases/download/${appSettings.specterdVersion}/specterd-${appSettings.specterdVersion}-${platformName}.zip`);
download(`https://github.com/cryptoadvance/specter-desktop/releases/download/${appSettings.specterdVersion}/specterd-${appSettings.specterdVersion}-${platformName}.zip`, specterdPath + '.zip', function() {
updatingLoaderMsg('Unpacking files...')
extract(specterdPath + '.zip', { dir: specterdPath + '-dir' }).then(function () {
let extraPath = ''
switch (process.platform) {
case 'darwin':
extraPath = 'specterd'
break
case 'win32':
extraPath = 'specterd.exe'
break
case 'linux':
extraPath = 'specterd'
}
var oldPath = specterdPath + `-dir/${extraPath}`
var newPath = specterdPath + (platformName == 'win64' ? '.exe' : '')
fs.renameSync(oldPath, newPath)
updatingLoaderMsg('Cleaning up...')
fs.unlinkSync(specterdPath + '.zip')
fs.rmdirSync(specterdPath + '-dir', { recursive: true });
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function(specterdHash) {
if (appSettings.specterdVersion.toLowerCase() === specterdHash || appSettings.specterdVersion == "") {
startSpecterd(specterdPath)
} else {
updatingLoaderMsg('Specterd version could not be validated.')
// app.quit()
// TODO: This should never happen unless the specterd file was swapped on GitHub.
// Think of what would be the appropriate way to handle this...
}
})
})
})
}
function getFileHash(filename, callback) {
let shasum = crypto.createHash('sha256')
// Updating shasum with file content
, s = fs.ReadStream(filename)
s.on('data', function(data) {
shasum.update(data)
})
// making digest
s.on('end', function() {
var hash = shasum.digest('hex')
callback(hash)
})
}
function updatingLoaderMsg(msg) {
let code = `
var launchText = document.getElementById('launch-text');
launchText.innerHTML = '${msg}';
`;
mainWindow.webContents.executeJavaScript(code);
}
function startSpecterd(specterdPath) {
if (platformName == 'win64') {
specterdPath += '.exe'
}
let appSettings = getAppSettings()
let hwiBridgeMode = appSettings.mode == 'hwibridge'
updatingLoaderMsg('Launching Specter Desktop...')
let specterdArgs = hwiBridgeMode ? ['--hwibridge'] : null
if (appSettings.specterdCLIArgs != '') {
if (specterdArgs == null) {
specterdArgs = []
}
let specterdExtraArgs = appSettings.specterdCLIArgs.split('--')
specterdExtraArgs = specterdExtraArgs.filter(Boolean)
specterdExtraArgs.forEach((arg, index) => specterdExtraArgs[index] = '--' + arg.trim())
specterdArgs = specterdArgs.concat(specterdExtraArgs)
}
specterdProcess = spawn(specterdPath, specterdArgs);
specterdProcess.stdout.on('data', (_) => {
if (mainWindow) {
createWindow(appSettings.specterURL)
}
});
specterdProcess.stderr.on('data', function(_) {
// https://stackoverflow.com/questions/20792427/why-is-my-node-child-process-that-i-created-via-spawn-hanging
// needed so specterd won't get stuck
});
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL)
})
// since these are streams, you can pipe them elsewhere
specterdProcess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
mainWindow = null
if (process.platform !== 'darwin') app.quit()
})
app.on('before-quit', () => {
mainWindow = null;
if (platformName == 'win64') {
exec('taskkill -F -T -PID ' + specterdProcess.pid);
process.kill(-specterdProcess.pid)
}
if (specterdProcess) {
specterdProcess.kill('SIGINT')
}
})
function setMainMenu() {
const menu = defaultMenu(app, shell);
// Add custom menu
if (platformName == 'osx') {
menu[0].submenu.splice(1, 0,
{
label: 'Preferences',
click: openPreferences,
accelerator: "CmdOrCtrl+,"
}
);
} else {
menu.unshift({
label: 'Specter',
submenu: [{
label: 'Preferences',
click: openPreferences,
accelerator: "CmdOrCtrl+,"
}]
}
);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
}
function openPreferences() {
let prefWindow = new BrowserWindow({
width: 700,
height: 750,
parent: mainWindow,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
})
prefWindow.loadURL(`file://${__dirname}/settings.html`)
prefWindow.show()
}
function showError(error) {
dialog.showErrorBox('Specter Desktop encounter an error', error.toString())
updatingLoaderMsg('Specter Desktop encounter an error:<br>' + error.toString())
}
process.on('unhandledRejection', error => {
showError(error)
})
process.on("uncaughtException", error => {
showError(error)
})

2403
pyinstaller/electron/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
{
"name": "specter-desktop",
"version": "v0.8.2-pre1",
"description": "Specter Desktop Electron application",
"main": "main.js",
"scripts": {
"start": "electron .",
"pack": "electron-builder --dir",
"dist": "electron-builder --publish=never",
"postinstall": "electron-builder install-app-deps"
},
"repository": "https://github.com/cryptoadvance/specter-desktop",
"keywords": [
"Electron",
"quick",
"start",
"tutorial",
"demo"
],
"author": "Specter",
"license": "MIT",
"devDependencies": {
"electron": "^10.1.3",
"electron-builder": "^22.9.1"
},
"build": {
"productName": "Specter",
"appId": "solutions.specter.desktop",
"mac": {
"category": "public.app-category.utilities",
"identity": "Ben Kaufman (6RPRX6WRJ5)",
"entitlements": "./build/entitlements.mac.plist",
"entitlementsInherit": "./build/entitlements.mac.plist",
"hardenedRuntime": true
},
"win": {
"icon": "../icons/icon.ico"
},
"linux": {
"target": [
"AppImage"
]
}
},
"dependencies": {
"electron-default-menu": "^1.0.2",
"extract-zip": "^2.0.1",
"request": "^2.88.2"
}
}

View file

@ -0,0 +1,12 @@
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})

View file

@ -0,0 +1,6 @@
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.

View file

@ -0,0 +1,25 @@
const fs = require('fs')
const crypto = require('crypto')
const version = process.argv[2]
async function setVersion() {
let package = require('./package.json')
package.version = version
fs.writeFileSync('./package.json', JSON.stringify(package, undefined, 2))
if (process.argv[3]) {
let versionData = {
version,
sha256: (await createHashFromFile(process.argv[3]))
}
fs.writeFileSync('./version-data.json', JSON.stringify(versionData, undefined, 2))
}
}
const createHashFromFile = filePath => new Promise(resolve => {
const hash = crypto.createHash('sha256');
fs.createReadStream(filePath).on('data', data => hash.update(data)).on('end', () => resolve(hash.digest('hex')));
});
setVersion()

View file

@ -0,0 +1,101 @@
<html>
<link rel="stylesheet" type="text/css" href="./styles.css">
<body style="overflow: auto; height: 100%;">
<div style="margin: auto; text-align: left;">
<h1 style="margin-top: 20px;">Preferences</h1>
<form action="" class="card">
<div>
<h2>Specter Server</h2>
<div style="margin: 10px;">
Run Specter:<br>
<label><input type="radio" class="inline" name="mode" value="specterd" onclick="toggleHWIBridgeView(false)" checked>Run local Specter server</label><br>
<label><input type="radio" class="inline" id="hwibridge-mode-active" name="mode" value="hwibridge" onclick="toggleHWIBridgeView(true);">Use a remote Specter server</label><br>
<div id="hwibridge-mode-settings"><br>
<input id="specter-url" style="margin-top: 10px;" type="url" placeholder="Please enter the remote Specter URL" />
<br>
<p style="margin-top: 20px;">
<img style="width: 25px; margin-right: 7px; vertical-align: bottom;" src="./tor.svg"/><span style="vertical-align: bottom; margin-right: 10px;">Connect over Tor: </span><label class="switch">
<input type="checkbox" id="tor-checkbox">
<span class="slider"></span>
</label>
</p>
<input id="proxy-url" type="url" placeholder="Proxy URL" />
</div>
</div>
<br>
<h2>Specter daemon configurations</h2>
<div style="margin: 10px;">
<p>Specterd Version: <input id="specterd-version" style="margin-top: 10px;" type="text" placeholder="The specterd version to install (leave blank to skip installation)" /></p>
<p>Specterd File Hash: <input id="specterd-hash" style="margin-top: 10px;" type="text" placeholder="The specterd expected file hash (leave blank to skip check)" /></p>
<p>Specterd CLI args: <input id="specterd-cli-args" style="margin-top: 10px;" type="text" placeholder="example: --tor --port=25441" /></p>
</div>
</div><br>
<div class="row flex-center">
<button type="button" save-btn" class="btn" onclick="savePreferences()" style="margin: 5px;">Save</button>
<button type="button" id="cancel-btn" class="btn" onclick="window.close()" style="margin: 5px;">Cancel</button>
</div>
</form>
</div>
<script>
const fs = require('fs')
const path = require('path')
const { dialog } = require('electron').remote
const helpers = require('./helpers')
const getAppSettings = helpers.getAppSettings
let appSettingsPath = helpers.appSettingsPath
function toggleHWIBridgeView(isActive) {
document.getElementById('hwibridge-mode-active').checked = isActive
document.getElementById('hwibridge-mode-settings').style.display = isActive ? 'block' : 'none'
}
function savePreferences() {
let hwiBridgeMode = document.getElementById('hwibridge-mode-active').checked
let remoteSpecterURL = document.getElementById('specter-url').value
if (hwiBridgeMode) {
let hwiBridgeSettingsPath = path.resolve(require('os').homedir(), '.specter/hwi_bridge_config.json')
let defaultHWIBridgeSettings = {
whitelisted_domains: "http://127.0.0.1:25441/"
}
try {
fs.writeFileSync(hwiBridgeSettingsPath, JSON.stringify(defaultHWIBridgeSettings), { flag: 'wx' });
} catch {
// settings file already exists
}
let hwiBridgeSettings = require(hwiBridgeSettingsPath)
if (hwiBridgeSettings) {
hwiBridgeSettings.whitelisted_domains += `\n${remoteSpecterURL}`
}
fs.writeFileSync(hwiBridgeSettingsPath, JSON.stringify(hwiBridgeSettings));
}
let appSettings = getAppSettings()
appSettings.mode = hwiBridgeMode ? 'hwibridge' : 'specterd'
appSettings.specterURL = hwiBridgeMode ? remoteSpecterURL : 'http://localhost:25441'
appSettings.tor = document.getElementById('tor-checkbox').checked
appSettings.proxyURL = document.getElementById('proxy-url').value
appSettings.specterdVersion = document.getElementById('specterd-version').value
appSettings.specterdHash = document.getElementById('specterd-hash').value
appSettings.specterdCLIArgs = document.getElementById('specterd-cli-args').value
fs.writeFileSync(appSettingsPath, JSON.stringify(appSettings));
dialog.showMessageBox({message: 'Specter settings were saved successfully!\nPlease restart the app to activate the changes.', buttons: ['Continue'] });
window.close()
}
document.addEventListener("DOMContentLoaded", function() {
let appSettings = getAppSettings()
let hwiBridgeMode = appSettings.mode == 'hwibridge'
if (hwiBridgeMode) {
document.getElementById('specter-url').value = appSettings.specterURL
}
document.getElementById('proxy-url').value = appSettings.proxyURL
document.getElementById('tor-checkbox').checked = appSettings.tor
document.getElementById('specterd-version').value = appSettings.specterdVersion
document.getElementById('specterd-hash').value = appSettings.specterdHash
document.getElementById('specterd-cli-args').value = appSettings.specterdCLIArgs
toggleHWIBridgeView(hwiBridgeMode)
});
</script>
</body>
</html>

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="./styles.css">
<body style="background-color: #000">
<div width="100%" height="100%" class="container">
<img style="margin: auto;" src="./loader.gif"/>
<p id="launch-text" style="margin: auto; color: #fff; font-size: 1.5em;">Launching Specter Desktop...</p>
</div>
</body>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2="0%" id="linearGradient-1">
<stop stop-color="#420C5D" offset="0%"></stop>
<stop stop-color="#951AD1" offset="100%"></stop>
</linearGradient>
<path d="M25,29 C152.577777,29 256,131.974508 256,259 C256,386.025492 152.577777,489 25,489 L25,29 Z" id="path-2"></path>
<filter x="-18.2%" y="-7.4%" width="129.4%" height="114.8%" filterUnits="objectBoundingBox" id="filter-3">
<feOffset dx="-8" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="tor-browser-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_512x512">
<g id="Group">
<g id="tb_icon/Stable">
<g id="Stable">
<circle id="background" fill="#F2E4FF" fill-rule="nonzero" cx="256" cy="256" r="246"></circle>
<path d="M256.525143,465.439707 L256.525143,434.406609 C354.826191,434.122748 434.420802,354.364917 434.420802,255.992903 C434.420802,157.627987 354.826191,77.8701558 256.525143,77.5862948 L256.525143,46.5531962 C371.964296,46.8441537 465.446804,140.489882 465.446804,255.992903 C465.446804,371.503022 371.964296,465.155846 256.525143,465.439707 Z M256.525143,356.820314 C311.970283,356.529356 356.8487,311.516106 356.8487,255.992903 C356.8487,200.476798 311.970283,155.463547 256.525143,155.17259 L256.525143,124.146588 C329.115485,124.430449 387.881799,183.338693 387.881799,255.992903 C387.881799,328.654211 329.115485,387.562455 256.525143,387.846316 L256.525143,356.820314 Z M256.525143,201.718689 C286.266674,202.00255 310.3026,226.180407 310.3026,255.992903 C310.3026,285.812497 286.266674,309.990353 256.525143,310.274214 L256.525143,201.718689 Z M0,255.992903 C0,397.384044 114.60886,512 256,512 C397.384044,512 512,397.384044 512,255.992903 C512,114.60886 397.384044,0 256,0 C114.60886,0 0,114.60886 0,255.992903 Z" id="center" fill="url(#linearGradient-1)"></path>
<g id="half" transform="translate(140.500000, 259.000000) scale(-1, 1) translate(-140.500000, -259.000000) ">
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
<use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -1,3 +1 @@
pyinstaller
PyQt5
PyQtWebEngine

View file

@ -15,43 +15,6 @@ pyinstaller-hooks-contrib==2020.9 \
pyinstaller==4.0 \
--hash=sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0 \
# via -r requirements.in
pyqt5-sip==12.8.1 \
--hash=sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194 \
--hash=sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9 \
--hash=sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0 \
--hash=sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd \
--hash=sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c \
--hash=sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d \
--hash=sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9 \
--hash=sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115 \
--hash=sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507 \
--hash=sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb \
--hash=sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f \
--hash=sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec \
--hash=sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2 \
--hash=sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369 \
--hash=sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c \
--hash=sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5 \
--hash=sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0 \
--hash=sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777 \
--hash=sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6 \
--hash=sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a \
--hash=sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13 \
# via pyqt5, pyqtwebengine
pyqt5==5.15.1 \
--hash=sha256:17a6d5258796bae16e447aa3efa00258425c09cf88ef68238762628a5dde7c6f \
--hash=sha256:4e47021c2b8e89a3bc64247dfb224144e5c8d77e3ab44f3842d120aab6b3cbd4 \
--hash=sha256:b1ea7e82004dc7b311d1e29df2f276461016e2d180e10c73805ace4376125ed9 \
--hash=sha256:b9e7cc3ec69f80834f3f7507478c77e4d42411d5e9e557350e61b2660d12abc2 \
--hash=sha256:d9a76b850246d08da9863189ecb98f6c2aa9b4d97a3e85e29330a264aed0f9a1 \
# via -r requirements.in, pyqtwebengine
pyqtwebengine==5.15.1 \
--hash=sha256:211e5b10667181dec74f2ef0fd56b94b3cdabaa3e0bdc0a477220b3a19ec92c1 \
--hash=sha256:79fcbf3321457e2acc3b0010e80f987efb611fe7c9216a529f1e71f8f741fb4d \
--hash=sha256:9d03d85a7a4ca3c1e751ff87f9b830c36be94dd87cadede6defad9639a79333f \
--hash=sha256:d2f0785b3b9d0779cb7db4c05e15d630bc4fd7f202c7a4cad37dcc45580b3b2f \
--hash=sha256:f0ca7915ee206ba5d703168c6ca40b0aad62c67360328fae4af5359cdbcee439 \
# via -r requirements.in
# WARNING: The following packages were not pinned, but pip requires them to be
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.

View file

@ -1,58 +0,0 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Specter"
; Maybe we should remove the next 3 lines altogether
; See https://stackoverflow.com/questions/13423317/inno-setup-ide-and-iscc-ispp-passing-define
; for reasoning
; So this can now be overridden via iscc ... /DMyAppVersion="v0.7.2"
; as it didn't work, we're replacing it directly in the file
#ifndef myarg
#define MyAppVersion "x.y.z"
#endif
#define MyAppPublisher "CryptoAdvance GmbH"
#define MyAppURL "https://github.com/cryptoadvance/specter-desktop/"
#define MyAppExeName "specter_desktop.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{25BB5828-3DCE-42B5-A666-00A431A2026E}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\SpecterDesktop
DisableProgramGroupPage=yes
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir=Z:\work\release
OutputBaseFilename=specter_desktop_setup
SetupIconFile=Z:\work\icons\icon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "Z:\work\dist\specter_desktop\specter_desktop.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "Z:\work\dist\specter_desktop\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

View file

@ -1,498 +0,0 @@
from PyQt5.QtGui import QIcon, QCursor, QDesktopServices
from PyQt5.QtWidgets import (
QApplication,
QSystemTrayIcon,
QMenu,
QAction,
QDialog,
QDialogButtonBox,
QVBoxLayout,
QRadioButton,
QLineEdit,
QFileDialog,
QLabel,
QWidget,
)
from PyQt5.QtCore import (
QRunnable,
QThreadPool,
QSettings,
QUrl,
Qt,
pyqtSignal,
pyqtSlot,
QObject,
QSize,
QPoint,
QEvent,
)
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
import sys
import os
import subprocess
import webbrowser
import json
import platform
import time
import signal
import requests
from cryptoadvance.specter.config import DATA_FOLDER
from cryptoadvance.specter.helpers import deep_update
from cryptoadvance.specter.cli import server
import threading
running = True
path = os.path.dirname(os.path.abspath(__file__))
is_specterd_running = False
specterd_thread = None
settings = QSettings("cryptoadvance", "specter")
wait_for_specterd_process = None
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class SpecterPreferencesDialog(QDialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle("Specter Preferences")
self.layout = QVBoxLayout()
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# Mode setting
self.mode_local = QRadioButton("Run Local Specter Server")
self.mode_local.toggled.connect(self.toggle_mode)
self.mode_remote = QRadioButton("Use a Remote Specter Server")
self.mode_remote.toggled.connect(self.toggle_mode)
self.specter_url = QLineEdit(
placeholderText="Please enter the remote Specter URL"
)
is_remote_mode = settings.value("remote_mode", defaultValue=False, type=bool)
if is_remote_mode:
self.mode_remote.setChecked(True)
else:
self.mode_local.setChecked(True)
self.specter_url.hide()
settings.setValue("remote_mode_temp", is_remote_mode)
remote_specter_url = (
settings.value("specter_url", defaultValue="", type=str)
if is_remote_mode
else ""
)
settings.setValue("specter_url_temp", remote_specter_url)
self.specter_url.setText(remote_specter_url)
self.specter_url.textChanged.connect(
lambda: settings.setValue("specter_url_temp", self.specter_url.text())
)
self.layout.addWidget(self.mode_local)
self.layout.addWidget(self.mode_remote)
self.layout.addWidget(self.specter_url)
self.layout.addWidget(self.buttonBox)
self.resize(500, 180)
self.setLayout(self.layout)
def toggle_mode(self):
if self.mode_local.isChecked():
settings.setValue("remote_mode_temp", False)
self.specter_url.hide()
else:
settings.setValue("remote_mode_temp", True)
self.specter_url.show()
# Cross communication between threads via signals
# https://www.learnpyqt.com/courses/concurrent-execution/multithreading-pyqt-applications-qthreadpool/
class ProcessSignals(QObject):
error = pyqtSignal()
result = pyqtSignal()
class ProcessRunnable(QRunnable):
def __init__(self, menu):
super().__init__()
self.menu = menu
self.signals = ProcessSignals()
@pyqtSlot()
def run(self):
menu = self.menu
start_specterd_menu = menu.actions()[0]
start_specterd_menu.setEnabled(False)
start_specterd_menu.setText(
"Starting up Specter{} daemon...".format(
" HWIBridge"
if settings.value("remote_mode", defaultValue=False, type=bool)
else ""
)
)
while running:
is_remote_mode = settings.value(
"remote_mode", defaultValue=False, type=bool
)
try:
if is_remote_mode:
requests.get(
"http://localhost:25441/hwi/settings", allow_redirects=False
)
else:
requests.get("http://localhost:25441/login", allow_redirects=False)
start_specterd_menu.setText(
"Specter{} daemon is running".format(
" HWIBridge"
if settings.value("remote_mode", defaultValue=False, type=bool)
else ""
)
)
toggle_specterd_status(menu)
self.signals.result.emit()
return
except:
pass
time.sleep(0.1)
def start(self):
QThreadPool.globalInstance().start(self)
def watch_specterd(menu, view, first_time=False):
global specterd_thread, wait_for_specterd_process
try:
wait_for_specterd_process = ProcessRunnable(menu)
wait_for_specterd_process.signals.result.connect(
lambda: open_webview(view, first_time)
)
wait_for_specterd_process.signals.error.connect(lambda: print("error"))
wait_for_specterd_process.start()
except Exception as e:
print("* Failed to start Specter daemon {}".format(e))
def open_specter_window():
webbrowser.open(settings.value("specter_url", type=str), new=1)
def toggle_specterd_status(menu):
global is_specterd_running
start_specterd_menu = menu.actions()[0]
open_webview_menu = menu.actions()[1]
open_browser_menu = menu.actions()[2]
if is_specterd_running:
start_specterd_menu.setEnabled(False)
open_webview_menu.setEnabled(True)
open_browser_menu.setEnabled(True)
else:
start_specterd_menu.setText(
"Start Specter{} daemon".format(
" HWIBridge"
if settings.value("remote_mode", defaultValue=False, type=bool)
else ""
)
)
start_specterd_menu.setEnabled(True)
open_webview_menu.setEnabled(False)
open_browser_menu.setEnabled(False)
is_specterd_running = not is_specterd_running
def quit_specter(app):
global running
running = False
app.quit()
def open_settings():
dlg = SpecterPreferencesDialog()
if dlg.exec_():
is_remote_mode = settings.value(
"remote_mode_temp", defaultValue=False, type=bool
)
settings.setValue("remote_mode", is_remote_mode)
specter_url_temp = settings.value(
"specter_url_temp", defaultValue="http://localhost:25441/", type=str
)
if not specter_url_temp.endswith("/"):
specter_url_temp += "/"
# missing schema?
if "://" not in specter_url_temp:
specter_url_temp = "http://" + specter_url_temp
settings.setValue(
"specter_url",
specter_url_temp if is_remote_mode else "http://localhost:25441/",
)
hwibridge_settings_path = os.path.join(
os.path.expanduser(DATA_FOLDER), "hwi_bridge_config.json"
)
if is_remote_mode:
config = {"whitelisted_domains": "http://127.0.0.1:25441/"}
if os.path.isfile(hwibridge_settings_path):
with open(hwibridge_settings_path, "r") as f:
file_config = json.loads(f.read())
deep_update(config, file_config)
with open(hwibridge_settings_path, "w") as f:
if "whitelisted_domains" in config:
whitelisted_domains = ""
if specter_url_temp not in config["whitelisted_domains"].split():
config["whitelisted_domains"] += " " + specter_url_temp
for url in config["whitelisted_domains"].split():
if not url.endswith("/") and url != "*":
# make sure the url end with a "/"
url += "/"
whitelisted_domains += url.strip() + "\n"
config["whitelisted_domains"] = whitelisted_domains
f.write(json.dumps(config, indent=4))
# TODO: Add PORT setting
def open_webview(view, first_time=False):
url = settings.value("specter_url", type=str).strip("/")
if first_time and settings.value("remote_mode", defaultValue=False, type=bool):
url += "/settings/hwi"
# missing schema?
if "://" not in url:
url = "http://" + url
# if https:// or .onion - use browser
if "https://" in url or ".onion" in url:
webbrowser.open(settings.value("specter_url", type=str), new=1)
return
if not view.isVisible():
view.load(QUrl(url))
view.show()
# if the window is already open just bring it to top
# hack to make it pop-up
else:
view.show()
getattr(view, "raise")()
view.activateWindow()
class WebEnginePage(QWebEnginePage):
"""Web page"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.featurePermissionRequested.connect(self.onFeaturePermissionRequested)
self.profile().downloadRequested.connect(self.onDownloadRequest)
def onFeaturePermissionRequested(self, url, feature):
"""Enable camera and other stuff"""
# allow everything
self.setFeaturePermission(url, feature, QWebEnginePage.PermissionGrantedByUser)
def onDownloadRequest(self, item):
"""Catch dowload files requests"""
options = QFileDialog.Options()
path = QFileDialog.getSaveFileName(
None, "Where to save?", item.path(), options=options
)[0]
if path:
item.setPath(path)
item.accept()
def createWindow(self, _type):
"""
Catch clicks on _blank urls
and open it in default browser
"""
page = WebEnginePage(self)
page.urlChanged.connect(self.open_browser)
return page
def open_browser(self, url):
page = self.sender()
QDesktopServices.openUrl(url)
page.deleteLater()
class WebView(QWidget):
"""Window with the web browser"""
def __init__(self, tray, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setStyleSheet("background-color:#263044;")
self.tray = tray
self.browser = QWebEngineView()
self.browser.page = WebEnginePage()
self.browser.setPage(self.browser.page)
# loading progress widget
self.progress = QWidget()
self.progress.setFixedHeight(1)
self.progress.setStyleSheet("background-color:#263044;")
vbox = QVBoxLayout()
vbox.addWidget(self.progress, stretch=0)
vbox.addWidget(self.browser)
vbox.setSpacing(0)
vbox.setContentsMargins(0, 0, 0, 0)
self.setLayout(vbox)
self.resize(settings.value("size", QSize(1200, 900)))
self.move(settings.value("pos", QPoint(50, 50)))
self.browser.loadStarted.connect(self.loadStartedHandler)
self.browser.loadProgress.connect(self.loadProgressHandler)
self.browser.loadFinished.connect(self.loadFinishedHandler)
self.browser.urlChanged.connect(self.loadFinishedHandler)
self.setWindowTitle("Specter Desktop")
def load(self, *args, **kwargs):
self.browser.load(*args, **kwargs)
def loadStartedHandler(self):
"""Set waiting cursor when the page is loading"""
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
def loadProgressHandler(self, progress):
# just changes opacity over time for now
alpha = int(time.time() * 100) % 100
self.progress.setStyleSheet(f"background-color:rgba(75,140,26,{alpha});")
def loadFinishedHandler(self, *args, **kwargs):
"""Recover cursor when done"""
self.progress.setStyleSheet("background-color:#263044;")
QApplication.restoreOverrideCursor()
def closeEvent(self, *args, **kwargs):
"""
Notify about tray app when window is closed
for the first time.
Also save geometry of the window.
"""
settings.setValue("size", self.size())
settings.setValue("pos", self.pos())
if settings.value("first_time_close", defaultValue=True, type=bool):
settings.setValue("first_time_close", False)
self.tray.showMessage(
"Specter is still running!",
"Use tray icon to quit or reopen",
self.tray.icon(),
)
super().closeEvent(*args, **kwargs)
class Application(QApplication):
def event(self, event):
# not sure what 20 means
if event and event.type() in [QEvent.Close, 20]:
quit_specter(self)
return False
def init_desktop_app():
app = Application([])
app.setQuitOnLastWindowClosed(False)
def sigint_handler(*args):
"""Handler for the SIGINT signal."""
quit_specter(app)
# fix termination ctrl+c
signal.signal(signal.SIGINT, sigint_handler)
# This is the place to uncomment if we ever have issues like
# https://github.com/cryptoadvance/specter-desktop/issues/373 again
# So maybe let's keep it in here.
if os.environ.get("DEP_REPORTING"):
import psutil
print(
"---------------------------DEP_REPORTING--------------------------------------------"
)
for item in psutil.Process().memory_maps():
print(item.path)
print(
"-----------------------------DEP_REPORTING(end)-------------------------------------"
)
# Create the icon
icon = QIcon(os.path.join(resource_path("icons"), "icon.png"))
# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)
# Create webview
view = WebView(tray)
# Create the menu
menu = QMenu()
start_specterd_menu = QAction(
"Start Specter{} daemon".format(
" HWIBridge"
if settings.value("remote_mode", defaultValue=False, type=bool)
else ""
)
)
start_specterd_menu.triggered.connect(lambda: watch_specterd(menu, view))
menu.addAction(start_specterd_menu)
open_webview_menu = QAction("Open Specter App")
open_webview_menu.triggered.connect(lambda: open_webview(view))
menu.addAction(open_webview_menu)
open_specter_menu = QAction("Open in the browser")
open_specter_menu.triggered.connect(open_specter_window)
menu.addAction(open_specter_menu)
toggle_specterd_status(menu)
open_settings_menu = QAction("Preferences")
open_settings_menu.triggered.connect(open_settings)
menu.addAction(open_settings_menu)
# Add a Quit option to the menu.
quit = QAction("Quit")
quit.triggered.connect(lambda: quit_specter(app))
menu.addAction(quit)
# Add the menu to the tray
tray.setContextMenu(menu)
app.setWindowIcon(icon)
# Setup settings
first_time = settings.value("first_time", defaultValue=True, type=bool)
if first_time:
settings.setValue("first_time", False)
settings.setValue("remote_mode", False)
settings.setValue("specter_url", "http://localhost:25441/")
open_settings()
# start server
global specterd_thread
# add hwibridge to args
if settings.value("remote_mode", defaultValue=False, type=bool):
sys.argv.append("--hwibridge")
# start thread
specterd_thread = threading.Thread(target=server)
specterd_thread.daemon = True
specterd_thread.start()
watch_specterd(menu, view)
sys.exit(app.exec_())
if __name__ == "__main__":
init_desktop_app()

View file

@ -1,134 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import platform
import subprocess
import mnemonic, os, sys
mnemonic_path = os.path.join(mnemonic.__path__[0], "wordlist")
block_cipher = None
binaries = []
if platform.system() == 'Windows':
binaries = [("./windll/libusb-1.0.dll", ".")]
elif platform.system() == 'Linux':
arch = platform.processor()
binaries = [(f"/lib/{arch}-linux-gnu/libusb-1.0.so.0", "."),
(f"/usr/lib/{arch}-linux-gnu/dri/iris_dri.so","."),
(f"/usr/lib/{arch}-linux-gnu/gio/modules/libgvfsdbus.so","."),
(f"/usr/lib/{arch}-linux-gnu/gvfs/libgvfscommon.so","."),
(f"/usr/lib/{arch}-linux-gnu/gtk-3.0/modules/libcanberra-gtk3-module.so","."),
(f"/usr/lib/{arch}-linux-gnu/libcanberra-gtk3.so.0","."),
(f"/usr/lib/{arch}-linux-gnu/libcanberra.so.0","."),
(f"/usr/lib/{arch}-linux-gnu/libdrm_amdgpu.so.1","."),
(f"/usr/lib/{arch}-linux-gnu/libdrm_nouveau.so.2","."),
(f"/usr/lib/{arch}-linux-gnu/libdrm_radeon.so.1","."),
(f"/usr/lib/{arch}-linux-gnu/libedit.so.2","."),
(f"/usr/lib/{arch}-linux-gnu/libelf.so.1","."),
(f"/usr/lib/{arch}-linux-gnu/libLLVM-10.so.1","."),
(f"/usr/lib/{arch}-linux-gnu/libltdl.so.7","."),
(f"/usr/lib/{arch}-linux-gnu/libsensors.so.4","."),
(f"/usr/lib/{arch}-linux-gnu/libtdb.so.1","."),
]
elif platform.system() == 'Darwin':
find_brew_libusb_proc = subprocess.Popen(['brew', '--prefix', 'libusb'], stdout=subprocess.PIPE)
libusb_path = find_brew_libusb_proc.communicate()[0]
binaries = [(libusb_path.rstrip().decode() + "/lib/libusb-1.0.dylib", ".")]
a = Analysis(['specter_desktop.py'],
binaries=binaries,
datas=[('../src/cryptoadvance/specter/templates', 'templates'),
('../src/cryptoadvance/specter/static', 'static'),
("./icons", "icons"),
(mnemonic_path, 'mnemonic/wordlist'),
("version.txt", "."),
],
hiddenimports=[
'pkg_resources.py2_warn',
'cryptoadvance.specter.config'
],
hookspath=['hooks/'],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
if platform.system() == 'Linux':
import hwilib
a.datas += Tree('../udev', prefix='hwilib/udev')
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
if sys.platform == 'darwin':
exe = EXE(pyz,
a.scripts,
[],
name='Specter',
exclude_binaries=True,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True )
app = BUNDLE(
exe,
a.binaries,
a.zipfiles,
a.datas,
name='Specter.app',
icon='icons/icon.icns',
bundle_identifier=None,
info_plist={
'NSPrincipleClass': 'NSApplication',
'NSAppleScriptEnabled': False,
'NSHighResolutionCapable': 'True',
'NSRequiresAquaSystemAppearance': 'True',
'LSUIElement': 'False',
'LSBackgroundOnly': 'False'
}
)
if sys.platform == 'linux':
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='Specter',
debug=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=False,
icon='icons/icon.ico'
)
if sys.platform == 'win32' or sys.platform == 'win64':
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='specter_desktop',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon='icons/icon.ico' )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
console=False,
name='specter_desktop')

View file

@ -180,11 +180,11 @@ def combine(wallet_alias):
raw = app.specter.finalize(combined)
if "psbt" not in raw:
raw["psbt"] = combined
psbt = wallet.update_pending_psbt(combined, txid, raw)
except RpcError as e:
return e.error_msg, e.status_code
except Exception as e:
return "Unknown error: %r" % e, 500
psbt = wallet.update_pending_psbt(combined, txid, raw)
devices = []
raw["devices"] = psbt["devices_signed"]
return json.dumps(raw)

View file

@ -100,6 +100,12 @@ def init_app(app, hwibridge=False, specter=None):
def index():
return redirect("/hwi/settings")
@app.context_processor
def inject_tor():
if app.config["DEBUG"]:
return dict(tor_service_id="", tor_enabled=False)
return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled)
return app

View file

@ -129,10 +129,10 @@ upub5En4f7k8gaG2KDHvBeEYox...rFpJRHpiZ4DE
<table>
{% for i in range(wordslist|length // 4) %}
<tr>
<td>{{ 4 * i + 1 }}. {{ wordslist[4 * i] }}.</td>
<td>{{ 4 * i + 2 }}. {{ wordslist[4 * i + 1] }}</td>
<td>{{ 4 * i + 3 }}. {{ wordslist[4 * i + 2] }}</td>
<td>{{ 4 * i + 4 }}. {{ wordslist[4 * i + 3] }}</td>
<td>{{ 4 * i + 1 }} {{ wordslist[4 * i] }}</td>
<td>{{ 4 * i + 2 }} {{ wordslist[4 * i + 1] }}</td>
<td>{{ 4 * i + 3 }} {{ wordslist[4 * i + 2] }}</td>
<td>{{ 4 * i + 4 }} {{ wordslist[4 * i + 3] }}</td>
</tr>
{% endfor %}
</table>

View file

@ -5,9 +5,18 @@
{% endblock %}
{% block main %}
<div class="card">
<div class="card" style="width: 600px;">
<form action="." method="POST" role="form">
<h1>HWI Bridge Settings</h1>
{% if tor_service_id %}
<img style="width: 28px; padding:3px;float: left;margin-right: 5px;" src="{{ url_for('static', filename='img/tor.svg') }}"/>
<span style="float: left;">
HWIBridge is also accessible ove Tor:<br>
<span title="Copy Tor address" class="explorer-link" onclick="copyText('{{ tor_service_id }}.onion', 'Copied Tor hidden service address: {{ tor_service_id }}.onion')">
{{ tor_service_id }}.onion
</span>
</span><br><br><br>
{% endif %}
<label>Whitelisted domains</label>
<textarea class="form-control" placeholder="e.i. http://127.0.0.1:25441" name="whitelisted_domains">{{ whitelisted_domains }}</textarea>
<div class="note">

View file

@ -1,6 +1,6 @@
{% extends "base.jinja" %}
{% block main %}
<form action="?" method="POST">
<form action="?" method="POST" onsubmit="showPacman()">
<h1 id="title" class="settings-title">General settings - Specter Desktop {{ current_version }}</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('general_settings', current_user) }}
@ -44,7 +44,7 @@
</div>
<span id="filesloaded"></span>
<div class="row" style="margin-top: 5px;">
<button type="submit" class="btn hidden" name="action" value="restore" id="restore" style="margin-bottom: 5px;">Load Specter backup</button>
<button onsubmit="showPacman()" type="submit" class="btn hidden" name="action" value="restore" id="restore" style="margin-bottom: 5px;">Load Specter backup</button>
</div><br><br>
<h1> Miscellaneous </h1>
Bitcoin unit to use (BTC/sats):