From 400ef69e223352f058981f636554ae2920f270fc Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 7 Jan 2019 22:44:00 -0500 Subject: [PATCH 001/255] Update bitcoin.conf --- .../generators/app/templates/bitcoin/bitcoin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index c02e2fc..62462b7 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -36,7 +36,7 @@ main.wallet=spending01.dat main.wallet=ln01.dat <% } %> -walletnotify=curl proxy:8888/conf/%s +walletnotify=/usr/bin/curl proxy:8888/conf/%s <% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> uacomment=<%= bitcoin_uacomment %> From 0ab14e2ac86d2aee1b15659c83ec0c1add25c5c5 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 7 Jan 2019 22:56:55 -0500 Subject: [PATCH 002/255] Update setup.sh --- dist/setup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 95e6fea..94b25c4 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -638,14 +638,14 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 -# CYPHERNODE VERSION "v0.1.0-rc.2" +# CYPHERNODE VERSION "v0.1.0" VERSION_OVERRIDE="true" -CONF_VERSION="v0.1-rc.2" -GATEKEEPER_VERSION="v0.1-rc.2" -PROXY_VERSION="v0.1-rc.2" -PROXYCRON_VERSION="v0.1-rc.2" -OTSCLIENT_VERSION="v0.1-rc.2" -PYCOIN_VERSION="v0.1-rc.2" +CONF_VERSION="v0.1" +GATEKEEPER_VERSION="v0.1" +PROXY_VERSION="v0.1" +PROXYCRON_VERSION="v0.1" +OTSCLIENT_VERSION="v0.1" +PYCOIN_VERSION="v0.1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" From 4b2b5a63740ef7aa4f967c347c04042060abba6c Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 9 Jan 2019 15:49:08 -0500 Subject: [PATCH 003/255] Callback failed if response is HTTP code 4xx or 5xx --- proxy_docker/app/script/ots.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index 70548cb..b77d79d 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -201,11 +201,14 @@ serve_ots_backoffice() trace "[serve_ots_backoffice] url=${url}" # Call back newly upgraded stamps - curl -H "X-Forwarded-Proto: https" ${url} + trace "[serve_ots_backoffice] curl -s -o /dev/null -w \"%{http_code}\" -H \"X-Forwarded-Proto: https\" ${url}" + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Proto: https" ${url}) returncode=$? trace_rc ${returncode} - if [ "${returncode}" -eq "0" ]; then + # Even if curl executed ok, we need to make sure the http return code is also ok + + if [ "${returncode}" -eq "0" ] && [ "${rc}" -lt "400" ]; then sql "UPDATE stamp SET calledback=1 WHERE hash=\"${hash}\"" trace_rc $? fi From a8f7cfb2ef89ee1df83795b8c36ff7b4c0738c9b Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 10 Jan 2019 10:05:24 -0500 Subject: [PATCH 004/255] Added PID in traces --- api_auth_docker/trace.sh | 4 ++-- otsclient_docker/script/trace.sh | 4 ++-- proxy_docker/app/script/trace.sh | 4 ++-- pycoin_docker/script/trace.sh | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api_auth_docker/trace.sh b/api_auth_docker/trace.sh index c2c46ed..a5141fb 100644 --- a/api_auth_docker/trace.sh +++ b/api_auth_docker/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" 1>&2 + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" 1>&2 + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] $$ Last return code: ${1}" 1>&2 fi } diff --git a/otsclient_docker/script/trace.sh b/otsclient_docker/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/otsclient_docker/script/trace.sh +++ b/otsclient_docker/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi } diff --git a/proxy_docker/app/script/trace.sh b/proxy_docker/app/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/proxy_docker/app/script/trace.sh +++ b/proxy_docker/app/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi } diff --git a/pycoin_docker/script/trace.sh b/pycoin_docker/script/trace.sh index 680f3f2..4c0a1c2 100644 --- a/pycoin_docker/script/trace.sh +++ b/pycoin_docker/script/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) ${1}" 1>&2 + echo "$(date -Is) $$ ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "$(date -Is) Last return code: ${1}" 1>&2 + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 fi } From 06a9665339fa1e0b9156d4197f53e4b979f90c35 Mon Sep 17 00:00:00 2001 From: Francis Pouliot Date: Sun, 13 Jan 2019 19:42:50 -0500 Subject: [PATCH 005/255] Update README.md --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 6e901f9..a391033 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,38 @@ The docker containers used in this project are hosted at www.bitcoindockers.com The project is in **heavy development** - we are currently looking for review, new features, user feedback and contributors to our roadmap. +# Cyphernode Architecture +Cyphernode is an assembly of Docker containers being called by a request dispatcher. + +The request dispatcher (requesthandler.sh) is the HTTP entry point. +The request dispatcher is stateful: it keeps some data to be more effective on next calls. +The request dispatcher is where Cyphernode scales with new features: add your switch, dispatch requests to your stuff. +We are trying to construct each container so that it can be used separately, as a standalone reusable component. + +Important to us: + +Be as optimized as possible, using Alpine when possible and having the smallest Docker image size possible +Reuse existing software: built-in shell commands, well-established pieces of software, etc. +Use open-source software +Don't reinvent the wheel +Expose the less possible surface +Center element: proxy_docker +The proxy_docker is the container receiving and dispatching calls from clients. When adding a feature to Cyphernode, it is the first part to be modified to integrate the new feature. + +proxy_docker/app/script/requesthandler.sh +You will find in there the switch statement used to dispatch the requests. Just add a case block with your command, using other cases as examples for POST or GET requests. + +proxy_docker/app/config +You will find there config files. config.properties should be used to centralize configs. spender and watcher properties are used to obfuscate credentials on curl calls. + +proxy_docker/app/data +watching.sql contains the data model. Called "watching" because in the beginning of the project, it was only used for watching addresses. Now could be used to index the blockchain (build an explorer) and add more features. + +cron_docker +If you have jobs to be scheduled, use this container. Just add an executable and add it to the crontab. + +Currently used to make sure callbacks have been called for missed transactions. + # About this project - Created and maintained by www.satoshiportal.com From 2db77a9aba2bc438157ede2aacc5dd688f6c052c Mon Sep 17 00:00:00 2001 From: SKP Date: Thu, 17 Jan 2019 18:04:16 +0100 Subject: [PATCH 006/255] Fixed installation directory problems --- build.sh | 38 +++++---- dist/setup.sh | 9 +++ .../generators/app/index.js | 2 + .../generators/app/prompters/999_installer.js | 80 ++++++++++++------- 4 files changed, 82 insertions(+), 47 deletions(-) diff --git a/build.sh b/build.sh index a03dd62..a5deed5 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,17 @@ TRACING=1 +# CYPHERNODE VERSION "v0.1" +CONF_VERSION="v0.1" +GATEKEEPER_VERSION="v0.1" +PROXY_VERSION="v0.1" +PROXYCRON_VERSION="v0.1" +OTSCLIENT_VERSION="v0.1" +PYCOIN_VERSION="v0.1" +BITCOIN_VERSION="v0.17.0" +LIGHTNING_VERSION="v0.6.2" +GRAFANA_VERSION="v0.1" + trace() { if [ -n "${TRACING}" ]; then @@ -38,6 +49,7 @@ build_docker_images() { local bitcoin_dockerfile=Dockerfile.amd64 local clightning_dockerfile=Dockerfile.amd64 local proxy_dockerfile=Dockerfile.amd64 + local grafana_dockerfile=Dockerfile.amd64 # compat mode for SatoshiPortal repo # TODO: add more mappings? @@ -45,31 +57,23 @@ build_docker_images() { bitcoin_dockerfile="Dockerfile.arm32v6" clightning_dockerfile="Dockerfile.arm32v6" proxy_dockerfile="Dockerfile.arm32v6" + grafana_dockerfile="Dockerfile.arm32v6" fi trace "Creating cyphernodeconf image" - build_docker_image install/ cyphernode/cyphernodeconf:$CN_VERSION + build_docker_image install/ cyphernode/cyphernodeconf:$CONF_VERSION trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BC_VERSION $bitcoin_dockerfile - build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$CL_VERSION $clightning_dockerfile + build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BITCOIN_VERSION $bitcoin_dockerfile + build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$LIGHTNING_VERSION $clightning_dockerfile trace "Creating cyphernode images" - build_docker_image api_auth_docker/ cyphernode/gatekeeper:$CN_VERSION - build_docker_image proxy_docker/ cyphernode/proxy:$CN_VERSION $proxy_dockerfile - build_docker_image cron_docker/ cyphernode/proxycron:$CN_VERSION - build_docker_image pycoin_docker/ cyphernode/pycoin:$CN_VERSION - build_docker_image otsclient_docker/ cyphernode/otsclient:$CN_VERSION + build_docker_image api_auth_docker/ cyphernode/gatekeeper:$GATEKEEPER_VERSION + build_docker_image proxy_docker/ cyphernode/proxy:$PROXY_VERSION $proxy_dockerfile + build_docker_image cron_docker/ cyphernode/proxycron:$PROXYCRON_VERSION + build_docker_image pycoin_docker/ cyphernode/pycoin:$PYCOIN_VERSION + build_docker_image otsclient_docker/ cyphernode/otsclient:$OTSCLIENT_VERSION } -# CYPHERNODE VERSION -GATEKEEPER_VERSION="latest" -PROXY_VERSION="latest" -PROXYCRON_VERSION="latest" -OTSCLIENT_VERSION="latest" -PYCOIN_VERSION="latest" -BITCOIN_VERSION="latest" -LIGHTNING_VERSION="latest" - build_docker_images diff --git a/dist/setup.sh b/dist/setup.sh index 94b25c4..708361a 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -183,6 +183,8 @@ configure() { # configure features of cyphernode docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ + -e DEFAULT_DATADIR_BASE=$HOME \ + -e SETUP_DIR=$SETUP_DIR \ -e DEFAULT_CERT_HOSTNAME=$(hostname) \ -e VERSION_OVERRIDE=$VERSION_OVERRIDE \ -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ @@ -551,6 +553,11 @@ check_bitcoind() { echo 0 } +realpath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} + + sanity_checks() { echo " check requirements." @@ -649,6 +656,8 @@ PYCOIN_VERSION="v0.1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" +SETUP_DIR=$(dirname $(realpath $0)) + # trap ctrl-c and call ctrl_c() trap ctrl_c INT diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 6053f7e..feca97b 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -124,6 +124,8 @@ module.exports = class extends Generator { } async _initConfig() { + this.defaultDataDirBase = process.env.DEFAULT_DATADIR_BASE; + this.setupDir = process.env.SETUP_DIR; const versionOverride = process.env.VERSION_OVERRIDE==='true'; if( fs.existsSync(this.destinationPath('config.7z')) ) { let r = {}; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index a8e5ea7..24e04eb 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -37,16 +37,20 @@ module.exports = { default: utils._getDefault( 'gatekeeper_datapath' ), choices: [ { - name: "/var/run/cyphernode/gatekeeper (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/gatekeeper" + name: utils.setupDir+"/cyphernode/gatekeeper", + value: utils.setupDir+"/cyphernode/gatekeeper" }, { - name: "~/.cyphernode/gatekeeper", - value: "~/.cyphernode/gatekeeper" + name: utils.defaultDataDirBase+"/cyphernode/gatekeeper", + value: utils.defaultDataDirBase+"/cyphernode/gatekeeper" }, { - name: "~/gatekeeper", - value: "~/gatekeeper" + name: utils.defaultDataDirBase+"/.cyphernode/gatekeeper", + value: utils.defaultDataDirBase+"/.cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase+"/gatekeeper", + value: utils.defaultDataDirBase+"/gatekeeper" }, { name: "Custom path", @@ -71,16 +75,20 @@ module.exports = { default: utils._getDefault( 'proxy_datapath' ), choices: [ { - name: "/var/run/cyphernode/proxy (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/proxy" + name: utils.setupDir+"/cyphernode/proxy", + value: utils.setupDir+"/cyphernode/proxy" }, { - name: "~/.cyphernode/proxy", - value: "~/.cyphernode/proxy" + name: utils.defaultDataDirBase+"/cyphernode/proxy", + value: utils.defaultDataDirBase+"/cyphernode/proxy" }, { - name: "~/proxy", - value: "~/proxy" + name: utils.defaultDataDirBase+"/.cyphernode/proxy", + value: utils.defaultDataDirBase+"/.cyphernode/proxy" + }, + { + name: utils.defaultDataDirBase+"/proxy", + value: utils.defaultDataDirBase+"/proxy" }, { name: "Custom path", @@ -105,16 +113,20 @@ module.exports = { default: utils._getDefault( 'bitcoin_datapath' ), choices: [ { - name: "/var/run/cyphernode/bitcoin (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/bitcoin" + name: utils.setupDir+"/cyphernode/bitcoin", + value: utils.setupDir+"/cyphernode/bitcoin" }, { - name: "~/.cyphernode/bitcoin", - value: "~/.cyphernode/bitcoin" + name: utils.defaultDataDirBase+"/cyphernode/bitcoin", + value: utils.defaultDataDirBase+"/cyphernode/bitcoin" }, { - name: "~/bitcoin", - value: "~/bitcoin" + name: utils.defaultDataDirBase+"/.cyphernode/bitcoin", + value: utils.defaultDataDirBase+"/.cyphernode/bitcoin" + }, + { + name: utils.defaultDataDirBase+"/bitcoin", + value: utils.defaultDataDirBase+"/bitcoin" }, { name: "Custom path", @@ -139,16 +151,20 @@ module.exports = { default: utils._getDefault( 'lightning_datapath' ), choices: [ { - name: "/var/run/cyphernode/lightning (needs sudo - "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/lightning" + name: utils.setupDir+"/cyphernode/lightning", + value: utils.setupDir+"/cyphernode/lightning" }, { - name: "~/.cyphernode/lightning", - value: "~/.cyphernode/lightning" + name: utils.defaultDataDirBase+"/cyphernode/lightning", + value: utils.defaultDataDirBase+"/cyphernode/lightning" }, { - name: "~/lightning", - value: "~/lightning" + name: utils.defaultDataDirBase+"/.cyphernode/lightning", + value: utils.defaultDataDirBase+"/.cyphernode/lightning" + }, + { + name: utils.defaultDataDirBase+"/lightning", + value: utils.defaultDataDirBase+"/lightning" }, { name: "Custom path", @@ -173,16 +189,20 @@ module.exports = { default: utils._getDefault( 'otsclient_datapath' ), choices: [ { - name: "/var/run/cyphernode/otsclient (needs sudo and "+chalk.red('incompatible with OSX')+")", - value: "/var/run/cyphernode/otsclient" + name: utils.setupDir+"/cyphernode/otsclient", + value: utils.setupDir+"/cyphernode/otsclient" }, { - name: "~/.cyphernode/otsclient", - value: "~/.cyphernode/otsclient" + name: utils.defaultDataDirBase+"/cyphernode/otsclient", + value: utils.defaultDataDirBase+"/cyphernode/otsclient" }, { - name: "~/otsclient", - value: "~/otsclient" + name: utils.defaultDataDirBase+"/.cyphernode/otsclient", + value: utils.defaultDataDirBase+"/.cyphernode/otsclient" + }, + { + name: utils.defaultDataDirBase+"/otsclient", + value: utils.defaultDataDirBase+"/otsclient" }, { name: "Custom path", From 49b779927182f8a6f51ed64adb99735e1368848e Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 20 Jan 2019 12:44:02 +0100 Subject: [PATCH 007/255] Check if var with current directory is empty to prevent infinite loop --- dist/setup.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index 708361a..b7b7bd5 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -528,6 +528,9 @@ check_directory_owner() { local status=0 for d in "${directories[@]}" do + if [[ ''$d == '' ]]; then + continue + fi if [[ -e $d ]]; then # is it mine and does it have rw ? # don't care about group rights From ed62ef409f25ac6c23663b0407fd687edbce69f2 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 20 Jan 2019 13:00:04 +0100 Subject: [PATCH 008/255] using absolute paths in check_directory_owner() --- dist/setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/setup.sh b/dist/setup.sh index b7b7bd5..592f1dd 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -531,6 +531,7 @@ check_directory_owner() { if [[ ''$d == '' ]]; then continue fi + d=$(realpath $d) if [[ -e $d ]]; then # is it mine and does it have rw ? # don't care about group rights From 8d6b048a8edd855eb0bbeec119c32a36b6d5ad81 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 21 Jan 2019 11:21:45 -0500 Subject: [PATCH 009/255] Delete publish.sh --- publish.sh | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100755 publish.sh diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 95f8038..0000000 --- a/publish.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -ARCH=$(uname -m) - -docker tag cyphernodeconf registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker tag cyphernode/bitcoin registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker tag cyphernode/clightning registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker tag cyphernode/otsclient registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker tag cyphernode/proxy registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker tag cyphernode/proxycron registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker tag cyphernode/pycoin registry.skp.rocks:5000/$ARCH/cyphernode/pycoin - -docker push registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker push registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker push registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker push registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker push registry.skp.rocks:5000/$ARCH/cyphernode/pycoin From 1207116a848c6419f1c919e0b67c5d4ab3047652 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 21 Jan 2019 14:18:21 -0500 Subject: [PATCH 010/255] Delete publish.sh --- publish.sh | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100755 publish.sh diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 95f8038..0000000 --- a/publish.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -ARCH=$(uname -m) - -docker tag cyphernodeconf registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker tag cyphernode/bitcoin registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker tag cyphernode/clightning registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker tag cyphernode/otsclient registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker tag cyphernode/proxy registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker tag cyphernode/proxycron registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker tag cyphernode/pycoin registry.skp.rocks:5000/$ARCH/cyphernode/pycoin - -docker push registry.skp.rocks:5000/$ARCH/cyphernodeconf -docker push registry.skp.rocks:5000/$ARCH/cyphernode/bitcoin -docker push registry.skp.rocks:5000/$ARCH/cyphernode/clightning -docker push registry.skp.rocks:5000/$ARCH/cyphernode/otsclient -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxy -docker push registry.skp.rocks:5000/$ARCH/cyphernode/proxycron -docker push registry.skp.rocks:5000/$ARCH/cyphernode/pycoin From 640b30e65a79ac8ded2a0b2350ea5a95008fb44e Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 22 Jan 2019 13:16:55 -0500 Subject: [PATCH 011/255] Docker image tags for locally built images --- build.sh | 20 ++++++++++---------- dist/setup.sh | 28 +++++++++++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/build.sh b/build.sh index a5deed5..579ad12 100755 --- a/build.sh +++ b/build.sh @@ -2,16 +2,16 @@ TRACING=1 -# CYPHERNODE VERSION "v0.1" -CONF_VERSION="v0.1" -GATEKEEPER_VERSION="v0.1" -PROXY_VERSION="v0.1" -PROXYCRON_VERSION="v0.1" -OTSCLIENT_VERSION="v0.1" -PYCOIN_VERSION="v0.1" +# CYPHERNODE VERSION "v0.1.1" +CONF_VERSION="v0.1.1-local" +GATEKEEPER_VERSION="v0.1.1-local" +PROXY_VERSION="v0.1.1-local" +PROXYCRON_VERSION="v0.1.1-local" +OTSCLIENT_VERSION="v0.1.1-local" +PYCOIN_VERSION="v0.1.1-local" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" -GRAFANA_VERSION="v0.1" +GRAFANA_VERSION="v0.1.1-local" trace() { @@ -64,8 +64,8 @@ build_docker_images() { build_docker_image install/ cyphernode/cyphernodeconf:$CONF_VERSION trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BITCOIN_VERSION $bitcoin_dockerfile - build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$LIGHTNING_VERSION $clightning_dockerfile +# build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BITCOIN_VERSION $bitcoin_dockerfile +# build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$LIGHTNING_VERSION $clightning_dockerfile trace "Creating cyphernode images" build_docker_image api_auth_docker/ cyphernode/gatekeeper:$GATEKEEPER_VERSION diff --git a/dist/setup.sh b/dist/setup.sh index 592f1dd..6b55555 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -649,14 +649,14 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 -# CYPHERNODE VERSION "v0.1.0" +# CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.1" -GATEKEEPER_VERSION="v0.1" -PROXY_VERSION="v0.1" -PROXYCRON_VERSION="v0.1" -OTSCLIENT_VERSION="v0.1" -PYCOIN_VERSION="v0.1" +CONF_VERSION="v0.1.1" +GATEKEEPER_VERSION="v0.1.1" +PROXY_VERSION="v0.1.1" +PROXYCRON_VERSION="v0.1.1" +OTSCLIENT_VERSION="v0.1.1" +PYCOIN_VERSION="v0.1.1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" @@ -703,6 +703,20 @@ while getopts ":cirhys" opt; do esac done +nbbuiltimgs=$(docker images --filter=reference='cyphernode/*:*-local' | wc -l) +if [[ $nbbuiltimgs -gt 1 ]]; then + read -p "Locally built Cyphernode images found! Do you want to use them?" -n 1 -r + + if [[ $REPLY =~ ^[Yy]$ ]]; then + CONF_VERSION="$CONF_VERSION-local" + GATEKEEPER_VERSION="$GATEKEEPER_VERSION-local" + PROXY_VERSION="$PROXY_VERSION-local" + PROXYCRON_VERSION="$PROXYCRON_VERSION-local" + OTSCLIENT_VERSION="$OTSCLIENT_VERSION-local" + PYCOIN_VERSION="$PYCOIN_VERSION-local" + fi +fi + if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then CONFIGURE=1 INSTALL=1 From 938ab178f563940fd6a4bd713fd79b9c229127e9 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 22 Jan 2019 14:08:26 -0500 Subject: [PATCH 012/255] Recovered lost pre config and pre install in setup.sh --- dist/setup.sh | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 6b55555..c8c9617 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -562,19 +562,33 @@ realpath() { } -sanity_checks() { - - echo " check requirements." - +check_docker() { if ! [ -x "$(command -v docker)" ]; then echo " docker is not installed on your system. Please check https://www.docker.com/get-started." exit fi +} - if [[ $DOCKER_MODE == 'compose' && ! -x "$(command -v docker-compose)" ]]; then +check_docker_compose() { + if ! [ -x "$(command -v docker-compose)" ]; then echo " docker-compose is not installed on your system. Please check https://docs.docker.com/compose/install/." exit fi +} + +sanity_checks_pre_config() { + echo " check requirements for configuration step." + check_docker +} + +sanity_checks_pre_install() { + + echo " check requirements for installation step." + + check_docker + if [[ $DOCKER_MODE == 'compose' ]]; then + check_docker_compose + fi local OS=$(uname -s) @@ -723,6 +737,7 @@ if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then fi if [[ $CONFIGURE == 1 ]]; then + sanity_checks_pre_config configure $RECREATE fi @@ -737,7 +752,7 @@ if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cypherno fi if [[ $INSTALL == 1 ]]; then - sanity_checks + sanity_checks_pre_install create_user install modify_owner From 85d838c9c427cc53d151d8dc8ea17ce9e956d054 Mon Sep 17 00:00:00 2001 From: Fabian Rodriguez Date: Fri, 4 Jan 2019 13:39:18 -0500 Subject: [PATCH 013/255] Update INSTALL-MANUAL-STEPS.md Small correction to Debian mention --- doc/INSTALL-MANUAL-STEPS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 307f438..faf13b1 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -1,6 +1,6 @@ # This README file can be used if you want to install manually. This is the old documentation before there was the installer. -# Here are the exact steps I did to install cyphernode on a debian server running on x86 arch, as user debian. +# Here are the exact steps I did to install cyphernode on a Debian GNU/Linux server running on x86 arch, as user debian. ## Update server and install git @@ -11,10 +11,10 @@ sudo apt-get update ; sudo apt-get upgrade ; sudo apt-get install git ## Docker installation: https://docs.docker.com/install/linux/docker-ce/debian/ ```shell -sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common +sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - sudo apt-key fingerprint 0EBFCD88 -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian \ +sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian \ $(lsb_release -cs) \ stable" sudo apt-get update From 5a18be3d8ee59bb501d824c86a5b96201e8d771f Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 9 Feb 2019 00:11:28 +0100 Subject: [PATCH 014/255] added gatekeeper port option to config tool --- api_auth_docker/Dockerfile | 2 +- api_auth_docker/default-ssl.conf | 38 ------ api_auth_docker/default.conf | 12 +- .../generators/app/index.js | 1 + .../app/prompters/010_gatekeeper.js | 118 ++++++++++-------- .../installer/docker/docker-compose.yaml | 2 +- .../app/templates/installer/start.sh | 2 +- 7 files changed, 79 insertions(+), 96 deletions(-) delete mode 100644 api_auth_docker/default-ssl.conf diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 6a534f6..ebad970 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -11,7 +11,7 @@ RUN apk add --update --no-cache \ su-exec COPY auth.sh /etc/nginx/conf.d/ -COPY default-ssl.conf /etc/nginx/conf.d/default.conf +COPY default.conf /etc/nginx/conf.d/default.conf COPY statuspage.html /etc/nginx/conf.d/status/ COPY entrypoint.sh entrypoint.sh COPY trace.sh /etc/nginx/conf.d/ diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf deleted file mode 100644 index 69c7dc1..0000000 --- a/api_auth_docker/default-ssl.conf +++ /dev/null @@ -1,38 +0,0 @@ -server { - listen 443 ssl; - server_name localhost; - - #include /etc/nginx/conf.d/ip-whitelist.conf; - - ssl_certificate /etc/ssl/certs/cert.pem; - ssl_certificate_key /etc/ssl/private/key.pem; - - location /status { - auth_basic "status"; - auth_basic_user_file conf.d/status/htpasswd; - root /etc/nginx/conf.d; - index statuspage.html; - } - - location /v0/ { - auth_request /auth; - proxy_pass http://proxy:8888/; - } - - location /auth { - internal; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh; - fastcgi_pass unix:/var/run/fcgiwrap.socket; - } - - #error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - -} diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index d9da37a..69c7dc1 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -1,9 +1,19 @@ server { - listen 80; + listen 443 ssl; server_name localhost; #include /etc/nginx/conf.d/ip-whitelist.conf; + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + location /status { + auth_basic "status"; + auth_basic_user_file conf.d/status/htpasswd; + root /etc/nginx/conf.d; + index statuspage.html; + } + location /v0/ { auth_request /auth; proxy_pass http://proxy:8888/; diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index feca97b..62cd832 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -406,6 +406,7 @@ module.exports = class extends Generator { bitcoin_mode: 'internal', bitcoin_expose: false, lightning_expose: true, + gatekeeper_port: 443, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 9bf3321..7bc1993 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -39,62 +39,72 @@ module.exports = { filter: utils._trimFilter, validate: utils._notEmptyValidator }, - { - when: function( props ) { - // hacky hack - password = props.gatekeeper_clientkeyspassword; - return true; - }, - type: 'password', - name: 'gatekeeper_clientkeyspassword_c', - default: utils._getDefault( 'gatekeeper_clientkeyspassword_c' ), - message: prefix()+'Confirm your client keys password.'+utils._getHelp('gatekeeper_clientkeyspassword_c'), - filter: utils._trimFilter, - validate: function( input ) { - if(input !== password) { - throw new Error( 'Client keys passwords do not match' ); - } - return true; + { + when: function( props ) { + // hacky hack + password = props.gatekeeper_clientkeyspassword; + return true; + }, + type: 'password', + name: 'gatekeeper_clientkeyspassword_c', + default: utils._getDefault( 'gatekeeper_clientkeyspassword_c' ), + message: prefix()+'Confirm your client keys password.'+utils._getHelp('gatekeeper_clientkeyspassword_c'), + filter: utils._trimFilter, + validate: function( input ) { + if(input !== password) { + throw new Error( 'Client keys passwords do not match' ); } + return true; + } + }, + { + type: 'input', + name: 'gatekeeper_port', + default: utils._getDefault( 'gatekeeper_port' ), + message: prefix()+'The port gatekeeper will listen on for requests'+utils._getHelp('gatekeeper_port'), + filter: utils._trimFilter, + validate: function( port ) { + return utils._notEmptyValidator( port ) && !isNaN( parseInt(port) ) + } + }, + { + when: function() { return hasAuthKeys( utils.props ); }, + type: 'confirm', + name: 'gatekeeper_recreatekeys', + default: false, + message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') + }, + { + when: function() { return hasCert( utils.props ); }, + type: 'confirm', + name: 'gatekeeper_recreatecert', + default: false, + message: prefix()+'Recreate gatekeeper certificate?'+utils._getHelp('gatekeeper_recreatecert') + }, + { + when: function(props) { return !hasCert( utils.props ) || props.gatekeeper_recreatecert }, + type: 'input', + name: 'gatekeeper_cns', + default: utils._getDefault( 'gatekeeper_cns' ), + message: prefix()+'Gatekeeper cert CNS (ips, domains, wildcard domains seperated by comma)?'+utils._getHelp('gatekeeper_cns') + }, + { + type: 'confirm', + name: 'gatekeeper_edit_apiproperties', + default: false, + message: prefix()+'Edit API properties?'+utils._getHelp('gatekeeper_edit_apiproperties') + }, + { + when: function( props ) { + const r = props.gatekeeper_edit_apiproperties; + delete props.gatekeeper_edit_apiproperties; + return r; }, - { - when: function() { return hasAuthKeys( utils.props ); }, - type: 'confirm', - name: 'gatekeeper_recreatekeys', - default: false, - message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') - }, - { - when: function() { return hasCert( utils.props ); }, - type: 'confirm', - name: 'gatekeeper_recreatecert', - default: false, - message: prefix()+'Recreate gatekeeper certificate?'+utils._getHelp('gatekeeper_recreatecert') - }, - { - when: function(props) { return !hasCert( utils.props ) || props.gatekeeper_recreatecert }, - type: 'input', - name: 'gatekeeper_cns', - default: utils._getDefault( 'gatekeeper_cns' ), - message: prefix()+'Gatekeeper cert CNS (ips, domains, wildcard domains seperated by comma)?'+utils._getHelp('gatekeeper_cns') - }, - { - type: 'confirm', - name: 'gatekeeper_edit_apiproperties', - default: false, - message: prefix()+'Edit API properties?'+utils._getHelp('gatekeeper_edit_apiproperties') - }, - { - when: function( props ) { - const r = props.gatekeeper_edit_apiproperties; - delete props.gatekeeper_edit_apiproperties; - return r; - }, - type: 'editor', - name: 'gatekeeper_apiproperties', - message: utils._getHelp('gatekeeper_apiproperties')||' ', - default: utils._getDefault( 'gatekeeper_apiproperties' ) - }]; + type: 'editor', + name: 'gatekeeper_apiproperties', + message: utils._getHelp('gatekeeper_apiproperties')||' ', + default: utils._getDefault( 'gatekeeper_apiproperties' ) + }]; }, templates: function( props ) { return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem', 'htpasswd' ]; diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index eba5633..f6f5bf0 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -7,7 +7,7 @@ services: - "TRACING=1" image: cyphernode/gatekeeper:<%= gatekeeper_version %> ports: - - "443:443" + - "<%= gatekeeper_port %>:443" volumes: - "<%= gatekeeper_datapath %>/certs:/etc/ssl/certs" - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 8fed59f..1d5bb6d 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -46,5 +46,5 @@ fi printf "\r\n\033[0;92mDepending on your current location and DNS settings, point your favorite browser to one of the following URLs to access Cyphernode's status page:\r\n" printf "\r\n" -printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + '/status/\\r\\n') %><% }) %>\033[0m\r\n" +printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + ':'+ gatekeeper_port + '/status/\\r\\n') %><% }) %>\033[0m\r\n" printf "\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n\r\n\033[0m" From dd717ef4afa52348f92f9658adaabb8a6fd18f1b Mon Sep 17 00:00:00 2001 From: SKP Date: Fri, 8 Feb 2019 23:08:03 +0100 Subject: [PATCH 015/255] Bitcoin pruning will be disabled/not asked for during config, when lightning is enabled --- .../generator-cyphernode/generators/app/index.js | 13 ++++++++++++- .../{200_lightning.js => 100_lightning.js} | 0 .../prompters/{100_bitcoin.js => 900_bitcoin.js} | 10 +++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) rename install/generator-cyphernode/generators/app/prompters/{200_lightning.js => 100_lightning.js} (100%) rename install/generator-cyphernode/generators/app/prompters/{100_bitcoin.js => 900_bitcoin.js} (91%) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 62cd832..a6e882b 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -213,8 +213,10 @@ module.exports = class extends Generator { delete this.props.lightning_version; delete this.props.grafana_version; } - + this._assignConfigDefaults(); + this._resolveConfigConflicts(); + for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); } @@ -324,6 +326,8 @@ module.exports = class extends Generator { } async writing() { + this._resolveConfigConflicts() + const configJsonString = JSON.stringify(this.props, null, 4); const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); @@ -383,6 +387,13 @@ module.exports = class extends Generator { /* some utils */ + _resolveConfigConflicts() { + if( this.props.features.indexOf('lightning') !== -1 ) { + this.props.bitcoin_prune = false; + delete this.props.bitcoin_prune_size; + } + } + _assignConfigDefaults() { this.props = Object.assign( { features: [], diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/100_lightning.js similarity index 100% rename from install/generator-cyphernode/generators/app/prompters/200_lightning.js rename to install/generator-cyphernode/generators/app/prompters/100_lightning.js diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js similarity index 91% rename from install/generator-cyphernode/generators/app/prompters/100_bitcoin.js rename to install/generator-cyphernode/generators/app/prompters/900_bitcoin.js index 62d7862..faf1543 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js @@ -23,7 +23,7 @@ const bitcoinInternalAndPrune = function(props) { }; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -68,14 +68,18 @@ module.exports = { filter: utils._trimFilter, }, { - when: bitcoinInternal, + when: function(props) { + return bitcoinInternal( props ) && props.features.indexOf('lightning') === -1; + }, type: 'confirm', name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), message: prefix()+'Run bitcoin node in prune mode?'+utils._getHelp('bitcoin_prune'), }, { - when: bitcoinInternalAndPrune, + when: function(props) { + return bitcoinInternalAndPrune( props ) && props.features.indexOf('lightning') === -1; + }, type: 'input', name: 'bitcoin_prune_size', default: utils._getDefault( 'bitcoin_prune_size' ), From ca21d9b14dee7c2892d12ea0bf4bdd977a74c41f Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 9 Feb 2019 14:14:52 +0100 Subject: [PATCH 016/255] Fixed prune size bug --- install/generator-cyphernode/generators/app/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index a6e882b..87128e1 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -214,8 +214,8 @@ module.exports = class extends Generator { delete this.props.grafana_version; } - this._assignConfigDefaults(); this._resolveConfigConflicts(); + this._assignConfigDefaults(); for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); From 07d18699c34e312e51ae8b24a977d93988ea1a68 Mon Sep 17 00:00:00 2001 From: SKP Date: Thu, 24 Jan 2019 21:44:01 +0100 Subject: [PATCH 017/255] Removed option for external bitcoin node --- install/generator-cyphernode/generators/app/help.json | 2 +- .../generators/app/prompters/900_bitcoin.js | 8 ++------ .../generators/app/templates/bitcoin/bitcoin.conf | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 36032e7..90deeb1 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -17,7 +17,7 @@ "gatekeeper_edit_apiproperties": "If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization. (Not recommended)", "gatekeeper_apiproperties": "You are about to edit the api.properties file. The format of the file is pretty simple: for each action, you will find what access group can access it. Admin group can do what Spender group can, and Spender group can do what Watcher group can. Internal group is for the endpoints accessible only within the Docker network, like the backoffice tasks used by the Cron container. The access groups for each API id/key are found in the keys.properties file.", "gatekeeper_cns": "I use domain names and/or IP addresses to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, enter cyphernodehost, 192.168.7.44 as a possible domains. 127.0.0.1, localhost, gatekeeper will be automatically added to your list. Make sure the provided domain names are in your DNS or client's hosts file and is reachable.", - "bitcoin_mode": "Cyphernode can spawn a new Bitcoin Core full node for its own use. But if you already have a Bitcoin Core node running, Cyphernode can use that.", + "bitcoin_mode": "Cyphernode will spawn a new Bitcoin Core full node for its own use. If you already have Bitcoin Core node data, you can use the directory containing that data directly or copy the contents of it to a new directory to be used by cyphernode. Be aware that the files might change ownership, if you run cyphernode as a different user. In case you want to move the blockchain data to another node you might need to change the owner to fit the configuration of that node.", "bitcoin_node_ip": "Cyphernode uses Bitcoin Core RPC interface for its tasks. Please provide the IP address of your current Bitcoin Core node.", "bitcoin_rpcuser": "Bitcoin Core's RPC username used by Cyphernode when calling the node.", "bitcoin_rpcpassword": "Bitcoin Core's RPC password used by Cyphernode when calling the node.", diff --git a/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js index faf1543..9983e68 100644 --- a/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/900_bitcoin.js @@ -32,15 +32,11 @@ module.exports = { type: 'list', name: 'bitcoin_mode', default: utils._getDefault( 'bitcoin_mode' ), - message: prefix()+'Where is your bitcoin full node running?'+utils._getHelp('bitcoin_mode'), + message: prefix()+'Cyphernode will manage your bitcoin full node.'+utils._getHelp('bitcoin_mode'), choices: [ { - name: 'Nowhere! I want cyphernode to run one.', + name: 'Ok. That is awesome', value: 'internal' - }, - { - name: 'I have a full node running.', - value: 'external' } ] }, diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 62462b7..f73a7f4 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -3,7 +3,7 @@ testnet=1 <% } %> -<% if (bitcoin_prune) { %> +<% if (bitcoin_prune && bitcoin_mode === 'internal') { %> prune=<%= bitcoin_prune_size || 550 %> <% } else { %> txindex=1 From 5eb7d2d45caddb202a140d017414babe7e56e6da Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 16 Feb 2019 12:38:32 +0100 Subject: [PATCH 018/255] Fixed resolve conflict bug at new setup --- install/generator-cyphernode/generators/app/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 87128e1..5ac7a2e 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -388,7 +388,7 @@ module.exports = class extends Generator { /* some utils */ _resolveConfigConflicts() { - if( this.props.features.indexOf('lightning') !== -1 ) { + if( this.props.features && this.props.features.length && this.props.features.indexOf('lightning') !== -1 ) { this.props.bitcoin_prune = false; delete this.props.bitcoin_prune_size; } From fb49c3a060c811c903f654c0a09f3bc4653b1f02 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 8 Feb 2019 16:24:53 -0500 Subject: [PATCH 019/255] First pass on watchxpub --- api_auth_docker/api-sample.properties | 1 + doc/INSTALL-MANUAL-STEPS.md | 2 +- docker-compose-sample.yml | 1 + .../generators/app/index.js | 1 + .../installer/docker/docker-compose.yaml | 7 +- proxy_docker/README.md | 1 + proxy_docker/app/data/cyphernode.sql | 16 ++ .../app/data/sqlmigrate20181213_0-0.1.sh | 5 +- .../app/data/sqlmigrate20190130_0.1-0.2.sh | 11 + .../app/data/sqlmigrate20190130_0.1-0.2.sql | 18 ++ proxy_docker/app/script/bitcoin.sh | 9 + proxy_docker/app/script/callbacks_job.sh | 28 ++- proxy_docker/app/script/confirmation.sh | 26 ++- proxy_docker/app/script/importaddress.sh | 113 ++++++++- proxy_docker/app/script/requesthandler.sh | 13 +- proxy_docker/app/script/sendtobitcoinnode.sh | 20 +- proxy_docker/app/script/sql.sh | 11 +- proxy_docker/app/script/walletoperations.sh | 36 +-- proxy_docker/app/script/watchrequest.sh | 218 +++++++++++++++++- proxy_docker/env.properties | 7 +- 20 files changed, 490 insertions(+), 54 deletions(-) create mode 100644 proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh create mode 100644 proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties index f78e34b..616d936 100644 --- a/api_auth_docker/api-sample.properties +++ b/api_auth_docker/api-sample.properties @@ -3,6 +3,7 @@ # Watcher can: action_watch=watcher action_unwatch=watcher +action_watchxpub=watcher action_getactivewatches=watcher action_getbestblockhash=watcher action_getbestblockinfo=watcher diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index faf13b1..f5d2bb6 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -157,5 +157,5 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - -docker exec -it `docker ps -q -f name=cyphernodestack_cyphernode` curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" proxy:8888/derivepubpath +docker exec -it `docker ps -q -f name=cyphernode_proxy` curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" proxy:8888/derivepubpath ``` diff --git a/docker-compose-sample.yml b/docker-compose-sample.yml index 8b4adad..2c3bb36 100644 --- a/docker-compose-sample.yml +++ b/docker-compose-sample.yml @@ -40,6 +40,7 @@ services: - "WATCHER_BTC_NODE_PRUNED=false" - "OTSCLIENT_CONTAINER=otsclient:6666" - "OTS_FILES=/proxy/otsfiles" + - "XPUB_DERIVATION_GAP=100" image: cyphernode/proxy:latest volumes: diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5ac7a2e..cff9d00 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -23,6 +23,7 @@ const defaultAPIProperties = ` # Watcher can: action_watch=watcher action_unwatch=watcher +action_watchxpub=watcher action_getactivewatches=watcher action_getbestblockhash=watcher action_getbestblockinfo=watcher diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index f6f5bf0..43a61ff 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -30,10 +30,12 @@ services: # Bitcoin Mini Proxy environment: - "TRACING=1" - - "WATCHER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat" + - "WATCHER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet" + - "WATCHER_BTC_NODE_DEFAULT_WALLET=watching01.dat" - "WATCHER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - "WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties" - - "SPENDER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat" + - "SPENDER_BTC_NODE_RPC_URL=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet" + - "SPENDER_BTC_NODE_DEFAULT_WALLET=spending01.dat" - "SPENDER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" - "PROXY_LISTENING_PORT=8888" @@ -47,6 +49,7 @@ services: - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - "OTSCLIENT_CONTAINER=otsclient:6666" - "OTS_FILES=/proxy/otsfiles" + - "XPUB_DERIVATION_GAP=100" image: cyphernode/proxy:<%= proxy_version %> <% if ( devmode ) { %> ports: diff --git a/proxy_docker/README.md b/proxy_docker/README.md index f833882..ddffcb1 100644 --- a/proxy_docker/README.md +++ b/proxy_docker/README.md @@ -42,6 +42,7 @@ OTS_CONTAINER=otsnode:6666 DERIVATION_PUB32=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb DERIVATION_PATH=0/n WATCHER_BTC_NODE_PRUNED=false +XPUB_DERIVATION_GAP=100 ``` ## Choose the right architecture diff --git a/proxy_docker/app/data/cyphernode.sql b/proxy_docker/app/data/cyphernode.sql index 48bb1ad..62e0193 100644 --- a/proxy_docker/app/data/cyphernode.sql +++ b/proxy_docker/app/data/cyphernode.sql @@ -1,5 +1,19 @@ PRAGMA foreign_keys = ON; +CREATE TABLE watching_by_pub32 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pub32 TEXT UNIQUE, + label TEXT UNIQUE, + derivation_path TEXT, + callback0conf TEXT, + callback1conf TEXT, + last_imported_n INTEGER, + watching INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_watching_by_pub32_pub32 ON watching_by_pub32 (pub32); +CREATE INDEX idx_watching_by_pub32_label ON watching_by_pub32 (label); + CREATE TABLE watching ( id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT, @@ -9,6 +23,8 @@ CREATE TABLE watching ( callback1conf TEXT, calledback1conf INTEGER DEFAULT FALSE, imported INTEGER DEFAULT FALSE, + watching_by_pub32_id INTEGER REFERENCES watching_by_pub32, + pub32_index INTEGER, inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_watching_address ON watching (address); diff --git a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh index 0b69b16..7dce6ac 100644 --- a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh +++ b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh @@ -1,10 +1,11 @@ #!/bin/sh +echo "Checking for OTS support in DB..." sqlite3 db/proxydb ".tables" | grep "stamp" > /dev/null if [ "$?" -eq "1" ]; then # stamp not there, we have to migrate - echo "Migrating database from v0 to v0.1..." + echo "Migrating database for OTS support..." cat sqlmigrate20181213_0-0.1.sql | sqlite3 $DB_FILE else - echo "Database v0 to v0.1 migration already done, skipping!" + echo "Database OTS support migration already done, skipping!" fi diff --git a/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh new file mode 100644 index 0000000..55d2c03 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Checking for watch by xpub support in DB..." +sqlite3 db/proxydb ".tables" | grep "watching_by_pub32" > /dev/null +if [ "$?" -eq "1" ]; then + # watching_by_pub32 not there, we have to migrate + echo "Migrating database for watch by xpub support..." + cat sqlmigrate20190130_0.1-0.2.sql | sqlite3 $DB_FILE +else + echo "Database watch by xpub support migration already done, skipping!" +fi diff --git a/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql new file mode 100644 index 0000000..a760890 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sql @@ -0,0 +1,18 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE watching_by_pub32 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pub32 TEXT UNIQUE, + label TEXT UNIQUE, + derivation_path TEXT, + callback0conf TEXT, + callback1conf TEXT, + last_imported_n INTEGER, + watching INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_watching_by_pub32_pub32 ON watching_by_pub32 (pub32); +CREATE INDEX idx_watching_by_pub32_label ON watching_by_pub32 (label); + +ALTER TABLE watching ADD COLUMN watching_by_pub32_id INTEGER REFERENCES watching_by_pub32; +ALTER TABLE watching ADD COLUMN pub32_index INTEGER; diff --git a/proxy_docker/app/script/bitcoin.sh b/proxy_docker/app/script/bitcoin.sh index 5fc0632..6364022 100644 --- a/proxy_docker/app/script/bitcoin.sh +++ b/proxy_docker/app/script/bitcoin.sh @@ -21,6 +21,15 @@ deriveindex() return $? } +derivepubpath() { + trace "Entering derivepubpath()..." + + # {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"} + + send_to_pycoin $1 + return $? +} + send_to_pycoin() { trace "Entering send_to_pycoin()..." diff --git a/proxy_docker/app/script/callbacks_job.sh b/proxy_docker/app/script/callbacks_job.sh index 3edd87b..69ac041 100644 --- a/proxy_docker/app/script/callbacks_job.sh +++ b/proxy_docker/app/script/callbacks_job.sh @@ -11,7 +11,7 @@ do_callbacks() trace "Entering do_callbacks()..." # Let's fetch all the watching addresses still being watched but not called back - local callbacks=$(sql 'SELECT DISTINCT callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id WHERE NOT calledback0conf and watching_id NOT NULL and callback0conf NOT NULL and watching') + local callbacks=$(sql 'SELECT DISTINCT w.callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE NOT calledback0conf AND watching_id NOT NULL AND w.callback0conf NOT NULL AND w.watching') trace "[do_callbacks] callbacks0conf=${callbacks}" local returncode @@ -29,7 +29,7 @@ do_callbacks() fi done - callbacks=$(sql 'SELECT DISTINCT callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w, watching_tx wt, tx t WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf and confirmations>0 and callback1conf NOT NULL and watching') + callbacks=$(sql 'SELECT DISTINCT w.callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path FROM watching w, watching_tx wt, tx t LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf AND confirmations>0 AND w.callback1conf NOT NULL AND w.watching') trace "[do_callbacks] callbacks1conf=${callbacks}" for row in ${callbacks} @@ -66,6 +66,11 @@ build_callback() local blocktime local blockheight + local pub32_index + local pub32 + local label + local derivation_path + # callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id trace "[build_callback] row=${row}" @@ -79,7 +84,7 @@ build_callback() trace "[build_callback] txid=${txid}" vout_n=$(echo "${row}" | cut -d '|' -f4) trace "[build_callback] vout_n=${vout_n}" - sent_amount=$(echo "${row}" | cut -d '|' -f5) + sent_amount=$(echo "${row}" | cut -d '|' -f5 | awk '{ printf "%.8f", $0 }') trace "[build_callback] sent_amount=${sent_amount}" confirmations=$(echo "${row}" | cut -d '|' -f6) trace "[build_callback] confirmations=${confirmations}" @@ -106,6 +111,17 @@ build_callback() blocktime=$(echo "${row}" | cut -d '|' -f13) trace "[build_callback] blocktime=${blocktime}" + pub32_index=$(echo "${row}" | cut -d '|' -f16) + trace "[build_callback] pub32_index=${pub32_index}" + if [ -n "${pub32_index}" ]; then + pub32=$(echo "${row}" | cut -d '|' -f17) + trace "[build_callback] pub32=${pub32}" + label=$(echo "${row}" | cut -d '|' -f18) + trace "[build_callback] label=${label}" + derivation_path=$(echo "${row}" | cut -d '|' -f19) + trace "[build_callback] derivation_path=${derivation_path}" + fi + data="{\"id\":\"${id}\"," data="${data}\"address\":\"${address}\"," data="${data}\"hash\":\"${txid}\"," @@ -124,6 +140,12 @@ build_callback() data="${data}\"blocktime\":\"$(date -Is -d @${blocktime})\"," data="${data}\"blockheight\":${blockheight}" fi + if [ -n "${pub32_index}" ]; then + data="${data}\"pub32\":\"${pub32}\"," + data="${data}\"pub32_label\":\"${label}\"," + derivation_path=$(echo -e $derivation_path | sed -En "s/n/${pub32_index}/p") + data="${data}\"pub32_derivation_path\":\"${derivation_path}\"" + fi data="${data}}" trace "[build_callback] data=${data}" diff --git a/proxy_docker/app/script/confirmation.sh b/proxy_docker/app/script/confirmation.sh index ab37340..63fa42a 100644 --- a/proxy_docker/app/script/confirmation.sh +++ b/proxy_docker/app/script/confirmation.sh @@ -21,8 +21,10 @@ confirmation_request() return $? } -confirmation() -{ +confirmation() { + ( + flock -x 201 + trace "Entering confirmation()..." local returncode @@ -56,7 +58,7 @@ confirmation() notfirst=true fi done - local rows=$(sql "SELECT id, address FROM watching WHERE address IN (${addresseswhere}) AND watching") + local rows=$(sql "SELECT id, address, watching_by_pub32_id, pub32_index FROM watching WHERE address IN (${addresseswhere}) AND watching") if [ ${#rows} -eq 0 ]; then trace "[confirmation] No watched address in this tx!" return 0 @@ -140,7 +142,7 @@ confirmation() tx=$(sql "SELECT tx_id FROM watching_tx WHERE tx_id=\"${tx}\"") if [ -z "${tx}" ]; then - trace "[confirmation] For this tx, there's no watching_tx row, let's create" + trace "[confirmation] For this tx, there's no watching_tx row, let's create it" local watching_id # If the tx is batched and pays multiple watched addresses, we have to insert @@ -159,11 +161,27 @@ confirmation() fi ######################################################################################################## + ######################################################################################################## + # Let's now grow the watch window in the case of a xpub watcher... + + for row in ${rows} + do + watching_by_pub32_id=$(echo "${row}" | cut -d '|' -f3) + pub32_index=$(echo "${row}" | cut -d '|' -f4) + if [ -n "${watching_by_pub32_id}" ]; then + extend_watchers ${watching_by_pub32_id} ${pub32_index} + fi + done + + ######################################################################################################## + do_callbacks echo '{"result":"confirmed"}' return 0 + + ) 201>./.confirmation.lock } case "${0}" in *confirmation.sh) confirmation $@;; esac diff --git a/proxy_docker/app/script/importaddress.sh b/proxy_docker/app/script/importaddress.sh index fce18ee..753bb0f 100644 --- a/proxy_docker/app/script/importaddress.sh +++ b/proxy_docker/app/script/importaddress.sh @@ -3,19 +3,110 @@ . ./trace.sh . ./sendtobitcoinnode.sh -importaddress_rpc() -{ - trace "[Entering importaddress_rpc()]" +importaddress_rpc() { + trace "[Entering importaddress_rpc()]" - local address=${1} - local data="{\"method\":\"importaddress\",\"params\":[\"${address}\",\"\",false]}" - local result - result=$(send_to_watcher_node ${data}) - local returncode=$? + local address=${1} + local data="{\"method\":\"importaddress\",\"params\":[\"${address}\",\"\",false]}" + local result + result=$(send_to_watcher_node ${data}) + local returncode=$? - echo "${result}" + echo "${result}" - return ${returncode} + return ${returncode} } -case "${0}" in *importaddress.sh) importaddress_rpc $@;; esac +importmulti_rpc() { + trace "[Entering importmulti_rpc()]" + + local walletname=${1} + local addresses=$(echo "${2}" | jq ".addresses" | tr -d '\n ') + trace "[importmulti_rpc] addresses=${addresses}" + +# [{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},{"address":"2Mth8XDZpXkY9d95tort8HYEAuEesow2tF6"},{"address":"2MwqEmAXhUw6H7bJwMhD13HGWVEj2HgFiNH"},{"address":"2N2Y4BVRdrRFhweub2ehHXveGZC3nryMEJw"}] +# [{"scriptPubKey":{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},"timestamp":"now","watchonly":true},{"scriptPubKey":{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},"timestamp":"now","watchonly":true},{"scriptPubKey":{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},"timestamp":"now","watchonly":true}] + +# {"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"}, +# {"scriptPubKey":{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},"timestamp":"now","watchonly":true}, + + addresses=$(echo "${addresses}" | sed "s/\"address\"/\"scriptPubKey\":\{\"address\"/g" | sed "s/}/},\"timestamp\":\"now\",\"watchonly\":true,\"label\":\"${walletname}\"}/g") + trace "[importmulti_rpc] addresses=${addresses}" + +# {"method":"importmulti","params":["requests":[],"options":{"rescan":false}]} +# = {"address":"","timestamp":"now","watchonly":true},... + + local rpcstring="{\"method\":\"importmulti\",\"params\":[${addresses},{\"rescan\":false}]}" + trace "[importmulti_rpc] rpcstring=${rpcstring}" + + local result +# result=$(send_to_watcher_node_wallet ${walletname} ${rpcstring}) + result=$(send_to_watcher_node ${rpcstring}) + local returncode=$? + + echo "${result}" + + return ${returncode} +} + + +#[{"requests": +# [ +# {"scriptPubKey":{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},"timestamp":"now","watchonly":true}, +# {"scriptPubKey":{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},"timestamp":"now","watchonly":true}, +# {"scriptPubKey":{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},"timestamp":"now","watchonly":true} +# ]}, +#{"options": +# { +# "rescan":false +# } +#}] + + + + + + + + +# +# /usr/bin $ ./bitcoin-cli help importmulti +# importmulti "requests" ( "options" ) +# +# Import addresses/scripts (with private or public keys, redeem script (P2SH)), rescanning all addresses in one-shot-only (rescan can be disabled via options). Requires a new wallet backup. +# +# Arguments: +# 1. requests (array, required) Data to be imported +# [ (array of json objects) +# { +# "scriptPubKey": " - - - -
-

Hello World from Cyphernode!

-

If you are here, it means you successfully deployed Cyphernode. Congratulations, fellow Cyphernode Operator!

-
-
-
-

The following files have been encrypted with your configuration passphrase and your client keys passphrase, respectively:

- -
-
-

This is the status of Cyphernode's installation and running components

-

-  
- - From 2c3aa50f92b83d5825b88959111069459b58d9e1 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 29 Mar 2019 13:04:52 -0400 Subject: [PATCH 099/255] Next release version and LN waitanyinvoice only when LN installed --- build.sh | 18 +++++++++--------- dist/setup.sh | 16 ++++++++-------- proxy_docker/app/script/startproxy.sh | 5 ++++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/build.sh b/build.sh index daf241e..54312f3 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.1.1-local" -GATEKEEPER_VERSION="v0.1.1-local" -PROXY_VERSION="v0.1.1-local" -PROXYCRON_VERSION="v0.1.1-local" -OTSCLIENT_VERSION="v0.1.1-local" -PYCOIN_VERSION="v0.1.1-local" -BITCOIN_VERSION="v0.17.0" -LIGHTNING_VERSION="v0.6.2" -GRAFANA_VERSION="v0.1.1-local" +CONF_VERSION="v0.2.0-rc.1-local" +GATEKEEPER_VERSION="v0.2.0-rc.1-local" +PROXY_VERSION="v0.2.0-rc.1-local" +PROXYCRON_VERSION="v0.2.0-rc.1-local" +OTSCLIENT_VERSION="v0.2.0-rc.1-local" +PYCOIN_VERSION="v0.2.0-rc.1-local" +BITCOIN_VERSION="v0.17.1" +LIGHTNING_VERSION="v0.7.0" +GRAFANA_VERSION="v0.2.0-rc.1-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index ffe52be..a5aa4fb 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -693,14 +693,14 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.1.1" -GATEKEEPER_VERSION="v0.1.1" -PROXY_VERSION="v0.1.1" -PROXYCRON_VERSION="v0.1.1" -OTSCLIENT_VERSION="v0.1.1" -PYCOIN_VERSION="v0.1.1" -BITCOIN_VERSION="v0.17.0" -LIGHTNING_VERSION="v0.6.2" +CONF_VERSION="v0.2.0-rc.1" +GATEKEEPER_VERSION="v0.2.0-rc.1" +PROXY_VERSION="v0.2.0-rc.1" +PROXYCRON_VERSION="v0.2.0-rc.1" +OTSCLIENT_VERSION="v0.2.0-rc.1" +PYCOIN_VERSION="v0.2.0-rc.1" +BITCOIN_VERSION="v0.17.1" +LIGHTNING_VERSION="v0.7.0" SPARKWALLET_VERSION="standalone" SETUP_DIR=$(dirname $(realpath $0)) diff --git a/proxy_docker/app/script/startproxy.sh b/proxy_docker/app/script/startproxy.sh index 4ec85dc..a865012 100644 --- a/proxy_docker/app/script/startproxy.sh +++ b/proxy_docker/app/script/startproxy.sh @@ -45,6 +45,9 @@ chmod 0600 $DB_FILE createCurlConfig ${WATCHER_BTC_NODE_RPC_CFG} ${WATCHER_BTC_NODE_RPC_USER} createCurlConfig ${SPENDER_BTC_NODE_RPC_CFG} ${SPENDER_BTC_NODE_RPC_USER} -./waitanyinvoice.sh & +. ${DB_PATH}/config.sh +if [ "${FEATURE_LIGHTNING}" = "true" ]; then + ./waitanyinvoice.sh & +fi nc -vlkp${PROXY_LISTENING_PORT} -e ./requesthandler.sh From 62a0df968f58af62a782366f250b0bccdea31dc6 Mon Sep 17 00:00:00 2001 From: SKP Date: Fri, 29 Mar 2019 15:31:47 +0100 Subject: [PATCH 100/255] Added traefik feature to handle connections and tls of dockerised apps --- dist/setup.sh | 36 ++++++++++++------- .../generators/app/features.json | 4 +++ .../generators/app/index.js | 3 ++ .../generators/app/prompters/030_traefik.js | 15 ++++++++ .../generators/app/prompters/999_installer.js | 29 +++++++++++++++ .../app/templates/installer/config.sh | 4 +++ .../installer/docker/docker-compose.yaml | 15 ++++++++ .../app/templates/traefik/acme.json | 1 + .../app/templates/traefik/traefik.toml | 29 +++++++++++++++ 9 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/prompters/030_traefik.js create mode 100644 install/generator-cyphernode/generators/app/templates/traefik/acme.json create mode 100644 install/generator-cyphernode/generators/app/templates/traefik/traefik.toml diff --git a/dist/setup.sh b/dist/setup.sh index a5aa4fb..e6e33e9 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -110,7 +110,7 @@ sudo_if_required() { } modify_permissions() { - local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml" "$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml" "traefik" "$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH" "$TRAEFIK_DATAPATH") for d in "${directories[@]}" do if [[ -e $d ]]; then @@ -122,7 +122,7 @@ modify_permissions() { } modify_owner() { - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH" "$TRAEFIK_DATAPATH") local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) for d in "${directories[@]}" do @@ -390,6 +390,18 @@ install_docker() { copy_file $current_path/lightning/c-lightning/nginx-spark-conf $GATEKEEPER_DATAPATH/nginx-spark-conf 1 $SUDO_REQUIRED fi + if [[ $FEATURE_TRAEFIK == true ]]; then + if [ ! -d $TRAEFIK_DATAPATH ]; then + step " create $TRAEFIK_DATAPATH" + sudo_if_required mkdir -p $TRAEFIK_DATAPATH + next + fi + + copy_file $current_path/traefik/acme.json $TRAEFIK_DATAPATH/acme.json 1 $SUDO_REQUIRED + copy_file $current_path/traefik/traefik.toml $TRAEFIK_DATAPATH/traefik.toml 1 $SUDO_REQUIRED + + fi + if [ ! -d $PROXY_DATAPATH ]; then step " create $PROXY_DATAPATH" sudo_if_required mkdir -p $PROXY_DATAPATH @@ -502,25 +514,25 @@ install_docker() { local appsnet_entry=$(docker network ls | grep cyphernodeappsnet); - if [[ appsnet_entry =~ 'cyphernodeappsnet' ]]; then - if [[ appsnet_entry =~ 'local' && $DOCKER_MODE == 'swarm' ]]; then - step " recreate cyphernode network" - try docker network rm cyphernodenet > /dev/null 2>&1 + if [[ $appsnet_entry =~ 'cyphernodeappsnet' ]]; then + if [[ $appsnet_entry =~ 'local' && $DOCKER_MODE == 'swarm' ]]; then + step " recreate cyphernode apps network" + try docker network rm cyphernodeappsnet > /dev/null 2>&1 try docker network create -d overlay --attachable --opt encrypted cyphernodeappsnet > /dev/null 2>&1 next - elif [[ appsnet_entry =~ 'swarm' && $DOCKER_MODE == 'compose' ]]; then - step " recreate cyphernode network" + elif [[ $appsnet_entry =~ 'swarm' && $DOCKER_MODE == 'compose' ]]; then + step " recreate cyphernode apps network" try docker network rm cyphernodeappsnet > /dev/null 2>&1 try docker network create cyphernodeappsnet > /dev/null 2>&1 next fi else if [[ $DOCKER_MODE == 'swarm' ]]; then - step " create cyphernode network" + step " create cyphernode apps network" try docker network create -d overlay --attachable --opt encrypted cyphernodeappsnet > /dev/null 2>&1 next elif [[ $DOCKER_MODE == 'compose' ]]; then - step " create cyphernode network" + step " create cyphernode apps network" try docker network create cyphernodeappsnet > /dev/null 2>&1 next fi @@ -552,7 +564,7 @@ install_docker() { check_directory_owner() { # if one directory does not have access rights for $RUN_AS_USER, we echo 1, else we echo 0 - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$TRAEFIK_DATAPATH") local status=0 for d in "${directories[@]}" do @@ -656,7 +668,7 @@ sanity_checks_pre_install() { if [[ $sudo_reason == 'directories' ]]; then echo " or check your data volumes if they have the right owner." echo " The owner of the following folders should be '$RUN_AS_USER':" - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$TRAEFIK_DATAPATH") local status=0 for d in "${directories[@]}" do diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index 84a1e9a..c58ee3a 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -1,4 +1,8 @@ [ + { + "name": "Traefik proxy for cyphernode apps", + "value": "traefik" + }, { "name": "Lightning node", "value": "lightning" diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index da77200..96a07a2 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -362,6 +362,7 @@ module.exports = class extends Generator { const pathProps = [ 'gatekeeper_datapath', + 'traefik_datapath', 'proxy_datapath', 'bitcoin_datapath', 'lightning_datapath', @@ -449,6 +450,7 @@ module.exports = class extends Generator { gatekeeper_sslcert: '', gatekeeper_sslkey: '', gatekeeper_cns: process.env['DEFAULT_CERT_HOSTNAME'] || '', + gatekeeper_datapath: '', proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_external_ip: '', @@ -456,6 +458,7 @@ module.exports = class extends Generator { lightning_nodename: name.generate(), lightning_nodecolor: '', otsclient_datapath: '', + traefik_datapath: '', installer_cleanup: false, default_username: process.env.DEFAULT_USER || '', gatekeeper_version: process.env.GATEKEEPER_VERSION || 'latest', diff --git a/install/generator-cyphernode/generators/app/prompters/030_traefik.js b/install/generator-cyphernode/generators/app/prompters/030_traefik.js new file mode 100644 index 0000000..353b449 --- /dev/null +++ b/install/generator-cyphernode/generators/app/prompters/030_traefik.js @@ -0,0 +1,15 @@ +const chalk = require('chalk'); + +const name = 'traefik'; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return []; + }, + templates: function( props ) { + return [ 'acme.json', 'traefik.toml' ]; + } +}; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 24e04eb..7461666 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -59,6 +59,35 @@ module.exports = { ], message: prefix()+'Where do you want to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), }, + { + when: installerDocker, + type: 'list', + name: 'traefik_datapath', + default: utils._getDefault( 'traefik_datapath' ), + choices: [ + { + name: utils.setupDir+"/cyphernode/traefik", + value: utils.setupDir+"/cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase+"/cyphernode/traefik", + value: utils.defaultDataDirBase+"/cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase+"/.cyphernode/traefik", + value: utils.defaultDataDirBase+"/.cyphernode/traefik" + }, + { + name: utils.defaultDataDirBase+"/traefik", + value: utils.defaultDataDirBase+"/traefik" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your traefik data?'+utils._getHelp('traefik_datapath'), + }, { when: (props)=>{ return installerDocker(props) && (props.gatekeeper_datapath === '_custom') }, type: 'input', diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index ddd9ca3..4d1a844 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,5 +1,6 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> +FEATURE_TRAEFIK=<%= (features.indexOf('traefik') != -1)?'true':'false' %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> @@ -8,6 +9,9 @@ GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> +<% if ( features.indexOf('traefik') !== -1 ) { %> +TRAEFIK_DATAPATH=<%= traefik_datapath %> +<% } %> <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> LIGHTNING_DATAPATH=<%= lightning_datapath %> <% } %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 113eec5..9f632bd 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -147,6 +147,21 @@ services: restart: always <% } %> +<% if ( features.indexOf('traefik') !== -1 ) { %> + traefik: + image: traefik:v1.7.9-alpine + restart: always + ports: + - 80:80 + - 443:443 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" + - "<%= traefik_datapath%>/acme.json:/acme.json" + networks: + - cyphernodeappsnet +<% } %> + <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind diff --git a/install/generator-cyphernode/generators/app/templates/traefik/acme.json b/install/generator-cyphernode/generators/app/templates/traefik/acme.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/traefik/acme.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml new file mode 100644 index 0000000..06d9194 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml @@ -0,0 +1,29 @@ +debug = false + +logLevel = "ERROR" +defaultEntryPoints = ["https","http"] + +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + +[retry] + +[docker] +endpoint = "unix:///var/run/docker.sock" +domain = "cyphernode.localhost" +watch = true +exposedByDefault = false + +[acme] +email = "noreply@cnc.skp.rocks" +storage = "acme.json" +entryPoint = "https" +onHostRule = true +[acme.httpChallenge] +entryPoint = "http" From d17ffbac4d74a4fc2425bab95bbe08434d6d8a75 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 29 Mar 2019 13:05:31 -0400 Subject: [PATCH 101/255] Help for traefik and default port to 2009 for gatekeeper --- install/generator-cyphernode/generators/app/help.json | 2 ++ install/generator-cyphernode/generators/app/index.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 0084243..c321031 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -17,6 +17,8 @@ "gatekeeper_edit_apiproperties": "If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization. (Not recommended)", "gatekeeper_apiproperties": "You are about to edit the api.properties file. The format of the file is pretty simple: for each action, you will find what access group can access it. Admin group can do what Spender group can, and Spender group can do what Watcher group can. Internal group is for the endpoints accessible only within the Docker network, like the backoffice tasks used by the Cron container. The access groups for each API id/key are found in the keys.properties file.", "gatekeeper_cns": "I use domain names and/or IP addresses to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, enter cyphernodehost, 192.168.7.44 as a possible domains. 127.0.0.1, localhost, gatekeeper will be automatically added to your list. Make sure the provided domain names are in your DNS or client's hosts file and is reachable.", + "traefik_datapath": "The Traefik's files will be stored in a container's mounted directory. Please provide the local mounted path to that directory. If running on OSX, check mountable directories in Docker's File Sharing configs.", + "traefik_datapath_custom": "Provide the full path name where the Traefik's files will be saved.", "bitcoin_mode": "Cyphernode will spawn a new Bitcoin Core full node for its own use. If you already have Bitcoin Core node data, you can use the directory containing that data directly or copy the contents of it to a new directory to be used by cyphernode. Be aware that the files might change ownership, if you run cyphernode as a different user. In case you want to move the blockchain data to another node you might need to change the owner to fit the configuration of that node.", "bitcoin_node_ip": "Cyphernode uses Bitcoin Core RPC interface for its tasks. Please provide the IP address of your current Bitcoin Core node.", "bitcoin_rpcuser": "Bitcoin Core's RPC username used by Cyphernode when calling the node.", diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 96a07a2..8d9366c 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -443,7 +443,7 @@ module.exports = class extends Generator { bitcoin_mode: 'internal', bitcoin_expose: false, lightning_expose: true, - gatekeeper_port: 443, + gatekeeper_port: 2009, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, From 3ccdb07bba6664c6f76aa5685d8a3939f7762a16 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 29 Mar 2019 13:38:35 -0400 Subject: [PATCH 102/255] Traefik fixes during setup --- .../generators/app/prompters/999_installer.js | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 7461666..db54821 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -30,35 +30,6 @@ module.exports = { value: "docker" }] }, - { - when: installerDocker, - type: 'list', - name: 'gatekeeper_datapath', - default: utils._getDefault( 'gatekeeper_datapath' ), - choices: [ - { - name: utils.setupDir+"/cyphernode/gatekeeper", - value: utils.setupDir+"/cyphernode/gatekeeper" - }, - { - name: utils.defaultDataDirBase+"/cyphernode/gatekeeper", - value: utils.defaultDataDirBase+"/cyphernode/gatekeeper" - }, - { - name: utils.defaultDataDirBase+"/.cyphernode/gatekeeper", - value: utils.defaultDataDirBase+"/.cyphernode/gatekeeper" - }, - { - name: utils.defaultDataDirBase+"/gatekeeper", - value: utils.defaultDataDirBase+"/gatekeeper" - }, - { - name: "Custom path", - value: "_custom" - } - ], - message: prefix()+'Where do you want to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), - }, { when: installerDocker, type: 'list', @@ -88,6 +59,44 @@ module.exports = { ], message: prefix()+'Where do you want to store your traefik data?'+utils._getHelp('traefik_datapath'), }, + { + when: (props)=>{ return installerDocker(props) && (props.traefik_datapath === '_custom') }, + type: 'input', + name: 'traefik_datapath_custom', + default: utils._getDefault( 'traefik_datapath_custom' ), + filter: utils._trimFilter, + validate: utils._pathValidator, + message: prefix()+'Custom path for traefik data?'+utils._getHelp('traefik_datapath_custom'), + }, + { + when: installerDocker, + type: 'list', + name: 'gatekeeper_datapath', + default: utils._getDefault( 'gatekeeper_datapath' ), + choices: [ + { + name: utils.setupDir+"/cyphernode/gatekeeper", + value: utils.setupDir+"/cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase+"/cyphernode/gatekeeper", + value: utils.defaultDataDirBase+"/cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase+"/.cyphernode/gatekeeper", + value: utils.defaultDataDirBase+"/.cyphernode/gatekeeper" + }, + { + name: utils.defaultDataDirBase+"/gatekeeper", + value: utils.defaultDataDirBase+"/gatekeeper" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), + }, { when: (props)=>{ return installerDocker(props) && (props.gatekeeper_datapath === '_custom') }, type: 'input', From 03d03158c32508b08aab228633886707b4da1825 Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 30 Mar 2019 17:54:30 -0400 Subject: [PATCH 103/255] fixed: nginx won't start if sparkwallet not found --- .../app/templates/lightning/c-lightning/nginx-spark-conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf index 7de9208..dd0c824 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf @@ -13,6 +13,7 @@ location /sparkwallet/ { # Hardcoding sparkwallet password, it's only accessible from here anyway using htpasswd above proxy_set_header Authorization "Basic Y3lwaGVybm9kZTpzcGFya3dhbGxldA=="; - proxy_pass http://sparkwallet:9737/; + set $proxyurl http://sparkwallet:9737/; + proxy_pass $proxyurl; } <% } %> From a7e31612088e5e648d04443e7639f18703650fbf Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 30 Mar 2019 20:58:45 -0400 Subject: [PATCH 104/255] Added steps to build images --- doc/INSTALL.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 75fc6bb..676593f 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -20,6 +20,18 @@ Or you can simply run this magic command to start setup and installation: curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/master/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh ``` +### Build cyphernode yourself + +You can build cyphernode images yourself. The images will have the same name than the ones in the docker hub, with the suffix -local. + +```shell +git clone https://github.com/SatoshiPortal/cyphernode.git +cd cyphernode +./build.sh +cd dist +./setup.sh +``` + ## Upgrading Your proxy's database won't be lost. Migration scripts are taking care of automatically migrating the database when starting the proxy. From c4159b628f450b4d0bb1b6e718ce37a4dcf70b49 Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 30 Mar 2019 14:45:56 +0100 Subject: [PATCH 105/255] Fixed acme storage --- .../generators/app/templates/traefik/traefik.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml index 06d9194..e20deee 100644 --- a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml +++ b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml @@ -22,7 +22,7 @@ exposedByDefault = false [acme] email = "noreply@cnc.skp.rocks" -storage = "acme.json" +storage = "/acme.json" entryPoint = "https" onHostRule = true [acme.httpChallenge] From ec6ed5e1af54cdb8e7229d46d5d31f5e8f6a856d Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 30 Mar 2019 23:01:47 +0100 Subject: [PATCH 106/255] Moved htpasswd file to traffic and changed hashing algo to bcrypt --- dist/setup.sh | 62 +++++++++---------- install/Dockerfile | 2 +- .../generators/app/index.js | 3 +- .../generators/app/lib/cert.js | 18 +----- .../generators/app/lib/htpasswd.js | 21 +++++++ .../app/prompters/010_gatekeeper.js | 2 +- .../generators/app/prompters/030_traefik.js | 2 +- .../app/templates/gatekeeper/htpasswd | 1 - .../generators/app/templates/traefik/htpasswd | 1 + 9 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/htpasswd.js delete mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd create mode 100644 install/generator-cyphernode/generators/app/templates/traefik/htpasswd diff --git a/dist/setup.sh b/dist/setup.sh index e6e33e9..8a871d4 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -366,42 +366,40 @@ install_docker() { next fi - if [ -d $GATEKEEPER_DATAPATH ]; then - if [[ ! -f $GATEKEEPER_DATAPATH/installation.json ]]; then - # prevent mounting installation.json as a directory - sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json - fi - - if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then - sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 - fi - - if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then - sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 - fi - - copy_file $current_path/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED - copy_file $current_path/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED - copy_file $current_path/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED - copy_file $current_path/gatekeeper/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED - copy_file $current_path/lightning/c-lightning/nginx-spark-conf $GATEKEEPER_DATAPATH/nginx-spark-conf 1 $SUDO_REQUIRED + if [[ ! -f $GATEKEEPER_DATAPATH/installation.json ]]; then + # prevent mounting installation.json as a directory + sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json fi - if [[ $FEATURE_TRAEFIK == true ]]; then - if [ ! -d $TRAEFIK_DATAPATH ]; then - step " create $TRAEFIK_DATAPATH" - sudo_if_required mkdir -p $TRAEFIK_DATAPATH - next - fi - - copy_file $current_path/traefik/acme.json $TRAEFIK_DATAPATH/acme.json 1 $SUDO_REQUIRED - copy_file $current_path/traefik/traefik.toml $TRAEFIK_DATAPATH/traefik.toml 1 $SUDO_REQUIRED - + if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 fi + if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 + fi + + copy_file $current_path/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED + copy_file $current_path/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED + copy_file $current_path/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED + copy_file $current_path/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED + copy_file $current_path/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED + copy_file $current_path/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED + copy_file $current_path/lightning/c-lightning/nginx-spark-conf $GATEKEEPER_DATAPATH/nginx-spark-conf 1 $SUDO_REQUIRED + copy_file $current_path/traefik/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED + + + if [ ! -d $TRAEFIK_DATAPATH ]; then + step " create $TRAEFIK_DATAPATH" + sudo_if_required mkdir -p $TRAEFIK_DATAPATH + next + fi + + copy_file $current_path/traefik/acme.json $TRAEFIK_DATAPATH/acme.json 1 $SUDO_REQUIRED + copy_file $current_path/traefik/traefik.toml $TRAEFIK_DATAPATH/traefik.toml 1 $SUDO_REQUIRED + copy_file $current_path/traefik/htpasswd $TRAEFIK_DATAPATH/htpasswd 1 $SUDO_REQUIRED + + if [ ! -d $PROXY_DATAPATH ]; then step " create $PROXY_DATAPATH" sudo_if_required mkdir -p $PROXY_DATAPATH diff --git a/install/Dockerfile b/install/Dockerfile index 7d8e6d3..dd55465 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:11.1-alpine -RUN apk add --update bash su-exec p7zip openssl nano && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip openssl nano apache2-utils && rm -rf /var/cache/apk/* RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 8d9366c..aafc2c8 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -10,6 +10,7 @@ const name = require('./lib/name.js'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); const Cert = require('./lib/cert.js'); +const htpasswd = require( './lib/htpasswd.js') const featureChoices = require('./features.json'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars @@ -219,7 +220,7 @@ module.exports = class extends Generator { // migrate here } - this.props.gatekeeper_statuspw = await new Cert().passwd(this.configurationPassword); + this.props.initial_admin_password = await htpasswd(this.configurationPassword); if( versionOverride ) { delete this.props.gatekeeper_version; diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index 1a61c99..3049906 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -113,21 +113,5 @@ module.exports = class Cert { getFullPath() { return path.join( this.folder, this.filename ); } - - async passwd( pw ) { - const openssl = spawn('openssl', [ "passwd", pw ], {stdio: ['ignore', 'pipe', 'ignore' ]}); - - const result = await new Promise( function(resolve, reject ) { - let result = ''; - openssl.stdout.on('data', (data) => { - result += data.toString(); - }); - - openssl.on('exit', (code) => { - resolve(result); - }); - }); - - return result; - } + } diff --git a/install/generator-cyphernode/generators/app/lib/htpasswd.js b/install/generator-cyphernode/generators/app/lib/htpasswd.js new file mode 100644 index 0000000..7531794 --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/htpasswd.js @@ -0,0 +1,21 @@ +const exec = require('child_process').exec; + +module.exports = async ( password ) => { + + if( !password ) { + return null; + } + + return await new Promise( (resolve) => { + exec('htpasswd -bnB admin '+password+' | cut -sd \':\' -f2', (error, stdout, stderr) => { + if (error) { + return resolve(null); + } + // remove newline at the end + resolve(stdout.substr(0,stdout.length-1)); + }); + }); + + +}; + diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 7bc1993..194b1da 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -107,6 +107,6 @@ module.exports = { }]; }, templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem', 'htpasswd' ]; + return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem' ]; } }; diff --git a/install/generator-cyphernode/generators/app/prompters/030_traefik.js b/install/generator-cyphernode/generators/app/prompters/030_traefik.js index 353b449..7561e97 100644 --- a/install/generator-cyphernode/generators/app/prompters/030_traefik.js +++ b/install/generator-cyphernode/generators/app/prompters/030_traefik.js @@ -10,6 +10,6 @@ module.exports = { return []; }, templates: function( props ) { - return [ 'acme.json', 'traefik.toml' ]; + return [ 'acme.json', 'traefik.toml', 'htpasswd' ]; } }; diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd deleted file mode 100644 index 7cf9383..0000000 --- a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd +++ /dev/null @@ -1 +0,0 @@ -admin:<%- gatekeeper_statuspw %> diff --git a/install/generator-cyphernode/generators/app/templates/traefik/htpasswd b/install/generator-cyphernode/generators/app/templates/traefik/htpasswd new file mode 100644 index 0000000..fba8148 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/traefik/htpasswd @@ -0,0 +1 @@ +admin:<%- initial_admin_password %> From 15bc997723a6cfe61cc1080412cfa132abead909 Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 30 Mar 2019 23:02:25 +0100 Subject: [PATCH 107/255] traefik is now core feature --- .../generators/app/features.json | 4 --- .../app/templates/installer/config.sh | 6 ++-- .../installer/docker/docker-compose.yaml | 29 +++++++++---------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index c58ee3a..84a1e9a 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -1,8 +1,4 @@ [ - { - "name": "Traefik proxy for cyphernode apps", - "value": "traefik" - }, { "name": "Lightning node", "value": "lightning" diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 4d1a844..e8af685 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,17 +1,15 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> -FEATURE_TRAEFIK=<%= (features.indexOf('traefik') != -1)?'true':'false' %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> +TRAEFIK_DATAPATH=<%= traefik_datapath %> DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> -<% if ( features.indexOf('traefik') !== -1 ) { %> -TRAEFIK_DATAPATH=<%= traefik_datapath %> -<% } %> +SHARED_HTPASSWD_PATH=<%= traefik_datapath %>/htpasswd <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> LIGHTNING_DATAPATH=<%= lightning_datapath %> <% } %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 9f632bd..db13aff 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -27,6 +27,20 @@ services: - cyphernodenet - cyphernodeappsnet restart: always + + traefik: + image: traefik:v1.7.9-alpine + restart: always + ports: + - 80:80 + - 443:443 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" + - "<%= traefik_datapath%>/acme.json:/acme.json" + networks: + - cyphernodeappsnet + proxy: command: $USER ./startproxy.sh # Bitcoin Mini Proxy @@ -147,21 +161,6 @@ services: restart: always <% } %> -<% if ( features.indexOf('traefik') !== -1 ) { %> - traefik: - image: traefik:v1.7.9-alpine - restart: always - ports: - - 80:80 - - 443:443 - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" - - "<%= traefik_datapath%>/acme.json:/acme.json" - networks: - - cyphernodeappsnet -<% } %> - <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind From 8d2670f58952ac36e68fbe2c25f500d8bbfead47 Mon Sep 17 00:00:00 2001 From: SKP Date: Sat, 30 Mar 2019 23:02:40 +0100 Subject: [PATCH 108/255] Removed debug stuff --- api_auth_docker/default.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index 32d9fb4..c259c7a 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -25,7 +25,6 @@ server { } location /auth { - error_log /var/log/shice.log debug; internal; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh; From 769520343af5d3d70ae7e273a12c12f196eca1fb Mon Sep 17 00:00:00 2001 From: kexkey Date: Sun, 31 Mar 2019 15:35:38 -0400 Subject: [PATCH 109/255] Cyphernode build of sparkwallet --- dist/setup.sh | 3 ++- .../app/templates/installer/docker/docker-compose.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 8a871d4..14653ae 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -711,7 +711,7 @@ OTSCLIENT_VERSION="v0.2.0-rc.1" PYCOIN_VERSION="v0.2.0-rc.1" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -SPARKWALLET_VERSION="standalone" +SPARKWALLET_VERSION="v0.2.3" SETUP_DIR=$(dirname $(realpath $0)) @@ -767,6 +767,7 @@ if [[ $nbbuiltimgs -gt 1 ]]; then PROXYCRON_VERSION="$PROXYCRON_VERSION-local" OTSCLIENT_VERSION="$OTSCLIENT_VERSION-local" PYCOIN_VERSION="$PYCOIN_VERSION-local" + SPARKWALLET_VERSION="$SPARKWALLET_VERSION-local" fi fi diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index db13aff..8490686 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -136,7 +136,7 @@ services: sparkwallet: command: --login "cyphernode:sparkwallet" --no-tls - image: shesek/spark-wallet:standalone + image: cyphernode/sparkwallet:<%= sparkwallet_version %> volumes: - "<%= lightning_datapath %>:/etc/lightning" - "<%= lightning_datapath %>/sparkwallet:/data" From 4b98241cbb6c700c3f3c18def1711ec00ac87a7f Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 1 Apr 2019 16:40:37 -0400 Subject: [PATCH 110/255] nginx now starts even if sparkwallet host not found --- .../app/templates/lightning/c-lightning/nginx-spark-conf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf index dd0c824..4223633 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf @@ -13,7 +13,9 @@ location /sparkwallet/ { # Hardcoding sparkwallet password, it's only accessible from here anyway using htpasswd above proxy_set_header Authorization "Basic Y3lwaGVybm9kZTpzcGFya3dhbGxldA=="; - set $proxyurl http://sparkwallet:9737/; - proxy_pass $proxyurl; + # https://cyphernode:2009/sparkwallet/hello -> http://sparkwallet:9737/hello + rewrite ^/sparkwallet(/.*) $1 break; + resolver 127.0.0.11; + proxy_pass http://sparkwallet:9737$uri; } <% } %> From f7ac03002dd28aeed4bfdb03437b02c3310f47e2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 1 Apr 2019 16:45:47 -0400 Subject: [PATCH 111/255] getblockchaininfo docs --- api_auth_docker/api-sample.properties | 5 ++- doc/API.v0.md | 64 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties index b3d6f88..63851c6 100644 --- a/api_auth_docker/api-sample.properties +++ b/api_auth_docker/api-sample.properties @@ -1,6 +1,9 @@ # The file api.properties generated by the installer should look like this. -# Watcher can: +# Stats can: +action_getblockchaininfo=stats + +# Watcher can do what the stats can do, plus: action_watch=watcher action_unwatch=watcher action_watchxpub=watcher diff --git a/doc/API.v0.md b/doc/API.v0.md index fdf1b37..fcfccda 100644 --- a/doc/API.v0.md +++ b/doc/API.v0.md @@ -292,6 +292,70 @@ When cyphernode receives a transaction confirmation (/conf endpoint) on a watche } ``` +### Get the blockchain information (called by application) + +Returns the blockchain information of the Bitcoin node. Used for example by the welcome app to get syncing progression. + +```http +GET http://cyphernode:8888/getblockchaininfo +``` + +Proxy response: + +```json +{ + "chain": "test", + "blocks": 1486864, + "headers": 1486864, + "bestblockhash": "000000000000002fb99d683e64bbfc2b7ad16f9a425cf7be77b481fb1afa363b", + "difficulty": 13971064.71015782, + "mediantime": 1554149114, + "verificationprogress": 0.9999994536561675, + "initialblockdownload": false, + "chainwork": "000000000000000000000000000000000000000000000103ceb57a5896f347ce", + "size_on_disk": 23647567017, + "pruned": false, + "softforks": [ + { + "id": "bip34", + "version": 2, + "reject": { + "status": true + } + }, + { + "id": "bip66", + "version": 3, + "reject": { + "status": true + } + }, + { + "id": "bip65", + "version": 4, + "reject": { + "status": true + } + } + ], + "bip9_softforks": { + "csv": { + "status": "active", + "startTime": 1456790400, + "timeout": 1493596800, + "since": 770112 + }, + "segwit": { + "status": "active", + "startTime": 1462060800, + "timeout": 1493596800, + "since": 834624 + } + }, + "warnings": "Warning: unknown new rules activated (versionbit 28)" +} +``` + ### Get the Best Block Hash (called by application) Returns the best block hash of the watching Bitcoin node. From 429a55484410df41d7b301ac787ca76815caed7c Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 10:46:56 -0400 Subject: [PATCH 112/255] sparkwallet through traefik --- .../app/templates/installer/docker/docker-compose.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 8490686..6e302dc 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -140,6 +140,13 @@ services: volumes: - "<%= lightning_datapath %>:/etc/lightning" - "<%= lightning_datapath %>/sparkwallet:/data" + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" + - "traefik.frontend.passHostHeader=true" + - "traefik.frontend.auth.basic.usersFile=$SHARED_HTPASSWD_PATH" + - "traefik.enable=true" + - "traefik.port=9737" networks: - cyphernodenet restart: always From ac927c8131f15137669876aa1f1a452ba0336e62 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 12:12:04 -0400 Subject: [PATCH 113/255] Auth with sparkwallet --- .../app/templates/installer/docker/docker-compose.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 6e302dc..8fb3d65 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -38,6 +38,7 @@ services: - "/var/run/docker.sock:/var/run/docker.sock" - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" - "<%= traefik_datapath%>/acme.json:/acme.json" + - "<%= traefik_datapath%>/htpasswd:/htpasswd/htpasswd" networks: - cyphernodeappsnet @@ -140,15 +141,18 @@ services: volumes: - "<%= lightning_datapath %>:/etc/lightning" - "<%= lightning_datapath %>/sparkwallet:/data" + - "<%= traefik_datapath%>/htpasswd:/htpasswd/htpasswd" labels: - "traefik.docker.network=cyphernodeappsnet" - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" - "traefik.frontend.passHostHeader=true" - - "traefik.frontend.auth.basic.usersFile=$SHARED_HTPASSWD_PATH" + - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" + - "traefik.frontend.headers.customRequestHeaders=Authorization:Basic Y3lwaGVybm9kZTpzcGFya3dhbGxldA==" - "traefik.enable=true" - "traefik.port=9737" networks: - cyphernodenet + - cyphernodeappsnet restart: always <% } %> <% if ( features.indexOf('otsclient') !== -1 ) { %> From fd6e791d3cd4ece2f8e32c700393b3ae68bb2efe Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 31 Mar 2019 23:00:44 +0200 Subject: [PATCH 114/255] Added support for welcome app --- dist/apps/welcome/docker-compose.yaml | 22 ++++++++++++++ dist/apps/welcome/start.sh | 8 +++++ dist/apps/welcome/stop.sh | 7 +++++ dist/apps/welcome/test.sh | 7 +++++ .../app/templates/installer/start.sh | 30 +++++++++++++++++++ .../app/templates/installer/stop.sh | 29 ++++++++++++++++++ 6 files changed, 103 insertions(+) create mode 100644 dist/apps/welcome/docker-compose.yaml create mode 100644 dist/apps/welcome/start.sh create mode 100644 dist/apps/welcome/stop.sh create mode 100644 dist/apps/welcome/test.sh diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml new file mode 100644 index 0000000..60def8e --- /dev/null +++ b/dist/apps/welcome/docker-compose.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + cyphernode_welcome: + environment: + - "TRACING=1" + image: cyphernode_welcome + volumes: + - "/Users/jash/go/src/cyphernode_welcome/data_docker:/data" + networks: + - cyphernodeappsnet + restart: always + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.rule=PathPrefix:/welcome; PathPrefixStrip:/welcome" + - "traefik.frontend.passHostHeader=true" + - "traefik.enable=true" + - "traefik.port=8080" +# - "traefik.frontend.auth.basic.usersFile=${SHARED_HTPASSWD_PATH}" +networks: + cyphernodeappsnet: + external: true diff --git a/dist/apps/welcome/start.sh b/dist/apps/welcome/start.sh new file mode 100644 index 0000000..8318bb2 --- /dev/null +++ b/dist/apps/welcome/start.sh @@ -0,0 +1,8 @@ + +echo "SCRIPT_NAME: $SCRIPT_NAME" +echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" +echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" +echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" + +export SHARED_HTPASSWD_PATH +docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans diff --git a/dist/apps/welcome/stop.sh b/dist/apps/welcome/stop.sh new file mode 100644 index 0000000..cdedfa2 --- /dev/null +++ b/dist/apps/welcome/stop.sh @@ -0,0 +1,7 @@ + +echo "SCRIPT_NAME: $SCRIPT_NAME" +echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" +echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" +echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" + +docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down diff --git a/dist/apps/welcome/test.sh b/dist/apps/welcome/test.sh new file mode 100644 index 0000000..1a5563f --- /dev/null +++ b/dist/apps/welcome/test.sh @@ -0,0 +1,7 @@ + +echo "SCRIPT_NAME: $SCRIPT_NAME" +echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" +echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" +echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" + +echo "No tests" \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 1d5bb6d..a0c11d6 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -48,3 +48,33 @@ printf "\r\n\033[0;92mDepending on your current location and DNS settings, point printf "\r\n" printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + ':'+ gatekeeper_port + '/status/\\r\\n') %><% }) %>\033[0m\r\n" printf "\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n\r\n\033[0m" + + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +start_apps() { + local SCRIPT_NAME="start.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + + for i in "$current_path/apps/*" + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d $APP_SCRIPT_PATH ]; then + APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + + if [ -f $APP_START_SCRIPT_PATH ]; then + APP_ID=$(basename $APP_SCRIPT_PATH) + source $APP_START_SCRIPT_PATH + fi + fi + done +} + +. ./installer/config.sh +start_apps diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index 7fc1279..aeaf1b3 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -11,3 +11,32 @@ export USER=$(id -u):$(id -g) export ARCH=$(uname -m) docker-compose -f $current_path/docker-compose.yaml down <% } %> + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +stop_apps() { + local SCRIPT_NAME="stop.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + + for i in "$current_path/apps/*" + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d $APP_SCRIPT_PATH ]; then + APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + + if [ -f $APP_START_SCRIPT_PATH ]; then + APP_ID=$(basename $APP_SCRIPT_PATH) + source $APP_START_SCRIPT_PATH + fi + fi + done +} + +. ./installer/config.sh +stop_apps \ No newline at end of file From caa8a0a50c3465e86366144ed9bb7157e19717b7 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 2 Apr 2019 14:34:36 -0400 Subject: [PATCH 115/255] Replaced source by point --- .../generators/app/templates/installer/start.sh | 2 +- .../generators/app/templates/installer/stop.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index a0c11d6..60e429c 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -70,7 +70,7 @@ start_apps() { if [ -f $APP_START_SCRIPT_PATH ]; then APP_ID=$(basename $APP_SCRIPT_PATH) - source $APP_START_SCRIPT_PATH + . $APP_START_SCRIPT_PATH fi fi done diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index aeaf1b3..0c7e6d1 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -32,7 +32,7 @@ stop_apps() { if [ -f $APP_START_SCRIPT_PATH ]; then APP_ID=$(basename $APP_SCRIPT_PATH) - source $APP_START_SCRIPT_PATH + . $APP_START_SCRIPT_PATH fi fi done From c3932fea715bdbc25c001fdd902776dcb06c7261 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 2 Apr 2019 14:38:54 -0400 Subject: [PATCH 116/255] Path of volume --- dist/apps/welcome/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml index 60def8e..30db232 100644 --- a/dist/apps/welcome/docker-compose.yaml +++ b/dist/apps/welcome/docker-compose.yaml @@ -6,7 +6,7 @@ services: - "TRACING=1" image: cyphernode_welcome volumes: - - "/Users/jash/go/src/cyphernode_welcome/data_docker:/data" + - "~/cyphernode_welcome/data:/data" networks: - cyphernodeappsnet restart: always From b50da5f051f3901181890745c9f05ee0a49e48a7 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 09:10:27 -0400 Subject: [PATCH 117/255] We only need the welcome app image --- dist/apps/welcome/config.toml | 14 ++++++++++++++ dist/apps/welcome/docker-compose.yaml | 4 +++- dist/apps/welcome/start.sh | 10 +++++++++- dist/apps/welcome/stop.sh | 7 ++++++- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 dist/apps/welcome/config.toml diff --git a/dist/apps/welcome/config.toml b/dist/apps/welcome/config.toml new file mode 100644 index 0000000..9d024b7 --- /dev/null +++ b/dist/apps/welcome/config.toml @@ -0,0 +1,14 @@ +[server] +listen = "0.0.0.0:8080" +index_template = "templates/index.html" +path_prefix = "/welcome" + +[gatekeeper] +status_url = "https://gatekeeper/v0/getblockchaininfo" +installation_info_url = "https://gatekeeper/s/stats/installation.json" +config_archive_url = "https://gatekeeper/s/stats/config.7z" +certs_url = "https://gatekeeper/s/stats/client.7z" + +key_label = "000" +key_file = "/data/keys.properties" +cert_file = "/data/cert.pem" diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml index 30db232..ed04237 100644 --- a/dist/apps/welcome/docker-compose.yaml +++ b/dist/apps/welcome/docker-compose.yaml @@ -6,7 +6,9 @@ services: - "TRACING=1" image: cyphernode_welcome volumes: - - "~/cyphernode_welcome/data:/data" + - "$GATEKEEPER_DATAPATH/certs/cert.pem:/data/cert.pem" + - "$GATEKEEPER_DATAPATH/keys.properties:/data/keys.properties" + - "$APP_SCRIPT_PATH/config.toml:/data/config.toml" networks: - cyphernodeappsnet restart: always diff --git a/dist/apps/welcome/start.sh b/dist/apps/welcome/start.sh index 8318bb2..6c6e768 100644 --- a/dist/apps/welcome/start.sh +++ b/dist/apps/welcome/start.sh @@ -3,6 +3,14 @@ echo "SCRIPT_NAME: $SCRIPT_NAME" echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" +echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" export SHARED_HTPASSWD_PATH -docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans +export GATEKEEPER_DATAPATH +export APP_SCRIPT_PATH + +if [ "$DOCKER_MODE" == "swarm" ]; then + docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml cn_welcome +elif [ "$DOCKER_MODE" == "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans +fi diff --git a/dist/apps/welcome/stop.sh b/dist/apps/welcome/stop.sh index cdedfa2..db05c94 100644 --- a/dist/apps/welcome/stop.sh +++ b/dist/apps/welcome/stop.sh @@ -3,5 +3,10 @@ echo "SCRIPT_NAME: $SCRIPT_NAME" echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" +echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" -docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down +if [ "$DOCKER_MODE" == "swarm" ]; then + docker stack rm cn_welcome +elif [ "$DOCKER_MODE" == "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down +fi From 54b0bf9d7b245e0d8a755acee2fcd1cb6f138f6f Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 12:12:21 -0400 Subject: [PATCH 118/255] Auth in welcome app --- dist/apps/welcome/docker-compose.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml index ed04237..ff8e3ce 100644 --- a/dist/apps/welcome/docker-compose.yaml +++ b/dist/apps/welcome/docker-compose.yaml @@ -9,6 +9,7 @@ services: - "$GATEKEEPER_DATAPATH/certs/cert.pem:/data/cert.pem" - "$GATEKEEPER_DATAPATH/keys.properties:/data/keys.properties" - "$APP_SCRIPT_PATH/config.toml:/data/config.toml" + - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" networks: - cyphernodeappsnet restart: always @@ -18,7 +19,7 @@ services: - "traefik.frontend.passHostHeader=true" - "traefik.enable=true" - "traefik.port=8080" -# - "traefik.frontend.auth.basic.usersFile=${SHARED_HTPASSWD_PATH}" + - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" networks: cyphernodeappsnet: external: true From d9a1a34e008e4f20a99e905581525d6fb5f320d5 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 17:18:36 -0400 Subject: [PATCH 119/255] sparkwallet auth through traefik --- api_auth_docker/default.conf | 2 -- dist/setup.sh | 3 ++- .../installer/docker/docker-compose.yaml | 4 ++-- .../templates/lightning/c-lightning/cookie | 2 ++ .../lightning/c-lightning/nginx-spark-conf | 21 ------------------- 5 files changed, 6 insertions(+), 26 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie delete mode 100644 install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index c259c7a..f8b4193 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -10,8 +10,6 @@ server { root /etc/nginx/conf.d; } - include /etc/nginx/conf.d/nginx-spark-conf; - location /v0/ { auth_request /auth; proxy_pass http://proxy:8888/; diff --git a/dist/setup.sh b/dist/setup.sh index 14653ae..e9d9a71 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -457,12 +457,13 @@ install_docker() { fi if [ ! -d $LIGHTNING_DATAPATH/sparkwallet ]; then step " create $LIGHTNING_DATAPATH" - sudo_if_required mkdir -p $LIGHTNING_DATAPATH/sparkwallet + sudo_if_required mkdir -p $LIGHTNING_DATAPATH/sparkwallet/spark next fi if [ -d $LIGHTNING_DATAPATH ]; then copy_file $current_path/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED copy_file $current_path/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $current_path/lightning/c-lightning/cookie $LIGHTNING_DATAPATH/sparkwallet/spark/cookie 1 $SUDO_REQUIRED fi fi fi diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 8fb3d65..1ccbbfe 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -136,7 +136,7 @@ services: restart: always sparkwallet: - command: --login "cyphernode:sparkwallet" --no-tls + command: --no-tls image: cyphernode/sparkwallet:<%= sparkwallet_version %> volumes: - "<%= lightning_datapath %>:/etc/lightning" @@ -147,7 +147,7 @@ services: - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" - "traefik.frontend.passHostHeader=true" - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" - - "traefik.frontend.headers.customRequestHeaders=Authorization:Basic Y3lwaGVybm9kZTpzcGFya3dhbGxldA==" + - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" - "traefik.enable=true" - "traefik.port=9737" networks: diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie new file mode 100644 index 0000000..247ec65 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie @@ -0,0 +1,2 @@ +# echo -n "access-key" | openssl dgst -hmac "cyphernode:sparkwallet" -sha256 -binary | base64 | sed 's/[\+\W]//g' +cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf deleted file mode 100644 index 4223633..0000000 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/nginx-spark-conf +++ /dev/null @@ -1,21 +0,0 @@ -<% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> -location /sparkwallet/ { - auth_basic "sparkwallet"; - auth_basic_user_file conf.d/status/htpasswd; - - proxy_set_header Host $host; - proxy_set_header Referer $http_referer; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Host $host; - - # Hardcoding sparkwallet password, it's only accessible from here anyway using htpasswd above - proxy_set_header Authorization "Basic Y3lwaGVybm9kZTpzcGFya3dhbGxldA=="; - - # https://cyphernode:2009/sparkwallet/hello -> http://sparkwallet:9737/hello - rewrite ^/sparkwallet(/.*) $1 break; - resolver 127.0.0.11; - proxy_pass http://sparkwallet:9737$uri; -} -<% } %> From 136a49fd2797e39665f489059ce5662328b42113 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 17:23:48 -0400 Subject: [PATCH 120/255] Remove nginx-spark --- dist/setup.sh | 1 - .../generators/app/prompters/100_lightning.js | 2 +- .../app/templates/installer/docker/docker-compose.yaml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index e9d9a71..10ec07b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -385,7 +385,6 @@ install_docker() { copy_file $current_path/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED copy_file $current_path/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED copy_file $current_path/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED - copy_file $current_path/lightning/c-lightning/nginx-spark-conf $GATEKEEPER_DATAPATH/nginx-spark-conf 1 $SUDO_REQUIRED copy_file $current_path/traefik/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED diff --git a/install/generator-cyphernode/generators/app/prompters/100_lightning.js b/install/generator-cyphernode/generators/app/prompters/100_lightning.js index 438b5b4..222c472 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/100_lightning.js @@ -17,7 +17,7 @@ const featureCondition = function(props) { const templates = { 'lnd': [ path.join('lnd','lnd.conf') ], - 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf'), path.join('c-lightning','nginx-spark-conf') ] + 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf'), path.join('c-lightning','cookie') ] }; module.exports = { diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 1ccbbfe..5fcaddb 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -17,7 +17,6 @@ services: - "<%= gatekeeper_datapath %>/installation.json:/etc/nginx/conf.d/s/stats/installation.json" - "<%= gatekeeper_datapath %>/client.7z:/etc/nginx/conf.d/s/stats/client.7z" - "<%= gatekeeper_datapath %>/config.7z:/etc/nginx/conf.d/s/stats/config.7z" - - "<%= gatekeeper_datapath %>/nginx-spark-conf:/etc/nginx/conf.d/nginx-spark-conf" command: $USER # deploy: From e8d9ea5cb10599e1b768350741632d9428b5614d Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 18:01:16 -0400 Subject: [PATCH 121/255] cookie file cleaned --- .../generators/app/templates/installer/testfeatures.sh | 2 +- .../generators/app/templates/lightning/c-lightning/README.md | 5 +++++ .../generators/app/templates/lightning/c-lightning/cookie | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/lightning/c-lightning/README.md diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index a1f7f65..68eb103 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -124,7 +124,7 @@ checksparkwallet() { echo -en "\r\n\e[1;36mTesting Spark Wallet... " > /dev/console local rc - rc=$(curl -s -o /dev/null -w "%{http_code}" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/sparkwallet/) + rc=$(curl -s -o /dev/null -w "%{http_code}" http://sparkwallet:9737) [ "${rc}" -ne "401" ] && return 400 echo -e "\e[1;36mSpark Wallet rocks!" > /dev/console diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/README.md b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/README.md new file mode 100644 index 0000000..d2989c1 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/README.md @@ -0,0 +1,5 @@ +# How to create the hmac for the cookie file: + +``` +# echo -n "access-key" | openssl dgst -hmac "cyphernode:sparkwallet" -sha256 -binary | base64 | sed 's/[\+\W]//g' +``` diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie index 247ec65..830e6e6 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/cookie @@ -1,2 +1 @@ -# echo -n "access-key" | openssl dgst -hmac "cyphernode:sparkwallet" -sha256 -binary | base64 | sed 's/[\+\W]//g' -cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= +cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= \ No newline at end of file From 80164970f5cb1267e547943ea512e7ac345d7db1 Mon Sep 17 00:00:00 2001 From: leon-do Date: Thu, 4 Apr 2019 11:57:45 -0600 Subject: [PATCH 122/255] update docs to include /v0 --- doc/INSTALL-MANUAL-STEPS.md | 6 +++--- doc/INSTALL-MANUALLY.md | 6 +++--- doc/INSTALL.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index f5d2bb6..70c4dcb 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -142,9 +142,9 @@ sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f ## Test the deployment ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/ots_stamp ``` If you need the authorization header to copy/paste in another tool: diff --git a/doc/INSTALL-MANUALLY.md b/doc/INSTALL-MANUALLY.md index 475bf53..85b8f3e 100644 --- a/doc/INSTALL-MANUALLY.md +++ b/doc/INSTALL-MANUALLY.md @@ -124,9 +124,9 @@ pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode ## Test deployment from outside of the Swarm ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/ots_stamp ``` If you need the authorization header to copy/paste in another tool: diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 676593f..cbce0cc 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -51,9 +51,9 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech Directly using curl on command line, put your API ID (id=) and API key (k=) in the following commands: ```shell -id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbestblockhash -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/getbalance -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/ots_stamp +id="001";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbestblockhash +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/getbalance +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" -k https://127.0.0.1/v0/ots_stamp ``` ## Manually test your installation directly on the Proxy: From ef1196d672d6a8406ad65bc6b9d9f25f1f0682b4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 18:35:06 -0400 Subject: [PATCH 123/255] Changed the URLs at the end of the start script --- .../generators/app/templates/installer/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 60e429c..7882a26 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -46,7 +46,7 @@ fi printf "\r\n\033[0;92mDepending on your current location and DNS settings, point your favorite browser to one of the following URLs to access Cyphernode's status page:\r\n" printf "\r\n" -printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + ':'+ gatekeeper_port + '/status/\\r\\n') %><% }) %>\033[0m\r\n" +printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + '/welcome\\r\\n') %><% }) %>\033[0m\r\n" printf "\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n\r\n\033[0m" From 70ac3d15f84c4b6f63dc3949227513f905558b43 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 18:42:52 -0400 Subject: [PATCH 124/255] Docker build script --- docker-build.sh | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 docker-build.sh diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..aa25642 --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,125 @@ +#!/bin/sh + +# Must be logged to docker hub: +# docker login -u cyphernode + +image() { + local image=$1 + local dir=$2 + local arch=$3 + local dockerfile=${4:-"Dockerfile"} + + echo "Building and pushing $image from $dir for $arch using $dockerfile tagging as $v1, $v2 and $v3..." + + docker build -t cyphernode/${image}:${arch}-${v3} -t cyphernode/${image}:${arch}-${v2} -t cyphernode/${image}:${arch}-${v1} -f ${dir}/${dockerfile} ${dir}/. \ +&& docker push cyphernode/${image}:${arch}-${v3} \ +&& docker push cyphernode/${image}:${arch}-${v2} \ +&& docker push cyphernode/${image}:${arch}-${v1} + + return $? +} + +manifest() { + local image=$1 + + echo "Creating and pushing manifest for $image for version $v3..." + + docker manifest create cyphernode/${image}:${v3} cyphernode/${image}:${arm}-${v3} cyphernode/${image}:${x86}-${v3} \ +&& docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${arm}-${v3} --os linux --arch arm \ +&& docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${x86}-${v3} --os linux --arch amd64 \ +&& docker manifest push -p cyphernode/${image}:${v3} + + [ $? -ne 0 ] && return 1 + + echo "Creating and pushing manifest for $image for version $v2..." + + docker manifest create cyphernode/${image}:${v2} cyphernode/${image}:${arm}-${v2} cyphernode/${image}:${x86}-${v2} \ +&& docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${arm}-${v2} --os linux --arch arm \ +&& docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${x86}-${v2} --os linux --arch amd64 \ +&& docker manifest push -p cyphernode/${image}:${v2} + + [ $? -ne 0 ] && return 1 + + echo "Creating and pushing manifest for $image for version $v1..." + + docker manifest create cyphernode/${image}:${v1} cyphernode/${image}:${arm}-${v1} cyphernode/${image}:${x86}-${v1} \ +&& docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${arm}-${v1} --os linux --arch arm \ +&& docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${x86}-${v1} --os linux --arch amd64 \ +&& docker manifest push -p cyphernode/${image}:${v1} + + return $? + +} + +image_dockers() { + local image=$1 + local dir=$2 + local v=$3 + local arch=$4 + local dockerfile=${5:-"Dockerfile"} + + echo "Building and pushing $image from $dir for $arch using $dockerfile tagging as $v..." + + docker build -t cyphernode/${image}:${arch}-${v} -f ${dir}/${dockerfile} ${dir}/. \ +&& docker push cyphernode/${image}:${arch}-${v} + + return $? + +} + +manifest_dockers() { + local image=$1 + local v=$2 + + echo "Creating and pushing manifest for $image for version $v..." + + docker manifest create cyphernode/${image}:${v} cyphernode/${image}:${arm}-${v} cyphernode/${image}:${x86}-${v} \ +&& docker manifest annotate cyphernode/${image}:${v} cyphernode/${image}:${arm}-${v} --os linux --arch arm \ +&& docker manifest annotate cyphernode/${image}:${v} cyphernode/${image}:${x86}-${v} --os linux --arch amd64 \ +&& docker manifest push -p cyphernode/${image}:${v} + + return $? + +} + +x86="amd64" +arm="arm32v6" + +#arch=${arm} +arch=${x86} + +v1="v0-rc.1" +v2="v0.2-rc.1" +v3="v0.2.0-rc.1" + +echo "arch=$arch" + +image "gatekeeper" "api_auth_docker/" ${arch} \ +&& image "proxycron" "cron_docker/" ${arch} \ +&& image "otsclient" "otsclient_docker/" ${arch} \ +&& image "proxy" "proxy_docker/" ${arch} "Dockerfile.${arch}" \ +&& image "pycoin" "pycoin_docker/" ${arch} \ +&& image "cyphernodeconf" "install/" ${arch} + +[ $? -ne 0 ] && echo "Error" && return 1 + +[ "${arch}" = "${x86}" ] && echo "Built and pushed amd64 only" && return 0 + +manifest "gatekeeper" \ +&& manifest "proxycron" \ +&& manifest "otsclient" \ +&& manifest "proxy" \ +&& manifest "pycoin" \ +&& manifest "cyphernodeconf" + +[ $? -ne 0 ] && echo "Error" && return 1 + +image_dockers "clightning" "../dockers/c-lightning 0.6.2" ${arch} "Dockerfile.${arch}" \ +&& image_dockers "bitcoin" "../dockers/bitcoin-core 0.17.0" ${arch} "Dockerfile.${arch}" + +[ $? -ne 0 ] && echo "Error" && return 1 + +manifest_dockers "clightning" "0.6.2" \ +&& manifest_dockers "bitcoin" "0.17.0" + +[ $? -ne 0 ] && echo "Error" && return 1 From bfd949f4c86bcf5a20f2e4d8b979d5710e0bfdc1 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 18:56:11 -0400 Subject: [PATCH 125/255] app_welcome version and docker image --- dist/apps/welcome/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml index ff8e3ce..2cdcedc 100644 --- a/dist/apps/welcome/docker-compose.yaml +++ b/dist/apps/welcome/docker-compose.yaml @@ -4,7 +4,7 @@ services: cyphernode_welcome: environment: - "TRACING=1" - image: cyphernode_welcome + image: cyphernode/app_welcome:v0.2.0-rc.1 volumes: - "$GATEKEEPER_DATAPATH/certs/cert.pem:/data/cert.pem" - "$GATEKEEPER_DATAPATH/keys.properties:/data/keys.properties" From 9569e4d7c40fec19e6968c949dce54720bef577e Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 19:27:58 -0400 Subject: [PATCH 126/255] sparkwallet was the wrong version --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 10ec07b..230a341 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -711,7 +711,7 @@ OTSCLIENT_VERSION="v0.2.0-rc.1" PYCOIN_VERSION="v0.2.0-rc.1" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -SPARKWALLET_VERSION="v0.2.3" +SPARKWALLET_VERSION="v0.2.5" SETUP_DIR=$(dirname $(realpath $0)) From 03f106cf16890bdf2ed2c61fa5504b1c9856ee1c Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 20:49:50 -0400 Subject: [PATCH 127/255] Added app_welcome and sparkwallet to the docker build --- docker-build.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docker-build.sh b/docker-build.sh index aa25642..263063b 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -3,6 +3,9 @@ # Must be logged to docker hub: # docker login -u cyphernode +# Must enable experimental cli features +# "experimental": "enabled" in ~/.docker/config.json + image() { local image=$1 local dir=$2 @@ -114,12 +117,16 @@ manifest "gatekeeper" \ [ $? -ne 0 ] && echo "Error" && return 1 -image_dockers "clightning" "../dockers/c-lightning 0.6.2" ${arch} "Dockerfile.${arch}" \ -&& image_dockers "bitcoin" "../dockers/bitcoin-core 0.17.0" ${arch} "Dockerfile.${arch}" +image_dockers "clightning" "../dockers/c-lightning v0.7.0" ${arch} "Dockerfile.${arch}" \ +&& image_dockers "bitcoin" "../dockers/bitcoin-core v0.17.1" ${arch} "Dockerfile.${arch}" \ +&& image_dockers "app_welcome" "../cyphernode_welcome ${v3}" ${arch} \ +&& image_dockers "sparkwallet" "../spark-wallet v0.2.5" ${arch} "Dockerfile-cyphernode" [ $? -ne 0 ] && echo "Error" && return 1 -manifest_dockers "clightning" "0.6.2" \ -&& manifest_dockers "bitcoin" "0.17.0" +manifest_dockers "clightning" "v0.7.0" \ +&& manifest_dockers "bitcoin" "v0.17.1" \ +&& manifest_dockers "app_welcome" "${v3}" \ +&& manifest_dockers "sparkwallet" "v0.2.5" [ $? -ne 0 ] && echo "Error" && return 1 From 5dde035668608838651938c4c1473b75e61b48cc Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Apr 2019 21:33:58 -0400 Subject: [PATCH 128/255] shell condition --- dist/apps/welcome/start.sh | 4 ++-- dist/apps/welcome/stop.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/apps/welcome/start.sh b/dist/apps/welcome/start.sh index 6c6e768..058328e 100644 --- a/dist/apps/welcome/start.sh +++ b/dist/apps/welcome/start.sh @@ -9,8 +9,8 @@ export SHARED_HTPASSWD_PATH export GATEKEEPER_DATAPATH export APP_SCRIPT_PATH -if [ "$DOCKER_MODE" == "swarm" ]; then +if [ "$DOCKER_MODE" = "swarm" ]; then docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml cn_welcome -elif [ "$DOCKER_MODE" == "compose" ]; then +elif [ "$DOCKER_MODE" = "compose" ]; then docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans fi diff --git a/dist/apps/welcome/stop.sh b/dist/apps/welcome/stop.sh index db05c94..3edb586 100644 --- a/dist/apps/welcome/stop.sh +++ b/dist/apps/welcome/stop.sh @@ -5,8 +5,8 @@ echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" -if [ "$DOCKER_MODE" == "swarm" ]; then +if [ "$DOCKER_MODE" = "swarm" ]; then docker stack rm cn_welcome -elif [ "$DOCKER_MODE" == "compose" ]; then +elif [ "$DOCKER_MODE" = "compose" ]; then docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down fi From 8b334d7260545669dd76f1f3229ce41c4d050e0b Mon Sep 17 00:00:00 2001 From: Chris Moore Date: Fri, 5 Apr 2019 21:19:33 -0700 Subject: [PATCH 129/255] Replace Blocypher with BlockCypher --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6aaf0a3..9abc7e9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Modular Bitcoin full-node microservices API server architecture and utilities to # What is cyphernode? -Cyphernode is a Free open-source alternative to hosted services and commercial Bitcoin APIs such as Blockchain.info, Bitpay, Coinbase, Blocypher, Bitgo, etc. You can use it to build Bitcoin services and applications using your own Bitcoin and Lightning Network full nodes. It is a substitute for the Bitcore and Insight software projects. +Cyphernode is a Free open-source alternative to hosted services and commercial Bitcoin APIs such as Blockchain.info, Bitpay, Coinbase, BlockCypher, Bitgo, etc. You can use it to build Bitcoin services and applications using your own Bitcoin and Lightning Network full nodes. It is a substitute for the Bitcore and Insight software projects. It implements a self-hosted API which allows you to spawn and call your encrypted overlay network of dockerized Bitcoin and crypto software projects (virtual machines). The Docker containers used in this project are hosted at www.bitcoindockers.com. From 729bef034a9387cdcd8ba6e86caa8ecaf17a1374 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 13:45:36 +0200 Subject: [PATCH 130/255] Some cleanup in welcome app starting. --- dist/apps/welcome/start.sh | 11 ++++------- dist/apps/welcome/stop.sh | 12 ++++++------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/dist/apps/welcome/start.sh b/dist/apps/welcome/start.sh index 058328e..76a6fad 100644 --- a/dist/apps/welcome/start.sh +++ b/dist/apps/welcome/start.sh @@ -1,16 +1,13 @@ - -echo "SCRIPT_NAME: $SCRIPT_NAME" -echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" -echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" -echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" -echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" +# APP_SCRIPT_PATH +# APP_START_SCRIPT_PATH +# APP_ID export SHARED_HTPASSWD_PATH export GATEKEEPER_DATAPATH export APP_SCRIPT_PATH if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml cn_welcome + docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID elif [ "$DOCKER_MODE" = "compose" ]; then docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans fi diff --git a/dist/apps/welcome/stop.sh b/dist/apps/welcome/stop.sh index 3edb586..a5a3b6b 100644 --- a/dist/apps/welcome/stop.sh +++ b/dist/apps/welcome/stop.sh @@ -1,12 +1,12 @@ -echo "SCRIPT_NAME: $SCRIPT_NAME" -echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" -echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" -echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" -echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" +#echo "SCRIPT_NAME: $SCRIPT_NAME" +#echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" +#echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" +#echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" +#echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack rm cn_welcome + docker stack rm $APP_ID elif [ "$DOCKER_MODE" = "compose" ]; then docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down fi From c16fd9304dfcf0f0142ca326aaf25f692870a2b2 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 13:45:48 +0200 Subject: [PATCH 131/255] added tests for welcome app --- dist/apps/welcome/test.sh | 41 ++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/dist/apps/welcome/test.sh b/dist/apps/welcome/test.sh index 1a5563f..0f10641 100644 --- a/dist/apps/welcome/test.sh +++ b/dist/apps/welcome/test.sh @@ -1,7 +1,38 @@ +#!/bin/bash -echo "SCRIPT_NAME: $SCRIPT_NAME" -echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" -echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" -echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" +timeout_feature() { + local interval=10 + local totaltime=60 + local testwhat=${1} + local returncode + local endtime=$(($(date +%s) + ${totaltime})) -echo "No tests" \ No newline at end of file + while : + do + eval ${testwhat} + returncode=$? + + # If no error or 2 minutes passed, we get out of this loop + ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break + + printf "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left).\e[1;0m" + + sleep ${interval} + done + + return ${returncode} +} + +do_test() { + local rc + rc=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost/welcome) + [ "${rc}" -ne "401" ] && return 400 + return 0 +} + +timeout_feature do_test +returncode=$? + +# return 0: tests cool +# return 1: tests failed +return $returncode From d12467eca8c1beb7d3b9c31da201a0a9b7b79256 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 13:46:37 +0200 Subject: [PATCH 132/255] Moved start_apps to right place --- .../app/templates/installer/start.sh | 96 +++++++++++++------ 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 7882a26..55b60b6 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -1,5 +1,63 @@ #!/bin/sh +. ./installer/config.sh + +# be aware that randomly downloaded cyphernode apps will have access to +# your configuration and filesystem. +# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! +# TODO: Test if we can mitigate this security issue by +# running app dockers inside a docker container + +start_apps() { + local SCRIPT_NAME="start.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + + for i in $current_path/apps/* + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d $APP_SCRIPT_PATH ]; then + APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + + if [ -f $APP_START_SCRIPT_PATH ]; then + APP_ID=$(basename $APP_SCRIPT_PATH) + . $APP_START_SCRIPT_PATH + fi + fi + done +} + +test_apps() { + local SCRIPT_NAME="test.sh" + local APP_SCRIPT_PATH + local APP_START_SCRIPT_PATH + local APP_ID + local returncode=0 + + for i in $current_path/apps/* + do + APP_SCRIPT_PATH=$(echo $i) + if [ -d $APP_SCRIPT_PATH ]; then + APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + + if [ -f $APP_START_SCRIPT_PATH ]; then + APP_ID=$(basename $APP_SCRIPT_PATH) + printf "\r\n\e[1;36mTesting $APP_ID... \e[1;0m" + . $APP_START_SCRIPT_PATH + local rc=$? + + if [ ""$rc -eq "0" ]; then + printf "\e[1;36m$APP_ID rocks!\e[1;0m" + fi + returncode=$(($rc | ${returncode})) + echo "" + fi + fi + done + return $returncode +} + <% if (run_as_different_user) { %> OS=$(uname -s) if [ "$OS" = "Darwin" ]; then @@ -21,6 +79,8 @@ docker stack deploy -c $current_path/docker-compose.yaml cyphernode docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans <% } %> +start_apps + arch=$(uname -m) case "${arch}" in arm*) printf "\r\n\033[1;31mSince we're on a slow RPi, let's give Docker 60 more seconds before performing our tests...\033[0m\r\n\r\n" @@ -39,6 +99,12 @@ if [ -f $current_path/exitStatus.sh ]; then rm -f $current_path/exitStatus.sh fi +test_apps + +EXIT_STATUS=$(($? | ${EXIT_STATUS})) + +printf "\r\n\e[1;32mTests finished.\e[0m\n" + if [ "$EXIT_STATUS" -ne "0" ]; then printf "\r\n\033[1;31mThere was an error during cyphernode installation. Please see Docker's logs for more information. Run ./stop.sh to stop cyphernode.\r\n\r\n\033[0m" exit 1 @@ -48,33 +114,3 @@ printf "\r\n\033[0;92mDepending on your current location and DNS settings, point printf "\r\n" printf "\033[0;95m<% cns.forEach(cn => { %><%= ('https://' + cn + '/welcome\\r\\n') %><% }) %>\033[0m\r\n" printf "\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n\r\n\033[0m" - - -# be aware that randomly downloaded cyphernode apps will have access to -# your configuration and filesystem. -# !!!!!!!!! DO NOT INCLUDE APPS WITHOUT REVIEW !!!!!!!!!! -# TODO: Test if we can mitigate this security issue by -# running app dockers inside a docker container - -start_apps() { - local SCRIPT_NAME="start.sh" - local APP_SCRIPT_PATH - local APP_START_SCRIPT_PATH - local APP_ID - - for i in "$current_path/apps/*" - do - APP_SCRIPT_PATH=$(echo $i) - if [ -d $APP_SCRIPT_PATH ]; then - APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" - - if [ -f $APP_START_SCRIPT_PATH ]; then - APP_ID=$(basename $APP_SCRIPT_PATH) - . $APP_START_SCRIPT_PATH - fi - fi - done -} - -. ./installer/config.sh -start_apps From 9a2d64c74e21fdb31a481a38e9da97b56c654cab Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 13:46:52 +0200 Subject: [PATCH 133/255] moved stop_apps to correct place --- .../app/templates/installer/stop.sh | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index 0c7e6d1..e9bbf26 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -2,15 +2,6 @@ current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" -<% if (docker_mode == 'swarm') { %> -export USER=$(id -u):$(id -g) -export ARCH=$(uname -m) -docker stack rm cyphernode -<% } else if(docker_mode == 'compose') { %> -export USER=$(id -u):$(id -g) -export ARCH=$(uname -m) -docker-compose -f $current_path/docker-compose.yaml down -<% } %> # be aware that randomly downloaded cyphernode apps will have access to # your configuration and filesystem. @@ -24,7 +15,7 @@ stop_apps() { local APP_START_SCRIPT_PATH local APP_ID - for i in "$current_path/apps/*" + for i in $current_path/apps/* do APP_SCRIPT_PATH=$(echo $i) if [ -d $APP_SCRIPT_PATH ]; then @@ -39,4 +30,14 @@ stop_apps() { } . ./installer/config.sh -stop_apps \ No newline at end of file +stop_apps + +<% if (docker_mode == 'swarm') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker stack rm cyphernode +<% } else if(docker_mode == 'compose') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker-compose -f $current_path/docker-compose.yaml down +<% } %> From fbb5b8a647cdb139353f365cb5d38ebb4fa04ba8 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 13:47:58 +0200 Subject: [PATCH 134/255] Move spark wallet to app zone --- dist/apps/sparkwallet/docker-compose.yaml | 27 +++++++++++++ dist/apps/sparkwallet/start.sh | 10 +++++ dist/apps/sparkwallet/stop.sh | 10 +++++ dist/apps/sparkwallet/test.sh | 38 +++++++++++++++++++ .../installer/docker/docker-compose.yaml | 19 ---------- .../app/templates/installer/testfeatures.sh | 34 ++--------------- 6 files changed, 88 insertions(+), 50 deletions(-) create mode 100644 dist/apps/sparkwallet/docker-compose.yaml create mode 100644 dist/apps/sparkwallet/start.sh create mode 100644 dist/apps/sparkwallet/stop.sh create mode 100644 dist/apps/sparkwallet/test.sh diff --git a/dist/apps/sparkwallet/docker-compose.yaml b/dist/apps/sparkwallet/docker-compose.yaml new file mode 100644 index 0000000..35e8979 --- /dev/null +++ b/dist/apps/sparkwallet/docker-compose.yaml @@ -0,0 +1,27 @@ +version: "3" + +services: + cyphernode_sparkwallet: + command: --no-tls + image: cyphernode/sparkwallet:v0.2.3-local + volumes: + - "$LIGHTNING_DATAPATH/:/etc/lightning" + - "$LIGHTNING_DATAPATH/sparkwallet:/data" + - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" + labels: + - "traefik.docker.network=cyphernodeappsnet" + - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" + - "traefik.frontend.passHostHeader=true" + - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" + - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" + - "traefik.enable=true" + - "traefik.port=9737" + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always +networks: + cyphernodeappsnet: + external: true + cyphernodenet: + external: true \ No newline at end of file diff --git a/dist/apps/sparkwallet/start.sh b/dist/apps/sparkwallet/start.sh new file mode 100644 index 0000000..ab2bc70 --- /dev/null +++ b/dist/apps/sparkwallet/start.sh @@ -0,0 +1,10 @@ +export SHARED_HTPASSWD_PATH +export GATEKEEPER_DATAPATH +export LIGHTNING_DATAPATH +export APP_SCRIPT_PATH + +if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID +elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans +fi diff --git a/dist/apps/sparkwallet/stop.sh b/dist/apps/sparkwallet/stop.sh new file mode 100644 index 0000000..378ff39 --- /dev/null +++ b/dist/apps/sparkwallet/stop.sh @@ -0,0 +1,10 @@ +export SHARED_HTPASSWD_PATH +export GATEKEEPER_DATAPATH +export LIGHTNING_DATAPATH +export APP_SCRIPT_PATH + +if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack rm $APP_ID +elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down +fi diff --git a/dist/apps/sparkwallet/test.sh b/dist/apps/sparkwallet/test.sh new file mode 100644 index 0000000..8c8a142 --- /dev/null +++ b/dist/apps/sparkwallet/test.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +timeout_feature() { + local interval=10 + local totaltime=60 + local testwhat=${1} + local returncode + local endtime=$(($(date +%s) + ${totaltime})) + + while : + do + eval ${testwhat} + returncode=$? + + # If no error or 2 minutes passed, we get out of this loop + ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break + + printf "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left).\e[1;0m" + + sleep ${interval} + done + + return ${returncode} +} + +do_test() { + local rc + rc=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost/sparkwallet) + [ "${rc}" -ne "401" ] && return 400 + return 0 +} + +timeout_feature do_test +returncode=$? + +# return 0: tests cool +# return 1: tests failed +return $returncode diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 5fcaddb..6776c45 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -134,25 +134,6 @@ services: - cyphernodenet restart: always - sparkwallet: - command: --no-tls - image: cyphernode/sparkwallet:<%= sparkwallet_version %> - volumes: - - "<%= lightning_datapath %>:/etc/lightning" - - "<%= lightning_datapath %>/sparkwallet:/data" - - "<%= traefik_datapath%>/htpasswd:/htpasswd/htpasswd" - labels: - - "traefik.docker.network=cyphernodeappsnet" - - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" - - "traefik.frontend.passHostHeader=true" - - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" - - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" - - "traefik.enable=true" - - "traefik.port=9737" - networks: - - cyphernodenet - - cyphernodeappsnet - restart: always <% } %> <% if ( features.indexOf('otsclient') !== -1 ) { %> otsclient: diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 68eb103..31f0af6 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -120,18 +120,6 @@ checklnnode() { return 0 } -checksparkwallet() { - echo -en "\r\n\e[1;36mTesting Spark Wallet... " > /dev/console - local rc - - rc=$(curl -s -o /dev/null -w "%{http_code}" http://sparkwallet:9737) - [ "${rc}" -ne "401" ] && return 400 - - echo -e "\e[1;36mSpark Wallet rocks!" > /dev/console - - return 0 -} - checkservice() { local interval=10 local totaltime=120 @@ -145,12 +133,12 @@ checkservice() { while : do outcome=0 - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning sparkwallet ':'' %>; do + for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do echo -e " \e[0;32mVerifying \e[0;33m${container}\e[0;32m..." > /dev/console (ping -c 10 ${container} 2> /dev/null | grep "0% packet loss" > /dev/null) & eval ${container}=$! done - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning sparkwallet ':'' %>; do + for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do eval wait '$'${container} ; returncode=$? ; outcome=$((${outcome} + ${returncode})) eval c_${container}=${returncode} done @@ -171,9 +159,8 @@ checkservice() { # { "name": "otsclient", "active":true }, # { "name": "bitcoin", "active":true }, # { "name": "lightning", "active":true }, - # { "name": "sparkwallet", "active":true } # ] - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning sparkwallet ':'' %>; do + for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do [ -n "${result}" ] && result="${result}," result="${result}{\"name\":\"${container}\",\"active\":" eval "returncode=\$c_${container}" @@ -232,7 +219,6 @@ feature_status() { # { "name": "otsclient", "active":true }, # { "name": "bitcoin", "active":true }, # { "name": "lightning", "active":true }, -# { "name": "sparkwallet", "active":true } # ], # "features": [ # { "name": "gatekeeper", "working":true }, @@ -240,7 +226,6 @@ feature_status() { # { "name": "otsclient", "working":true }, # { "name": "bitcoin", "working":true }, # { "name": "lightning", "working":true }, -# { "name": "sparkwallet", "working":true } # ] #} @@ -270,7 +255,6 @@ fi # { "name": "otsclient", "working":true }, # { "name": "bitcoin", "working":true }, # { "name": "lightning", "working":true }, -# { "name": "sparkwallet", "working":true } # ] result="${containers},\"features\":[{\"coreFeature\":true, \"name\":\"cyphernode proxy\",\"working\":true}, {\"coreFeature\":true, \"name\":\"gatekeeper\",\"working\":" @@ -331,22 +315,10 @@ fi finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Lightning error!')}" -result="${result},{\"name\":\"sparkwallet\",\"working\":" -status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"sparkwallet\") | .active") -if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then - timeout_feature checksparkwallet - returncode=$? -else - returncode=1 -fi -finalreturncode=$((${returncode} | ${finalreturncode})) -result="${result}$(feature_status ${returncode} 'Spark Wallet error!')}" <% } %> result="{${result}]}" echo "${result}" > /gatekeeper/installation.json -echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console - echo "EXIT_STATUS=${finalreturncode}" > /dist/exitStatus.sh From 7bb05a6c339774d260e22943241dd54ce0d8c97e Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 16:51:06 +0200 Subject: [PATCH 135/255] removed all traces of spark wallet from core features --- dist/apps/sparkwallet/cookie | 1 + dist/apps/sparkwallet/docker-compose.yaml | 2 +- .../generators/app/prompters/100_lightning.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 dist/apps/sparkwallet/cookie diff --git a/dist/apps/sparkwallet/cookie b/dist/apps/sparkwallet/cookie new file mode 100644 index 0000000..c9558e6 --- /dev/null +++ b/dist/apps/sparkwallet/cookie @@ -0,0 +1 @@ +cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= diff --git a/dist/apps/sparkwallet/docker-compose.yaml b/dist/apps/sparkwallet/docker-compose.yaml index 35e8979..abfa627 100644 --- a/dist/apps/sparkwallet/docker-compose.yaml +++ b/dist/apps/sparkwallet/docker-compose.yaml @@ -6,7 +6,7 @@ services: image: cyphernode/sparkwallet:v0.2.3-local volumes: - "$LIGHTNING_DATAPATH/:/etc/lightning" - - "$LIGHTNING_DATAPATH/sparkwallet:/data" + - "$APP_SCRIPT_PATH/cookie:/data/spark/cookie" - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" labels: - "traefik.docker.network=cyphernodeappsnet" diff --git a/install/generator-cyphernode/generators/app/prompters/100_lightning.js b/install/generator-cyphernode/generators/app/prompters/100_lightning.js index 222c472..fb7becb 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/100_lightning.js @@ -17,7 +17,7 @@ const featureCondition = function(props) { const templates = { 'lnd': [ path.join('lnd','lnd.conf') ], - 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf'), path.join('c-lightning','cookie') ] + 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf') ] }; module.exports = { From fb1be7df8a34f375ecea8bbfaac8eeab1c703d59 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 17:13:17 +0200 Subject: [PATCH 136/255] Removed last traces of spark wallet from setup.sh --- dist/setup.sh | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 230a341..51f0e30 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -194,7 +194,6 @@ configure() { -e PYCOIN_VERSION=$PYCOIN_VERSION \ -e BITCOIN_VERSION=$BITCOIN_VERSION \ -e LIGHTNING_VERSION=$LIGHTNING_VERSION \ - -e SPARKWALLET_VERSION=$SPARKWALLET_VERSION \ --log-driver=none$pw_env \ --network none \ --rm$interactive cyphernode/cyphernodeconf:$CONF_VERSION $user yo --no-insight cyphernode$gen_options $recreate @@ -454,15 +453,9 @@ install_docker() { if [[ $archpath == "rpi" ]]; then dockerfile="Dockerfile-alpine" fi - if [ ! -d $LIGHTNING_DATAPATH/sparkwallet ]; then - step " create $LIGHTNING_DATAPATH" - sudo_if_required mkdir -p $LIGHTNING_DATAPATH/sparkwallet/spark - next - fi if [ -d $LIGHTNING_DATAPATH ]; then copy_file $current_path/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED copy_file $current_path/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED - copy_file $current_path/lightning/c-lightning/cookie $LIGHTNING_DATAPATH/sparkwallet/spark/cookie 1 $SUDO_REQUIRED fi fi fi @@ -711,7 +704,6 @@ OTSCLIENT_VERSION="v0.2.0-rc.1" PYCOIN_VERSION="v0.2.0-rc.1" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -SPARKWALLET_VERSION="v0.2.5" SETUP_DIR=$(dirname $(realpath $0)) @@ -767,7 +759,6 @@ if [[ $nbbuiltimgs -gt 1 ]]; then PROXYCRON_VERSION="$PROXYCRON_VERSION-local" OTSCLIENT_VERSION="$OTSCLIENT_VERSION-local" PYCOIN_VERSION="$PYCOIN_VERSION-local" - SPARKWALLET_VERSION="$SPARKWALLET_VERSION-local" fi fi From 37ceb4703fbea559415d1ce79fe18b00b531a598 Mon Sep 17 00:00:00 2001 From: SKP Date: Sun, 7 Apr 2019 17:18:42 +0200 Subject: [PATCH 137/255] upgrade to spark-wallet 0.2.5 --- dist/apps/sparkwallet/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/apps/sparkwallet/docker-compose.yaml b/dist/apps/sparkwallet/docker-compose.yaml index abfa627..af35289 100644 --- a/dist/apps/sparkwallet/docker-compose.yaml +++ b/dist/apps/sparkwallet/docker-compose.yaml @@ -3,7 +3,7 @@ version: "3" services: cyphernode_sparkwallet: command: --no-tls - image: cyphernode/sparkwallet:v0.2.3-local + image: cyphernode/sparkwallet:v0.2.5 volumes: - "$LIGHTNING_DATAPATH/:/etc/lightning" - "$APP_SCRIPT_PATH/cookie:/data/spark/cookie" From 57c3abb6907131e0f06812cffe6dd17a694700f1 Mon Sep 17 00:00:00 2001 From: SKP Date: Mon, 8 Apr 2019 19:26:27 +0200 Subject: [PATCH 138/255] Renamed swagger to opens-i --- doc/{swagger => openapi}/openapi-generator-cli.sh | 0 doc/{swagger => openapi}/v0/cyphernode-api.yaml | 0 doc/{swagger => openapi}/v0/cyphernode-callbacks.yaml | 0 doc/{swagger => openapi}/v0/cyphernode-internal.yaml | 0 doc/{swagger => openapi}/v1/cyphernode-api.yaml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename doc/{swagger => openapi}/openapi-generator-cli.sh (100%) rename doc/{swagger => openapi}/v0/cyphernode-api.yaml (100%) rename doc/{swagger => openapi}/v0/cyphernode-callbacks.yaml (100%) rename doc/{swagger => openapi}/v0/cyphernode-internal.yaml (100%) rename doc/{swagger => openapi}/v1/cyphernode-api.yaml (100%) diff --git a/doc/swagger/openapi-generator-cli.sh b/doc/openapi/openapi-generator-cli.sh similarity index 100% rename from doc/swagger/openapi-generator-cli.sh rename to doc/openapi/openapi-generator-cli.sh diff --git a/doc/swagger/v0/cyphernode-api.yaml b/doc/openapi/v0/cyphernode-api.yaml similarity index 100% rename from doc/swagger/v0/cyphernode-api.yaml rename to doc/openapi/v0/cyphernode-api.yaml diff --git a/doc/swagger/v0/cyphernode-callbacks.yaml b/doc/openapi/v0/cyphernode-callbacks.yaml similarity index 100% rename from doc/swagger/v0/cyphernode-callbacks.yaml rename to doc/openapi/v0/cyphernode-callbacks.yaml diff --git a/doc/swagger/v0/cyphernode-internal.yaml b/doc/openapi/v0/cyphernode-internal.yaml similarity index 100% rename from doc/swagger/v0/cyphernode-internal.yaml rename to doc/openapi/v0/cyphernode-internal.yaml diff --git a/doc/swagger/v1/cyphernode-api.yaml b/doc/openapi/v1/cyphernode-api.yaml similarity index 100% rename from doc/swagger/v1/cyphernode-api.yaml rename to doc/openapi/v1/cyphernode-api.yaml From 829c33cfac5e4f2ef64b5bfe41106280c18f2448 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 8 Apr 2019 13:58:04 -0400 Subject: [PATCH 139/255] rc.2 --- build.sh | 14 +++++++------- dist/setup.sh | 12 ++++++------ docker-build.sh | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.sh b/build.sh index 54312f3..38c562c 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.1-local" -GATEKEEPER_VERSION="v0.2.0-rc.1-local" -PROXY_VERSION="v0.2.0-rc.1-local" -PROXYCRON_VERSION="v0.2.0-rc.1-local" -OTSCLIENT_VERSION="v0.2.0-rc.1-local" -PYCOIN_VERSION="v0.2.0-rc.1-local" +CONF_VERSION="v0.2.0-rc.2-local" +GATEKEEPER_VERSION="v0.2.0-rc.2-local" +PROXY_VERSION="v0.2.0-rc.2-local" +PROXYCRON_VERSION="v0.2.0-rc.2-local" +OTSCLIENT_VERSION="v0.2.0-rc.2-local" +PYCOIN_VERSION="v0.2.0-rc.2-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.1-local" +GRAFANA_VERSION="v0.2.0-rc.2-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index 51f0e30..c277130 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -696,12 +696,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.1" -GATEKEEPER_VERSION="v0.2.0-rc.1" -PROXY_VERSION="v0.2.0-rc.1" -PROXYCRON_VERSION="v0.2.0-rc.1" -OTSCLIENT_VERSION="v0.2.0-rc.1" -PYCOIN_VERSION="v0.2.0-rc.1" +CONF_VERSION="v0.2.0-rc.2" +GATEKEEPER_VERSION="v0.2.0-rc.2" +PROXY_VERSION="v0.2.0-rc.2" +PROXYCRON_VERSION="v0.2.0-rc.2" +OTSCLIENT_VERSION="v0.2.0-rc.2" +PYCOIN_VERSION="v0.2.0-rc.2" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" diff --git a/docker-build.sh b/docker-build.sh index 263063b..604aa23 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.1" -v2="v0.2-rc.1" -v3="v0.2.0-rc.1" +v1="v0-rc.2" +v2="v0.2-rc.2" +v3="v0.2.0-rc.2" echo "arch=$arch" From 49f0200091edc93994888694196aa444558b319c Mon Sep 17 00:00:00 2001 From: SKP Date: Mon, 8 Apr 2019 20:04:57 +0200 Subject: [PATCH 140/255] added getblockchaininfo endpoint to v0 openapi doc --- doc/openapi/v0/cyphernode-api.yaml | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/doc/openapi/v0/cyphernode-api.yaml b/doc/openapi/v0/cyphernode-api.yaml index bc01874..e304de9 100644 --- a/doc/openapi/v0/cyphernode-api.yaml +++ b/doc/openapi/v0/cyphernode-api.yaml @@ -177,6 +177,31 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' + /getblockchaininfo: + get: + tags: + - "stats" + - "core features" + summary: "Show blockchain info" + description: "Returns detailed blockchain information." + operationId: "getBlockchainInfo" + responses: + '200': + description: "successful operation" + content: + application/json: + schema: + $ref: '#/components/schemas/BlockchainInfo' + '401': + $ref: '#/components/schemas/ApiResponseNotAllowed' + '404': + $ref: '#/components/schemas/ApiResponseNotFound' + '503': + description: "Resource temporarily unavailable" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseTemporarilyUnavailable' /getblockinfo/{blockHash}: get: parameters: @@ -858,6 +883,42 @@ components: type: "integer" blocktime: type: "integer" + BlockchainInfo: + type: "object" + properties: + chain: + type: "string" + enum: ["test", "main"] + blocks: + type: "integer" + headers: + type: "integer" + bestblockhash: + $ref: '#/components/schemas/TypeHashString' + difficulty: + type: "number" + mediantime: + type: "integer" + verificationprogress: + type: "number" + initialblockdownload: + type: "boolean" + chainwork: + $ref: '#/components/schemas/TypeHashString' + size_on_disk: + type: "integer" + pruned: + type: "boolean" + warnings: + type: "string" + softforks: + type: "array" + items: + $ref: '#/components/schemas/TypeSoftFork' + bip9_softforks: + type: "object" + additionalProperties: + $ref: '#/components/schemas/TypeBip9SoftFork' Input: type: "object" properties: @@ -910,6 +971,29 @@ components: description: "8 character hex string" type: "string" pattern: "^([a-fA-F0-9][a-fA-F0-9]){1,4}$" + TypeSoftFork: + type: "object" + properties: + id: + type: "string" + version: + type: "integer" + reject: + type: "object" + properties: + status: + type: "boolean" + TypeBip9SoftFork: + type: "object" + properties: + status: + type: "string" + startTime: + type: "integer" + timeout: + type: "integer" + since: + type: "integer" ApiResponseTemporarilyUnavailable: type: "object" properties: From b2aee0a326b201d90f4208aea584bce2828fb02b Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 8 Apr 2019 14:37:45 -0400 Subject: [PATCH 141/255] Fixed docker builder --- docker-build.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-build.sh b/docker-build.sh index 604aa23..9b1b510 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -119,14 +119,18 @@ manifest "gatekeeper" \ image_dockers "clightning" "../dockers/c-lightning v0.7.0" ${arch} "Dockerfile.${arch}" \ && image_dockers "bitcoin" "../dockers/bitcoin-core v0.17.1" ${arch} "Dockerfile.${arch}" \ -&& image_dockers "app_welcome" "../cyphernode_welcome ${v3}" ${arch} \ -&& image_dockers "sparkwallet" "../spark-wallet v0.2.5" ${arch} "Dockerfile-cyphernode" +&& image_dockers "app_welcome" "../cyphernode_welcome" "${v3}" ${arch} \ +&& image_dockers "app_welcome" "../cyphernode_welcome" "${v2}" ${arch} \ +&& image_dockers "app_welcome" "../cyphernode_welcome" "${v1}" ${arch} \ +&& image_dockers "sparkwallet" "../spark-wallet" "v0.2.5" ${arch} "Dockerfile-cyphernode" [ $? -ne 0 ] && echo "Error" && return 1 manifest_dockers "clightning" "v0.7.0" \ && manifest_dockers "bitcoin" "v0.17.1" \ && manifest_dockers "app_welcome" "${v3}" \ +&& manifest_dockers "app_welcome" "${v2}" \ +&& manifest_dockers "app_welcome" "${v1}" \ && manifest_dockers "sparkwallet" "v0.2.5" [ $? -ne 0 ] && echo "Error" && return 1 From dd3740ee0778621f402197eb25e4dc947cc32124 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 8 Apr 2019 15:26:22 -0400 Subject: [PATCH 142/255] v0.2.0-rc.2 for welcome app --- dist/apps/welcome/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml index 2cdcedc..ab22517 100644 --- a/dist/apps/welcome/docker-compose.yaml +++ b/dist/apps/welcome/docker-compose.yaml @@ -4,7 +4,7 @@ services: cyphernode_welcome: environment: - "TRACING=1" - image: cyphernode/app_welcome:v0.2.0-rc.1 + image: cyphernode/app_welcome:v0.2.0-rc.2 volumes: - "$GATEKEEPER_DATAPATH/certs/cert.pem:/data/cert.pem" - "$GATEKEEPER_DATAPATH/keys.properties:/data/keys.properties" From a361fb20212944a2276fc921ef2395f664e02dc7 Mon Sep 17 00:00:00 2001 From: SKP Date: Mon, 8 Apr 2019 23:28:49 +0200 Subject: [PATCH 143/255] Added lightning directory creation --- dist/setup.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index c277130..0be8a75 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -453,10 +453,16 @@ install_docker() { if [[ $archpath == "rpi" ]]; then dockerfile="Dockerfile-alpine" fi - if [ -d $LIGHTNING_DATAPATH ]; then - copy_file $current_path/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED - copy_file $current_path/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + + if [ ! -d $LIGHTNING_DATAPATH ]; then + step " create $LIGHTNING_DATAPATH" + sudo_if_required mkdir -p $LIGHTNING_DATAPATH + next fi + + copy_file $current_path/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED + copy_file $current_path/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + fi fi From 347baa4cb9570157cb0621a3f2f94f4343d01bb7 Mon Sep 17 00:00:00 2001 From: SKP Date: Tue, 9 Apr 2019 17:57:09 +0200 Subject: [PATCH 144/255] Added generic start/stop for apps with only a docker-compose.yaml. Added "ignoreThisApp" file option for disabling an app --- dist/apps/sparkwallet/start.sh | 10 ------- dist/apps/sparkwallet/stop.sh | 10 ------- dist/apps/welcome/start.sh | 13 --------- dist/apps/welcome/stop.sh | 12 -------- .../app/templates/installer/start.sh | 28 ++++++++++++++----- .../app/templates/installer/stop.sh | 23 ++++++++++++--- 6 files changed, 40 insertions(+), 56 deletions(-) delete mode 100644 dist/apps/sparkwallet/start.sh delete mode 100644 dist/apps/sparkwallet/stop.sh delete mode 100644 dist/apps/welcome/start.sh delete mode 100644 dist/apps/welcome/stop.sh diff --git a/dist/apps/sparkwallet/start.sh b/dist/apps/sparkwallet/start.sh deleted file mode 100644 index ab2bc70..0000000 --- a/dist/apps/sparkwallet/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -export SHARED_HTPASSWD_PATH -export GATEKEEPER_DATAPATH -export LIGHTNING_DATAPATH -export APP_SCRIPT_PATH - -if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID -elif [ "$DOCKER_MODE" = "compose" ]; then - docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans -fi diff --git a/dist/apps/sparkwallet/stop.sh b/dist/apps/sparkwallet/stop.sh deleted file mode 100644 index 378ff39..0000000 --- a/dist/apps/sparkwallet/stop.sh +++ /dev/null @@ -1,10 +0,0 @@ -export SHARED_HTPASSWD_PATH -export GATEKEEPER_DATAPATH -export LIGHTNING_DATAPATH -export APP_SCRIPT_PATH - -if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack rm $APP_ID -elif [ "$DOCKER_MODE" = "compose" ]; then - docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down -fi diff --git a/dist/apps/welcome/start.sh b/dist/apps/welcome/start.sh deleted file mode 100644 index 76a6fad..0000000 --- a/dist/apps/welcome/start.sh +++ /dev/null @@ -1,13 +0,0 @@ -# APP_SCRIPT_PATH -# APP_START_SCRIPT_PATH -# APP_ID - -export SHARED_HTPASSWD_PATH -export GATEKEEPER_DATAPATH -export APP_SCRIPT_PATH - -if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID -elif [ "$DOCKER_MODE" = "compose" ]; then - docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans -fi diff --git a/dist/apps/welcome/stop.sh b/dist/apps/welcome/stop.sh deleted file mode 100644 index a5a3b6b..0000000 --- a/dist/apps/welcome/stop.sh +++ /dev/null @@ -1,12 +0,0 @@ - -#echo "SCRIPT_NAME: $SCRIPT_NAME" -#echo "SHARED_HTPASSWD_PATH: $SHARED_HTPASSWD_PATH" -#echo "APP_SCRIPT_PATH: $APP_SCRIPT_PATH" -#echo "APP_START_SCRIPT_PATH: $APP_START_SCRIPT_PATH" -#echo "GATEKEEPER_DATAPATH: $GATEKEEPER_DATAPATH" - -if [ "$DOCKER_MODE" = "swarm" ]; then - docker stack rm $APP_ID -elif [ "$DOCKER_MODE" = "compose" ]; then - docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down -fi diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 55b60b6..5ac9c51 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -17,12 +17,26 @@ start_apps() { for i in $current_path/apps/* do APP_SCRIPT_PATH=$(echo $i) - if [ -d $APP_SCRIPT_PATH ]; then + if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" - if [ -f $APP_START_SCRIPT_PATH ]; then + if [ -f "$APP_START_SCRIPT_PATH" ]; then APP_ID=$(basename $APP_SCRIPT_PATH) . $APP_START_SCRIPT_PATH + elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then + export SHARED_HTPASSWD_PATH + export GATEKEEPER_DATAPATH + export LIGHTNING_DATAPATH + export BITCOIN_DATAPATH + export APP_SCRIPT_PATH + export APP_ID + export DOCKER_MODE + + if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack deploy -c $APP_SCRIPT_PATH/docker-compose.yaml $APP_ID + elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml up -d --remove-orphans + fi fi fi done @@ -38,13 +52,13 @@ test_apps() { for i in $current_path/apps/* do APP_SCRIPT_PATH=$(echo $i) - if [ -d $APP_SCRIPT_PATH ]; then - APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + if [ -d "$APP_SCRIPT_PATH" ]; then + APP_TEST_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" - if [ -f $APP_START_SCRIPT_PATH ]; then - APP_ID=$(basename $APP_SCRIPT_PATH) + if [ -f "$APP_TEST_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then + APP_ID=$(basename "$APP_SCRIPT_PATH") printf "\r\n\e[1;36mTesting $APP_ID... \e[1;0m" - . $APP_START_SCRIPT_PATH + . $APP_TEST_SCRIPT_PATH local rc=$? if [ ""$rc -eq "0" ]; then diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index e9bbf26..247f7b7 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -18,12 +18,27 @@ stop_apps() { for i in $current_path/apps/* do APP_SCRIPT_PATH=$(echo $i) - if [ -d $APP_SCRIPT_PATH ]; then - APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then + APP_STOP_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" - if [ -f $APP_START_SCRIPT_PATH ]; then + if [ -f "$APP_STOP_SCRIPT_PATH" ]; then APP_ID=$(basename $APP_SCRIPT_PATH) - . $APP_START_SCRIPT_PATH + . $APP_STOP_SCRIPT_PATH + elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then + export SHARED_HTPASSWD_PATH + export GATEKEEPER_DATAPATH + export LIGHTNING_DATAPATH + export BITCOIN_DATAPATH + export APP_SCRIPT_PATH + export APP_ID + export DOCKER_MODE + + if [ "$DOCKER_MODE" = "swarm" ]; then + docker stack rm $APP_ID + elif [ "$DOCKER_MODE" = "compose" ]; then + docker-compose -f $APP_SCRIPT_PATH/docker-compose.yaml down + fi + fi fi done From 7bc7f3b49527c68d9306d84c3037d5d4ff2007b2 Mon Sep 17 00:00:00 2001 From: SKP Date: Tue, 9 Apr 2019 18:34:02 +0200 Subject: [PATCH 145/255] apps are now pulled from a different repo --- dist/apps/sparkwallet/cookie | 1 - dist/apps/sparkwallet/docker-compose.yaml | 27 ---------------- dist/apps/sparkwallet/test.sh | 38 ----------------------- dist/apps/welcome/config.toml | 14 --------- dist/apps/welcome/docker-compose.yaml | 25 --------------- dist/apps/welcome/test.sh | 38 ----------------------- dist/setup.sh | 9 ++++++ 7 files changed, 9 insertions(+), 143 deletions(-) delete mode 100644 dist/apps/sparkwallet/cookie delete mode 100644 dist/apps/sparkwallet/docker-compose.yaml delete mode 100644 dist/apps/sparkwallet/test.sh delete mode 100644 dist/apps/welcome/config.toml delete mode 100644 dist/apps/welcome/docker-compose.yaml delete mode 100644 dist/apps/welcome/test.sh diff --git a/dist/apps/sparkwallet/cookie b/dist/apps/sparkwallet/cookie deleted file mode 100644 index c9558e6..0000000 --- a/dist/apps/sparkwallet/cookie +++ /dev/null @@ -1 +0,0 @@ -cyphernode:sparkwallet:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc= diff --git a/dist/apps/sparkwallet/docker-compose.yaml b/dist/apps/sparkwallet/docker-compose.yaml deleted file mode 100644 index af35289..0000000 --- a/dist/apps/sparkwallet/docker-compose.yaml +++ /dev/null @@ -1,27 +0,0 @@ -version: "3" - -services: - cyphernode_sparkwallet: - command: --no-tls - image: cyphernode/sparkwallet:v0.2.5 - volumes: - - "$LIGHTNING_DATAPATH/:/etc/lightning" - - "$APP_SCRIPT_PATH/cookie:/data/spark/cookie" - - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" - labels: - - "traefik.docker.network=cyphernodeappsnet" - - "traefik.frontend.rule=ReplacePathRegex: ^/sparkwallet(.*) $$1" - - "traefik.frontend.passHostHeader=true" - - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" - - "traefik.frontend.headers.customRequestHeaders=X-Access:FoeDdQw5yl7pPfqdlGy3OEk/txGqyJjSbVtffhzs7kc=" - - "traefik.enable=true" - - "traefik.port=9737" - networks: - - cyphernodenet - - cyphernodeappsnet - restart: always -networks: - cyphernodeappsnet: - external: true - cyphernodenet: - external: true \ No newline at end of file diff --git a/dist/apps/sparkwallet/test.sh b/dist/apps/sparkwallet/test.sh deleted file mode 100644 index 8c8a142..0000000 --- a/dist/apps/sparkwallet/test.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -timeout_feature() { - local interval=10 - local totaltime=60 - local testwhat=${1} - local returncode - local endtime=$(($(date +%s) + ${totaltime})) - - while : - do - eval ${testwhat} - returncode=$? - - # If no error or 2 minutes passed, we get out of this loop - ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - - printf "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left).\e[1;0m" - - sleep ${interval} - done - - return ${returncode} -} - -do_test() { - local rc - rc=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost/sparkwallet) - [ "${rc}" -ne "401" ] && return 400 - return 0 -} - -timeout_feature do_test -returncode=$? - -# return 0: tests cool -# return 1: tests failed -return $returncode diff --git a/dist/apps/welcome/config.toml b/dist/apps/welcome/config.toml deleted file mode 100644 index 9d024b7..0000000 --- a/dist/apps/welcome/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[server] -listen = "0.0.0.0:8080" -index_template = "templates/index.html" -path_prefix = "/welcome" - -[gatekeeper] -status_url = "https://gatekeeper/v0/getblockchaininfo" -installation_info_url = "https://gatekeeper/s/stats/installation.json" -config_archive_url = "https://gatekeeper/s/stats/config.7z" -certs_url = "https://gatekeeper/s/stats/client.7z" - -key_label = "000" -key_file = "/data/keys.properties" -cert_file = "/data/cert.pem" diff --git a/dist/apps/welcome/docker-compose.yaml b/dist/apps/welcome/docker-compose.yaml deleted file mode 100644 index ab22517..0000000 --- a/dist/apps/welcome/docker-compose.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: "3" - -services: - cyphernode_welcome: - environment: - - "TRACING=1" - image: cyphernode/app_welcome:v0.2.0-rc.2 - volumes: - - "$GATEKEEPER_DATAPATH/certs/cert.pem:/data/cert.pem" - - "$GATEKEEPER_DATAPATH/keys.properties:/data/keys.properties" - - "$APP_SCRIPT_PATH/config.toml:/data/config.toml" - - "$GATEKEEPER_DATAPATH/htpasswd:/htpasswd/htpasswd" - networks: - - cyphernodeappsnet - restart: always - labels: - - "traefik.docker.network=cyphernodeappsnet" - - "traefik.frontend.rule=PathPrefix:/welcome; PathPrefixStrip:/welcome" - - "traefik.frontend.passHostHeader=true" - - "traefik.enable=true" - - "traefik.port=8080" - - "traefik.frontend.auth.basic.usersFile=/htpasswd/htpasswd" -networks: - cyphernodeappsnet: - external: true diff --git a/dist/apps/welcome/test.sh b/dist/apps/welcome/test.sh deleted file mode 100644 index 0f10641..0000000 --- a/dist/apps/welcome/test.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -timeout_feature() { - local interval=10 - local totaltime=60 - local testwhat=${1} - local returncode - local endtime=$(($(date +%s) + ${totaltime})) - - while : - do - eval ${testwhat} - returncode=$? - - # If no error or 2 minutes passed, we get out of this loop - ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - - printf "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left).\e[1;0m" - - sleep ${interval} - done - - return ${returncode} -} - -do_test() { - local rc - rc=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost/welcome) - [ "${rc}" -ne "401" ] && return 400 - return 0 -} - -timeout_feature do_test -returncode=$? - -# return 0: tests cool -# return 1: tests failed -return $returncode diff --git a/dist/setup.sh b/dist/setup.sh index 0be8a75..658d9f2 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -684,6 +684,14 @@ sanity_checks_pre_install() { fi } +install_apps() { + if [ ! -d "$current_path/apps" ]; then + local apps_repo="https://github.com/SatoshiPortal/cypherapps.git" + echo " clone $apps_repo into apps" + docker run --rm -v "$current_path":/git alpine/git clone "$apps_repo" apps > /dev/null 2>&1 + fi +} + install() { if [[ ''$INSTALLER_MODE == 'none' ]]; then echo "Skipping installation phase" @@ -794,6 +802,7 @@ if [[ $INSTALL == 1 ]]; then install modify_owner modify_permissions + install_apps fi if [[ $AUTOSTART == 1 ]]; then From 6470b684a8219b5d9f7b76f5116bf0eb7da3ca5e Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 9 Apr 2019 13:20:16 -0400 Subject: [PATCH 146/255] Cypherapps docs and fix in start/stop --- doc/CYPHERAPPS.md | 17 +++++++++++++++++ .../generators/app/templates/installer/start.sh | 2 +- .../generators/app/templates/installer/stop.sh | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 doc/CYPHERAPPS.md diff --git a/doc/CYPHERAPPS.md b/doc/CYPHERAPPS.md new file mode 100644 index 0000000..44263ee --- /dev/null +++ b/doc/CYPHERAPPS.md @@ -0,0 +1,17 @@ +# Cyphernode Apps + +We are providing one default Cyphernode application: The Cyphernode Welcome App. It is a simple Golang application that uses the Cyphernode API to get some information about it: the Bitcoin Core syncing progression, the installed components, a link to download the encrypted config file, a link to download the encrypted API ID/keys file and a link to Spark Wallet, if LN is installed. + +We are also providing Spark Wallet as a Cyphernode application. It is a hybrid application, directly using the c-lightning directory instead of only using the Cyphernode API. + +## Concept + +As you already know it, we want Cyphernode to be modular and decoupled. That's why we created a completely separated repository for the Cyphernode Apps: https://github.com/SatoshiPortal/cypherapps + +Cypherapps acts as an indirection layer between Cyphernode and the actual applications. The repo is cloned into the Cyphernode directory during setup, depending on the selected optional features. The corresponding docker images are taken from our Docker hub. + +Separating Cypherapps from Cyphernode allows us to add applications without changing Cyphernode. + +## Examples + +Welcome App: https://github.com/SatoshiPortal/cyphernode_welcome diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 5ac9c51..b3038ee 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -19,9 +19,9 @@ start_apps() { APP_SCRIPT_PATH=$(echo $i) if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then APP_START_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + APP_ID=$(basename $APP_SCRIPT_PATH) if [ -f "$APP_START_SCRIPT_PATH" ]; then - APP_ID=$(basename $APP_SCRIPT_PATH) . $APP_START_SCRIPT_PATH elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then export SHARED_HTPASSWD_PATH diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index 247f7b7..88b28bc 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -20,9 +20,9 @@ stop_apps() { APP_SCRIPT_PATH=$(echo $i) if [ -d "$APP_SCRIPT_PATH" ] && [ ! -f "$APP_SCRIPT_PATH/ignoreThisApp" ]; then APP_STOP_SCRIPT_PATH="$APP_SCRIPT_PATH/$SCRIPT_NAME" + APP_ID=$(basename $APP_SCRIPT_PATH) if [ -f "$APP_STOP_SCRIPT_PATH" ]; then - APP_ID=$(basename $APP_SCRIPT_PATH) . $APP_STOP_SCRIPT_PATH elif [ -f "$APP_SCRIPT_PATH/docker-compose.yaml" ]; then export SHARED_HTPASSWD_PATH From ef5d261ad3f481de8ca7f6ef1d3ed153aaddc394 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 9 Apr 2019 14:39:33 -0400 Subject: [PATCH 147/255] rc3 --- build.sh | 14 +++++++------- dist/setup.sh | 12 ++++++------ docker-build.sh | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.sh b/build.sh index 38c562c..239271d 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.2-local" -GATEKEEPER_VERSION="v0.2.0-rc.2-local" -PROXY_VERSION="v0.2.0-rc.2-local" -PROXYCRON_VERSION="v0.2.0-rc.2-local" -OTSCLIENT_VERSION="v0.2.0-rc.2-local" -PYCOIN_VERSION="v0.2.0-rc.2-local" +CONF_VERSION="v0.2.0-rc.3-local" +GATEKEEPER_VERSION="v0.2.0-rc.3-local" +PROXY_VERSION="v0.2.0-rc.3-local" +PROXYCRON_VERSION="v0.2.0-rc.3-local" +OTSCLIENT_VERSION="v0.2.0-rc.3-local" +PYCOIN_VERSION="v0.2.0-rc.3-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.2-local" +GRAFANA_VERSION="v0.2.0-rc.3-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index 658d9f2..c7f677b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -710,12 +710,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.2" -GATEKEEPER_VERSION="v0.2.0-rc.2" -PROXY_VERSION="v0.2.0-rc.2" -PROXYCRON_VERSION="v0.2.0-rc.2" -OTSCLIENT_VERSION="v0.2.0-rc.2" -PYCOIN_VERSION="v0.2.0-rc.2" +CONF_VERSION="v0.2.0-rc.3" +GATEKEEPER_VERSION="v0.2.0-rc.3" +PROXY_VERSION="v0.2.0-rc.3" +PROXYCRON_VERSION="v0.2.0-rc.3" +OTSCLIENT_VERSION="v0.2.0-rc.3" +PYCOIN_VERSION="v0.2.0-rc.3" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" diff --git a/docker-build.sh b/docker-build.sh index 9b1b510..3f3e15c 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.2" -v2="v0.2-rc.2" -v3="v0.2.0-rc.2" +v1="v0-rc.3" +v2="v0.2-rc.3" +v3="v0.2.0-rc.3" echo "arch=$arch" From 97f06a6db5f9b7bb48e5c5c1aa79f3cc276f09c1 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 9 Apr 2019 16:03:04 -0400 Subject: [PATCH 148/255] Using cyphernodeconf for git container instead of alpinegit --- dist/setup.sh | 2 +- install/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index c7f677b..85a3491 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -688,7 +688,7 @@ install_apps() { if [ ! -d "$current_path/apps" ]; then local apps_repo="https://github.com/SatoshiPortal/cypherapps.git" echo " clone $apps_repo into apps" - docker run --rm -v "$current_path":/git alpine/git clone "$apps_repo" apps > /dev/null 2>&1 + docker run --rm -v "$current_path":/git --entrypoint git cyphernode/cyphernodeconf:$CONF_VERSION clone "$apps_repo" /git/apps > /dev/null 2>&1 fi } diff --git a/install/Dockerfile b/install/Dockerfile index dd55465..34b9138 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:11.1-alpine -RUN apk add --update bash su-exec p7zip openssl nano apache2-utils && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip openssl nano apache2-utils git && rm -rf /var/cache/apk/* RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config From 0ebaa3bd99af5b9f23ac53c79b7bbde00b59f71a Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 9 Apr 2019 17:03:15 -0400 Subject: [PATCH 149/255] rc4 --- build.sh | 14 +++++++------- dist/setup.sh | 12 ++++++------ docker-build.sh | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.sh b/build.sh index 239271d..a930205 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.3-local" -GATEKEEPER_VERSION="v0.2.0-rc.3-local" -PROXY_VERSION="v0.2.0-rc.3-local" -PROXYCRON_VERSION="v0.2.0-rc.3-local" -OTSCLIENT_VERSION="v0.2.0-rc.3-local" -PYCOIN_VERSION="v0.2.0-rc.3-local" +CONF_VERSION="v0.2.0-rc.4-local" +GATEKEEPER_VERSION="v0.2.0-rc.4-local" +PROXY_VERSION="v0.2.0-rc.4-local" +PROXYCRON_VERSION="v0.2.0-rc.4-local" +OTSCLIENT_VERSION="v0.2.0-rc.4-local" +PYCOIN_VERSION="v0.2.0-rc.4-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.3-local" +GRAFANA_VERSION="v0.2.0-rc.4-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index 85a3491..c6fc17b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -710,12 +710,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.3" -GATEKEEPER_VERSION="v0.2.0-rc.3" -PROXY_VERSION="v0.2.0-rc.3" -PROXYCRON_VERSION="v0.2.0-rc.3" -OTSCLIENT_VERSION="v0.2.0-rc.3" -PYCOIN_VERSION="v0.2.0-rc.3" +CONF_VERSION="v0.2.0-rc.4" +GATEKEEPER_VERSION="v0.2.0-rc.4" +PROXY_VERSION="v0.2.0-rc.4" +PROXYCRON_VERSION="v0.2.0-rc.4" +OTSCLIENT_VERSION="v0.2.0-rc.4" +PYCOIN_VERSION="v0.2.0-rc.4" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" diff --git a/docker-build.sh b/docker-build.sh index 3f3e15c..0435742 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.3" -v2="v0.2-rc.3" -v3="v0.2.0-rc.3" +v1="v0-rc.4" +v2="v0.2-rc.4" +v3="v0.2.0-rc.4" echo "arch=$arch" From bedd7cd7039286921c98cbd12ed9a405ed5cadd4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Apr 2019 15:06:54 -0400 Subject: [PATCH 150/255] generic e-mail address for letsencrypt --- .../generators/app/templates/traefik/traefik.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml index e20deee..0ee3195 100644 --- a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml +++ b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml @@ -21,7 +21,7 @@ watch = true exposedByDefault = false [acme] -email = "noreply@cnc.skp.rocks" +email = "letsencrypt@yourdomain.com" storage = "/acme.json" entryPoint = "https" onHostRule = true From 2029dcf81532f6061b9f39ed34757273777be809 Mon Sep 17 00:00:00 2001 From: SKP Date: Wed, 10 Apr 2019 21:39:36 +0200 Subject: [PATCH 151/255] Fixed htpasswd. Now produces passwords for any string --- install/generator-cyphernode/generators/app/lib/htpasswd.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/lib/htpasswd.js b/install/generator-cyphernode/generators/app/lib/htpasswd.js index 7531794..1a0f612 100644 --- a/install/generator-cyphernode/generators/app/lib/htpasswd.js +++ b/install/generator-cyphernode/generators/app/lib/htpasswd.js @@ -6,8 +6,10 @@ module.exports = async ( password ) => { return null; } + password = password.replace(/'/g, `'\\''`); + return await new Promise( (resolve) => { - exec('htpasswd -bnB admin '+password+' | cut -sd \':\' -f2', (error, stdout, stderr) => { + exec('htpasswd -bnB admin \''+password+'\' | cut -sd \':\' -f2', (error, stdout, stderr) => { if (error) { return resolve(null); } From 6823f4b08c22b9a593a37f8215e89d980ab3ce55 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Apr 2019 18:59:20 -0400 Subject: [PATCH 152/255] Added more time for the tests --- .../generators/app/templates/installer/testfeatures.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 31f0af6..40bff9b 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -121,8 +121,8 @@ checklnnode() { } checkservice() { - local interval=10 - local totaltime=120 + local interval=15 + local totaltime=180 local outcome local returncode=0 local endtime=$(($(date +%s) + ${totaltime})) @@ -179,8 +179,8 @@ checkservice() { } timeout_feature() { - local interval=10 - local totaltime=60 + local interval=15 + local totaltime=120 local testwhat=${1} local returncode local endtime=$(($(date +%s) + ${totaltime})) From fa5b9f2daf5b66f5a850a183c1dab4c101aa2a21 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Apr 2019 19:00:50 -0400 Subject: [PATCH 153/255] rc5 --- build.sh | 14 +++++++------- dist/setup.sh | 12 ++++++------ docker-build.sh | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build.sh b/build.sh index a930205..df5c74e 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.4-local" -GATEKEEPER_VERSION="v0.2.0-rc.4-local" -PROXY_VERSION="v0.2.0-rc.4-local" -PROXYCRON_VERSION="v0.2.0-rc.4-local" -OTSCLIENT_VERSION="v0.2.0-rc.4-local" -PYCOIN_VERSION="v0.2.0-rc.4-local" +CONF_VERSION="v0.2.0-rc.5-local" +GATEKEEPER_VERSION="v0.2.0-rc.5-local" +PROXY_VERSION="v0.2.0-rc.5-local" +PROXYCRON_VERSION="v0.2.0-rc.5-local" +OTSCLIENT_VERSION="v0.2.0-rc.5-local" +PYCOIN_VERSION="v0.2.0-rc.5-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.4-local" +GRAFANA_VERSION="v0.2.0-rc.5-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index c6fc17b..b51b1da 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -710,12 +710,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.4" -GATEKEEPER_VERSION="v0.2.0-rc.4" -PROXY_VERSION="v0.2.0-rc.4" -PROXYCRON_VERSION="v0.2.0-rc.4" -OTSCLIENT_VERSION="v0.2.0-rc.4" -PYCOIN_VERSION="v0.2.0-rc.4" +CONF_VERSION="v0.2.0-rc.5" +GATEKEEPER_VERSION="v0.2.0-rc.5" +PROXY_VERSION="v0.2.0-rc.5" +PROXYCRON_VERSION="v0.2.0-rc.5" +OTSCLIENT_VERSION="v0.2.0-rc.5" +PYCOIN_VERSION="v0.2.0-rc.5" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" diff --git a/docker-build.sh b/docker-build.sh index 0435742..61b6106 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.4" -v2="v0.2-rc.4" -v3="v0.2.0-rc.4" +v1="v0-rc.5" +v2="v0.2-rc.5" +v3="v0.2.0-rc.5" echo "arch=$arch" From d8a8963e8eb9fdc5cf9d86fa679fd8a9aa2044fd Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Apr 2019 19:17:42 -0400 Subject: [PATCH 154/255] letsencrypt domain name config --- .../generators/app/templates/traefik/traefik.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml index 0ee3195..11210b2 100644 --- a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml +++ b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml @@ -27,3 +27,5 @@ entryPoint = "https" onHostRule = true [acme.httpChallenge] entryPoint = "http" +[[acme.domains]] + main = "cyphernode.yourdomain.com" From 452ad78b321fdd6c5b867c7163c2f689329f2e70 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 10 Apr 2019 19:17:59 -0400 Subject: [PATCH 155/255] Like in traefik docs --- .../generators/app/templates/traefik/traefik.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml index 11210b2..0b6d48c 100644 --- a/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml +++ b/install/generator-cyphernode/generators/app/templates/traefik/traefik.toml @@ -22,7 +22,7 @@ exposedByDefault = false [acme] email = "letsencrypt@yourdomain.com" -storage = "/acme.json" +storage = "acme.json" entryPoint = "https" onHostRule = true [acme.httpChallenge] From 9b8db7d5de02e2d0cf2681e3b0ac1f9519b5caac Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 25 Apr 2019 13:28:08 -0400 Subject: [PATCH 156/255] Correctly check groups access and upgrade to 0.2 instructions --- api_auth_docker/api-sample.properties | 1 + api_auth_docker/auth.sh | 14 ++++++++------ doc/UPGRADE.md | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 doc/UPGRADE.md diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties index 63851c6..78c3e4b 100644 --- a/api_auth_docker/api-sample.properties +++ b/api_auth_docker/api-sample.properties @@ -38,6 +38,7 @@ action_ln_newaddr=spender action_ots_stamp=spender action_ots_getfile=spender action_ln_getinvoice=spender +action_ln_decodebolt11=spender action_ln_connectfund=spender # Admin can do what the spender can do, plus: diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh index 272ab6c..895847b 100755 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -106,10 +106,10 @@ verify_group() eval ugroups='$ugroups_'$id trace "[verify_group] user groups=${ugroups}" - if [ $context = "s" ]; then + if [ ${context} = "s" ]; then # static files only accessible by a certain group needed_group=${action} - elif [ $context = "v0" ]; then + elif [ ${context} = "v0" ]; then # actual api calls # It is so much faster to include the keys here instead of grep'ing the file for key. . ./api.properties @@ -118,10 +118,12 @@ verify_group() trace "[verify_group] needed_group=${needed_group}" - - case "${ugroups}" in - *${needed_group}*) trace "[verify_group] Access granted"; return 0 ;; - esac + # If needed_group is empty, the action was not found in api.propeties. + if [ -n "${needed_group}" ]; then + case "${ugroups}" in + *${needed_group}*) trace "[verify_group] Access granted"; return 0 ;; + esac + fi trace "[verify_group] Access NOT granted" return 1 diff --git a/doc/UPGRADE.md b/doc/UPGRADE.md new file mode 100644 index 0000000..be7961e --- /dev/null +++ b/doc/UPGRADE.md @@ -0,0 +1,27 @@ +# Upgrade notes from 0.1 to 0.2 + +1. cd currentInstallation, where setup.sh is located +2. ./stop.sh current running cyphernode +3. Execute: + +``` +docker run --rm -it -v "$PWD:/conf" alpine:3.8 +apk add --no-cache --update jq curl p7zip +cd conf +7z e config.7z +``` + + + +``` +k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\\"000\\\";kapi_key=\\\"$k\\\";kapi_groups=\\\"stats\\\";eval ugroups_\${kapi_id}=\${kapi_groups};eval ukey_\${kapi_id}=\${kapi_key}" && cat config.json | sed 's/kapi_groups=\\"/kapi_groups=\\"stats,/g' | jq ".gatekeeper_keys.configEntries = [\"$l\"] + .gatekeeper_keys.configEntries" | jq ".gatekeeper_keys.clientInformation = [\"000=$k\"] + .gatekeeper_keys.clientInformation" | jq ".gatekeeper_apiproperties = \"$(curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0-rc.5/api_auth_docker/api-sample.properties | paste -s -d '\n')\"" > config.json + +7z u config.7z config.json +``` + + + + +``` +curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0-rc.5/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh +``` From 1ec95a9c74c65a845372f599c1275ff3a3266cc3 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 25 Apr 2019 15:50:46 -0400 Subject: [PATCH 157/255] Added temporary migration code to setup.sh --- dist/setup.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index b51b1da..782682e 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -700,6 +700,7 @@ install() { fi } + CONFIGURE=0 INSTALL=0 RECREATE=0 @@ -731,6 +732,31 @@ function ctrl_c() { export current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + +#*************************************************************** +# Temporary code for upgrading from v0.1 to v0.2 +#*************************************************************** + +grep "xpub" gatekeeper/api.properties > /dev/null +returncode=$? +if [[ $returncode -eq 1 ]]; then + # grep found the file but didn't find xpub in it + + echo "Previous Cyphernode installation detected." + echo "Running migration scripts..." + + echo "You will be asked to enter your admin passphrase twice while migrating. It is the passphrase you used when installing previous verison of Cyphernode." + + # We want to add the 000 KEY_ID (Stats) and update the api.properties file with new endpoints + docker run --rm -it -v "$SETUP_DIR:/conf" alpine:3.8 sh -c "apk add --no-cache --update curl ; curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/${GATEKEEPER_VERSION}/api_auth_docker/api-sample.properties -o /conf/api-sample.properties" + docker run --rm -it -v "$SETUP_DIR:/conf" alpine:3.8 sh -c 'apk add --no-cache --update jq p7zip;apk add --no-cache --update jq curl p7zip;cd conf;7z e config.7z;k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\\"000\\\";kapi_key=\\\"$k\\\";kapi_groups=\\\"stats\\\";eval ugroups_\${kapi_id}=\${kapi_groups};eval ukey_\${kapi_id}=\${kapi_key}" && cat config.json | sed 's/kapi_groups=\\"/kapi_groups=\\"stats,/g' | jq ".gatekeeper_keys.configEntries = [\"$l\"] + .gatekeeper_keys.configEntries" | jq ".gatekeeper_keys.clientInformation = [\"000=$k\"] + .gatekeeper_keys.clientInformation" | jq ".gatekeeper_apiproperties = \"$(cat api-sample.properties | paste -s -d '\\\\n')\"" > config.json;7z u config.7z config.json;' +fi + +#*************************************************************** +# Temporary code for upgrading from v0.1 to v0.2 +#*************************************************************** + + while getopts ":cirhys" opt; do case $opt in r) From a758ab8c89dd7ccde40bb36efff552bcd2ed8ff3 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 25 Apr 2019 16:55:43 -0400 Subject: [PATCH 158/255] Preparing for rc.6 --- build.sh | 14 +++++++------- dist/setup.sh | 12 ++++++------ doc/CYPHERAPPS.md | 2 +- doc/INSTALL.md | 2 ++ doc/UPGRADE.md | 8 +++++--- docker-build.sh | 6 +++--- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/build.sh b/build.sh index df5c74e..3557eb1 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.5-local" -GATEKEEPER_VERSION="v0.2.0-rc.5-local" -PROXY_VERSION="v0.2.0-rc.5-local" -PROXYCRON_VERSION="v0.2.0-rc.5-local" -OTSCLIENT_VERSION="v0.2.0-rc.5-local" -PYCOIN_VERSION="v0.2.0-rc.5-local" +CONF_VERSION="v0.2.0-rc.6-local" +GATEKEEPER_VERSION="v0.2.0-rc.6-local" +PROXY_VERSION="v0.2.0-rc.6-local" +PROXYCRON_VERSION="v0.2.0-rc.6-local" +OTSCLIENT_VERSION="v0.2.0-rc.6-local" +PYCOIN_VERSION="v0.2.0-rc.6-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.5-local" +GRAFANA_VERSION="v0.2.0-rc.6-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index 782682e..6ecafa3 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -711,12 +711,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.5" -GATEKEEPER_VERSION="v0.2.0-rc.5" -PROXY_VERSION="v0.2.0-rc.5" -PROXYCRON_VERSION="v0.2.0-rc.5" -OTSCLIENT_VERSION="v0.2.0-rc.5" -PYCOIN_VERSION="v0.2.0-rc.5" +CONF_VERSION="v0.2.0-rc.6" +GATEKEEPER_VERSION="v0.2.0-rc.6" +PROXY_VERSION="v0.2.0-rc.6" +PROXYCRON_VERSION="v0.2.0-rc.6" +OTSCLIENT_VERSION="v0.2.0-rc.6" +PYCOIN_VERSION="v0.2.0-rc.6" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" diff --git a/doc/CYPHERAPPS.md b/doc/CYPHERAPPS.md index 44263ee..e60a62f 100644 --- a/doc/CYPHERAPPS.md +++ b/doc/CYPHERAPPS.md @@ -8,7 +8,7 @@ We are also providing Spark Wallet as a Cyphernode application. It is a hybrid As you already know it, we want Cyphernode to be modular and decoupled. That's why we created a completely separated repository for the Cyphernode Apps: https://github.com/SatoshiPortal/cypherapps -Cypherapps acts as an indirection layer between Cyphernode and the actual applications. The repo is cloned into the Cyphernode directory during setup, depending on the selected optional features. The corresponding docker images are taken from our Docker hub. +Cypherapps acts as an indirection layer between Cyphernode and the actual applications. The repo is cloned into the Cyphernode directory during setup, depending on the selected optional features. The corresponding docker images are taken from the Docker hub repositories. Separating Cypherapps from Cyphernode allows us to add applications without changing Cyphernode. diff --git a/doc/INSTALL.md b/doc/INSTALL.md index cbce0cc..0b4839c 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -34,6 +34,8 @@ cd dist ## Upgrading +To upgrade to the most recent version, just get and run the most recent version of the setup.sh file as described in the previous section. Migration should be taken care by the script. + Your proxy's database won't be lost. Migration scripts are taking care of automatically migrating the database when starting the proxy. ``` diff --git a/doc/UPGRADE.md b/doc/UPGRADE.md index be7961e..8e2f3ae 100644 --- a/doc/UPGRADE.md +++ b/doc/UPGRADE.md @@ -1,4 +1,6 @@ -# Upgrade notes from 0.1 to 0.2 +# Upgrade notes from 0.1 to 0.2, to upgrade manually + +Usually no need to do this since it will be done during setup.sh v0.2. 1. cd currentInstallation, where setup.sh is located 2. ./stop.sh current running cyphernode @@ -14,7 +16,7 @@ cd conf ``` -k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\\"000\\\";kapi_key=\\\"$k\\\";kapi_groups=\\\"stats\\\";eval ugroups_\${kapi_id}=\${kapi_groups};eval ukey_\${kapi_id}=\${kapi_key}" && cat config.json | sed 's/kapi_groups=\\"/kapi_groups=\\"stats,/g' | jq ".gatekeeper_keys.configEntries = [\"$l\"] + .gatekeeper_keys.configEntries" | jq ".gatekeeper_keys.clientInformation = [\"000=$k\"] + .gatekeeper_keys.clientInformation" | jq ".gatekeeper_apiproperties = \"$(curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0-rc.5/api_auth_docker/api-sample.properties | paste -s -d '\n')\"" > config.json +k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\\"000\\\";kapi_key=\\\"$k\\\";kapi_groups=\\\"stats\\\";eval ugroups_\${kapi_id}=\${kapi_groups};eval ukey_\${kapi_id}=\${kapi_key}" && cat config.json | sed 's/kapi_groups=\\"/kapi_groups=\\"stats,/g' | jq ".gatekeeper_keys.configEntries = [\"$l\"] + .gatekeeper_keys.configEntries" | jq ".gatekeeper_keys.clientInformation = [\"000=$k\"] + .gatekeeper_keys.clientInformation" | jq ".gatekeeper_apiproperties = \"$(curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0/api_auth_docker/api-sample.properties | paste -s -d '\n')\"" > config.json 7z u config.7z config.json ``` @@ -23,5 +25,5 @@ k=$(dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32) && l="kapi_id=\\ ``` -curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0-rc.5/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh +curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/v0.2.0/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh ``` diff --git a/docker-build.sh b/docker-build.sh index 61b6106..e05fca4 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.5" -v2="v0.2-rc.5" -v3="v0.2.0-rc.5" +v1="v0-rc.6" +v2="v0.2-rc.6" +v3="v0.2.0-rc.6" echo "arch=$arch" From ddfca01f16727492351fe8b76b0dc8236b8c1a69 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 26 Apr 2019 12:48:42 -0400 Subject: [PATCH 159/255] Aligned with c-lightning official docker using usr local --- docker-build.sh | 4 ++-- proxy_docker/Dockerfile.amd64 | 2 +- proxy_docker/Dockerfile.arm32v6 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-build.sh b/docker-build.sh index e05fca4..9d3ffc2 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -117,8 +117,8 @@ manifest "gatekeeper" \ [ $? -ne 0 ] && echo "Error" && return 1 -image_dockers "clightning" "../dockers/c-lightning v0.7.0" ${arch} "Dockerfile.${arch}" \ -&& image_dockers "bitcoin" "../dockers/bitcoin-core v0.17.1" ${arch} "Dockerfile.${arch}" \ +image_dockers "clightning" "../dockers/c-lightning" "v0.7.0" ${arch} "Dockerfile.${arch}" \ +&& image_dockers "bitcoin" "../dockers/bitcoin-core" "v0.17.1" ${arch} "Dockerfile.${arch}" \ && image_dockers "app_welcome" "../cyphernode_welcome" "${v3}" ${arch} \ && image_dockers "app_welcome" "../cyphernode_welcome" "${v2}" ${arch} \ && image_dockers "app_welcome" "../cyphernode_welcome" "${v1}" ${arch} \ diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 806a713..e3aa367 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -28,7 +28,7 @@ WORKDIR ${HOME} COPY app/data/* ./ COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.7.0 /usr/bin/lightning-cli ./ +COPY --from=cyphernode/clightning:v0.7.0 /usr/local/bin/lightning-cli ./ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ && chmod o+w . \ diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index f4a0646..97ca5ec 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -24,7 +24,7 @@ WORKDIR ${HOME} COPY app/data/* ./ COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.7.0 /usr/bin/lightning-cli ./ +COPY --from=cyphernode/clightning:v0.7.0 /usr/local/bin/lightning-cli ./ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ && chmod o+w . \ From 0229ce35137927779a2ef692b3765a70d4af39c2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 7 May 2019 16:24:21 -0400 Subject: [PATCH 160/255] v0.2.0 final release --- build.sh | 14 +++++++------- dist/setup.sh | 18 +++++++++--------- docker-build.sh | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/build.sh b/build.sh index 3557eb1..fdda705 100755 --- a/build.sh +++ b/build.sh @@ -3,15 +3,15 @@ TRACING=1 # CYPHERNODE VERSION "v0.1.1" -CONF_VERSION="v0.2.0-rc.6-local" -GATEKEEPER_VERSION="v0.2.0-rc.6-local" -PROXY_VERSION="v0.2.0-rc.6-local" -PROXYCRON_VERSION="v0.2.0-rc.6-local" -OTSCLIENT_VERSION="v0.2.0-rc.6-local" -PYCOIN_VERSION="v0.2.0-rc.6-local" +CONF_VERSION="v0.2.0-local" +GATEKEEPER_VERSION="v0.2.0-local" +PROXY_VERSION="v0.2.0-local" +PROXYCRON_VERSION="v0.2.0-local" +OTSCLIENT_VERSION="v0.2.0-local" +PYCOIN_VERSION="v0.2.0-local" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" -GRAFANA_VERSION="v0.2.0-rc.6-local" +GRAFANA_VERSION="v0.2.0-local" trace() { diff --git a/dist/setup.sh b/dist/setup.sh index 6ecafa3..bb68b4d 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -711,12 +711,12 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-rc.6" -GATEKEEPER_VERSION="v0.2.0-rc.6" -PROXY_VERSION="v0.2.0-rc.6" -PROXYCRON_VERSION="v0.2.0-rc.6" -OTSCLIENT_VERSION="v0.2.0-rc.6" -PYCOIN_VERSION="v0.2.0-rc.6" +CONF_VERSION="v0.2.0" +GATEKEEPER_VERSION="v0.2.0" +PROXY_VERSION="v0.2.0" +PROXYCRON_VERSION="v0.2.0" +OTSCLIENT_VERSION="v0.2.0" +PYCOIN_VERSION="v0.2.0" BITCOIN_VERSION="v0.17.1" LIGHTNING_VERSION="v0.7.0" @@ -742,10 +742,10 @@ returncode=$? if [[ $returncode -eq 1 ]]; then # grep found the file but didn't find xpub in it - echo "Previous Cyphernode installation detected." - echo "Running migration scripts..." + echo "\nPrevious Cyphernode installation detected." + echo "Running migration scripts...\n" - echo "You will be asked to enter your admin passphrase twice while migrating. It is the passphrase you used when installing previous verison of Cyphernode." + echo "You will be asked to enter your admin passphrase twice while migrating. It is the passphrase you used when installing previous verison of Cyphernode.\n" # We want to add the 000 KEY_ID (Stats) and update the api.properties file with new endpoints docker run --rm -it -v "$SETUP_DIR:/conf" alpine:3.8 sh -c "apk add --no-cache --update curl ; curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/${GATEKEEPER_VERSION}/api_auth_docker/api-sample.properties -o /conf/api-sample.properties" diff --git a/docker-build.sh b/docker-build.sh index 9d3ffc2..f6264de 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -91,9 +91,9 @@ arm="arm32v6" #arch=${arm} arch=${x86} -v1="v0-rc.6" -v2="v0.2-rc.6" -v3="v0.2.0-rc.6" +v1="v0" +v2="v0.2" +v3="v0.2.0" echo "arch=$arch" From 5a31fc6c969a674191be1b76a1252934083d9270 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 7 May 2019 16:50:09 -0400 Subject: [PATCH 161/255] return vs exit --- docker-build.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-build.sh b/docker-build.sh index f6264de..82304d3 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -104,9 +104,9 @@ image "gatekeeper" "api_auth_docker/" ${arch} \ && image "pycoin" "pycoin_docker/" ${arch} \ && image "cyphernodeconf" "install/" ${arch} -[ $? -ne 0 ] && echo "Error" && return 1 +[ $? -ne 0 ] && echo "Error" && exit 1 -[ "${arch}" = "${x86}" ] && echo "Built and pushed amd64 only" && return 0 +[ "${arch}" = "${x86}" ] && echo "Built and pushed amd64 only" && exit 0 manifest "gatekeeper" \ && manifest "proxycron" \ @@ -115,7 +115,7 @@ manifest "gatekeeper" \ && manifest "pycoin" \ && manifest "cyphernodeconf" -[ $? -ne 0 ] && echo "Error" && return 1 +[ $? -ne 0 ] && echo "Error" && exit 1 image_dockers "clightning" "../dockers/c-lightning" "v0.7.0" ${arch} "Dockerfile.${arch}" \ && image_dockers "bitcoin" "../dockers/bitcoin-core" "v0.17.1" ${arch} "Dockerfile.${arch}" \ @@ -124,7 +124,7 @@ image_dockers "clightning" "../dockers/c-lightning" "v0.7.0" ${arch} "Dockerfile && image_dockers "app_welcome" "../cyphernode_welcome" "${v1}" ${arch} \ && image_dockers "sparkwallet" "../spark-wallet" "v0.2.5" ${arch} "Dockerfile-cyphernode" -[ $? -ne 0 ] && echo "Error" && return 1 +[ $? -ne 0 ] && echo "Error" && exit 1 manifest_dockers "clightning" "v0.7.0" \ && manifest_dockers "bitcoin" "v0.17.1" \ @@ -133,4 +133,4 @@ manifest_dockers "clightning" "v0.7.0" \ && manifest_dockers "app_welcome" "${v1}" \ && manifest_dockers "sparkwallet" "v0.2.5" -[ $? -ne 0 ] && echo "Error" && return 1 +[ $? -ne 0 ] && echo "Error" && exit 1 From 8d36466da8eca28b836047b6a8883a7a09c53dbd Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 14 May 2019 16:13:08 -0400 Subject: [PATCH 162/255] Using alpine-glibc-base base image --- build.sh | 49 ++----------- dist/setup.sh | 16 ++--- docker-build.sh | 120 +++++++++++--------------------- install/Dockerfile | 3 +- proxy_docker/Dockerfile | 23 ++++++ proxy_docker/Dockerfile.amd64 | 39 ----------- proxy_docker/Dockerfile.arm32v6 | 35 ---------- proxy_docker/README.md | 16 ----- 8 files changed, 80 insertions(+), 221 deletions(-) create mode 100644 proxy_docker/Dockerfile delete mode 100644 proxy_docker/Dockerfile.amd64 delete mode 100644 proxy_docker/Dockerfile.arm32v6 diff --git a/build.sh b/build.sh index fdda705..59516e3 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ TRACING=1 -# CYPHERNODE VERSION "v0.1.1" +# CYPHERNODE VERSION "v0.2.0" CONF_VERSION="v0.2.0-local" GATEKEEPER_VERSION="v0.2.0-local" PROXY_VERSION="v0.2.0-local" @@ -27,53 +27,18 @@ trace_rc() fi } - -build_docker_image() { - - local dockerfile="Dockerfile" - - if [[ ""$3 != "" ]]; then - dockerfile=$3 - fi - - trace "building docker image: $2" - #docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null - docker build $1 -f $1/$dockerfile -t $2 - -} - build_docker_images() { trace "Updating SatoshiPortal repos" -# git submodule update --recursive --remote - - local bitcoin_dockerfile=Dockerfile.amd64 - local clightning_dockerfile=Dockerfile.amd64 - local proxy_dockerfile=Dockerfile.amd64 - local grafana_dockerfile=Dockerfile.amd64 - - # compat mode for SatoshiPortal repo - # TODO: add more mappings? - if [[ $(uname -m) == 'armv7l' ]]; then - bitcoin_dockerfile="Dockerfile.arm32v6" - clightning_dockerfile="Dockerfile.arm32v6" - proxy_dockerfile="Dockerfile.arm32v6" - grafana_dockerfile="Dockerfile.arm32v6" - fi trace "Creating cyphernodeconf image" - build_docker_image install/ cyphernode/cyphernodeconf:$CONF_VERSION - - trace "Creating SatoshiPortal images" -# build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:$BITCOIN_VERSION $bitcoin_dockerfile -# build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:$LIGHTNING_VERSION $clightning_dockerfile + docker build install/ -t cyphernode/cyphernodeconf:$CONF_VERSION trace "Creating cyphernode images" - build_docker_image api_auth_docker/ cyphernode/gatekeeper:$GATEKEEPER_VERSION - build_docker_image proxy_docker/ cyphernode/proxy:$PROXY_VERSION $proxy_dockerfile - build_docker_image cron_docker/ cyphernode/proxycron:$PROXYCRON_VERSION - build_docker_image pycoin_docker/ cyphernode/pycoin:$PYCOIN_VERSION - build_docker_image otsclient_docker/ cyphernode/otsclient:$OTSCLIENT_VERSION - + docker build api_auth_docker/ -t cyphernode/gatekeeper:$GATEKEEPER_VERSION + docker build proxy_docker/ -t cyphernode/proxy:$PROXY_VERSION + docker build cron_docker/ -t cyphernode/proxycron:$PROXYCRON_VERSION + docker build pycoin_docker/ -t cyphernode/pycoin:$PYCOIN_VERSION + docker build otsclient_docker/ -t cyphernode/otsclient:$OTSCLIENT_VERSION } build_docker_images diff --git a/dist/setup.sh b/dist/setup.sh index bb68b4d..f84e6b7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -711,14 +711,14 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0" -GATEKEEPER_VERSION="v0.2.0" -PROXY_VERSION="v0.2.0" -PROXYCRON_VERSION="v0.2.0" -OTSCLIENT_VERSION="v0.2.0" -PYCOIN_VERSION="v0.2.0" -BITCOIN_VERSION="v0.17.1" -LIGHTNING_VERSION="v0.7.0" +CONF_VERSION="v0.2.0-test" +GATEKEEPER_VERSION="v0.2.0-test" +PROXY_VERSION="v0.2.0-test" +PROXYCRON_VERSION="v0.2.0-test" +OTSCLIENT_VERSION="v0.2.0-test" +PYCOIN_VERSION="v0.2.0-test" +BITCOIN_VERSION="v0.17.1-test" +LIGHTNING_VERSION="v0.7.0-test" SETUP_DIR=$(dirname $(realpath $0)) diff --git a/docker-build.sh b/docker-build.sh index 82304d3..040c62e 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -10,14 +10,13 @@ image() { local image=$1 local dir=$2 local arch=$3 - local dockerfile=${4:-"Dockerfile"} echo "Building and pushing $image from $dir for $arch using $dockerfile tagging as $v1, $v2 and $v3..." - docker build -t cyphernode/${image}:${arch}-${v3} -t cyphernode/${image}:${arch}-${v2} -t cyphernode/${image}:${arch}-${v1} -f ${dir}/${dockerfile} ${dir}/. \ -&& docker push cyphernode/${image}:${arch}-${v3} \ -&& docker push cyphernode/${image}:${arch}-${v2} \ -&& docker push cyphernode/${image}:${arch}-${v1} + docker build -t cyphernode/${image}:${arch}-${v3} -t cyphernode/${image}:${arch}-${v2} -t cyphernode/${image}:${arch}-${v1} ${dir}/. \ + && docker push cyphernode/${image}:${arch}-${v3} \ + && docker push cyphernode/${image}:${arch}-${v2} \ + && docker push cyphernode/${image}:${arch}-${v1} return $? } @@ -27,86 +26,64 @@ manifest() { echo "Creating and pushing manifest for $image for version $v3..." - docker manifest create cyphernode/${image}:${v3} cyphernode/${image}:${arm}-${v3} cyphernode/${image}:${x86}-${v3} \ -&& docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${arm}-${v3} --os linux --arch arm \ -&& docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${x86}-${v3} --os linux --arch amd64 \ -&& docker manifest push -p cyphernode/${image}:${v3} + docker manifest create cyphernode/${image}:${v3} cyphernode/${image}:${arm_docker}-${v3} cyphernode/${image}:${x86_docker}-${v3} cyphernode/${image}:${aarch64_docker}-${v3} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${arm_docker}-${v3} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${x86_docker}-${v3} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v3} cyphernode/${image}:${aarch64_docker}-${v3} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v3} [ $? -ne 0 ] && return 1 echo "Creating and pushing manifest for $image for version $v2..." - docker manifest create cyphernode/${image}:${v2} cyphernode/${image}:${arm}-${v2} cyphernode/${image}:${x86}-${v2} \ -&& docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${arm}-${v2} --os linux --arch arm \ -&& docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${x86}-${v2} --os linux --arch amd64 \ -&& docker manifest push -p cyphernode/${image}:${v2} + docker manifest create cyphernode/${image}:${v2} cyphernode/${image}:${arm_docker}-${v2} cyphernode/${image}:${x86_docker}-${v2} cyphernode/${image}:${aarch64_docker}-${v2} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${arm_docker}-${v2} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${x86_docker}-${v2} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v2} cyphernode/${image}:${aarch64_docker}-${v2} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v2} [ $? -ne 0 ] && return 1 echo "Creating and pushing manifest for $image for version $v1..." - docker manifest create cyphernode/${image}:${v1} cyphernode/${image}:${arm}-${v1} cyphernode/${image}:${x86}-${v1} \ -&& docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${arm}-${v1} --os linux --arch arm \ -&& docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${x86}-${v1} --os linux --arch amd64 \ -&& docker manifest push -p cyphernode/${image}:${v1} + docker manifest create cyphernode/${image}:${v1} cyphernode/${image}:${arm_docker}-${v1} cyphernode/${image}:${x86_docker}-${v1} cyphernode/${image}:${aarch64_docker}-${v1} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${arm_docker}-${v1} --os linux --arch ${arm_docker} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${x86_docker}-${v1} --os linux --arch ${x86_docker} \ + && docker manifest annotate cyphernode/${image}:${v1} cyphernode/${image}:${aarch64_docker}-${v1} --os linux --arch ${aarch64_docker} \ + && docker manifest push -p cyphernode/${image}:${v1} return $? - } -image_dockers() { - local image=$1 - local dir=$2 - local v=$3 - local arch=$4 - local dockerfile=${5:-"Dockerfile"} +x86_docker="amd64" +arm_docker="arm" +aarch64_docker="arm64" - echo "Building and pushing $image from $dir for $arch using $dockerfile tagging as $v..." +# Build amd64 and arm64 first, building for arm will trigger the manifest creation and push on hub - docker build -t cyphernode/${image}:${arch}-${v} -f ${dir}/${dockerfile} ${dir}/. \ -&& docker push cyphernode/${image}:${arch}-${v} +#arch_docker=${arm_docker} +#arch_docker=${aarch64_docker} +arch_docker=${x86_docker} - return $? +v1="v0-test" +v2="v0.2-test" +v3="v0.2.0-test" -} +echo "\nBuilding Cyphernode Core containers\n" +echo "arch_docker=$arch_docker\n" -manifest_dockers() { - local image=$1 - local v=$2 - - echo "Creating and pushing manifest for $image for version $v..." - - docker manifest create cyphernode/${image}:${v} cyphernode/${image}:${arm}-${v} cyphernode/${image}:${x86}-${v} \ -&& docker manifest annotate cyphernode/${image}:${v} cyphernode/${image}:${arm}-${v} --os linux --arch arm \ -&& docker manifest annotate cyphernode/${image}:${v} cyphernode/${image}:${x86}-${v} --os linux --arch amd64 \ -&& docker manifest push -p cyphernode/${image}:${v} - - return $? - -} - -x86="amd64" -arm="arm32v6" - -#arch=${arm} -arch=${x86} - -v1="v0" -v2="v0.2" -v3="v0.2.0" - -echo "arch=$arch" - -image "gatekeeper" "api_auth_docker/" ${arch} \ -&& image "proxycron" "cron_docker/" ${arch} \ -&& image "otsclient" "otsclient_docker/" ${arch} \ -&& image "proxy" "proxy_docker/" ${arch} "Dockerfile.${arch}" \ -&& image "pycoin" "pycoin_docker/" ${arch} \ -&& image "cyphernodeconf" "install/" ${arch} +image "gatekeeper" "api_auth_docker/" ${arch_docker} \ +&& image "proxycron" "cron_docker/" ${arch_docker} \ +&& image "otsclient" "otsclient_docker/" ${arch_docker} \ +&& image "proxy" "proxy_docker/" ${arch_docker} \ +&& image "pycoin" "pycoin_docker/" ${arch_docker} \ +&& image "cyphernodeconf" "install/" ${arch_docker} [ $? -ne 0 ] && echo "Error" && exit 1 -[ "${arch}" = "${x86}" ] && echo "Built and pushed amd64 only" && exit 0 +[ "${arch_docker}" = "${x86_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${aarch64_docker}" ] && echo "Built and pushed ${arch_docker} only" && exit 0 +[ "${arch_docker}" = "${arm_docker}" ] && echo "Built and pushed images, now building and pushing manifest for all archs..." manifest "gatekeeper" \ && manifest "proxycron" \ @@ -117,20 +94,3 @@ manifest "gatekeeper" \ [ $? -ne 0 ] && echo "Error" && exit 1 -image_dockers "clightning" "../dockers/c-lightning" "v0.7.0" ${arch} "Dockerfile.${arch}" \ -&& image_dockers "bitcoin" "../dockers/bitcoin-core" "v0.17.1" ${arch} "Dockerfile.${arch}" \ -&& image_dockers "app_welcome" "../cyphernode_welcome" "${v3}" ${arch} \ -&& image_dockers "app_welcome" "../cyphernode_welcome" "${v2}" ${arch} \ -&& image_dockers "app_welcome" "../cyphernode_welcome" "${v1}" ${arch} \ -&& image_dockers "sparkwallet" "../spark-wallet" "v0.2.5" ${arch} "Dockerfile-cyphernode" - -[ $? -ne 0 ] && echo "Error" && exit 1 - -manifest_dockers "clightning" "v0.7.0" \ -&& manifest_dockers "bitcoin" "v0.17.1" \ -&& manifest_dockers "app_welcome" "${v3}" \ -&& manifest_dockers "app_welcome" "${v2}" \ -&& manifest_dockers "app_welcome" "${v1}" \ -&& manifest_dockers "sparkwallet" "v0.2.5" - -[ $? -ne 0 ] && echo "Error" && exit 1 diff --git a/install/Dockerfile b/install/Dockerfile index 34b9138..435f6bd 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -5,7 +5,8 @@ RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config -RUN npm install -g yo +# Workaround for https://github.com/npm/uid-number/issues/3 +RUN NPM_CONFIG_UNSAFE_PERM=true npm install -g yo COPY generator-cyphernode /app WORKDIR /app/generator-cyphernode RUN npm link diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile new file mode 100644 index 0000000..fad4969 --- /dev/null +++ b/proxy_docker/Dockerfile @@ -0,0 +1,23 @@ +FROM cyphernode/alpine-glibc-base:3.8 + +ENV HOME /proxy + +RUN apk add --update --no-cache \ + sqlite \ + jq \ + curl \ + su-exec + +WORKDIR ${HOME} + +COPY app/data/* ./ +COPY app/script/* ./ +COPY --from=cyphernode/clightning:v0.7.0-test /usr/local/bin/lightning-cli ./ + +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ + && chmod o+w . \ + && mkdir db + +VOLUME ["${HOME}/db", "/.lightning"] + +ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 deleted file mode 100644 index e3aa367..0000000 --- a/proxy_docker/Dockerfile.amd64 +++ /dev/null @@ -1,39 +0,0 @@ -FROM alpine:3.8 - -# Taking care of glibc shit (glibc not natively supported by Alpine but lightning-cli uses it) - -ENV GLIBC_VERSION 2.27-r0 -ENV GLIBC_SHA256 938bceae3b83c53e7fa9cc4135ce45e04aae99256c5e74cf186c794b97473bc7 -ENV GLIBCBIN_SHA256 3a87874e57b9d92e223f3e90356aaea994af67fb76b71bb72abfb809e948d0d6 -# Download and install glibc (https://github.com/jeanblanchard/docker-alpine-glibc/blob/master/Dockerfile) -RUN wget -O /etc/apk/keys/sgerrand.rsa.pub https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$GLIBC_VERSION/sgerrand.rsa.pub \ - && wget -O glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" \ - && echo "$GLIBC_SHA256 glibc.apk" | sha256sum -c - \ - && wget -O glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" \ - && echo "$GLIBCBIN_SHA256 glibc-bin.apk" | sha256sum -c - \ - && apk add --update --no-cache glibc-bin.apk glibc.apk \ - && /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib \ - && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \ - && rm -rf glibc.apk glibc-bin.apk - -ENV HOME /proxy - -RUN apk add --update --no-cache \ - sqlite \ - jq \ - curl \ - su-exec - -WORKDIR ${HOME} - -COPY app/data/* ./ -COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.7.0 /usr/local/bin/lightning-cli ./ - -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ - && chmod o+w . \ - && mkdir db - -VOLUME ["${HOME}/db", "/.lightning"] - -ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 deleted file mode 100644 index 97ca5ec..0000000 --- a/proxy_docker/Dockerfile.arm32v6 +++ /dev/null @@ -1,35 +0,0 @@ -FROM alpine:3.8 - -# Taking care of glibc shit (glibc not natively supported by Alpine but lightning-cli uses it) - -ENV GLIBC_VERSION 2.27-r0 -# Download and install glibc (https://github.com/jeanblanchard/docker-alpine-glibc/blob/master/Dockerfile) -RUN apk add --update --no-cache wget \ - && wget -O glibc.apk "https://github.com/yangxuan8282/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" \ - && wget -O glibc-bin.apk "https://github.com/yangxuan8282/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" \ - && apk add --allow-untrusted --update --no-cache glibc-bin.apk glibc.apk \ - && /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib \ - && echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf \ - && rm -rf glibc.apk glibc-bin.apk - -ENV HOME /proxy - -RUN apk add --update --no-cache \ - sqlite \ - jq \ - curl \ - su-exec - -WORKDIR ${HOME} - -COPY app/data/* ./ -COPY app/script/* ./ -COPY --from=cyphernode/clightning:v0.7.0 /usr/local/bin/lightning-cli ./ - -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ - && chmod o+w . \ - && mkdir db - -VOLUME ["${HOME}/db", "/.lightning"] - -ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/README.md b/proxy_docker/README.md index 9d4bdd9..60e41c2 100644 --- a/proxy_docker/README.md +++ b/proxy_docker/README.md @@ -48,22 +48,6 @@ OTS_FILES=/proxy/otsfiles XPUB_DERIVATION_GAP=100 ``` -## Choose the right architecture - -...by modifying the following line in Dockerfile: - -```shell -COPY app/bin/lightning-cli_x86 ${HOME}/lightning-cli -``` - -...to lightning-cli_arm if running on a RPi. - -## Building docker image - -```shell -docker build -t btcproxyimg . -``` - ## Create sqlite3 database path and give rights ```shell From 2c3b28bc847764bf035cb690a65b69df429bddd1 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 23 May 2019 12:26:21 -0400 Subject: [PATCH 163/255] Added addresstype when calling getnewaddress. --- doc/API.v0.md | 11 +- proxy_docker/app/script/requesthandler.sh | 3 +- proxy_docker/app/script/walletoperations.sh | 284 ++++++++++---------- 3 files changed, 158 insertions(+), 140 deletions(-) diff --git a/doc/API.v0.md b/doc/API.v0.md index fcfccda..8298218 100644 --- a/doc/API.v0.md +++ b/doc/API.v0.md @@ -554,10 +554,13 @@ Proxy response: ### Get a new Bitcoin address from spending wallet (called by application) -Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor). +Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor). Will derive the default address type (set in your bitcoin.conf file, p2sh-segwit if not specified) or you can supply the address type like the following examples. ```http GET http://cyphernode:8888/getnewaddress +GET http://cyphernode:8888/getnewaddress/bech32 +GET http://cyphernode:8888/getnewaddress/legacy +GET http://cyphernode:8888/getnewaddress/p2sh-segwit ``` Proxy response: @@ -568,6 +571,12 @@ Proxy response: } ``` +```json +{ + "address":"tb1ql7yvh3lmajxmaljsnsu3w8lhwczu963tvjfzpj" +} +``` + ### Spend coins from spending wallet (called by application) Calls sendtoaddress RPC on the spending wallet with supplied info. diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 1206c0f..ceb1216 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -227,8 +227,9 @@ main() ;; getnewaddress) # curl (GET) http://192.168.111.152:8080/getnewaddress + # curl (GET) http://192.168.111.152:8080/getnewaddress/bech32 - response=$(getnewaddress) + response=$(getnewaddress $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) response_to_client "${response}" ${?} break ;; diff --git a/proxy_docker/app/script/walletoperations.sh b/proxy_docker/app/script/walletoperations.sh index fd2b57f..4a62a0d 100644 --- a/proxy_docker/app/script/walletoperations.sh +++ b/proxy_docker/app/script/walletoperations.sh @@ -4,71 +4,71 @@ . ./sendtobitcoinnode.sh spend() { - trace "Entering spend()..." + trace "Entering spend()..." - local data - local request=${1} - local address=$(echo "${request}" | jq ".address" | tr -d '"') - trace "[spend] address=${address}" - local amount=$(echo "${request}" | jq ".amount" | awk '{ printf "%.8f", $0 }') - trace "[spend] amount=${amount}" - local response - local id_inserted + local data + local request=${1} + local address=$(echo "${request}" | jq ".address" | tr -d '"') + trace "[spend] address=${address}" + local amount=$(echo "${request}" | jq ".amount" | awk '{ printf "%.8f", $0 }') + trace "[spend] amount=${amount}" + local response + local id_inserted - response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}") - local returncode=$? - trace_rc ${returncode} - trace "[spend] response=${response}" + response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}") + local returncode=$? + trace_rc ${returncode} + trace "[spend] response=${response}" - if [ "${returncode}" -eq 0 ]; then - local txid=$(echo "${response}" | jq ".result" | tr -d '"') - trace "[spend] txid=${txid}" + if [ "${returncode}" -eq 0 ]; then + local txid=$(echo "${response}" | jq ".result" | tr -d '"') + trace "[spend] txid=${txid}" - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" - trace_rc $? - id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") - trace_rc $? - sql "INSERT OR IGNORE INTO recipient (address, amount, tx_id) VALUES (\"${address}\", ${amount}, ${id_inserted})" - trace_rc $? + # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address + sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + trace_rc $? + id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") + trace_rc $? + sql "INSERT OR IGNORE INTO recipient (address, amount, tx_id) VALUES (\"${address}\", ${amount}, ${id_inserted})" + trace_rc $? - data="{\"status\":\"accepted\"" - data="${data},\"hash\":\"${txid}\"}" - else - local message=$(echo "${response}" | jq -e ".error.message") - data="{\"message\":${message}}" - fi + data="{\"status\":\"accepted\"" + data="${data},\"hash\":\"${txid}\"}" + else + local message=$(echo "${response}" | jq -e ".error.message") + data="{\"message\":${message}}" + fi - trace "[spend] responding=${data}" - echo "${data}" + trace "[spend] responding=${data}" + echo "${data}" - return ${returncode} + return ${returncode} } getbalance() { - trace "Entering getbalance()..." + trace "Entering getbalance()..." - local response - local data='{"method":"getbalance"}' - response=$(send_to_spender_node "${data}") - local returncode=$? - trace_rc ${returncode} - trace "[getbalance] response=${response}" + local response + local data='{"method":"getbalance"}' + response=$(send_to_spender_node "${data}") + local returncode=$? + trace_rc ${returncode} + trace "[getbalance] response=${response}" - if [ "${returncode}" -eq 0 ]; then - local balance=$(echo ${response} | jq ".result") - trace "[getbalance] balance=${balance}" + if [ "${returncode}" -eq 0 ]; then + local balance=$(echo ${response} | jq ".result") + trace "[getbalance] balance=${balance}" - data="{\"balance\":${balance}}" - else - trace "[getbalance] Coudn't get balance!" - data="" - fi + data="{\"balance\":${balance}}" + else + trace "[getbalance] Coudn't get balance!" + data="" + fi - trace "[getbalance] responding=${data}" - echo "${data}" + trace "[getbalance] responding=${data}" + echo "${data}" - return ${returncode} + return ${returncode} } getbalancebyxpublabel() { @@ -100,8 +100,8 @@ getbalancebyxpub() { trace "[getbalancebyxpub] event=${event}" local addresses local balance - local data - local returncode + local data + local returncode # addresses=$(./bitcoin-cli -rpcwallet=xpubwatching01.dat getaddressesbylabel upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb | jq "keys" | tr -d '\n ') data="{\"method\":\"getaddressesbylabel\",\"params\":[${xpub}]}" @@ -112,127 +112,135 @@ getbalancebyxpub() { data="{\"method\":\"listunspent\",\"params\":[0, 9999999, \"${addresses}\"]}" trace "[getbalancebyxpub] data=${data}" - balance=$(send_to_xpub_watcher_wallet ${data} | jq "[.[].amount] | add | . * 100000000 | trunc | . / 100000000") - returncode=$? + balance=$(send_to_xpub_watcher_wallet ${data} | jq "[.[].amount] | add | . * 100000000 | trunc | . / 100000000") + returncode=$? trace_rc ${returncode} trace "[getbalancebyxpub] balance=${balance}" data="{\"event\":\"${event}\",\"xpub\":\"${xpub}\",\"balance\":${balance}}" - echo "${data}" + echo "${data}" return ${returncode} } getnewaddress() { - trace "Entering getnewaddress()..." + trace "Entering getnewaddress()..." - local response - local data='{"method":"getnewaddress"}' - response=$(send_to_spender_node "${data}") - local returncode=$? - trace_rc ${returncode} - trace "[getnewaddress] response=${response}" + local address_type=${1} + trace "[getnewaddress] address_type=${address_type}" - if [ "${returncode}" -eq 0 ]; then - local address=$(echo ${response} | jq ".result") - trace "[getnewaddress] address=${address}" + local response + local data + if [ -z "${address_type}" ]; then + data='{"method":"getnewaddress"}' + else + data="{\"method\":\"getnewaddress\",\"params\":[\"\",\"${address_type}\"]}" + fi + response=$(send_to_spender_node "${data}") + local returncode=$? + trace_rc ${returncode} + trace "[getnewaddress] response=${response}" - data="{\"address\":${address}}" - else - trace "[getnewaddress] Coudn't get a new address!" - data="" - fi + if [ "${returncode}" -eq 0 ]; then + local address=$(echo ${response} | jq ".result") + trace "[getnewaddress] address=${address}" - trace "[getnewaddress] responding=${data}" - echo "${data}" + data="{\"address\":${address}}" + else + trace "[getnewaddress] Coudn't get a new address!" + data="" + fi - return ${returncode} + trace "[getnewaddress] responding=${data}" + echo "${data}" + + return ${returncode} } addtobatching() { - trace "Entering addtobatching()..." + trace "Entering addtobatching()..." - local address=${1} - trace "[addtobatching] address=${address}" - local amount=${2} - trace "[addtobatching] amount=${amount}" + local address=${1} + trace "[addtobatching] address=${address}" + local amount=${2} + trace "[addtobatching] amount=${amount}" - sql "INSERT OR IGNORE INTO recipient (address, amount) VALUES (\"${address}\", ${amount})" - returncode=$? - trace_rc ${returncode} + sql "INSERT OR IGNORE INTO recipient (address, amount) VALUES (\"${address}\", ${amount})" + returncode=$? + trace_rc ${returncode} - return ${returncode} + return ${returncode} } batchspend() { - trace "Entering batchspend()..." + trace "Entering batchspend()..." - local data - local response - local recipientswhere - local recipientsjson - local id_inserted + local data + local response + local recipientswhere + local recipientsjson + local id_inserted - # We will batch all the addresses in DB without a TXID - local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL') - trace "[batchspend] batching=${batching}" + # We will batch all the addresses in DB without a TXID + local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL') + trace "[batchspend] batching=${batching}" - local returncode - local address - local amount - local notfirst=false - local IFS=$'\n' - for row in ${batching} - do - trace "[batchspend] row=${row}" - address=$(echo "${row}" | cut -d '|' -f1) - trace "[batchspend] address=${address}" - amount=$(echo "${row}" | cut -d '|' -f2) - trace "[batchspend] amount=${amount}" + local returncode + local address + local amount + local notfirst=false + local IFS=$'\n' + for row in ${batching} + do + trace "[batchspend] row=${row}" + address=$(echo "${row}" | cut -d '|' -f1) + trace "[batchspend] address=${address}" + amount=$(echo "${row}" | cut -d '|' -f2) + trace "[batchspend] amount=${amount}" - if ${notfirst}; then - recipientswhere="${recipientswhere}," - recipientsjson="${recipientsjson}," - else - notfirst=true - fi + if ${notfirst}; then + recipientswhere="${recipientswhere}," + recipientsjson="${recipientsjson}," + else + notfirst=true + fi - recipientswhere="${recipientswhere}\"${address}\"" - recipientsjson="${recipientsjson}\"${address}\":${amount}" - done + recipientswhere="${recipientswhere}\"${address}\"" + recipientsjson="${recipientsjson}\"${address}\":${amount}" + done - response=$(send_to_spender_node "{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}]}") - returncode=$? - trace_rc ${returncode} - trace "[batchspend] response=${response}" + response=$(send_to_spender_node "{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}]}") + returncode=$? + trace_rc ${returncode} + trace "[batchspend] response=${response}" - if [ "${returncode}" -eq 0 ]; then - local txid=$(echo "${response}" | jq ".result" | tr -d '"') - trace "[batchspend] txid=${txid}" + if [ "${returncode}" -eq 0 ]; then + local txid=$(echo "${response}" | jq ".result" | tr -d '"') + trace "[batchspend] txid=${txid}" - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -eq 0 ]; then - id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") - trace "[batchspend] id_inserted: ${id_inserted}" - sql "UPDATE recipient SET tx_id=${id_inserted} WHERE address IN (${recipientswhere})" - trace_rc $? - fi + # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address + sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") + trace "[batchspend] id_inserted: ${id_inserted}" + sql "UPDATE recipient SET tx_id=${id_inserted} WHERE address IN (${recipientswhere})" + trace_rc $? + fi - data="{\"status\":\"accepted\"" - data="${data},\"hash\":\"${txid}\"}" - else - local message=$(echo "${response}" | jq -e ".error.message") - data="{\"message\":${message}}" - fi + data="{\"status\":\"accepted\"" + data="${data},\"hash\":\"${txid}\"}" + else + local message=$(echo "${response}" | jq -e ".error.message") + data="{\"message\":${message}}" + fi - trace "[batchspend] responding=${data}" - echo "${data}" + trace "[batchspend] responding=${data}" + echo "${data}" - return ${returncode} + return ${returncode} } create_wallet() { From ed71a2ed8fd09ea3a11fa7c997243e6e9a6b3c5d Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 17 May 2019 18:29:06 -0400 Subject: [PATCH 164/255] First draft of the pub/sub notifier --- build.sh | 12 +-- dist/setup.sh | 19 +++-- .../generators/app/index.js | 2 + .../installer/docker/docker-compose.yaml | 23 ++++++ notifier_docker/Dockerfile | 17 +++++ notifier_docker/script/requesthandler.sh | 38 ++++++++++ notifier_docker/script/response.sh | 22 ++++++ notifier_docker/script/startnotifier.sh | 5 ++ notifier_docker/script/trace.sh | 15 ++++ notifier_docker/script/web.sh | 76 +++++++++++++++++++ proxy_docker/Dockerfile | 3 + proxy_docker/app/script/callbacks_job.sh | 9 ++- proxy_docker/app/script/ots.sh | 9 ++- 13 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 notifier_docker/Dockerfile create mode 100644 notifier_docker/script/requesthandler.sh create mode 100644 notifier_docker/script/response.sh create mode 100644 notifier_docker/script/startnotifier.sh create mode 100644 notifier_docker/script/trace.sh create mode 100644 notifier_docker/script/web.sh diff --git a/build.sh b/build.sh index 59516e3..1532442 100755 --- a/build.sh +++ b/build.sh @@ -6,6 +6,7 @@ TRACING=1 CONF_VERSION="v0.2.0-local" GATEKEEPER_VERSION="v0.2.0-local" PROXY_VERSION="v0.2.0-local" +NOTIFIER_VERSION="v0.2.0-local" PROXYCRON_VERSION="v0.2.0-local" OTSCLIENT_VERSION="v0.2.0-local" PYCOIN_VERSION="v0.2.0-local" @@ -34,11 +35,12 @@ build_docker_images() { docker build install/ -t cyphernode/cyphernodeconf:$CONF_VERSION trace "Creating cyphernode images" - docker build api_auth_docker/ -t cyphernode/gatekeeper:$GATEKEEPER_VERSION - docker build proxy_docker/ -t cyphernode/proxy:$PROXY_VERSION - docker build cron_docker/ -t cyphernode/proxycron:$PROXYCRON_VERSION - docker build pycoin_docker/ -t cyphernode/pycoin:$PYCOIN_VERSION - docker build otsclient_docker/ -t cyphernode/otsclient:$OTSCLIENT_VERSION + docker build api_auth_docker/ -t cyphernode/gatekeeper:$GATEKEEPER_VERSION \ + && docker build proxy_docker/ -t cyphernode/proxy:$PROXY_VERSION \ + && docker build notifier_docker/ -t cyphernode/notifier:$NOTIFIER_VERSION \ + && docker build cron_docker/ -t cyphernode/proxycron:$PROXYCRON_VERSION \ + && docker build pycoin_docker/ -t cyphernode/pycoin:$PYCOIN_VERSION \ + && docker build otsclient_docker/ -t cyphernode/otsclient:$OTSCLIENT_VERSION } build_docker_images diff --git a/dist/setup.sh b/dist/setup.sh index f84e6b7..455bab7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -189,6 +189,7 @@ configure() { -e VERSION_OVERRIDE=$VERSION_OVERRIDE \ -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ -e PROXY_VERSION=$PROXY_VERSION \ + -e NOTIFIER_VERSION=$NOTIFIER_VERSION \ -e PROXYCRON_VERSION=$PROXYCRON_VERSION \ -e OTSCLIENT_VERSION=$OTSCLIENT_VERSION \ -e PYCOIN_VERSION=$PYCOIN_VERSION \ @@ -711,14 +712,15 @@ AUTOSTART=0 # CYPHERNODE VERSION "v0.1.1" VERSION_OVERRIDE="true" -CONF_VERSION="v0.2.0-test" -GATEKEEPER_VERSION="v0.2.0-test" -PROXY_VERSION="v0.2.0-test" -PROXYCRON_VERSION="v0.2.0-test" -OTSCLIENT_VERSION="v0.2.0-test" -PYCOIN_VERSION="v0.2.0-test" -BITCOIN_VERSION="v0.17.1-test" -LIGHTNING_VERSION="v0.7.0-test" +CONF_VERSION="v0.2.0" +GATEKEEPER_VERSION="v0.2.0" +PROXY_VERSION="v0.2.0" +NOTIFIER_VERSION="v0.2.0" +PROXYCRON_VERSION="v0.2.0" +OTSCLIENT_VERSION="v0.2.0" +PYCOIN_VERSION="v0.2.0" +BITCOIN_VERSION="v0.17.1" +LIGHTNING_VERSION="v0.7.0" SETUP_DIR=$(dirname $(realpath $0)) @@ -796,6 +798,7 @@ if [[ $nbbuiltimgs -gt 1 ]]; then CONF_VERSION="$CONF_VERSION-local" GATEKEEPER_VERSION="$GATEKEEPER_VERSION-local" PROXY_VERSION="$PROXY_VERSION-local" + NOTIFIER_VERSION="$NOTIFIER_VERSION-local" PROXYCRON_VERSION="$PROXYCRON_VERSION-local" OTSCLIENT_VERSION="$OTSCLIENT_VERSION-local" PYCOIN_VERSION="$PYCOIN_VERSION-local" diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index aafc2c8..5a83b6a 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -225,6 +225,7 @@ module.exports = class extends Generator { if( versionOverride ) { delete this.props.gatekeeper_version; delete this.props.proxy_version; + delete this.props.notifier_version; delete this.props.proxycron_version; delete this.props.pycoin_version; delete this.props.otsclient_version; @@ -464,6 +465,7 @@ module.exports = class extends Generator { default_username: process.env.DEFAULT_USER || '', gatekeeper_version: process.env.GATEKEEPER_VERSION || 'latest', proxy_version: process.env.PROXY_VERSION || 'latest', + notifier_version: process.env.NOTIFIER_VERSION || 'latest', proxycron_version: process.env.PROXYCRON_VERSION || 'latest', pycoin_version: process.env.PYCOIN_VERSION || 'latest', otsclient_version: process.env.OTSCLIENT_VERSION || 'latest', diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index 6776c45..b6a9c64 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -167,6 +167,29 @@ services: restart: always <% } %> + broker: + image: eclipse-mosquitto:1.6 +# deploy: +# placement: +# constraints: [node.hostname==dev] +# ports: +# - "1883:1883" +# - "9001:9001" + networks: + - cyphernodenet + restart: always + + notifier: + image: cyphernode/notifier:<%= notifier_version %> + command: $USER ./startnotifier.sh +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + - cyphernodeappsnet + restart: always + networks: cyphernodenet: external: true diff --git a/notifier_docker/Dockerfile b/notifier_docker/Dockerfile new file mode 100644 index 0000000..3da0f2b --- /dev/null +++ b/notifier_docker/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-mosquitto:1.6 + +ENV HOME /notifier + +RUN apk --no-cache --update add jq curl su-exec + +WORKDIR ${HOME} + +COPY script/* ./ + +RUN chmod +x startnotifier.sh requesthandler.sh \ + && chmod o+w . + +ENTRYPOINT ["su-exec"] + +# docker run --rm -d -p 1883:1883 -p 9001:9001 --network cyphernodenet --name broker eclipse-mosquitto +# docker run --rm -it --network cyphernodenet --name mq1 mqtt-client diff --git a/notifier_docker/script/requesthandler.sh b/notifier_docker/script/requesthandler.sh new file mode 100644 index 0000000..51a14fe --- /dev/null +++ b/notifier_docker/script/requesthandler.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +. ./trace.sh +. ./web.sh +. ./response.sh + +main() { + trace "Entering main()..." + + local msg + local cmd + local response + local response_topic + + while read msg; do + trace "[main] msg=${msg}" + + cmd=$(echo ${msg} | jq ".cmd" | tr -d '"') + trace "[main] cmd=${cmd}" + + response_topic=$(echo ${msg} | jq '."response-topic"' | tr -d '"') + trace "[main] response_topic=${response_topic}" + + case "${cmd}" in + web) + response=$(web "${msg}") + publish_response "${response}" "${response_topic}" ${?} + trace "[main] PR" + ;; + esac + trace "[main] case finished" + done +} + +export TRACING=1 + +main +exit $? diff --git a/notifier_docker/script/response.sh b/notifier_docker/script/response.sh new file mode 100644 index 0000000..288687d --- /dev/null +++ b/notifier_docker/script/response.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +. ./trace.sh + +publish_response() { + trace "Entering publish_response()..." + + local response=${1} + local response_topic=${2} + local returncode=${3} + + trace "[publish_response] response=${response}" + trace "[publish_response] response_topic=${response_topic}" + trace "[publish_response] returncode=${returncode}" + + trace "[publish_response] mosquitto_pub -h broker -t \"${response_topic}\" -m \"${response}\"" + mosquitto_pub -h broker -t "${response_topic}" -m "${response}" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} diff --git a/notifier_docker/script/startnotifier.sh b/notifier_docker/script/startnotifier.sh new file mode 100644 index 0000000..64bc336 --- /dev/null +++ b/notifier_docker/script/startnotifier.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./trace.sh + +mosquitto_sub -h broker -t notifier | ./requesthandler.sh diff --git a/notifier_docker/script/trace.sh b/notifier_docker/script/trace.sh new file mode 100644 index 0000000..4c0a1c2 --- /dev/null +++ b/notifier_docker/script/trace.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "$(date -Is) $$ ${1}" 1>&2 + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "$(date -Is) $$ Last return code: ${1}" 1>&2 + fi +} diff --git a/notifier_docker/script/web.sh b/notifier_docker/script/web.sh new file mode 100644 index 0000000..f35d39f --- /dev/null +++ b/notifier_docker/script/web.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +. ./trace.sh + +web() { + trace "Entering web()..." + + local msg=${1} + local url + local body + local returncode + local http_code + local result + + trace "[web] msg=${msg}" + url=$(echo ${msg} | jq ".url") + trace "[web] url=${url}" + + body=$(echo ${msg} | jq -e ".body") + # jq -e will have a return code of 1 if the supplied tag is null. + if [ "$?" -eq "0" ]; then + # body tag not null, so it's a POST + trace "[web] body=${body}" + else + body= + trace "[web] no body, GET request" + fi + + http_code=$(curl_it "${url}" "${body}") + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + # {"result":"success", "response":""} + result="success" + else + # {"result":"error", "response":""} + result="error" + fi + + echo "{\"result\":\"${result}\",\"http_code\":\"${http_code}\"}" + + return ${returncode} +} + +curl_it() { + trace "Entering curl_it()..." + + local url=$(echo "${1}" | tr -d '"') + local data=${2} + local returncode + + if [ -n "${data}" ]; then + trace "[curl_it] curl -o /dev/null -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d ${data} ${url}" + rc=$(curl -o /dev/null -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d ${data} ${url}) + returncode=$? + else + trace "[curl_it] curl -o /dev/null -w \"%{http_code}\" ${url}" + rc=$(curl -o /dev/null -w "%{http_code}" ${url}) + returncode=$? + fi + trace "[curl_it] HTTP return code=${rc}" + trace_rc ${returncode} + + echo "${rc}" + + if [ "${returncode}" -eq "0" ]; then + if [ "${rc}" -lt "400" ]; then + return 0 + else + return ${rc} + fi + else + return ${returncode} + fi +} diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index fad4969..de7a270 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -13,6 +13,9 @@ WORKDIR ${HOME} COPY app/data/* ./ COPY app/script/* ./ COPY --from=cyphernode/clightning:v0.7.0-test /usr/local/bin/lightning-cli ./ +# COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_sub ./ +# COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_pub ./ +COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_rr ./ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ && chmod o+w . \ diff --git a/proxy_docker/app/script/callbacks_job.sh b/proxy_docker/app/script/callbacks_job.sh index 88425cd..ec3b761 100644 --- a/proxy_docker/app/script/callbacks_job.sh +++ b/proxy_docker/app/script/callbacks_job.sh @@ -237,9 +237,12 @@ curl_callback() { local data=${2} local returncode - trace "[curl_callback] curl -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" - rc=$(curl -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) - returncode=$? + #trace "[curl_callback] curl -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" + #rc=$(curl -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) + #returncode=$? + trace "[curl_callback] mosquitto_rr -h broker -t notifier -e jefsio -m \"{\"response-topic\":\"jefsio\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${data}\"}\"" + rc=$(./mosquitto_rr -h broker -t notifier -e jefsio -m "{\"response-topic\":\"jefsio\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${data}\"}") + rc=$(echo "${rc}" | jq ".http_code") trace "[curl_callback] HTTP return code=${rc}" trace_rc ${returncode} diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index e1fc41c..42d9474 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -202,9 +202,12 @@ serve_ots_backoffice() { trace "[serve_ots_backoffice] url=${url}" # Call back newly upgraded stamps - trace "[serve_ots_backoffice] curl -s -o /dev/null -w \"%{http_code}\" -H \"X-Forwarded-Proto: https\" ${url}" - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Proto: https" ${url}) - returncode=$? + #trace "[serve_ots_backoffice] curl -s -o /dev/null -w \"%{http_code}\" -H \"X-Forwarded-Proto: https\" ${url}" + #rc=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Proto: https" ${url}) + #returncode=$? + trace "[serve_ots_backoffice] mosquitto_rr -h broker -t notifier -e dhtsggs -m \"{\"response-topic\":\"dhtsggs\",\"cmd\":\"web\",\"url\":\"${url}\"}\"" + rc=$(./mosquitto_rr -h broker -t notifier -e dhtsggs -m "{\"response-topic\":\"dhtsggs\",\"cmd\":\"web\",\"url\":\"${url}\"}") + rc=$(echo "${rc}" | jq ".http_code") trace_rc ${returncode} # Even if curl executed ok, we need to make sure the http return code is also ok From ac2b031bf0dbc61761d8c328d396d3836dfd746c Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 20 May 2019 14:33:37 -0400 Subject: [PATCH 165/255] Working version of the async pub/sub notifier for callbacks --- notifier_docker/script/requesthandler.sh | 5 ++- notifier_docker/script/response.sh | 1 + notifier_docker/script/web.sh | 27 ++++++++---- proxy_docker/Dockerfile | 7 ++- proxy_docker/app/script/blockchainrpc.sh | 12 ++++-- proxy_docker/app/script/callbacks_job.sh | 22 ++-------- proxy_docker/app/script/callbacks_txid.sh | 16 +------ proxy_docker/app/script/confirmation.sh | 14 +++--- proxy_docker/app/script/notify.sh | 36 ++++++++++++++++ proxy_docker/app/script/ots.sh | 10 ++--- proxy_docker/app/script/requesthandler.sh | 1 + proxy_docker/app/script/walletoperations.sh | 48 ++++++++++++++++----- 12 files changed, 124 insertions(+), 75 deletions(-) create mode 100644 proxy_docker/app/script/notify.sh diff --git a/notifier_docker/script/requesthandler.sh b/notifier_docker/script/requesthandler.sh index 51a14fe..e150dfa 100644 --- a/notifier_docker/script/requesthandler.sh +++ b/notifier_docker/script/requesthandler.sh @@ -13,6 +13,7 @@ main() { local response_topic while read msg; do + trace "[main] New msg just arrived!" trace "[main] msg=${msg}" cmd=$(echo ${msg} | jq ".cmd" | tr -d '"') @@ -25,14 +26,14 @@ main() { web) response=$(web "${msg}") publish_response "${response}" "${response_topic}" ${?} - trace "[main] PR" ;; esac - trace "[main] case finished" + trace "[main] msg processed" done } export TRACING=1 main +trace "[requesthandler] exiting" exit $? diff --git a/notifier_docker/script/response.sh b/notifier_docker/script/response.sh index 288687d..e11f587 100644 --- a/notifier_docker/script/response.sh +++ b/notifier_docker/script/response.sh @@ -13,6 +13,7 @@ publish_response() { trace "[publish_response] response_topic=${response_topic}" trace "[publish_response] returncode=${returncode}" +# response=$(echo "${response}" | base64 | tr -d '\n') trace "[publish_response] mosquitto_pub -h broker -t \"${response_topic}\" -m \"${response}\"" mosquitto_pub -h broker -t "${response_topic}" -m "${response}" returncode=$? diff --git a/notifier_docker/script/web.sh b/notifier_docker/script/web.sh index f35d39f..5ec6d02 100644 --- a/notifier_docker/script/web.sh +++ b/notifier_docker/script/web.sh @@ -9,7 +9,7 @@ web() { local url local body local returncode - local http_code + local response local result trace "[web] msg=${msg}" @@ -20,13 +20,14 @@ web() { # jq -e will have a return code of 1 if the supplied tag is null. if [ "$?" -eq "0" ]; then # body tag not null, so it's a POST + body=$(echo "${body}" | base64 -d) trace "[web] body=${body}" else body= trace "[web] no body, GET request" fi - http_code=$(curl_it "${url}" "${body}") + response=$(curl_it "${url}" "${body}") returncode=$? trace_rc ${returncode} @@ -38,7 +39,8 @@ web() { result="error" fi - echo "{\"result\":\"${result}\",\"http_code\":\"${http_code}\"}" + echo "${response}" +# echo "{\"result\":\"${result}\",\"http_code\":\"${http_code}\"}" return ${returncode} } @@ -49,20 +51,29 @@ curl_it() { local url=$(echo "${1}" | tr -d '"') local data=${2} local returncode + local response + local rnd=$(dd if=/dev/urandom bs=5 count=1 | xxd -pc 5) if [ -n "${data}" ]; then - trace "[curl_it] curl -o /dev/null -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d ${data} ${url}" - rc=$(curl -o /dev/null -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d ${data} ${url}) + trace "[curl_it] curl -o webresponse-${rnd} -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d ${data} ${url}" + rc=$(curl -o webresponse-${rnd} -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d ${data} ${url}) returncode=$? else - trace "[curl_it] curl -o /dev/null -w \"%{http_code}\" ${url}" - rc=$(curl -o /dev/null -w "%{http_code}" ${url}) + trace "[curl_it] curl -o webresponse-$$ -w \"%{http_code}\" ${url}" + rc=$(curl -o webresponse-${rnd} -w "%{http_code}" ${url}) returncode=$? fi trace "[curl_it] HTTP return code=${rc}" trace_rc ${returncode} - echo "${rc}" + if [ "${returncode}" -eq "0" ]; then + response=$(cat webresponse-${rnd} | base64 | tr -d '"' ; rm webresponse-${rnd}) + else + response= + fi + response="{\"curl_code\":${returncode},\"http_code\":${rc},\"body\":\"${response}\"}" + + echo "${response}" if [ "${returncode}" -eq "0" ]; then if [ "${rc}" -lt "400" ]; then diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index de7a270..7a89aa1 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -13,11 +13,10 @@ WORKDIR ${HOME} COPY app/data/* ./ COPY app/script/* ./ COPY --from=cyphernode/clightning:v0.7.0-test /usr/local/bin/lightning-cli ./ -# COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_sub ./ -# COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_pub ./ -COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_rr ./ +COPY --from=eclipse-mosquitto /usr/bin/mosquitto_rr /usr/bin/mosquitto_sub /usr/bin/mosquitto_pub /usr/bin/ +COPY --from=eclipse-mosquitto /usr/lib/libmosquitto* /usr/lib/ -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh \ +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh tests* \ && chmod o+w . \ && mkdir db diff --git a/proxy_docker/app/script/blockchainrpc.sh b/proxy_docker/app/script/blockchainrpc.sh index 0e7df51..07b9c88 100644 --- a/proxy_docker/app/script/blockchainrpc.sh +++ b/proxy_docker/app/script/blockchainrpc.sh @@ -56,15 +56,21 @@ get_rawtransaction() return $? } -get_transaction() -{ +get_transaction() { trace "Entering get_transaction()..." local txid=${1} trace "[get_transaction] txid=${txid}" + local to_spender_node=${2} + trace "[get_transaction] to_spender_node=${to_spender_node}" + local data="{\"method\":\"gettransaction\",\"params\":[\"${txid}\",true]}" trace "[get_transaction] data=${data}" - send_to_watcher_node "${data}" + if [ -z "${to_spender_node}" ]; then + send_to_watcher_node "${data}" + else + send_to_spender_node "${data}" + fi return $? } diff --git a/proxy_docker/app/script/callbacks_job.sh b/proxy_docker/app/script/callbacks_job.sh index ec3b761..294880a 100644 --- a/proxy_docker/app/script/callbacks_job.sh +++ b/proxy_docker/app/script/callbacks_job.sh @@ -2,6 +2,7 @@ . ./trace.sh . ./sql.sh +. ./notify.sh do_callbacks() { ( @@ -233,28 +234,13 @@ build_callback() { curl_callback() { trace "Entering curl_callback()..." - local url=${1} - local data=${2} local returncode - #trace "[curl_callback] curl -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" - #rc=$(curl -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) - #returncode=$? - trace "[curl_callback] mosquitto_rr -h broker -t notifier -e jefsio -m \"{\"response-topic\":\"jefsio\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${data}\"}\"" - rc=$(./mosquitto_rr -h broker -t notifier -e jefsio -m "{\"response-topic\":\"jefsio\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${data}\"}") - rc=$(echo "${rc}" | jq ".http_code") - trace "[curl_callback] HTTP return code=${rc}" + notify_web "${1}" "${2}" + returncode=$? trace_rc ${returncode} - if [ "${returncode}" -eq "0" ]; then - if [ "${rc}" -lt "400" ]; then - return 0 - else - return ${rc} - fi - else - return ${returncode} - fi + return ${returncode} } case "${0}" in *callbacks_job.sh) do_callbacks $@;; esac diff --git a/proxy_docker/app/script/callbacks_txid.sh b/proxy_docker/app/script/callbacks_txid.sh index e7224d3..9e6c1e1 100644 --- a/proxy_docker/app/script/callbacks_txid.sh +++ b/proxy_docker/app/script/callbacks_txid.sh @@ -113,23 +113,11 @@ build_callback_txid() { curl_callback_txid() { trace "Entering curl_callback_txid()..." - local url=${1} - local data=${2} local returncode - trace "[curl_callback_txid] curl -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" - rc=$(curl -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) + notify_web "${1}" "${2}" returncode=$? - trace "[curl_callback_txid] HTTP return code=${rc}" trace_rc ${returncode} - if [ "${returncode}" -eq "0" ]; then - if [ "${rc}" -lt "400" ]; then - return 0 - else - return ${rc} - fi - else - return ${returncode} - fi + return ${returncode} } diff --git a/proxy_docker/app/script/confirmation.sh b/proxy_docker/app/script/confirmation.sh index c9f769b..1b80951 100644 --- a/proxy_docker/app/script/confirmation.sh +++ b/proxy_docker/app/script/confirmation.sh @@ -112,7 +112,7 @@ confirmation() { else # TX found in our DB. - # 1-conf or executecallbacks on an unconfirmed tx or spending watched address (in this case, we probably missed conf) + # 1-conf or executecallbacks on an unconfirmed tx or spending watched address (in this case, we probably missed conf) or spending to a watched address (in this case, spend inserted the tx in the DB) local tx_blockhash=$(echo "${tx_details}" | jq '.result.blockhash') trace "[confirmation] tx_blockhash=${tx_blockhash}" @@ -130,9 +130,8 @@ confirmation() { raw_tx=readfile('rawtx-${txid}.blob') WHERE txid=\"${txid}\"" trace_rc $? - - id_inserted=${tx} fi + id_inserted=${tx} fi # Delete the temp file containing the raw tx (see above) rm rawtx-${txid}.blob @@ -151,8 +150,8 @@ confirmation() { do watching_id=$(echo "${row}" | cut -d '|' -f1) address=$(echo "${row}" | cut -d '|' -f2) - tx_vout_n=$(echo "${tx_details}" | jq ".result.details[] | select(.address==\"${address}\") | .vout") - tx_vout_amount=$(echo "${tx_details}" | jq ".result.details[] | select(.address==\"${address}\") | .amount") + tx_vout_n=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .vout") + tx_vout_amount=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .amount | fabs" | awk '{ printf "%.8f", $0 }') sql "INSERT OR IGNORE INTO watching_tx (watching_id, tx_id, vout, amount) VALUES (${watching_id}, ${id_inserted}, ${tx_vout_n}, ${tx_vout_amount})" trace_rc $? done @@ -176,13 +175,12 @@ confirmation() { ######################################################################################################## - do_callbacks + ) 201>./.confirmation.lock + do_callbacks echo '{"result":"confirmed"}' return 0 - - ) 201>./.confirmation.lock } case "${0}" in *confirmation.sh) confirmation $@;; esac diff --git a/proxy_docker/app/script/notify.sh b/proxy_docker/app/script/notify.sh new file mode 100644 index 0000000..4fd23d7 --- /dev/null +++ b/proxy_docker/app/script/notify.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +. ./trace.sh + +notify_web() { + trace "Entering notify_web()..." + + local url=${1} + + # Let's encode the body to base64 so we won't have to escape the special chars... + local body=$(echo "${2}" | base64 | tr -d '\n') + + local returncode + local response + local http_code + + trace "[notify_web] mosquitto_rr -h broker -W 5 -t notifier -e \"response/$$\" -m \"{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}\"" + response=$(mosquitto_rr -h broker -W 5 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}") + returncode=$? + trace_rc ${returncode} + + trace "[notify_web] response=${response}" + http_code=$(echo "${response}" | jq ".http_code" | tr -d '"') + trace "[notify_web] http_code=${http_code}" + + if [ "${returncode}" -eq "0" ]; then + if [ "${http_code}" -lt "400" ]; then + return 0 + else + return ${http_code} + fi + else + return ${returncode} + fi + +} \ No newline at end of file diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index 42d9474..46e054e 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -202,17 +202,13 @@ serve_ots_backoffice() { trace "[serve_ots_backoffice] url=${url}" # Call back newly upgraded stamps - #trace "[serve_ots_backoffice] curl -s -o /dev/null -w \"%{http_code}\" -H \"X-Forwarded-Proto: https\" ${url}" - #rc=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Proto: https" ${url}) - #returncode=$? - trace "[serve_ots_backoffice] mosquitto_rr -h broker -t notifier -e dhtsggs -m \"{\"response-topic\":\"dhtsggs\",\"cmd\":\"web\",\"url\":\"${url}\"}\"" - rc=$(./mosquitto_rr -h broker -t notifier -e dhtsggs -m "{\"response-topic\":\"dhtsggs\",\"cmd\":\"web\",\"url\":\"${url}\"}") - rc=$(echo "${rc}" | jq ".http_code") + notify_web "${url}" + returncode=$? trace_rc ${returncode} # Even if curl executed ok, we need to make sure the http return code is also ok - if [ "${returncode}" -eq "0" ] && [ "${rc}" -lt "400" ]; then + if [ "${returncode}" -eq "0" ]; then sql "UPDATE stamp SET calledback=1 WHERE id=${id}" trace_rc $? fi diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index ceb1216..d2093f3 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -379,4 +379,5 @@ export DB_PATH export DB_FILE main +trace "[requesthandler] exiting" exit $? diff --git a/proxy_docker/app/script/walletoperations.sh b/proxy_docker/app/script/walletoperations.sh index 4a62a0d..7c1b134 100644 --- a/proxy_docker/app/script/walletoperations.sh +++ b/proxy_docker/app/script/walletoperations.sh @@ -14,6 +14,8 @@ spend() { trace "[spend] amount=${amount}" local response local id_inserted + local tx_details + local tx_raw_details response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}") local returncode=$? @@ -24,8 +26,23 @@ spend() { local txid=$(echo "${response}" | jq ".result" | tr -d '"') trace "[spend] txid=${txid}" + tx_details=$(get_transaction ${txid} "spender") + tx_raw_details=$(get_rawtransaction ${txid}) + + local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') + local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') + local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') + + local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') + local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') + local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') + tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0) + local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') + local rawtx=$(echo "${tx_details}" | jq '.result.hex') + # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + #sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" trace_rc $? id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") trace_rc $? @@ -127,16 +144,8 @@ getbalancebyxpub() { getnewaddress() { trace "Entering getnewaddress()..." - local address_type=${1} - trace "[getnewaddress] address_type=${address_type}" - local response - local data - if [ -z "${address_type}" ]; then - data='{"method":"getnewaddress"}' - else - data="{\"method\":\"getnewaddress\",\"params\":[\"\",\"${address_type}\"]}" - fi + local data='{"method":"getnewaddress"}' response=$(send_to_spender_node "${data}") local returncode=$? trace_rc ${returncode} @@ -181,6 +190,8 @@ batchspend() { local recipientswhere local recipientsjson local id_inserted + local tx_details + local tx_raw_details # We will batch all the addresses in DB without a TXID local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL') @@ -219,8 +230,23 @@ batchspend() { local txid=$(echo "${response}" | jq ".result" | tr -d '"') trace "[batchspend] txid=${txid}" + tx_details=$(get_transaction ${txid} "spender") + tx_raw_details=$(get_rawtransaction ${txid}) + + local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') + local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') + local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') + + local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') + local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') + local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') + tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0) + local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') + local rawtx=$(echo "${tx_details}" | jq '.result.hex') + # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + #sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" returncode=$? trace_rc ${returncode} if [ "${returncode}" -eq 0 ]; then From 73d7cdf8b25f706af09311822fa77baa727a4c70 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 20 May 2019 15:04:28 -0400 Subject: [PATCH 166/255] More comments, cleaned code --- notifier_docker/script/requesthandler.sh | 2 ++ notifier_docker/script/response.sh | 1 - notifier_docker/script/web.sh | 14 +++----------- proxy_docker/Dockerfile | 4 ++-- proxy_docker/app/script/confirmation.sh | 3 +++ proxy_docker/app/script/notify.sh | 3 +++ proxy_docker/app/script/walletoperations.sh | 12 ++++++------ 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/notifier_docker/script/requesthandler.sh b/notifier_docker/script/requesthandler.sh index e150dfa..916d110 100644 --- a/notifier_docker/script/requesthandler.sh +++ b/notifier_docker/script/requesthandler.sh @@ -12,6 +12,8 @@ main() { local response local response_topic + # Messages should have this form: + # {"response-topic":"response/5541","cmd":"web","url":"2557df870b9a:1111/callback1conf","body":"eyJpZCI6IjUxIiwiYWRkc...dCI6MTUxNzYwMH0K"} while read msg; do trace "[main] New msg just arrived!" trace "[main] msg=${msg}" diff --git a/notifier_docker/script/response.sh b/notifier_docker/script/response.sh index e11f587..288687d 100644 --- a/notifier_docker/script/response.sh +++ b/notifier_docker/script/response.sh @@ -13,7 +13,6 @@ publish_response() { trace "[publish_response] response_topic=${response_topic}" trace "[publish_response] returncode=${returncode}" -# response=$(echo "${response}" | base64 | tr -d '\n') trace "[publish_response] mosquitto_pub -h broker -t \"${response_topic}\" -m \"${response}\"" mosquitto_pub -h broker -t "${response_topic}" -m "${response}" returncode=$? diff --git a/notifier_docker/script/web.sh b/notifier_docker/script/web.sh index 5ec6d02..00277d1 100644 --- a/notifier_docker/script/web.sh +++ b/notifier_docker/script/web.sh @@ -20,6 +20,7 @@ web() { # jq -e will have a return code of 1 if the supplied tag is null. if [ "$?" -eq "0" ]; then # body tag not null, so it's a POST + # The body field has been based-64 to avoid dealing with escaping special chars body=$(echo "${body}" | base64 -d) trace "[web] body=${body}" else @@ -31,16 +32,7 @@ web() { returncode=$? trace_rc ${returncode} - if [ "${returncode}" -eq "0" ]; then - # {"result":"success", "response":""} - result="success" - else - # {"result":"error", "response":""} - result="error" - fi - echo "${response}" -# echo "{\"result\":\"${result}\",\"http_code\":\"${http_code}\"}" return ${returncode} } @@ -55,8 +47,8 @@ curl_it() { local rnd=$(dd if=/dev/urandom bs=5 count=1 | xxd -pc 5) if [ -n "${data}" ]; then - trace "[curl_it] curl -o webresponse-${rnd} -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d ${data} ${url}" - rc=$(curl -o webresponse-${rnd} -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d ${data} ${url}) + trace "[curl_it] curl -o webresponse-${rnd} -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" + rc=$(curl -o webresponse-${rnd} -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) returncode=$? else trace "[curl_it] curl -o webresponse-$$ -w \"%{http_code}\" ${url}" diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index 7a89aa1..e46f981 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -13,8 +13,8 @@ WORKDIR ${HOME} COPY app/data/* ./ COPY app/script/* ./ COPY --from=cyphernode/clightning:v0.7.0-test /usr/local/bin/lightning-cli ./ -COPY --from=eclipse-mosquitto /usr/bin/mosquitto_rr /usr/bin/mosquitto_sub /usr/bin/mosquitto_pub /usr/bin/ -COPY --from=eclipse-mosquitto /usr/lib/libmosquitto* /usr/lib/ +COPY --from=eclipse-mosquitto:1.6 /usr/bin/mosquitto_rr /usr/bin/mosquitto_sub /usr/bin/mosquitto_pub /usr/bin/ +COPY --from=eclipse-mosquitto:1.6 /usr/lib/libmosquitto* /usr/lib/ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh waitanyinvoice.sh tests* \ && chmod o+w . \ diff --git a/proxy_docker/app/script/confirmation.sh b/proxy_docker/app/script/confirmation.sh index 1b80951..48bbfa1 100644 --- a/proxy_docker/app/script/confirmation.sh +++ b/proxy_docker/app/script/confirmation.sh @@ -150,6 +150,8 @@ confirmation() { do watching_id=$(echo "${row}" | cut -d '|' -f1) address=$(echo "${row}" | cut -d '|' -f2) + # In the case of us spending to a watched address, the address appears twice in the details, + # once on the spend side (negative amount) and once on the receiving side (positive amount) tx_vout_n=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .vout") tx_vout_amount=$(echo "${tx_details}" | jq ".result.details | map(select(.address==\"${address}\"))[0] | .amount | fabs" | awk '{ printf "%.8f", $0 }') sql "INSERT OR IGNORE INTO watching_tx (watching_id, tx_id, vout, amount) VALUES (${watching_id}, ${id_inserted}, ${tx_vout_n}, ${tx_vout_amount})" @@ -177,6 +179,7 @@ confirmation() { ) 201>./.confirmation.lock + # There's a lock in callbacks, let's get out of the confirmation lock before entering another one do_callbacks echo '{"result":"confirmed"}' diff --git a/proxy_docker/app/script/notify.sh b/proxy_docker/app/script/notify.sh index 4fd23d7..4715aee 100644 --- a/proxy_docker/app/script/notify.sh +++ b/proxy_docker/app/script/notify.sh @@ -14,11 +14,14 @@ notify_web() { local response local http_code + # We use the pid as the response-topic, so there's no conflict in responses. trace "[notify_web] mosquitto_rr -h broker -W 5 -t notifier -e \"response/$$\" -m \"{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}\"" response=$(mosquitto_rr -h broker -W 5 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}") returncode=$? trace_rc ${returncode} + # The response looks like this: {"curl_code":0,"http_code":200,"body":"..."} where the body + # is the base64(response body) but we don't need the response content here. trace "[notify_web] response=${response}" http_code=$(echo "${response}" | jq ".http_code" | tr -d '"') trace "[notify_web] http_code=${http_code}" diff --git a/proxy_docker/app/script/walletoperations.sh b/proxy_docker/app/script/walletoperations.sh index 7c1b134..56302b7 100644 --- a/proxy_docker/app/script/walletoperations.sh +++ b/proxy_docker/app/script/walletoperations.sh @@ -26,13 +26,14 @@ spend() { local txid=$(echo "${response}" | jq ".result" | tr -d '"') trace "[spend] txid=${txid}" + # Let's get transaction details on the spending wallet so that we have fee information tx_details=$(get_transaction ${txid} "spender") tx_raw_details=$(get_rawtransaction ${txid}) + # Amounts and fees are negative when spending so we absolute those fields local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') - local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') @@ -40,8 +41,7 @@ spend() { local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') local rawtx=$(echo "${tx_details}" | jq '.result.hex') - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - #sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + # Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" trace_rc $? id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"") @@ -230,13 +230,14 @@ batchspend() { local txid=$(echo "${response}" | jq ".result" | tr -d '"') trace "[batchspend] txid=${txid}" + # Let's get transaction details on the spending wallet so that we have fee information tx_details=$(get_transaction ${txid} "spender") tx_raw_details=$(get_rawtransaction ${txid}) + # Amounts and fees are negative when spending so we absolute those fields local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash') local tx_ts_firstseen=$(echo "${tx_details}" | jq '.result.timereceived') local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }') - local tx_size=$(echo "${tx_raw_details}" | jq '.result.size') local tx_vsize=$(echo "${tx_raw_details}" | jq '.result.vsize') local tx_replaceable=$(echo "${tx_details}" | jq '.result."bip125-replaceable"') @@ -244,8 +245,7 @@ batchspend() { local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }') local rawtx=$(echo "${tx_details}" | jq '.result.hex') - # Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address - #sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")" + # Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${rawtx})" returncode=$? trace_rc ${returncode} From ae12e1747da54e18b2219e44072338a7cda4c610 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 21 May 2019 14:47:54 -0400 Subject: [PATCH 167/255] Added timeouts on curl calls --- notifier_docker/script/web.sh | 8 ++++---- proxy_docker/app/script/notify.sh | 4 ++-- proxy_docker/app/script/sendtobitcoinnode.sh | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/notifier_docker/script/web.sh b/notifier_docker/script/web.sh index 00277d1..987ae7d 100644 --- a/notifier_docker/script/web.sh +++ b/notifier_docker/script/web.sh @@ -47,12 +47,12 @@ curl_it() { local rnd=$(dd if=/dev/urandom bs=5 count=1 | xxd -pc 5) if [ -n "${data}" ]; then - trace "[curl_it] curl -o webresponse-${rnd} -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" - rc=$(curl -o webresponse-${rnd} -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) + trace "[curl_it] curl -o webresponse-${rnd} -m 20 -w \"%{http_code}\" -H \"Content-Type: application/json\" -H \"X-Forwarded-Proto: https\" -d \"${data}\" ${url}" + rc=$(curl -o webresponse-${rnd} -m 20 -w "%{http_code}" -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}) returncode=$? else - trace "[curl_it] curl -o webresponse-$$ -w \"%{http_code}\" ${url}" - rc=$(curl -o webresponse-${rnd} -w "%{http_code}" ${url}) + trace "[curl_it] curl -o webresponse-$$ -m 20 -w \"%{http_code}\" ${url}" + rc=$(curl -o webresponse-${rnd} -m 20 -w "%{http_code}" ${url}) returncode=$? fi trace "[curl_it] HTTP return code=${rc}" diff --git a/proxy_docker/app/script/notify.sh b/proxy_docker/app/script/notify.sh index 4715aee..5525151 100644 --- a/proxy_docker/app/script/notify.sh +++ b/proxy_docker/app/script/notify.sh @@ -15,8 +15,8 @@ notify_web() { local http_code # We use the pid as the response-topic, so there's no conflict in responses. - trace "[notify_web] mosquitto_rr -h broker -W 5 -t notifier -e \"response/$$\" -m \"{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}\"" - response=$(mosquitto_rr -h broker -W 5 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}") + trace "[notify_web] mosquitto_rr -h broker -W 21 -t notifier -e \"response/$$\" -m \"{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}\"" + response=$(mosquitto_rr -h broker -W 21 -t notifier -e "response/$$" -m "{\"response-topic\":\"response/$$\",\"cmd\":\"web\",\"url\":\"${url}\",\"body\":\"${body}\"}") returncode=$? trace_rc ${returncode} diff --git a/proxy_docker/app/script/sendtobitcoinnode.sh b/proxy_docker/app/script/sendtobitcoinnode.sh index f1bd46c..b82a641 100644 --- a/proxy_docker/app/script/sendtobitcoinnode.sh +++ b/proxy_docker/app/script/sendtobitcoinnode.sh @@ -58,8 +58,8 @@ send_to_bitcoin_node() local config=${2} local data=${3} - trace "[send_to_bitcoin_node] curl -s --config ${config} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" - result=$(curl -s --config ${config} -H "Content-Type: application/json" -d "${data}" ${node_url}) + trace "[send_to_bitcoin_node] curl -m 20 -s --config ${config} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" + result=$(curl -m 20 -s --config ${config} -H "Content-Type: application/json" -d "${data}" ${node_url}) returncode=$? trace_rc ${returncode} trace "[send_to_bitcoin_node] result=${result}" From 73746710e7f6cf1f586440d0b45e6ee17133fc22 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 21 May 2019 22:59:03 -0400 Subject: [PATCH 168/255] Schema in README, added broker and notifier tests --- README.md | 6 ++- doc/CN-Arch-0.2.1.jpg | Bin 0 -> 74189 bytes .../installer/docker/docker-compose.yaml | 9 ++-- .../app/templates/installer/testfeatures.sh | 48 ++++++++++++++++-- 4 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 doc/CN-Arch-0.2.1.jpg diff --git a/README.md b/README.md index 9abc7e9..ce671e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # cyphernode -Modular Bitcoin full-node microservices API server architecture and utilities toolkit to build scalable, secure and featureful apps and services without trusted third parties. +Modular Bitcoin full-node microservices API server architecture and utilities toolkit to build scalable, secure and featureful apps and services without trusted third parties. # What is cyphernode? @@ -14,9 +14,11 @@ It is currently in production by Bylls.com, Canada's first and largest Bitcoin p The project is in **heavy development** - we are currently looking for reviews, new features, user feedback and contributors to our roadmap. +![image](doc/CN-Arch-0.2.1.jpg) + # Low requirements, efficient use of resources -Cyphernode is designed to be deployed on virtual machines with launch scripts, but with efficiency and minimalism in mind so that it can also run on multiple Rasberry Pi with very low computing ressources (and extremely low if installing pre-synchronized blockchain and pruned). Because of the modular architecture, heavier modules like blockchain indexers are optional (and not needed for most commercial use-cases). +Cyphernode is designed to be deployed on virtual machines with launch scripts, but with efficiency and minimalism in mind so that it can also run on multiple Rasberry Pi with very low computing ressources (and extremely low if installing pre-synchronized blockchain and pruned). Because of the modular architecture, heavier modules like blockchain indexers are optional (and not needed for most commercial use-cases). * For a full-node and all modules: * 350 GB of storage, 2GB of RAM. diff --git a/doc/CN-Arch-0.2.1.jpg b/doc/CN-Arch-0.2.1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0163945e92ccb22aa7fe1869fb051be3d8917982 GIT binary patch literal 74189 zcmeFZ1z1#Ty9T@v5CjBCX;2W5l$X#uNAbyf1tTz6d;cDIqNZAlv}}1n>`l&j4Zo>fO62 zcac$1P*CpOLq)^DyN`j6jzNTji-ku> zK>R1M!2kaw*e`NngL2(LLPA7B`6(B|9Vc)@#707X#CjJ;R1xK^9qwZ`Z&bYJQK=;@ z_o&#F4)OKh4WkiIb1XeM`YGCPlKtld^Z9Q{_BX-)MXqT80}%miJVb0j7`V8mOY=he zYy1Cq9dsXYr8{Mswz}a0HPPGCKZu?C(yq$7u=P_venp`rBIBEAu!ph1fk?8dTfg@U zw@m(CSS6QjFyC4V*u6|Ru$n8>wSskxxe2+X`2q)`^HS2hc;UcbMgFRnzdFNTgXAyG z@RvIL%S`<5+s4#F9p|POhp>!1GHtQ18qr>cts;qz_Dvqas@cX6%2SS@xQLNy=TU~BAAoI4+2Rwra(v3L-Ypu0`#kGl$ zA*+rZERU4u^-DN7`j4O_jsuLUDkX^2_TFlFxSe{(RL;JzmMY=vr#T(E-+1!M!>M2o zC;gf>+BD!m6D#DXS9r<8SsUig8F6#+5Dt(QC2kfeZ@-U_-~Kq5_RDYSznpqCA1st5 zC6x2(`aiyC`u2O@|C>u5qU#DBpR!g|#=ZG8S0deEb?Md?ue_ts1<4$+>b9C0_PWU; zy|swPWuzg;`M7qNH@&?c*(_m2os?AyX}vzIf?irsT&%OecqO4MMd*-0{}MRhsSgKM zV!I)&BpHw)7C1m|bCeEyFvkG%hFqRN!kj5~2y~&*P~q>}unyhHc?URf*jUd52ZV^b zZm&Y%K)50tK;LOyI9bYvEPQIZT(g}{{=i5zvY)i@O31anQ!pcFE}VZ7;np_~GQ_t7 zyG=MfB;3VhV^(yp+#k7pWxi(qr4klU{~tWyNng4omBhB{SY)rO&(n$>#wWD7ys-b9 z21_oqEZhx7&(lm{u8Wi*(rp_v$<7FFJzk!c3ACb(Ms1pS85Ny#M&&Iz{uE{0j6&o; zlC7GEhs<1hF=5)+-#O{2`T=#YYQPw=P|j~X(xy!q|C9yaB65q1Dosmzi21c+bqU9coMTW&U>31``SW19LVF-BnC^dhvy7%z zERW_(qhtKu1t5&Yh+wAG-)LTTEEVuc_;>9Q=jYmL4NHE+x_z&YK)zF3lXbb_sBz^< z^nt}{+MD(?*h?@W__%?6#VT|^FK2Uf+Hl38zV%Jx#v@E2uVnKj_qRvnaG+z7UChXi zsW3EOkuoe;pxNnTxC?AUwDX-Q;#`rt`cgla!Ts~5jG0$_U7WL~3sJmze2#VB%&%bI z$1QCf&u%zdr6X=u6Rt$DsR+eY$-VT$Le}$BtYR_M#CeQ_Qo==3unM!17);4s)}&Iy zWCWt0sVeYA%ZuueXB?Ucl5hE%TQ!Em(0%}wFWOPV-f}I>5F8IH zLnQ9I_{Kw8vwgWMCucuVwuv~$0oEL9CMXMvZJWS~h;E)tonK&pZ`F7o+BC1kQUp0JnZP&{>2z^WC7N ze2k?T4#bsomUKc6N=ZP~o2lt;HAEAubRj>MKX&%9v-I(;V~I_VMcZZyR^DlYp4`N4 z+{vqY|NXh@1zZ!9p=%0*e%+!t0q$jz19S`4Z8c0+Zvmp#f#f-%TU$2@OW)J=&pAIV zh~2rTJ=&cJj~yl4Afkf^=^=LA)A@Msrmo0VbFK( zgT1hM0GZ<2fCJ*3Ruif z=@?)S)Aon-94j5@1<+KN7u(UK#V9VVpJ8WMRHm%JKtSz`Pv4 zets_f?KtA*Q;Xy8YHXD>d3lO+O_v5k8*4hn!^rp(m)6tw-Uks&Q8}Iv8b#s`sX6&A z1zSBgisP_-+>McPfj>@KC_AjP(B9O&=v-cNdijb7_j_0j&n#;~)@_3DMh-O3P3J{~ zX7vkIRirm`%@lO`*UNcC3OV#nKPoQBK3Yh(yyVkU?s1o2QYj9;P;b4Bmf!$N>BSuGga`4)2xd=^9CYLde%Z~anc4TNSdrT6;{xRlb)rZ9$ zGOxqRW6MWV!`%7en`GUal>O6!8TRDvNKtE8KaO3|R3&8%t9^!&dd|6P(AG#g?4T_u zvm04aSeB=m_)JRrL5pA10Q;yt5^oV<&U#~AwLhv!ZIVVo%Y*c8O#_r{1=sgv47@FU z#wI+xmV>HCVU0yO&Pp&c36i>ylVul!(c$3)G4i%p(Mc0T!sWb6WT0aI}959pl zrsXm;y`?Cz%bfW}6vNA-gO@t#!yEgAlOBg#W4AAEpDQf3Xe;4>BGoKEhu(g)GxX`9 zbBu09ptwzeIjh9IM^v>AMAXr?Sns5v=7KS6YeNx+S1!`0d13Nxe2JNvj4y2HY|0wO z8`TR(wvz5C51n(C92vk~I#$3=^{$^X#8!{7Sg-i=f2moQLcnH>Ccs`@-5Y$BWEQWk z{;&$m&dH9c{UB_lckM@Y7xl4o0k6ltft;S(r#;wx&_$9dh69Dlkj=`XEQ@s(U&yi2 zuJF#sg=?m~t?84JruHjxPxi}XO0c#g*Z#K=G7{<_40*=z9O66QzuN+2(nVkE9ai#+ z2Wv) zpeT+9C?lCZVM9ikdJ{FD{4c}sZ^O|*qN(xl?7KRj;A~|~XR6bqrRGgC*JtOPuY-YW z$>!G_A)Q+MFE=C2x!Rx1qP;^9By!ucBsIxI-AmZs*;x31EBmk=Kb&zlp;z^z<1|_% z1koD~+!3lc#sAxo|NUOS@9_sO4`9+8L6BX1&~r&ks72d=I>7zD<|6$+|P@#EpT)s;B)8mC=zL%M*x;D%Mvx}#QO zhy{nb`ph@BUB+i^M2K5^I(BNWQzEhpPegtL?}p&OrbMZL-o>6!#4Up_SMz9&zs9Cp z!MExC)k(c%gDm90kLL9P6?1w+>lO?uNZnH?w|bWo1&=l@)}IbWs|!}k$If2OMU5H7 zdR^d6pR!Ouy2%${jVLgm@r{Zb8qM(Qb29a{wK~_=H{@2XuY29&tHH|MB=q~|mLof8 zX}WsqNO9I{b4xIOy64_;Xajqx8*uv$gbl?jJosO~=Dtj(`rZk_T?Ko_JJQtO1CxK~ zUU}Ss?!5=|ibA#YYeHPNd^+9kbH#}o4~AvOpnUVVYmbJ^!Wg+5?9Af#A|6wRbcp3? z#ZZ~$JS?=C7C|U?bpC*%ctPDuQA7dnk-STb&S{h2LRz2J1%;qgkzMNoElN?I#iVkC z5%nZ@=}SLa|5vVZAF(Sm(@)4HiL{-2ntArSP=KRO`8N({a3D}iGJsQJ^nl5MmqrCs zYr1Fc6~(!q^TQwqze%WGzPV}HV_DlfjKd#ogM;z#1;PR7=AEQG2&90(<2h`7U8%*l z|B=Tx(nBo34Fk;w^dJlPg|W61{F9l^3UZ<+7_Xvj6dvxxXvU73LXN4O^KNJ4Z%IT* z`*{@?96OAsxk)h=T_+7}r{(-ooIMY2a3+qI&rvM=t4%1qA5qX@JZpE_NLeYc(4Nt_ zgy2 zuih3*>_+l74|+;>$Sm~{M9)uKBqB<2#j7K^TvXA}L^%O!l!j!ytB4|`s`#_w-?oSi zlUUtC9yuncY{unK74JM1A@sRN#=a9i*257(<|&M!BWgyF%$(uyq@Z;srfkg+;Ug;# zk3m-jV5hPkHmp#}7C`bbDy)s#(DfJ<$}q&K%OGhbtR9v!%-GoNxBGfQQIjSz&%iLG zexH$j>diP7v7X4awKtNh?nfF~=BG$eLEIADJ;#L_p-!u~J?maPy+8R6%kYv%PCSYe ziQ<3*1g&0-JYa<*`Wjio0iVE(%47518+F0relHK9*Up@$VeTI9*sWNo$v*h-lR6Z6 zR9!Zup=ak{kE)J+CVnz_LpKPG2$%!ht1QXD67u!yuHPM-s}6g$V}+!$JMz z^Mej?h^|N&4*0b!EnFQ<3Lk1ev2r%(a^((T-Y?*Q0~M0?mbu5`4%Wk56PqG})hU<> z!E`Bi{y`*e5y1!&=lqE18X$^2e~F5CTTRc40?L_0mBv@H`LtR;!T|R9dSP5^!Kh)<~@=KzNlG zWw_}SS9=W(9K3I8Z(`IA@+B~@Z;)7(h<^tgsI6Ygh?tdvOAx3fT6^WhrWz=T! zT3s*YC+M*WIi=DhXSq2~mW0LnTS<>MOFsBEvKCQWIWZ?tOT3uX6Zn45<8_0aapWmS zc|dAWm|$B!)B`j`ZjkFsifeTBq{&q08<+?v=Gmd}DM687(FBhPO#J8!^0_}8#{RR4 zPD1I@f`Omgf#-kZc^n`8^gN9JS3QqsP}OA(cZLjch+ut8emtu-@^b{_8Q^3~JGTFF zrcK#7Pu!;!J#f+V;86|ZV&ro!sg;S^m%jM@(nuf%{CiJ3RqLEtmXgFb@6Dy?y$XD0&Ex!du??_};~p^FJtB}n!J$RF> zc#yE3opOIC8CS6T2;jb4W^CRX950&n%HdXT{y{~x}+`bwcQR%W6eb<~xOxIUwIKRMTv}?w4 ziQs51za5cKU*u`1$-2OLiD}B6lsI1BtH_=cr!d$SEG;X|i^~5k8xDe+vcK}Ue1^2D z(PNUSZv#j*VGVB2@))eo8`H=&GkdE6r)WKyd z$5HXB`(NU2Pl}*rA3QG3n-|?8`6tH%Lx|}g`>7HF@9)$k-*u*GYni}^EU9k%gjKFv zyOd1k1;Zx`SeGtL`X_qy3@EdFO^k`Jx@C>|O(PYLQUt3g<>`Aoh%ct+SV*3DfoEZNM=4QLMzXpe{l z2i%3c8Ce8N+j}@q5(C%4bFQMjYG{iI+Ai zvE$!QIfaHAQs|kVLGD}F>uqtplY5IrBZjwsq{F zmB{2Uq16~>V{_M$v5yd=75D2(lATVt=$5$q06Lj63N5`E*G)S8rSqh;bW_ykd3@!? z5K)T$u#ZYKYOb37*2Cj+$HUg|dDXs{JWz4f!#=vxMmDGuFLgve>6E6=qZm|7BCfBrrYE7`l%|S0#Q*uLKR&y0;2}~ zv8!FOyK?mB^`aB|QoKYI*Qoi^SNW49N)0a0wdg8i>u6l7`LMHOrLE7<`o_M!cSPG2 zlItR`aNLGGz8i^;WKiI;5h9@JVU){AusoM;DoULr5bd$e_8r;M$oi%BRpL=}X3bhj ztE`KIZK_F4f+`9ne;fegypN~88x^WwfN#)k)zBJDmk9<_zX4!~4VNJJ{Dip;RwE0V zm+8jDqEC&#YR0}!IDotA@@a6{9qb^5Vjut3lK-qs{}JC;VUH{>=1C848`hJC^GEb% znmZ*$|FJa*p{{_*yNWziz=2&=^g>-FYIS6UZS7>xV*v92t&VZ_p#@1Dw|;gYYX zD9I_rWt@y0GGF&)Elf8Q%%b3kQLeg$cgXAWaQ+OvUd5^SA=W&y_pxif5E=vimfP`E zX@2?GG=F!6O?S51ABm!U*H##F^&FTOKkyNdP51L(c|Jx;IZclGYSDXvb^q2B5hr-W zw=IdXCQ=eB;LOpWQ#LfP>0%T>+0ac6@WkBF`jEq1_joHVQHyTQFtICBLe;azCIet; znZwaOz!fU&frVtH(?K*pJMK}48`Ew#R_u?X;td zX!EtdMUD6y*OJh97Hl{$MLz=vo^x*gdV~XE^Y@VQ_x=BuLQ6bAwSaHU@{GYEUa1c+ zc$Gy7A-7y*0L{+MjWx^Uy&IN)y{tvoR2O&jN|4ubfJ$=dg}uS#9&oVZwzs|0WJg0lLk`WgDDtey0~ zdr0rRDjGf{BzC43>P^l&gGFtq=Fykdfi|P=)gO%j=Ea|(^hW)Er7u`Wpz=}c2Eswd z;Mp~p3jeHG{*mxHqVHvezyu7p-1$i~4PQoXoCLkC;i{ePn#swC($e*dd7@Z$TENAe4Z^s{&$OvG{F z9DfvYT1gCSGW;pf$+P}763V5(p{KwR5=jfbb|i#SEgb|TSXUI7xw>j+#MZ@B!~6VaAszvbM|m%Chn;dR|e31KAnP?(v^2q zdRq;on=%94f z>9R=F4EYr#R;0wzGE#n^*~Qx_x1R_!pn9sAhelZ4t7hI6qo^mRUG#wiD}c|qCV$Hj z-Lg`xpRu#YP+wQ&j);;p^>_m^6nA+J%|l*%fhYa0%Ruyw6jGWtdOUl1lh*4lOnED4xsMzyZQ|VdXh{#m$sy-2Sp;!>~CZ>}Vv4OXR%tG}wb21-?>hjV+1Z!Gj_@sc*VO?8o ze`ie2kEsq9YmunC@zoe!ZIPb@uwN@4Np>{epI9Ho@>e6AcoHeUrupfe++59-ge!3` zVK-RAoeCYDN@Z#^|H=-0^28{%2_kmG^nWS^rXLtLCKXzm96d}O4298lgX&_H_xT^A zi+|8(OqONGSdV;X zSM@o6wFLdtW31fuhU5rjUQK_w{r{bgE%LmKfde#q4{u$jz`#y%(ra>5Yr&tn-r&%@ z>qTh-p-EEjP+osxx~T+jGi^#C)TuRm*Sh<#=#KmIBP@RbcA+mBGuqNt)CW`?k!55Z zD&(SMUimD5bCuGeb&0)4=C}KdmFom2L&Y;S^7_1s>LYOAem#-fqCB$hmih(31un<2 z9QmCYTSW<8#N>~Yz2_G>@77c`9xyJ&V9h4z7~r zIl{2xynJEH1nqu@yV_|NI7#JH;6l;PLbBb+887c4%j8|5dB-CW?RKwbKpM7L^lnsm zb$Y*P7vBlGT9i>5^w)|PsIwb!6~sk9cJiH51P7PJE?gFwTtvu=GsM^8fXb%V=cU)#eZ0X@8a zhxeSS)!}x8iO+3BQYGeUx_bF1V?77X<*im^Wj}xP&3d*1t9_z)gau8+5I)B*18-xH zL04TTr4$lU#?@pNad~sNaBE);vzyFrTiWCA5WZI)Mx`1pwYxsLhh8p+?LRI-8Y!v3 z(D$dZDcebewMP*xOx(Y3Z6v;Xu1uRkQpR z$r;7j~Dhl4)k$;YRFZA)FO{4N`kD$Jbn!A(;!?)5&dtd(4x{jf!u{`jC%R*G0>doZi?F zE!PWoCZnI}AxRs@PgdH0DP9xsnYHS)j}&gu84iglgXyN7yet2e`If3u3))4KkqsNbhTcK&0L%WV1YndI@3-2d>D@sBIO5p|)^tOmedxV#JMacKjq znT7D|WL)}zgK*9Q4!q`~7~os~Pqz^rBJLsz1-ebdt?+LWR1f}`KGu#P-Zo#dWN&tNGK47(yTl9EY+Xg{Rf;w8H_+3YLID%Y?AGjFx%~<=wSL-Rc1+ zgxe+Uw>3I7)J67|+zj!IvHR9gLWN`;jx{nQC6Mg&?Dr7VV|6D18zKHJ9MA!iF zN_d;%!1eT| zY0qQ9HKn%k(-8Ge_B3H4v?5ZjP7@B)V!8;8JO1bI|CwWWriJ=Ms-%NB-pSr8_pUT; zaSCWuBm5SR1_It%JoqGUlha;TACE*uR!W}y{^?v@R^LROY2hBF-=m->QY&`Wxl9X) zq?il8$tXhUCE6``p9$dqPl{lL2ORtz$%M<9ZfW3v_9`66hBWMq?t>i6g~wL80%FtO z{bdt2R!-Os2lY;^{uow&m?qP~>;9enUP=&4+ zV5miq%W?DJ|7Ia|#54_4SK$sTiwC`JQ~Na2j!gtOwbgdAXGh6hgWn$MOO)U=l%I5tu|U zq%OSDbVZx=4z{dhIpsGJqp~%aZeFah5BD8OW46sN+VaOc7Il zL8A+5GxE(=)Q8UQbM&pD_n9$4`)htQyxbr=O0aV@3uo?UU!mWUZ(kt(VHP!`X<`5P zc?hXgai?Q;Gh*y%(JMIM?-2s?2OG8d5tw;53z@S30}a~MXRiNrxJ>N?hFSm6V^DUjKUpRNa$w9B3!R=VMOOF*F+R>00@8s z4<(`OyLzW=!Y~9Z7<0{H_Vbr7B_60edh&#-m!#8^DxeM5^qINRecn{(#sRA5XqE5S zLr-GnYpmFwKE|<;&{-j((7HzacP?3Z{*iZV6nfty1JeOyG!c6^n~0oaR1RB)nK_AQ ziqVOgn&Li9f3PA#6nNvbr*oc2(q*YSmNV=8xZ&jAxvZa(Pck3Gi~b|t)hHC}(GXND z_tJV_rr_~WfpL^TrGu(vp=cN7#hudqvqa;KPK)Jx*3=#%@}y}SZ*F=46v8e-vO4t| zNSjax91sHmhs6hQV0rV`x>m|Si>rg)$#{u72SW+IY-@MD2u>QV(5_**Uu{Q3mu|gl zvhTggV9gTb$yGdTwP$&jldkbtDqR%A#5^0oR3B2$f&>5lJ;t~L>YNt3{SPm!Z?4GY z7VsOX*kY~Tt@=EZ{ILu^lLbrbJoOPufWy7Q_ct|6hS4lc$$iREk#^leJ`H96P7P&} zw_+{Rkaw~y3veB1PFTAeH&VteOO7EO^)fqR)u#YE!3qnmc@nNJ z&W>WV#UoPba|jd4y|8pjlTfVQm{u6;5laAv=KEWIh3JX7RWss8#H7&xZKV9$*PsIU zQ)f>9vpn)&u=@KU&F)j4))p|70B0HPiF%goey5CCX%mjfPoG;6W4n2-y!Vl9W})hr z{>-G}g|n7>fsaq&fMbKqpRe_&ex8~yOv}Q(D5k>eVslB3zs!Pj`K2SmdU|bkGR&Ir zh1L_@x{~Bt(2Gp?AGGW+en7BA!o@LaF1D;5gsPRZ2-`WLUA@mGPgmC7_JIUhy6TlE zH70PznX`0jqHtKS8t?j~Pjgm=;^TfU2}7ja3F0i;x$qw=p5kz@1TxeO4r(Of%i3D| zRD9!u?T`=I;+l2W>3!Co3{bI$n2#r|xg6WET_JTE#0Men>dVI76(K+NLlHh0rK!$* zHAx}&Y3TZQDyOUh)7pq8bWifj=a*f$huU_RVVQ9^^RH^IuZ$a$i`WmHS_z>~mZdoT zCK5K=3mX+HeUK?zG_aMm53onud0-sjhOU0q12Hm&ug!ndp8ws?^obV><)Etz ztA+!4H%)epaNtJxhHJV;<)HC4>l&oqyKeR7Zo)t-_XB)a4gqoqoa)sxePEa)S|iSSx?<+f`Ka9*B@K zHqAVrS1snN9`&6L>TlH{^3|8;e?u5!?H*CnHQz;4@TDbRa#6;}XMKHnQ$XMB1J1Wj zif7hWJTd${JXaCa_QA99dZTY@ljdEfQG94WPn#xjOnsW|-Mv#xMAX95FCieExmU$s z=&M73-ENtQ^MEz(N13f+ji)|q_qrFa5%D(7$Hz5e`%J#9jdn;S#l8c#)7dc_%zU^P zp>Km$Q*(#sEaD$uoqp!zHe)=qqIs%cTA?{2-6y|>E-k9ZIu&QDe&P02c)nALCk0b& zb^eP*j0snDt*bP88(F)$vF3tdl0Jdr#$!m{qW}bNQXkQ1+-0JA@pU2=#_p+!^J8(2 zl5#y0B1v2KlzC%lnfnjL0n~y1rqKMbRa;$s>ga*#Eu&C@pqt4kTkSW~DJfT9h~{SS z`7Vp({eP@ALCW;q0-MoB-ad*Y3sv8{g_+$lIkh{%t2H`!4vNF<(<|1apc& zN@9}Y4C1uUy#NPH;DGUr2(q#Hv|-h;rjFKZM#J3KPK_j~(7<=0AK3hroDGJ;MXt&C zz_|Iv;r3=3k$BJNgddOGGiQsVrHN{fVxF#4@+J7Mv?bl1gHzi5)uUQY2H(j-}f*itB zm*VKCJ>p7wQ}DZ$+39xkd$W=x{ZRBiKn!++gLtfW1Q?Vinx=1 zyPf^YB4BR@70qeMmWsX&mtra^IK7I}H;Pt|rJ{Lp6{vbw@i?*zZGjUIw_8-JuWN|g zQ9dD9=FcAgK-;hS*&C?QM|}mP0MB>_wMO}bawpGggT{7YGk7*gA2hk+sV#e_(AU>A z@OP@i<;fTl$(fE2r#uSOU!hGKUI&U{eDKBv3cMvzmF1bCmhJI;d{&F5D zN8cEQoN4%n>tJXKucmHM zoTGtuc(^ilRGpm``caSy%r{)P<{GIr2mbMp@qgEI_(z`iU-m)}6m@1>j4YV?$9gcZ zpSn=n`+?J#%(c3YnEX03QhSio(g$M1_$2ncX=CZ7i<4$5t)QlN){}E)%>2 zletg=uTO37k)K&t7vbpY&CY1*Xiv|Y)rOW<_=ww4i~pce$QYB@#q31AwGW&LPS4s> z;tpoP>5_Y6Sz+qc#9SSWToJ6}#E9HfO5$ zcg>Vu7%SQPKS2t$v4W>2uKA&Q>1zsadK==YHw4t~sFq zJcRKuzIMsA5PlCIt^k{l)l+logg05$TXNN<6?NF?Urdc3R7XFeep|`2*(Qj3hTls2 zp@P8zm0)7rAVHQ*s>rbyQJ(e-E|T#ebxN%0CZYkQO1fy>Nu1h{W_fZIM_#&rz=io6 zmHSnY_=`5Kp;_y>2`|1rq>&0H=K1A`>ABrCuWw>KfQxKsA?lLcUC9cXPt)S%7g}m@ zam9kEYDv@c&wSWcC?f$JkQvO-aA2GIgu;K?>5lI@mE(Zxc%BuaqK^l?-M2EC`)#qo5E?0~Hx~Z3DF?IB zkFk9|lOnGIA}A1r*InyVI+Cj{H_JxHPJ1V8Sba%9wi1$3{;p2oK*;(xlIl@d|7_Le zE?<)n`6mrA&5y2ew#J2WTQxtBx+ebqE22d1WRQP(ebGUj8Ay9L(wIn!Bam#Ej7g0l zLea2SS^Ej9mm-_fk=cVk{!4s{!EeAgrM4uw;$hwh#gg$r=H<XToMOgN*p zl&ZvZn=DY?WZ3~;D2Toqr5ZC^G4AZ8<&8HE<;vZ~vn#jyE*L5imijU3a3EqZgyI}E z1P(|WrC^RqPVQ}tp7Pi*M{c|NMaw-#T!%ywSYrCp7Tu|0=xZeZ%weUE%2^sczeDVf zYc|>9=pRI-GXqFZD<9Zhc=xQ=?z!$eSM2doM@4g#)z>%kU{ULHstTp!lyv3#PJ8k3 zF$`L{>*WZ3ls&&8f(_dkG0c8*mJ#FlLilX1yLpA${_CopmX>2hp1D>=#s?=ymSp+=FZWSw zsEtG^qdJjY?*sZhV0s@GNXQneUckc23lnNS|Jh_HAn5#(r=FMc3lukoF!V1|cJP2- zfHn)T_@6V`hfqXSDy+2n#72$A9Ee`2mEdFak?{6CQ$XMX&;E zVr)9irI-Do1HOfqj4!oeo*<5TsO$tz?aT%V?=MR>S65ANphXsR+=z>K)9OK}y|1L_ zEPm?L=2Ekf07PEDfMFNj92_{x{q+iY1E%RarMncG9uX4%>SF0I0vm9${5l*YaFDqn zQ1klrV4*tZmcK6jSB3mjcl)b{{4)Ojs*t}br1RfZs<|gniNo>!{;$tw&5IabIZ6`5 zJDeml^&q*pybb+|k%X{$ukddEX45er$Z9u%+|G#R_SW(mZ}wESKIymE2vLwuYXiBK zWZj>1niEK;MO?n(7`!^00IhcBzj5R+Xzbj5fy9t__koI)2%NO3iG$sk$#MVbASk3PtRDJcYYhP#R(r=ru@pQT?R5R3NQyGa-Y8RUjM2 z53*s}WB;uplSkIEkJ$t>%X{09<@}sQ$&d|=3MtTO3Vbx*n|$UdY!4D9ABD#vU7Z!O+t$ehG4QIVSusX8TVE^1RbPIOVHb;>E8I_jjHa?;Xn6%IrbYlF_9e%##!A zZVMG;zs)cnV7AzM{Tcv(wVOXcM^kCP)kFA(X{4vJ7Yg4o*14a;>Ik-ARYInkq z05lLyk7f>jhCq3@KxW43d0#6rLBipzG+PHAH@k?j0STkz*B8D0afCO`7(T8FC|YlW zf(gRzMeO6GuU)&UuvH}JFydT2wkZHd8v})149_weQeshEOXw;B7BL_A$&b zMq9hrYC<{2hU5X^9_cQ5+h~U3c(T}M(rxa_5)!WI5zc0@dr@eMUyov~5f%^anhTv2 zN9$j@>)GUpzs(v@(^Q*QtBXWUJN`uHfbCvm5G~3}jp_9^fhjXDZ7`4Q<#_5zJN3M# zO15sCASvQ^HApx7TIdc}a=hqIWZ`y3AyX>OJZl}fr2~oc4 z7eHG34x6A*bBfESbw8Q%L3a_*c})gYRT2NfE#`D_?nmqJS{@@Mf&Qc(12>x-4Q)Lw zsV9{+LB{?fsI#F6%D#h?!JWS(NRuP|Toz=h=muE}OsUIA|1URHXFSjj9>80T{5)65_1Pdqe7eNekO9xAYD7!VCugf}F}GTfL*2L!R` zzW(m^-H}TuEaq}7OS;77p|;tHM5%UL2Az~%tW}{1wNocVtI4>^vQW%Pdu$-)n~Zx& z&`Uf7nKxtu4~tgR+n!S2MG8+Lk8RP*4rwEQROMpo-F1^ z8Nb4WRZ$^TNrlBkZ6wpmY-}8XTB{e=R|M})mjaja%QFfh)Cwe=w5_PJ4k!l9;GLwq z;n3ETIuxYZL|zBQ=w^i31c!G=TaHfi%wr?lNC|WWi!->PDc#m~qm-CJnPmn{Z~*lz z%-e||B9HT1GAkTVAuIlT%7eJo!D5{Jx}|5`iN1G*zBxj{LRfoTxRfI#Z(KZfQDI)w z(u5joM^j>&iK>I(a4V8pTJT!|2X-j4N?1XJPBgm)cY?#mWK0SbBV`EvJ?fglf$swd zNEc)!s%U2A^8;<6_~V0OU|zdEa7GhP>A=~&JuDAdo~*PWv6xCNR}Uxo{E}|A{UM@R z&mxw&&yH-J?~4&emMN^ugtY8%r5Szx;<{?VdlWBYrOmQy2oe(0N-G^BZS7jp-|f#t z)e&{N6dE%~y)Po@AxOUSJlZVmQ825Yi8?^)dMs{Wre&P&k*T3KqdCp=Bg(|$we1@^TdS^%=(wtWh@f9glk z&@fJOVUgH$4QOp@JUui0@it>@X|wPIhTjzcsmX>cc0ypx2@9AUjGp_lFNHHs!@voZ zxTpB^*~PJTnp6R>s^t#bAz00G?)j&cvgV5O!ZQaSkN<=uE_B8dGN}ffVH*fqgm(o zGjyd2hi#5n*L|PH$w5Q_*$xi4C+Hfq^BrFfyYr`MnUxBkJIy4>{Ln?YZv7%LF0@bp zIGm~{&x6@3N)gT1l@;WlZ7@^QbvY}}zKz7`_Jw|YbJxB`s{5>PxufuP=fnC3A^r-6 z4>sNi{aE*6{3B+dh7~TnntT}pYjFZ+!sodbyMs3e-I(^|bM${&bA;SO98ayQ`fuIf z4~rH#{A(K?bP2kL|Mm2WJcZe zQmVAR15%x4BrSGdNYL%vm+~#&Rs@R@-W*Aj20bEzoyx@Cn%+5`XBe^SI!M~)dO#8{ zQTmC)?oM;r{Tjt%Zao{t&(=_xj32YN3DVzOE!{8=ZZK|99nC&w**>EgD8 z){l_PB3(l_@DPU0V2?)~WHop92s9~nBj@fx{ZWT&JepVuQC0=WugF9@$KPzu6B9pK zwb!1DmoE{wVCx~*5+jjnX+c7!?!6)uED2RR`)wMR%RxKV711fh^$F;>5AEflRe4fE zx(7jbZZh!z7GR~J`pr@B746}m%pol}1?fJ>d`^NhkeJB%e;U}|*WF5QP6o5fIDclB z>D}U$JO617oHBv3ls^v2UlFapzc$04?W?ZCJ7HYNsyLV#QPjtBOCOvGLC32!`8r|L z>76RabL6v?@m~$x5rh*X!O|JO)5olsoam9 zLG-$JB|_A*Z(2VbuW%%vY=`*etvl>*XfJ?cz5?b-!BpK8b-Aa(ZqN4f4dBLG>r{Co_Q38$XpKxvs$}fDmBT$Y$;4=? zQnrok-blN>mkHv*+S)UL5GNbD1WMf?tU+E{+MFGA(~R$X7Jl>XcSjK2S=^gWy@>*a z6AyBqwXGn3O5rm4~epJ;}qPrvYTJ{iNXWEKHx zyL$;Szj*QR47%2d<9)HQd%g4rsQ#`~pztbazjp@oZh^`CjHV!ta_kvdlRWK|R`g0! z8pOGAO%Oe8R-e5nqB%l=hPYKgu80hlpbt0J&6i;JP5MD3Sf-mLt^P~=D7?g6EWEv2Q6-c-H7NrcG6@C=Sp(S* zQB1d`57ya;6~M)cC={cvf{pfTGPagrfSpnie+0LMnMq~5hP~;C4=M)g2E6)BxjH$!br{_jDmn5K~O+IKyuDGj!G2CIU_me z3^TL8Vn@V%_BrRdzxzD*`$OsK>FVyPuBx@xyWX|R-Njn_eOy$4fFJ6fW74V^W6hyq zetr*N=Y5lpYTd#;e*ByRB`%wK09SxVliZ9D)=KlmG78p0&?U4T1l5DHslCDg9A00O}*R%6-_ zhyrQw-(T5M|C{5p5>fj{xM5kFx?cKSEsq=0Y&p7Eh{(qya}<_Y{33-;+#UVJE(7{z z&gFoqVeDoy?DuqWM2!oxcUo-hxNs?@+!+#GMx${9i(wbFEcU5~9t=Bn?9H_J(^NVe z>8lw%n5bMyHvULh1tT1<*nHe?Nv!Q5YPvDpT84KgEQ*eM294qx_Z$0F{dWt6bad$^ zkM6Qu@e;+ki{lep?N3ki=H6!g4QZJ#;eiw{F|HDeRzAdxJD89x+-Umz3)(g929d(A zwo!BC6*ea2QDe@W3tTn==pXa$$ouHA>NfbR8ID(7b$(>#E$^N-WTC{W(eJdclx2k_ zBKMtI7jNLLvd)Nv`FRP;D>h%eu_-JFRo%~h`gj%ZnS7S{5ruL}TXNggSCz$$lUi|3 zH={x?85UD{dk4Po)>fFZl&~&aEjebHP%tV*>9{s4GkDCFJ@PJETcu_}kJn>?VJ)if zKWwo~ilXs1@?r|rl_VUBw1SX~y;wlY5noxRE-hIfTt)RS7Nl)Ox*A9hwgWm>RJWso z-LcZf@u=QlGXIf7S=eX7BB$GicZruo>b8w3Jq_gf=UX>TGd1;eW8UkuRvQ$Tg@r!7 zVaz#iwP%@Z`{tY=1$uQqk@cQO{~P|H;76^G4t0%ZQR@`8N@77N-#j^HdY+DNT>CQl zQRNl{ZLQJnwm37|_5v?EWj@1lcKj+)@;#QvM z$dpJ~B4!dx5Q|SEZ~hp;G8P=h!XzCU^&(Vt{XXST6LbB5Tl9k%GCf-Jz05L`7@uqD z*RS`&srjd6k)f$iOB7d@6(aojmS@;+h_Bdgp$N{!BU>-`5!TzzLEm3ZfHglW?N-sTlo8#x5bP{XarYayUQ$1)LV)OFkc>30fKbz zx%pltsi*EQ{$_4qs%%W4L`wiU?=mj(R$1yLHugO0 z^~c*eW<^laDH+m<=E2MqC$C0Hjk)PJsxY+lV)#{yiJ>E>6#6%@>Y7#;rpB1i4pCa| zn42{I#FU@t=yIHP9%2#`d+%QQ7~H9+9b*>IC>RgLB3_xH$jhA)Xy@CVPjL#T=&7^B zi*Pj#;Ov`u^wINH#kciWRFuV6O84W3xvz$5;tDf#<{uKHFuuGsxAULPv>L|>#{Mx%yjhxN8;mGHMhSRW=_g&^S zNz+CMa#v3spsq9{c}IXB-rVo}5;mjK16zKDdYmC^HxW*dTw!f2yov%U=SN;YBnr@b z(vM+5umv2$U(jIiW9FrWlvU!byyZD|$03Te5d@e%cc8e(NWhy_0!ggEr0FE91Jz)A zYJFXhVuKcV-h2Dp=2@|cX@etlP1wXbswJfCU;y>JpL+ioq@K*p4G>L4sUe@$#X3Ql z&fKEt`$*okwoS~sZ&{k!BO|Z<1FDs+Zq~N(VX8E8Ypa<_0Z4104z_#9feP!;R!Wb( zSE1xE>zadWkjtWS;7=LIzaMc#iJXfycHEQAqjL#=*7;fH+V%WVNa4YTd<+j05Et7;Nh<9e#U+s8~=$=NV&Z_yz50|Bdbv zN-#QPD=Yoeigk)euP091Tp*czP7^K?Fw0)epFQe=uC*aP6gyt`&601PoUY18&*UEK zZS-4_PNkzdhd{TWN6*ucx>m0 zVM7`~WJu4iz=wZ9V|WYqY+W6LKVIXXCe7ln<+**}bu8JlBaehUEDjdbGvrKsYWrc$ z3C%cF=)>j&#l@gW1#!jhsf{)GNk)+ z$eGG`5CwBbAJ)3a_b@7!9J+b|Q$S?9^O0E&nuBqZwvLMtr6p}8ObAxk^q7vyUDt|b zX0;vNgzDf^yRE$OG~>KbYz->PIn9F1v1~Y{_(KwRgV$X;7DB_V&;f5B~$-GlkXg5lXidiPV6nHrQNbTh+?sRc4QLq#ZtBP z{&TN;Pw4Fw=|ryB4@9FaBKW7gr%ZHWEN|`M2+77(5Pp9qB-;dEV&cYqK zigmDqJz(apFpBR)k5^fIU31y>6m-i^3}7PtBM0c|EUr}Uo9HNM4@^|`I&4SS;!DoN zE9x!o1@YZ^cVH%rs1uq~hNxg6xVsTpCnCDeVDB1oOf@w&9vdWZoTg`;(rq+S+MZZ zVaa^=$F)melq5spmTPafoopm*5_{7cal8PE=M*;4Mj_tM;NDmvt3d24R5m{hz~EtH}5{Hi^9 zzO;4ybNkg%ilaw!A(`E)hkp5KgWDYkk_erXutEmZm7Bm*K>+(gJT-B+-zC0b2%LE& zd2(^6_Mst$Dz96bfvxm!nDBzJKg3sZak<&6`uP`M%qOnX=?O|0^Ka;WDr_4cBQQ-T zuJq^Y+Zb&49F(Cvb`%jLz_x5;JkwqJuZH{IRfd_jWoW%#B(~Eb9cVQS^dc4%QGt~? zz*aBxfEdoFp2I6@mZ;7+L%MYA4>ONY(to}E#J_uc<-|WP-fYv59pE7y*-4&bB7PeYC9YEbJ6MBBdX4l$0XCNMPt8ixJ* zv(tpoif4p8bc`#2bHN$u8swn%@2kT3ee975zq? z?$VyUo?by!ua%ss=L$3NEcyV2$^KsMQAfuy1?>WEF~)g?G@AS+NQt$wr8Jvtbi0C- z)|(*e2iwp33t!Csf)*D>kbm&4+7ec>u% zg@6_$*1YYaEiLtJ?zcS zM1P@?>#Az7ZTH9CXNHO9D!L8MwgOOPB74f@?(BP+`J>G*uaYV<)YxnwMJHrW7yU>rx_uyzTybMB$t9vtub8M?hUV3qZbRAOOEcG(1oJo{ zu?_$uf<0~~YqNZ6W{{s<`_NE0N{)!WU@*y+q+O7d;M}r(2Tm~?&sMS$$GG#;a|iuI zQD$ik(?Om~SZUD1MdZEBh3m_%xp+-9sOTJ)sdIi#6R}Dt!0#HmDR%Csm<*aKL-xR{|lfvnt6b3EZ14a|qwcg^WFy5y>tZ9;tJbx1j`*p2c>5{*so z-!pZ-hDEx&_`yxxGiY*5QdCE>sxp5oF?e3Nl9N+qPl$n2#t%k|w`pd`V zUQ>`7^N*uO_Q_+WT(@nHYo}7dune7yKRh_$xT*n#jw~rov@Xh-YpvMh;cMhgp0C>S zIToUlqB2~r<|N_tUxmvqswqD=juULOskHX93h`fGUoS2|#%B3{KNfr%!z*y%RlB%H z8CzMx$WkK+)jZRpOqO`+g9mHF@~$(rVWb3E3EjCH604}(V3+-?rla#u9V|68J2}=& z1NC)>Mn^R7`S=~_64`q*Fa$P#k?0^rqi|X>@$xXMC%c$@B-K!?9%QFyC3#VrF+t~Y zQyz`;RF<~O{ud&mQ0-yzX0|E43<@`??#vLwSyj-bg(BR5QA7;bM9h^Z6fFCua(nhU zJ)pqgWtcOiwr=S!ZB9s~`zo>v)Q6VJvXyhaWWFf)Y4yW>6m>P3$DlmN#MdcJ>lYX% z^<{WrhaLNbHJcjnGYw4t7ab~8$@1>!pHM^)U)Vgb-@#1RFhE%R)(u||%1nuplAc5q z@w}#p{`fU0pp0lc?$VJ)G>9=J2A`}0*kPy(yw3Vuo6_M^1YN5;ja6YX)~(&H5#05e zareH(wm)|?CFyW$&b^{0omydVH>`R%B5)q4dp*BD?tFB}f#lA_uOI$XYHo1A++NX| z;p)xNSqB!SRF@*9d|&hNG0P;I!e)ajvh&ePS!cv|K<=tIz2F*9Wgg#Ak_yi)${2}> z6~9wcS-MRh^jJ2hmlMJ`h{GuEDl>C6-0YaReqx6O(4f9c0T7TXN9}S*f@h0IK(AW- ziotbQXATTdeu&@*w~#YMLzR5wyJN!iEb+nGTfkJ%KIyCTY%oBgID3NXOWLD7l@&Wr zpf9``@PjV{^KyZ=tB+0Rix?ZcmnZ90HKz-Hw>|xfTuVD?&Zc#@{<1v|fB1qr82q1J z8=yqB<$NH72@l?_6tgI)!wQ~(^->b7u_z4i)XgRFvnM(RRaX=XS-(p&X@7BY(wT1i;-d6DClSNEx91rMc zilEK8bcfMhFr1_CJ)BIK4Xa7^a2i=A35F$!XU13aNK@FRIq=-q=?-C$`m0BYEf z=(TM7Ne}uZS=fy19DU=Y@%GHt%QV5aEn`K_3?IxGM4?8%r9%JX?s+&t#l+71w)cqK zV{09v=u5kNs6Jh-3zl88XaKGlO)&u=y8c%WdNC9ES!@7agKv6{eS*9F(NE@0?O+(f=oAsp6IY zEwfZf%Ec@KeBvLnjh-#rN5HnkEnvIz#M``G{X_|qU)f7EdMzF0W0*{WjBT%BToZCk zhfPxS!WSNcc~Glf63Lpqrc<;`*3XcN1|CJcAQSwAAYZkzD^%Q{k1o%Oc+&6<6A$O;FHIb4)6p}LzA0RJE6zAu zV{@+KKH2AxyU;_n^NZJS1#$Q*)T-(d`5Un9A)HuJ3a+@jbs!9dr7 zIf-}VeyyT4=J&gpBe-ZDI)!N(i-pgjXuEA~r-TiY6nT-owzvF6_yQNLh;FK$m(t5$ zdB?q_;oWF4u%}jcu)f;8ScaR(`!y(m(J2PbVO}{DQVv<$?h)sI@B|$`9?#JV=O3HT z>yFi>_Fh@L3TI1cHpcW#v=kZ|oOgA)irXtmL#=MH{@c!Qc^I8~#v6_A*FLK~ouYsH z{NiW*eFN4}7nO$t&jeF>gz8?QzGCT^OVd%)tigM)>%LD<4SQSAp~nENqKpkrgJxi9 zw<`(>j2MPPT6D@W;ge0gR^=YpbnvUPP#GK_u^2`PcAILyu9k8LYjjXYIu_$iNg<+6`1+`n*)rFbF1P9ZH5N0W_f8~tTzH24!^&_?Y;E;gDuhQTD%PTXp6Nu z{(x%RDRG=Z3BaVn;@t^hU5dFv6gATKic0ZgQ*XeSo9T5%;HHlR9-^=-w(j zJW(+M#D6S~Q89PbHl^y3w@PKSv#rWXUzia*lxc)8Lj>jn;L!BpaXoz<2VM8EI z(zgOF)$Qd7LZ{dHztvX!r`jb1dVVGG+v3&#rVXo^zFFCWPa-yTE%?u0N{OP8ruk4s zajt1T?i2o#DqN`e9L%*9_NB}Z<*Q)51+=Xb*zi<|a70OJ&>eIs_i;M|im+OIhYJb| z2g8*l*IB=_!w;lQ85ULE+hVDL`cvp#s#On4Kqtp@mlEYqT z{HS|6AxDTp5LJ+#m3s!sR_&NHifG-A4jMPMheDy|Y-4S`z5)sUq4L)P=OazgXQf3g zNgC3s(WnH5mZ`Q5+_k^Rk~P7miYHA64Vhpc=qhco|Hk13a)6Hl?2_XAL~7 zA0~HO1-k=XOd=+&;Tyj{DowxbgzyXFn;!_A?*9Gh);~V(jHLmdUh0%DaPo9EYSmO~ zjTbXNSr|W^a6=M!D8Kj6$9%2#InzAvDX%EeT8nVhea^4+`9hI{9U)=TqIgxt0op5K zw_r_i-XJBNBC@nR7c4$1g*`X$$93uDTplX2dZ1Cf-N|K@x`~xuKcAx@t?q9XV8M6L zw^z7EIy?sHkJX)H5cSBhfR7X|M(YJtHnz$`1U3YgbQWm{TeP5a^-h> z_Hr)#pwC8CS6X97?(v(MZUD02@uR%n|6IL*tawCQTGVVc&0aGlei9Fy4lL{8p$=eMdwE-3`nSf zh)YL3V=Y@xH)kaqpPK8XwR$hecQoo)H*Ky;mW&IUCU~RjZ|mFfUPJ9>wUuN?>5|&NVmsy+7;^r|a*k|a@1qOQaMh5}`be!**z$^*UxFBV=JLE(M zS~M2#?@5GzsjY8s?}FDaXr^N(F9JWfdQNYr!Om9lwh-EXyD8KT)Z~hw3q`?0p0Fsz zma3MDsWgJzGG?_|a`Otni#b~@YQeTz=wb{(`d3=*`t0o6sy?I_^SX5VvMMuf1zM=t zs{^`?`b7Y;|669;1zBYnkp`@$0X#(8lO^gcoJwFMav*}tTp%bZ zQGQf;$Q)D(!M~(2$d{fN(E8?->GX#O;JnX1;$^Y*5yBdY$M#fR^mIw1-~|eA2c)SK zGb7#hRX%?q^}LBX&8f+k_HvYa~{Yr(8` z6GjI#($g1)97qGt0vRvv@hmhY@ippV4Kz-mGxh@=;;uBHP&~_V zC{}v3XN!sv%=@d}iL!g@(Usba`R>31RroM~pwo^=;*j|PWfS&=Hv_07UFon17Ckls zP0eqjI2;6dNV5}BQms+i^EjkX!5eIrbt zsK9ikJ4P~(1V{y^d7aiMF&2ejds2b^No)u+b$?3}b%(8ZDx=Vdy7%84_t*7bOQE?Y zlUa}W#y>4B0!tVC)mV<9iKCL`o*>ud4|t@Jp69-M3FF-_?Nm|J(W%m5Ql6v6CrPv+ zd7pDXyNbA*EqQa_wav|u;M^P)S>l<;$4})>kg}+6VzcMevvZA}Wm({MD5MnNE z@a|m6oq67ZsaTGOn__Tej8(+4shJ51swdq*CEJ%oZUbper(T@9ybAVDW`&LjkLk!5 z5a}q;gviB_zaJ)c;BMl&VV8-k%c&AFMIuO>rNfq~|763Zc6`o+iuyug1phm~Z85!; z3bDuI=S1d3W(Lw@g)a8>s0i0~zTqvO5AyM*617Wwd(1U)3_aGFKUN$dQ1)-`A#*UICq!6$bgdOE}BTpmG_j6Ajy z#N7G3pLah`^xy?RR>Z)%ukQ>$qAcO9$RTulwJ9Hu4(%VV)@Ve@@2Y;i9~*$fw|%)qwY~>SKOQ&?lODk{mX0Ne3Fg@N_%nuWmH|d@+4KTjJar`Dq z+Axq)=>wJPJngb*JN{XM5?a^C5jc#AZ&f zPOke#w}`V_rH9wBrfljR#kWL*<4W{N^ugGxbfTL9OUwT;ti81GAABpp=~2N*d;Y0p zYxbx_2t^xqY3iL&w41Q5wD~tuE{~JGTUv?AI$q*M_n<9*)IMTLvNIMwRRe1jW9&xU zix#bYKVT<|a~Q42)#wtg(i~-9(RnpHiL`?^R97>goltbViOnLIyK~O4lUFc7R(z7K zH;I)?sY}AZSn)^X0e~8cP>w~q_}KTu($5`6mR}F$%~zL45n{KXeWI2dN(}Iq{W|7f zI(D+1@XOQMyA6stzZmF4W0iNjm^jE=mAR`U?nmr0j8_Rm9g0hW$5{_;8C5Z6o(ti& zR49_bnj0ozM+osFd<{b#RqHTu@z!q3t$Kip*0*jScU*1a_qSY4wPUyX9z7)s*^n+rnV9L>6?%Q!}n`NF@c0LXJV*|W;Q zFc$anFau_Rn-$SC=d2@V0c{e{Vz+rGu(xr1Vm`v>tTcejwZK zcKp!V0l(4UKo)Ztr@3<5rec(iDbSeY^P*&GUfCQ)z%k{6c#plo*)hC;CnlWoq=Lhr z&_AzTqCxRmRaD_#b8%~A!rW_B{>tjBWn%2rY9n;+#V7QQEQ5{6C)7KHWr#L2X1iMd zsB+~v)u-& z#*{q6V?pgb;STHrU6=P3MIBj7e2K#wI!%=MepVB=7`{%BQ_Ob25 zox%~16Av_Oar&cj6|I@&uP;SuNMFG{;-PGCE?MkJxhmz76Fev>lDRMm9CunHu+F9& zfYXBIk~9fL7+Z$C;x>Vj12{$W=w@(+ZK8|3Qg4nXYyalDI!pOgh}ACkLC1r#vf7Bf z@x8CvVS=UsE;e)gMwv_J1W7U60_`ps6s94v)>ww>3f=;Uqtb5q<&&mDo;=U|UM%(EBc%*f&}7b~j$R-F9m%w8$)D{KNA*qg+y&Jt7j;S>t0m z!y{t^C1EnAvD4Tjq15JgWWS0YkTgBpg#6klT(hf9!vEZ;0b41mmiVo_a?uaMRD{X8 zvd)Q`x~(HA5#xds)1*cpqpNho<6jr+G1OH)wIB0~VxzstD7}aI==&b#k`xGanZI9y z%U0hxRDYFDmcBJ;?am}*3fdCRwA9`C-C;JXpwr@SHFFE>3Bm>x^^Yd^cgGmfecujyeb8<}fDm0vI=-zy3T_TA3eu z`<1=1kMK2s{G5PhBcTIp1*-I4$KVP-`YLkOe{ly$QT$_yuoX)sP57U4fcnoPs&wScEs!we5!2UdP7u+-a_30Nq460O3QH^Fwjx$Twx z^G{*^9Fa>W2;XV%&v_=J_oLdQBYM9gwh>H;m*m0%{ruE1XPs8n;U&-H!Sp(zVn5%D zM?C)M+s-CmFGLk@_xPB%ttUH+pabs_+nf#mjVofNDxX*N;0Ts@K<=Qt3rKwfFoaV| ziG6U5oNa6hc~JGFKfTek`O1+W$44~d3 zj<^VNJuokaV{#ky`$M)UVE+t(4#K`v0OrOffJiN!0iptI9Upr@PMHSK>hL>kwnt;Lfxdbdfl!?pv?%Lwb045%;y$NPw7Wn0pyCRs1 zKfdIE;ZIe;s?*^MpAQC|>kWmYgrx=^?G?k`0M+eG*MyvT&D^>(HNmw1q1RvMeqIHw zzR`3R1y+AQF>Y|zIm2Q=qg#T!;aar7LmE3DcwgeYFmbM%hW$_Pn5$7gjN|85eK+N9 zDl&tvqNLJX9r`u(iqiMXFqgJKYpeIm3cPlUy~%T7!6|Z#iP>wHXB@x|EzD$aq$_xQ zAz+;b$AeYyE?D^TK#%Xq{faRDs=5E)M;I9stGU0OxTF8uf1ss5=svozK=B!)4LH)1 z8i~b{pJTQGnLmu=@Vx;bi+gzAtgjqwCj{Sj82>oEoenQrfzP*3ppdv5Sv*Qk#4Iqg zAy`K&9C<`N3EA_AL>!I)6(Ruz7?aM1PU@%*fRe@w!;W{vzl5wDa~y61VFAB{irQrEx^TUvS8%+)uYZOrk2|83>_{ z@K_?JzO>-xibMI*P8N9xcd=jYK;OuzWZG`&uh8u-$#n$z?(gVOx}c$vrJ=zFN@|>W zXS8u&fk1QElLpI_VSOoQ2ll&P-8u_VOFSdR_a?SG1UuXY!CZTt1M92+6U7@0mTAJp zo;(GKTSt`nSr-UmgupJj=J?QYikj}J4|nz%ugPiH=u5N-E8=QX5|19JA(Rv7k0utX4QkDe7@l#$l%~JW$o=8 zhp!rrF7AplC<%?aHNNG|9E~xCvO3E(TwWW^zWiZ^Vsk=Nn&lEgcne-MQ(BAdoI04y z=gjKIB2-~CE792hRk=1$O6_a|0Mg&k4`0v)tH|f96aR;3jo2i3P?y-HCjVQ#(ch#h z<+I+enL@LSwk1C?8j?FxvrF50De8xJg5<@Ed;>}%uWv-}P0bLJdud`@A6W?mTn z0XsKU(L-8t2Ev)XsmA%CzwOg)hvyfOId&1fIZyixk7S28eo~%AON1>=cto<*h?I?l zep`p^UokVx;g69J=Vq{a{mViBx{$qfOo9g~)TmSO)W{+5O6I+kU>OQeN0n7KJ7I`z z%B5Sl_=nc``1e8zG_T1tx*ZuWs_!1$s_a}dEW2WAS>?2Q{HYgz;jONJN_h>A)3`624er!` zU6G5?!z~hKB>J&J?ctrTpDvd-i7N7}lUoTlHjtEva_I=JHNHg!5pdjo10lUaU=obm z;(=}ysaalBR8}60&FJ$*$G`WGS(^VsQ-3T#`I;lNw5MjtTfJnk8_~3pCCcw9P&fBU{QADMh!##Mjc4qH!%hHsND^E#j9IpoFH!_Nh z_Djq%D^hx=mw<$#{AltDtF>Hp*D<2o%Ax_8SfX66Uj+pbnMhNHTwlxOgmxFkbBXuS zJ`vkL!rjkG%C-Lt+qw{sg5LtLVOCOR)@DKPL6qb%N>ZC2>B*0J6;aAU^+T!m6KKVL z;oqn)u=GdM)(kM>d7DS)zg2OLvm+EC?th=Z66)3}iby$)&}X-pe62;ose*J8-Jn z>Dx&sxb7hyLbOd|=lh zv$^hP#Wq{%42^mNG6-<)nTOn+2-qj{w{jhfIaZLY(35ZCi+&uEZqR1V$*0j~vXODY zpTwwc8`x;q!dEbGMdPmF;7=UE&o>d)ITU zQi8{^1q{RFGfXbWq_7fVW_+1PVK9-`M|2x3>T5A~ z;}38M!M*ro9(tJ9sUM^9vc13S3$3zG`zOuDKs!71vb!ozs3`l{tczXOa`cEO%W{Ney%Z(9BcL;Ul3+g9{z`gdryyPtDF^>3DT zIV!L>K8YPZQb-v{W(mL3wxTy}kmarxGjVNU*s{5_w(sd`ngDXNh`CHufFZ5MUc2li z2SPRj>dY?`H2xv%UWAiBZrJB(rD|a-X_hDwWOv#n(SqQ4?Hc$dWpy;VV!8#HXi9ZU zVLcI+)=dmSR<0-`yQ&aMp@*36&QokeUsklomP&1Ot*~chxw1^ADXve@Z4;*G>xo7r zT0b*L)LslpS8z@n9`Z<*CClV|5$O=~O4xO5%U6U-NF#Nm-mo;_mfgxLf`X+%$|vXL zwtoy9(n!#Ve)g~+M5(`6M@!!XQdslVS2AdSwJKy*!3qjFcr~+zkWU*~!dvY8x|elG zZt)qHo@zEk3O!>zxUWFpnV}BE7w7~D?Is6ON0aI{Hn=}up4iBl-FWa`^*Uc-am&I1*JGj&KF1#eW1>+gqxij>?t43osJBG9R+C2wWN~G9CGz4M7BJ= zeDUq9`1KyNfKBh8o<@n>3|@%O!~r_>XLa}y)06)yIhX%r>>s@JU;o;pxB$D4vJC0t zFo9(At-O3@b7M3I!ZQhPm$O0-Vn9>{TNBb#3%01L#(kifQY115lJ;EL`tnBrMbU1o zwkWwGzxDNR+nKE9fkoF}^cS;y*i1vbcm(EiGi-;s(p-?f#j83Ds3cfafP^F221qzN zz%K&HfOQtZKuRJu2M3k8kS&zqN#HAj-YpV|%@0UF-6wYAUo}d)x*$BT$nC49gMhX} z2gqnzK=Y`L2sx11i-r*_#A^sC?#eN^R7$h`J~z!H!dvlTi4g z@+cqB=;K9bQerR=pB!@(-wN7Kzbf@c=#s$>>jY39_M9q# z(Azi{7{?qtcs-{1=#sU9Y>P^%=8f4M$wnBDcl&0zK>J0pRp6fe>m5M@h5VB%oOa2h zE&BhduOd%A48#A${*3dk-=>u8@7{(~6(7sL3zoeMl8CM9;n>H~{XhZkj#gKo3D1rO zHp<7pb55Q(DbJSz8Toh7)WwefA`~eRMZU|jLazEd#}@6hm!;i>19b=Z%m!vAjtCtb z&lFP(g2Gj%O#cbAPywLDcngr)+k(`u;9DFzC-;K&S1!$X(gGhw4VV!Ip}(MMFK6Xt zj8IR>&-{d1=&_(6;y8bBbKUw};u7~BHeJ~-^KF&7wUDNhT4rjOT`iPP6VA#R_p(oX zwT1=ddkjY~0;kAy3BHCIq1#pa$|w5)ET4ZtE8WREB3}a}`h@rscmP?J0_&V`2I@2L z-XokMnL_$f#;7)>T*UXr!vP%PF@YlBJ^>`e&s1!oR*!UWZy@~s3)+aA1`=lu){wFK+C)00C7lqCziv$urILsC}0ZuYBK0njCBlc`jrC`Qqc`&?G4y}QaRwP zpt*jg&8dZzJ=WIuB95w-h&dZuY9A->OW_5qeKLD`Ud;~jF*PY8g;hAPr;=wOF`%fL zCAtvcuRfWufe!|)1M<3kk%)I}DyZry)Z@3b?s+OHAaM2vD)0XvUV8LXKI_Xyz! z7Jd-WcedfZur0EU{S)Zm;>jH#0NKgIg$+ysB^#QQb91BVXi2pc@E(2(F8o1wf?&gw z^8br~!#`i=hc*0+%AdoGc$yi1o$(C4(s<%5-I98eYEJ`!8`X@E0;C6kyu;~GB?B5a zHYt2IX0ALys}6j7K-L4qQB&Q-4iWGsIPMA)gr%C(FIyXxQzBX2fAAErgIF`i!x>Bms zUB+36^6aXU=3BDLqPjWYM^jK6;Hti)!R+@&?3+Nx63g(ipp|QRYOMUn#DF=v&3hfw zzTG14!S2j9mJ!pjjVm=efNq3`FmucpB%Ht&!-Xo#S=doGn6(`!VwIYRfE!}`y+8nt6n zOq0-Gp>j9oEi(47&M@EG?+^30%%#&ZvPH<%(R5z;X;lL1FG2xc+*5+QJhe3?KuhGD zGjTAveghI0*psoW5uzpm z2GSh%5G6J5p_r03E!bDdlayl|a~2(cjUuS-6NV0Inxk^VJd5mr83? zM4jG+Gpg=$q02L#z?5!&4fJt?2!kUvLdU){x^5JLRe31RUW!1&*A?)eexp9=&IgIl zU}GXZfz_L!0nw!nSzmifUn)&y`fe=@SJh?kdWg}b>0S@0HeQXC(IU%N%2~qj$>WhW zMFldSQRt;|;RGL;m)MZ#*X2X?9J8BdkMF8Gfj)TicWcW90(GY}KRgeFput<%f?C%=`~k5>6+ATCauRO@LN}@_=gMGv_|VP6 zBSdcJa(A`v<25}sb73Elz2@3!gW~k@S^Z}q`XJt*m$jryM|L#=1b(!ZZe7J_`Q+2Q z+kr`GxIkt3tIJ9$GncJcOad;Y(sjY8)IU@-==6f`Q$AB!sk@> z+EKWMs#%X-63VY~WwGn6&7m%Jr}t`i<}>k%%IdDW-?m0RVO;yT9;_9TD~motSm?d? zK>zWFyrI$M^b^!z?zhaU!l?^s^Fc86wM@{;(v z`uv__$Zv=-Fkmj%ZNd>RXnr?u7NL0=!;!TpDQl2O>^In09z_5%(*#QR3EXS|@MjgC zLV!R2btdUpsOdL6xf|$5T8Yp9hd<*Gbr0i`yI3Za1@Q5Q1ty}|?r9-e29}$0@+^5K z2wXhZj+V%Eo~+YP@N8j7wH1Fi4MC@WvCF#v+xXxDLN{Ws1K<-6wyE}^Or%v~6K82k zK`kr!oJu}avYI4fW!+x6g8-R zXq0nNR13<^4qNLu=K7{4^zx}%^&AdeMc^I@yQkFw#pM!WA1k_|?gW0k#~oVnO7`B{ z^dZ6*sHWSgL$l)l`R)%dkXvJoe$Lh&k9i+(H*0k1!W}^tRt^$_bXFYMG#CA;mY%xX z#haYnl*7vBkaG*k55$QvZmYyxN$gb)M+6+W2UX}>1hFhHoGaX;KB zChK?wz+hG6ZngRsY)xf|ayv-d+p?a( z;F7@wGPwnP>iq5q6~>YmMqGG7Gglu7o6HA*G5GHqVyzL2xEFMDlgf|cRc?@}+vWUb zanUl1Mfjw^MkEqP2&CTtHo{a^;HFbW^0NM|uHf)YErUx>*AN|rqx4l=dk5{^wjU@w zZ~z?*r0Wf-*ia8*YZ=qI^K|XnjA-q7Fvn7JXg>8GLj>2`h;7-S^vXX@QS#~Mr{~&H#Gmi9sDQ??5AChjM-v&)x)Zp)y zAd}??ru|@z1ulJa~d!k_6=dMZXd(^Le^EEoE2E=s#lOQ)y0{XoRX(S>9

ySC@gDj0SWSZ@J~pA{yb8RlzG;bLLlAo=UqSi^Oyy(J%`KUxME<9?@_K&t9S7T;uoWf% zGYk+mw27#97WcG-N++?deL8m#JyoxwI+t`kx)>zRk(J`5Gzrpspyn&lD3GQh_(n^6 zStz!mqGtztB+Hshrr#ub>@I;_(DQEZ{dd847Jm-nUKJw;>S8$1{J_*+R4wTnNTXom zdn>GViN_W5$ItWAv{ub6(Gg}Dd^-Owd;2cyLIPF`q|EiZ(|EO`;8fPqyC(2Obk0oh zpC)|HXC7z1%?`XlU} zLCNkp;Somm#EwWFDD4`F03Drpth=KNKd>ribin)smIo3|gZ)+@Q}UFt_J>E|L1fGo z@DgIL&igqw($7)^ABoCPVpe1kel`AJgkAOeVC5;#;ks2EL(DgyX<`ot4O|4)JFg#bPavJO^8kCUK3XgY`lyz;))@a;D?6L#W0AM0s@~{59ZKl|C9UCp z$tkSyam9CmLKL^>Cu+3Qb}gJ{7sL(8HUC{8m1hxKwM6Q;0>;A!TJsSuN#rD^2}Xri zAx5|ZB^;S5#6Z5p|MOU{qW34Tqz_Jih0V?R3C)qdJL}@Z76}X!yz$?Isi=PprphR< zDuahL{(`0$*Rn_}TU`;y*TVVe5@V}Sl5+>)s^e@e55T`(h$FpLVLO%$#s;{TRP64l zC6RnCHtCWg{A71xH&M)cfkA*Rj`URc%_=)n)>bqP9Ksi_11#lx;xY&b z8YogyX>YBq5NqCy7qdRX06Ri4R)V$P!F3zf$pB#9&r(2BAkTZ8807UQ8( z0AUq!`tgn{y-MwM%WqdgNVn$za`d1KglN6aL-9+FdE1q4>5PxGEVRFNG-D<2lfejD z;tu-u!7X6V;tWcvqHGzdNGPZvm23z?rp;_lLE?YFHOaFcJ$W77q^$CY|Qu5xi-9OuG@@% z3%0v%OR|%Tsdr4+A2cPydw3{;IBuIX)! zR%OBHa;OX5N25emd&b0m8wm@^KH`7}V-gD~;i{MG@t94(gAim4$OxUHnou2jhx1qL zTGvB#98%ArwzMuTv$2}VwJj4d@V3U))miI{o_`^E-FNC8*{Bi4>i*`FInlKGoIv?r znXabJ-lpoenp4Sq+*ztOfrCS9TugSUY!U4wX~H06R5*~U?gpnM`{u;q7Qm)?!f=fE zrZM^@3!y90d(YN&S4BjAQ~b8J3u)fb-~Fg0BE{gIy)}iKyHFt6*`Q$36tK6Vr#sRv z0LI>!Hnqa8(mQe2NiR-pU?1+@N&wP|WJvFyMP%hIKM1k6+#&&n^f%de`Ctseyd*Gz zKsgYSG&}!VR~p0vI5J_h=Uar{ynTyr)9N-Y1EeRZ=Yx^eZ-C<~#awo0S8}{l$!YQY z07WfZl(2or`)A>;q_PpN35utcdq+fseEDk=ONUG1s&x)@bFA8k*;q4o{Ve+L-L^#YTFA>is9&vm~L@N`X(omvgW;e zLX`CCQ-c{pVX_J%kgc6w@Lw{Bq1rrDF^Sn;;v5EZT_(r0IEohFSPAs z_a7t2tAzHw7`{7%0c>d!klP?+cFSD5wSCGL8L-C+fgZFb)_d*XL}`Kt*F4T7Ns?Id zwNBwZ0LG>g0n}_l%Z(bNqvO8Bszf$%lm_nFwAr0XPCbjdE~&~>pEme=(xe000{r#* zfZgHyPp04^ss^2%8;>swIu^{r z9-4p<%|1dwJ#au4OvpogA0U>yAnJaCFt4*)Ry>%KZ#zJ$=;-c0=TRF^w2mZw?@{i$ zU^zjbTyyO4M9Ml)xjCk5Zevl@_(4Cu+?oM(#is{Z3RE-A4mCOo%)3Uviun><)A3Sr zz11D!+qY{SN?SY#GrToCcim&n*`<$;0l5x!)4zA^1ZECtqf@SvI>POAvx3_OO>1vX zDrTUP_gy)sYywVaNA5#Cw76$s?k#Dk&<^8k0F1m<&r2VomYlL5VXkVm)RBggF_{7J zZkbDPy22kj=&6`sjJCUQp~|)Qj;jxQ1=yYM?F`$pb&!d8$YA2;SafE7~S69Z^F+~fO zvOD3{Xnpw7`lU#u?gLTnlCc`>%8J8jNP;ltp6S>4>8pg|$FD4?RdF$%CiJIb3vWM- zfG-tnk`zqz2{SN^_vSB@l?g3&SN+md8$Yd3-#qcOECx~9?_H~<&r;)m~ zpB)!1&g3FhadFYBa}h?2gtyTbJ}4~t%=)*O0kt&B>1TihL&gb5N0R4NGS5(&I;%_J zK15e%&<|2-sxiF2EyQ2o5s*g%XjrvYsmHZOMRh_KZ98%^z&yu7{2ICI$ycAf`3*;{ z9fc70ge{-z4ATT|bE!XHmSgO#MM7{zk1#Tr|B%raIbLMLz`2}4d+^ubYo3d+NmN9z$>a-`Vd}Q z;-PVzb}<3C@@8X!tqI^C0J4$1g(;2CuhLPSe~m*KqYxNK;x~Ro11Ht<{!<>);m_VnCRb?E8I+o`wuvJv6Ya7N6 ziw~HRrnV$!#CXF#E)x+sZxgB2139%m6(?R9@8q*C%W1VL&N8<-?x5EbkV z1^Nv{3cFSXAG-D4vAl77jPHEnX?YR=NKMBm*^JiAqckc5ZII?S&;%44kC!mQb?l7k zbKE)PEFKT&yU<5Tz?Z>OkWISs6C+PmxYozb?_w-=yVwLpy2Lz(e(iKH2kK&3xZ+8F zqz9?ic4b=glReuCRFh-nIJ*oq4WL4mU;bcnEWwX_Hdb}ST*AOk-Bc@sS7H>#9?Ccy z0cXvYD80ely!^GIxRC;uFiHEiFj!w%S8gNWjg|;OcLWH^HZx>on%(7MP{7JKRg-`x zvo=Jtm5PKDITsm$f+7P8YDgP$sa=SWI8k@hmdfz9hqJ%d%MK0#BjGv{W z`n{Okm@GaF==@UMP*G=T85FO;BI=E*_JtLxJXLsatjdy0Y;=9J_)WNJ{F-I(+?wlV z*Eh!N75lbUUS388{A1IcKR-5~ zHt(`ZDDi&dkB|Db@3g<)xOzad5c#JKX3k9j{5XM?Y4)^MQ;*`>hbdzPuPIYa;})v} zdQB-)u~3@|v}lXwtrlYzFJ3DtKGGftKDZZ5` z-+u7DS$;q{Y=LYNGJ$uPzngnYfVmf;`awN)Di@&1DK!_|<}2-YeUzIKFj?hWuDfqF zm(ot6QO4XoVOUfB^BWhT;dmXKO99n#Q*)mPiAGsw7RJ3g&2`dFE~@%vuzwu^y5DET zmE^xXdv%s*=sJroTeniYLlqm8?m#wHaMh4=>C*PVFd#P`)9Sa?oC7v(^Qnm?k0p=m zLOZU?yBV52W82B)*P-7S$R=L$P(&m<9u?Dt=Q~L4nvLNgcK zaQ~?d4I1;cPr)7zj+xCBMfaabbZBn1wu7-u-g^w)MM4lRO6soOT76vhs}TT-?WXyY z2~srFL~ox_yyqD^wq3F_ltEo|2jA>zsSzq~#d73wnGVb@?$-flyJo=H_H!?N;L+qo zu8Fmn+Z8M>GUQ7ytWl{|32*B0NXs95M_)@GNLI~5W`L=46ezU8u||4xM|y)IYcU+J z)$%PSmtH{b{?A|QFEStfPppk!#;L}aPur|`Rk!S=54uA#Egp2%=Ny#}HO`o^?f~&W z-AhrbTG#cIH}$^8>S8f-Tatv9PbB-EQ(_dhonsyR)HguMOiE=N&q~{CQUB@UQTeCW z^Dlk=b#5bn7qtKS>iBiJ{T+q#)@hX{#wlsnHARg;=2Ie#Re=-?Q-}9sE|`gliTEgs zq}YXUHnE1Lh&d(X7zqZ1j7$3U?PvuFy_u~fM>_ZXYW8gbE}W2xf8b75+NX0O{mwvY z;u1M5hhtf&5*n(bBXLv{BZR#mM6mP=Q&N6uhD*##V=&Tk+-qNy&Qgkp-#}{O;V+N+ zIHb*&x|1Q$TUO#*PGrT!WzK3BG2%2rzF}T8Qw=wL$GN^9a_Qgf=hKk&`5F&9{V&_Y zRvbS#k;=Oa+WhL`bp%p`V2LCP zuifooo!!kO5S%FmLbfA?z}ZkN7odqPxg)pDH79A-=N`}Q=Q6dYqtpN8jshoVrDe_e z#ST0PeoMMXc-*g}(}wEDWzQ*;*&we3u#`kzi%~pRW9SAU$12{JFYl9DpGnL%mhug> zbq$yb#X;G-Jwu;*si6}0I?7Xw5rn0xve0g|c|C+XU`JruK;oO7{z%jvHF1p?O|cGO zbe=o-1Ecf8A8D|&yS4>%So4|Q3{5}aZ~+;N-?62Ct&jc_wRdd+?FQR_kMmoZxg!#} zwr@fIOD{fw+UN!m>9vWTbBRT8rdf@~k*}dbw89C?s>F84rXc?Ak$bM*tKp}vpFi|H zCXw_~co2w%jBRR^eQFnxdvTk(`Uehkpho*oFQB7_b&c}9^10=I=GNbfssG9wuGZ)B zN$<-ETF|@vcgt1$PE!do^n<4CPh}e6#EsR4oVXOE3&oH)>G0GLpXmVDkIcMZ!64ti zpSS!8j)))ePpXI%Vz%@C2v_0kKQ3QUj7WZt+AVkzn2Zvx1i z-#uY)4yl08JBk6QE`s(DpiBJIS1o18sOkgdyLbNqc=)@g6l&OImhb^RF;^s^I)>+i zCm#5Pr5D#L3P4Z|5CpplE#d)JxpYIN(CHkt$iC!S9z^+g31awF{Pr8^hm{LBK|L_t zxSLnx*>9lt^e%+#e|?Oa9G`N{B$;b*D3%lfP*j^r9#x>AW63m{Cbnj9&dK^)1JRp` zM!p=u==8Z+X^}XrL>(G22T{GJKybYc*#4#^bhdxk z(o_ZU!MYN}iGY&?JMrmX5nF%usTPa`hB7aDE ze=GrJzflQ50qb;3`TX1EO@|68+PsBUG9yvQerRw|4JuC`+|nbLp=As0$J_w`*5}C4 z@|9fupeJ@w0(zZsSRYDxwJW^Ay5QmtOild>adG*qkCi*aRZOGed=Zr~A6s3}+t)ym z)m?-x2QBpkDc83ZpQS@Oyvw_cUi(VW2~%XdHb-YaZUBsAId`+uUgDwbx=m$iANmhN zZcd$wb-t%^LN5ryqC0b3abM3}PQ6;kOl;O|!)Z?SOPy&T}*Y@w8rYnN$8nNStOwNGZ zzEq0lV3Ijb%}EUeBtM^hy3dZQgvw4Dyt;-w8)MdDg{%v7Hj(CS3@y(|Ux*74Z0(oY z*f?@)3?yss+80l_Qo7oS4@U3fG3bD+Esp*UbCyQ_G}D_So1Xr0ChP3@`GE#ftJgI1 zPtUTPZi}NvqpLvNZRnk zKAY6$maO13Wt7wXJ_+yf-|e~ZwaKWL{??nt#&SucF(&>!wZ`gO_L!crF3P7@-m{c> z*JyfK(^BbuRAb|IABMuzS7hfCO-Nlx-=25lQ4p!y{GBe#q0&h$El2;JeD@!-jfoBN zWZ$*?=^gD!`u*2`$pQR#mNI~M_iyZFI=?qz+0jg_kD(=I$+!^pGP2QM(V57=R4{p( z>-e@k-|TcZ-;sLdbe>&X9ADJds}X4qaL)X|i&n)hM=pRVj}y!u>v2FK3F*h-5umNE zp@>bgkRt%p4x7P$KnC|c-{P351tbP6?-A&JmqO^HJy!6pXF-kZl zaG0{jV!Qy|+#ILIEtZ zDZvTay%*a3qXJUomXgefft{VdLjw5MIse;d@iMear0D4SG~+g?xqL1~-8cO|M-OTwjq2*_JshoBU6fLc{Q`)~14MWI ztNuCr_kvtNcq*in<8x#w*!cvl*p(#iAL8y@5`GR(tMQvMByHYF$CD3D3D7@aMk0bo z#=qC@Vt$~Dgvj$#0g*%bV8F6NXn_=*TK>pMQc{3<`DJV7=nj)(6Ep9^03Di38 zqAP=u4(W1X&j1ST3Qfy;^;kPc+mW24E`tiz>j8X+7*x09hZMT_9KB_c6TOdAF*25- z`2IbcA%RGp_O4{4+hUz`;WW7ymMLo~eQ0@ujP~U-;S^ei-r@7nVW;=(HxGv+Uk>Lico?CMdL zRW!$&#Z|U&ZsBFq;rr!L(0M8t&5y-Zo;RNOUt$c}UDCcKCx=b^fLZ5F=@;AGBCu0? zy=+wYxz@O;AyhcNlPTx%edW8&k9ADZ_$*hE9(DjBdVdDwlWcXlYe3VkYQAHOknN$o0hR|P9oQ*9rDK$>8iCI4 z`P>MJH)o&BbUR$#9cu9KzG+Tf{^yNb zjV6Gt9lNKUsxvKcFsBCY@|z7ZXH>T>^4QZ&)%uzV3`Yu12IaO*Pj1ZC{n%AFeXfWqNSq}{Iq0<_sXDHi+k;+eIF{r4W zyeYGBX5U)EOQ%PY?P>VPCG?CWtr!~3nhe!yFpx`|_o*CUpktjU%mEGxzfiG<> zZ}r$*sxK2c7xfJHnJ*%eGm;eO!C)e;Uo=;HaJ|2m@|AOTB^%nZ5K3LsH4!SWyM~G! zF3!^Se+yRn?7t5ytp)T4YP8FvLn(YEjNQS+k1MpeN*Gf6bSA&s0*lxH`vBr2K+A}@ zqun7YoB#=%D7X*4m!uGkv~@jZ$_5fs`hBy}SIfsyW-aHqUw|me2cZ0p&??y|;6Ho~ zWd1#SV7Hg~V&B{q{-qO5h#I6FXx>{Tc$)iSzdp15SWVK)u>r$mq{abV9HkB=RL$5!?kfNq|C20a=D0M-wD`UdpLI{sHoK4#$ zb*BAVOv=*88iAs^CIUf~K`HO0(hQW3vbysfB?0q2+`iB;l~5OuKmG5Mpuo_8*>_98 z{wKEp>|8{pWtn28zl_8$lF`f5_IpFr86~q)X8o^A%$0O7o}UgCfsVQ?B2tQ^uxG49 zJLVXsAUu*Os@pX@Ir+VX;UaU}Dq;>=IptLGY&kBG5B>vB!_vf@QQT}6Uk$*D9vrAd~i4XF6uiGdMlLvL_l#k!kX5yM=%qacoy5<0eS>bqHk1%BixA#|rnO3uB)v zz^gx92Z#WEXf!mC-91Sautq7hfDGDqK%bF*->?|b9{hO04{$V)QQNIZ2k8(1KjvrLBv#CkYJF(Q`du{LnnG z)!+aKV38Cce;YdX&aBo!;y?3xxTt1VGz7i^9KETwn) zX4?lW;_=E>wf=Fmx6_b%Q~Z_5(bay^{ZGX_VM7VBTCTc4(X(#K2WVj1{qq50fBfPg zcES8uSHYb9Q_UQY%dvYL{qTxKTz8<=Rzv(2VZ*%m$HPRcT-tIXBTBd1Fx8*u|L3t3 z_w$IfcN|aGm12GX3y^WH#P`fe3Z)8f`H!#2DeJzpKHCU3w04<8q=@WF|3_VOLp{d4 zebhRur^WyFcyVYN19~CQDA||Sz$b*^9XCVC!_@eqt`6+!luRPm2fdYdFxLQF`k5kiFbzY1u78h+M7ONYQBfC5OP2x`~%U1@9C@^T}e|5Ren|S zv{npZNoCbo(H{Ff4}LMhKV124DBQ(~d(E(aS_B2lX!Yp5&)$6;5Iy)DA4v>wvho9z zm%b%H`BinAkbklY%CEQ#jkG%h0BrhV_~C1H+Rbx~^j|J0!l(cGlOMiAX)ArHo_sf7 zEO^L5RW%@oRp4HTOt3D9qpw39=(?Kn;>As1V2M`e%!NyR2S9kxh<0V!Sjyi(Izvjh zZ;iWIb3&4Od5YTQB52UPYzpnoC*UmSX)4k(PSM~l-r%!(HfpcouioLL$EM35Xb*8KuxDhF2bmM&C?j z|E<8x_grD@?cYFX0Nmc@{k)^g-;PS)=ZHyA%i75~%&W~B>N3^i|dLA*Gft?`DP?xyJ; z?woEwl1yPQtkVG_aZ$B5 zmdfEqz|A_Pns3p64j6dgCD-hvq49y6RQ>Mz(t z&1=<~n&X#?f}PFiP~D7(l3|8-!MJy79tW3fra2v6J0Z**-IHm40D>q4bORl7T`d&s z#TnjC`UEH-RjwuCcP);#r~4-0q})tbr6WEXPpYlHQW@#S1d*==CQXbTYJWl_aIXd5 zQM*n{0B4rL>iRK5A7x?_Dv#RD>*lKdv>-vIHw$z3ynkd4^Zcw*k^}vCk~yw z?xpjTHIw9|uBKt)8}6(i__drShp&1(V~zTcA1jaOiL)_g%`9c!wiB`5Mk+m!o2BmHoZnSa>q2Jeo**#I06FLj z>CiutS2O}VD!@@X{6PRf>lOf-uQ&vJzR4_AI#{Qud8|L%Py;BN4#0>8{}AcMlvd#uS34WXZ@J3#+z zmD68eu~Y~Gc(2F<((VJSgw730g@3v5hmqW@@R)Y5fWF=R2I7qYLS#AoG8CDd^6gQ4 zrc-HusSk&3*sfkTVv{jh{21|oa8NOq@C%R1zPQLGbs>x<@g$?Px4URxjC$dW%W){7-BgP&49 z;76EAc>w^DtHBMzBsF5N-mLO)oIDtsQ<96ic+f?;WRZ$yP!s=FuI^u_i2vPF#5jYY zT2Os39u8uvB(b~@`t=0Y^cVAb&%ulFYp2Um_OX^LHF2rC0FV^_@x1Waf^_r?KZjll zI5m>2)VOt0oI9iRJ-0V$x#n&~T=w1WTikvO$>J*>D^^18IiB=^Cki1K%y_#f4tac3 z>2K#X{cdYDswGYYdCRW4;0&YC6j6E7;r_BvcbLxQc+n8FELoOxIXXLv5dtz4GZNsM zt!BIV0e%{IAu%)XYm0}MfSvvDv&fB)xp-`}$)+HtxnaFGE#!FU{Y%Y{~Vd#Z!)5Frf=1$!S6!ec=t{PoNb%D#-krpNvwxcZroR|Io|d2-=(L&av=L=95O~14cNL zwkL`S+(B{ZV+oz}_?HXPsn>$F&`t95T97!#B4uqC%l0Gd9%L($bm_QmSOX)xl6mn- zvT1g00p)o?xdnQ<_q%n(Xtg_Dae}up*ddG~+J((M<4<8&wn5G`Nubr1st14xV5AiA z53RU^14x0$Jr;OJ18@%=3$%P~f(`)z=luh4h|oFx4j4kR3{a=Hbdug+p9|fg*&LS& z1BomETc-Y6vQXM6ERN2B$7Z2jZtZonBQ_KWQV@`hH1;Oc)Ebwq)+-YTmzM zgns{C6p1D0X|?+4wwR-DalU&yT@AMO1zrx?l{YvI}j%MD5=KK0eU1QP#{#LeII+^<)Qy(cFT3XYw+XH!6Hk9 zgP#6pNN5LvKY{N3#Hp)WdWvavlC~Xz4`KC89M}aP$=lg9e0CdKgqfM4nZ$gmw@4~a zdsh&z&fIF&Xx^Hln(!589vRFjamPmVlD9zkD_v0l?QQ(T9wEE*dBbE?DYE9Z-R|ik zLlV0))S+P{&hS<@A#Z@vX+e)RN?P4?iRoljllF^=?J=^c%?4k!3P!MPG;aA>?&(ZS z0hVuL$!gmLks|zO8UE*Q&rBq*zPwy&wKf_-Q#nx9_}~(%I9Y5nlwiZRr;xi_jWL%v zi#`y8=zv+Dr^i`nir;><*3g2mESS66VA*R!vg_)M-`_>SB8f`En0dHwpaC1(O?i=y z(g(zZfw@}&P9Gt-X_wdlglP1A6Etuj`SsFIHYE~0X$J|-I6nt08kq9EcHrs3L3&U> z`@v37W7?+NNTU=<1La9Lo7$HcP!rl)4CJgOYbi| z)RomCtJ#!R1{>b+9(PrxKNx)K{9a_f3b=!-qsYE4MKS07(UpZfvTobt-kkp~GMc)Wh}>GR^Q!?&7~ zZYz&!G3p@kF--C7wJiyg4rp@6x@tct{M4z@%s{QT`Q zA;)&kBgt{EYOII%mAWajH*|>2)qE>%PTy=XQuan=2WEqca)XtXH>M=wc~849!oJ8lR@JT$0sA2I zkPB0>TE+>=?1DNue;oxZ^l9l1LL1ElDu2RIojl1*OQSs6j-4=$SvrxoU8Dsqs3hl( z6r%3N(|y;$By^5lqE*^oVo~v}Kes<$w<*T^^J2E4H>#htT^B9PDAj}w;ksN(S#4~O z3f2q8ZtHObN~2uLSjaR7QNrWnu>%8FVY%0j&?EbDXztl`mM>FICLU#6_CCxYkb|Ee z!6Ku5DwThBsnwjB6C4nJ(OH{psDu^Z9O>b>3+4r#BUTa0Q7?aWwrwx07c;@^VAxU% z$X<{Q5g(6djB@=JGWakS&kX1Ceyh{C40G>s(8PgRv|mc%flv1qecYGni*Tk#zU?Xv z)xiWX`|6DQM&~CtTtjrO`U1RdIWEG0^;VwI09Ch4B zm~n1_`(3^!eWjLa-=))=H5UWCV5+DuBI9A_=}7PVS1{a=v}mEC<%s(0xzvfd1)g2N zPPNSshFv?LM!@SE?LHfk+m?uF(3PLo7Zt`B6?^A}bga7^?)cnCPk@A`b8}j_)iD^& zNdS@b9Z{++0=&j+*Ar4Hh<0Z8suj+Z*+#(DtwIuIqOuhJ^jkTf(f5K^BN}mL^^svT zknF6%yzI%T`UIFhKb1F;$t2Icc5aY0X3)imo@6PJXPIRvVHwa8f#9=QEoCf&$z2cD z;?}HAD$)%B5`&Qu_{aY>e8|J8+ESz#U@SY_U$L{rC*o z&QC8Bn1kMWNW=ghgs(l@U;K&Ow&1JXCcz_^h-5WO%|>SvQ^u5B?>l-MOHPj&@>+z* z)P@gCXzO}_<>eY)?sD2U$7iVz4bgh+l67VjLhoG=dA|a^Lh4oZhHyDpeM<%2-$MXB zZXgD$iN0wF%E+g8#o^%h8mj+b=7}QPBhhyCh>i=jcKea^Md1O6va| zDEZwqhw)FQ@Nlfc<$lU9IB4FG9yRnb06aHeb6gB2(bgYcC9FHIGl^6zr+2m4EF+ww zXnEbO*oxgPZ!sLyNT2HxXO;A=gsi^O{&G4#=3|b3D(Vz*&@QS-fl?d1?^|nT>kH;5 z7He-_c4U++N}_n@6WrAe(~u+z(83T9n>4DeZ8VM_n&>jijf(XmY>E!s*9o`UI(5}u zGU|MAnkE3ZHJ}mZl;!uA%!?qenKK(3JDx#b%aJ%eQC-iG+DM)@UqM6ZO^1*i4OUoM zHi>N2yE|=WsN+R2Ly+L`_8ng7!9zUF?N(aQQlvd!8Ai|^QjTRO7q=v#?pGR{9(l|= zgk7e?4`yiZE#plDm8h&2rb7r-#@`T zwURxSijVejA4=I_h?R~JUOt}GUj-7guG~^LxqW<>))MorN9?pbLxWF*P-ClW&EBE6 zWL);R{M*+Jx#B(Epj1pm-07!ZpY2t`!3b_s7m2Jk5P}iWP@aC;haQ+U{<6FoH$P2m z>q!eUOemiqKH7-6hUX|JEvI~W^Odldsxf-(-AB-_5sd*1zX7rVz+5^kt54uk*s-nN zIDx^)`Hf+JrMfW&3IV+k|9Kc|N45Rz4t+u0adVn;wmI*+aIWa=K^uW)JmX64+1mB! zZVnZ-W3|-m0)@P`yxk;|!9F@&&NREtQ18sgjZa)*yv0%Z0PSg7Y~e*Y%8#9?ufI@W z!8Pr4Rbwk@=pJ<7|B)~O)@F`X9I>rX{jz^mKUnaoB@Yu5Mb+_0DnbdhxwN4K9}EkF zxCnpjRoOW9?2-$;c>Ty*c9+cfAryhQ{p_l!tm?zM>$tj&jcuMGd2@E~Bl8=RZekn5J|Fu!Mk)c?&EVWn!4)at(w=_mPdIyGO#xR7hTze+6%3B zJ^ZEY0U)B1C+I#)^>Q>)3Iuyzl#P=|j>wu8Hm}X$s`kH7A`)aQKtJziF_hvt&`z@P z-aik`(`wh4$|&X^ng_BkT7&zf4GK5 z%dvi7vA%kvOu&iaSnAC)&VF|3Q|dMMnA+_L(_%NA;*~u)1yxUZ6j9}IW~y+*R9(UK zezt;bGg}eZuw)X1VJ-Dzy)gD-QrVDaPpihP*Bh%fmt)B>L=5efBeJtVDk9n;tCSTY%i zXi#^CK^hR|w-@d`lygG)ti=_qqDQSzP4(#&RKv#UYG)jVK#!q$ zR`PXtY`p~2V!d$^XvIUJNFM^$KQT$>!ep<{NwH!|%6ak9f$#pKKXLP$@Y z?A&5MBdVUjwReKa5-nMqZ6wBkv1o+2v8=&+%`OQZbp+Rr)3 z_!T$zsx$5Sc$0bOXD!h+`V>3aG|SqWh}kxq1VbDO7-IV_Z}$HiOX63{;(B>)OozF{ zJyXz0pon@|=xi942;6%0;Mh4mmdbf7mk@TovmPUAdJdO}7cN zwh`8C_z9!y5tu)|{>O59>66IDbWF%^pqo6X^Y0?eRpk9eq{Vr^rM5f0*+H>E1OmBk zStkaZDv38LDBtX7H~o(;`~Sw@$b%{!ZcIU%7D?IJiJEs~G#{SLrqs1vt>yKv4o?EQXg6HupM90oSZ{}Hm9I8wRGL7BY_wS5p<1I_M>Nb z$dOMG_2%n!0R;J*FA-WjSqH!bprZ>gy4f5`qXt2BzPTw@E_&ry!1BXUsnS>qPD_?T zCb6M_{AguscKA%9#}}x!ep8rK`r*nW^d*sO2&q_Anp}C(x=6^kDq=BYpBLSDbfIL-xY({64}Y*_P`HsJk&z zRymeQ9#_7cj-i7a2ADCt>vfm8vyhOv`L9lHLv{Jz>?ot@WgK8@Yu*SgF!sXNxNWhU z5|m`G33$m#h`S!LPuAR$BzQ3S7Qpi@R+nhTx2}8hCT*R&G9H`{2jKE%b|oom58!W+ zy{vikbQ6ch+r`7Q3FKxEX>X7GoMLS0L^_*sS7C3e_~|!LLz=#Im5S1dNqDy}N0USQ zO+fay@`_Ydmj&LUV0;5SwDh%#JC(IOE7WMq%>Jx~U9K${ciWdy+PoR|n$Pg^X7I@?qf;zLM1)&7Mq8ka}$1w4#SM?Nk?69DLCOMMQY>w8ebogAh0=) z-Gz_A*&`4*Fgey+^et*+nMv^AJX~cX>x5bBmbu`q3vX+U1$}~;^2^qYzOsjUI68YKZc;1LZE}bCf z8=FuMfg(LQ^IS`HT#YBbV-m=_9Z(a;t}s&-?}UO<38zaq@T?s945pMNQQxR+iHAIv z7Mj@zyVamsy=ULO@czrfIH>-t^{f_0yD7cvB5EzAa6*JVx)uW&rw|c@^ z4mqiqW!t5;OhQS$T6UH%dAci&jU?3zs*!UMxitKuZQVGu@i)mmIE zcqLg5>0PVH8@7ULpP3EPb5iSrvw02;Uco1sfZE}#QNOA#)72U#x;)J@+YI)j_Z#u$ z4H=wlR7p4EdOp89*BSR=9Qm^XYd!_{5Soow-0&Se2qM3^f;YY~ak$h9<#XvkZ#91~ zjomMWy$3n#ijTFd7fhNKCo`Hj;BV(B3j!&Hg>b^uqWk$JU!NT7$mfei%Lwy4Z#z`15i;Y6+<=;@<>OG@dS3MAJN&HYM&r2dSP4~?ac!giQg zpwP_a4Wr7+X6mQhjwqe#6xholZ)hfLb+NZM2}EfQ!o+X#OU-$imJbBP=f>UmL$@Vy z?Q^)$(_|wDH1PB8WU8AX9yb8^#F}yL8&&ly1AF51GZVFTZx?RHYc(*E1$@^{SN2*O zZBTJ#&-v}U?#09r8SWe3p0s03o%@Z;gyjmaPj6$zI48;?%qH9DKl#Hx;eAJO%nr3% zuOCv0Vtr~Uz^qx`SnF@H?rZV}hIUw2#&H&luG~OpDrB14Hz2>UmTB2`6C{ z;S05yW4!iiqTf)2>eg zTUSLFw_}lFHxi6=mBm-Kgja`|U>nl42urn=u)Umhg)0qMRvEP_u55Y2|8>p1HK77; z+Y}@$f!uMhpdCG*1iJKhjK&`IqX&r)ck=$5srbJisr@x_3w(!uU-5ay#ZM+dexi8F z-CTmsh0gssMU;dg{Z|i#;|kpoO4mxr#^m{GU!zRoh#Mi)VJZ4P4x!@A*>WM-guY8HUOi6C!6K*a@$nkv(>$4LVNXzE$6J3o|XNN#+Ma1 z0YI+dJKCuS&C|ZrOdDB6_})s5B1q&)WiZ8pi^VsO%;A}mk9Wu)`Zrhfq z$3IyO`Q&YL-w!;pyBKf}+!Mz8pzdqNyHJ87fTrd|L&_MIORAp`vQwEBkV-#ENn(=h z<^f++W6c(6tW)h7v0MzY>noRHuoEThXAi^R&eR>)cLVVyT{NKiT%PVIN;&82%vmZk z-TL9oK1Ft88e??|L-u9!b)Pl@e7J#H68=GTy3_TEIvg8)tj!s?pV0UN4NuF}G z5u1=cvE;;LzDlJDdl0TUE5y-3Gj!aAbWj`Pt|Z7o&u+c2ut{_c)i1p{I)Bl;3xCVSsuTmt zzLlDgT#Q>b6_r&1G<7i@9y7FdIWIHfD4;wpC8%~VSQhzKwg6G7sa>+whzw?W%A1$4 z7*ndb+IQ|qro7g70SZ}pKC{RLk5B}U2x_d1Uwbw2LWYnfTh!>2C{>N`?!9A{X*o_FVpIS!E^HWPt>cPAvSIZ>X-W2|*Sz2- zfu`a3p|^>XS+J&$qog<1r<*|b^+Ey6#wR)i2M-CE-&nM@Wvt_q9Pp8sUh5?48$yHh zaN+ZLN}A3G1$BjP1!V|L%aL&R2QfY#^I3JVXez9SbGz`icSMWwkLt6tv{~1l5lG>Y z$=CK!Zl|qsU|)p~@P^-)B&P3bRl1o!_7unuV~`8)RON%-hk^}&&%=TOC}j{HLhnBT z|Ds=CqRcn{Z|muc}C7n~$_nqHAhpDZhN`GKGya8s_b96h|0I%p+L!A@%vV zY3=c@)^%)WDI!Wy4@eCWKk9Ip!Y9mavcbyfaT=z+nGBY%JD*FgD=O!gs4pPr;XX6y zh3aVXq7g0as=-Ft`1ub^1CjN3(5ghp>*Lu5E)Jp-vHBB@5+&JLZUGy1^I~v?i8u(D zG=x2Hp==l~`}Fyry`R=sRyS9>TQ<0qkl6)>sG=5M!22ng-{_@feSJOtmhtVHQB3?u zk%dAD7E+BE$aH>(S0MXLU)!AX`4Xp$j>Oy0r^7m5@VBycnHA)us<6LQX>aR>+dox$ zrvGAQh?{tK5eq5>E%031)i@r7{OdC+gHv8@@&p5CXi1_dafdz4D2OP3pXS|%yq zA`d-t;9_mYZMh346yeOzZ|OfhH4#W@to8w90q;vEO$!~66PTr`HE3y?tvNqGgpfpQHHsk;_SzedA9E^mG9uqRhup=huS+}N zDGq(2EX!wuu)6{`yDoU*J^2t&`*9=?9$Lje-fT`jEt)+*SR3ZKf0kNTsHzC3-H%r?(L5m&7d}Q9?%l}u9#Lh&>1IVE zvBjICq4aU-`<&56E<6?llPSN-SneY-Q9QxddolM4S{D|yP-VFZ1k`Ku!uUopUX{DF ze=*1Ee!*;Ni0AjzC_Lu4JKlmBf z2_Pu{8x|e(|MG89Mb*X+HzC=p^N@pVOGyUnu~)u#d4m;ZG-cdEamJd9yLu>T9d7PB z1gYs)^^%=y=x6i#LO*Ty#eYdOj}rM`V(PMKp$hP8aDVYe_K`-uIbGj4@?T zlW%W6GJ)nknSUG3hn8NIt^8Akin8`@_CYGG;-bifeGk#;s!!q6r(IoJeN%jI>LZP~RjBQS{5Q_| zSA6Y~4)gwb!~K6isx-ZT!buGyac_(Dc3n!}9`b*jRZB6fix3CZJx|t)s`v!yJ#Ti zgg5sdF=_k$p&XF)5CBD20)D_CR(bFrbO6|yUV%@=fZWO5v~z09w5ywb0Ow2#ECZlX zh@_*!4JOC%?Y#-$#KHo96)gadB$j}e_n03fzYG4q`t+}H`j>xXr Wgf28C-S1Jov>VzWCT!*Z-vj_$p+q16 diff --git a/doc/CN-Arch.jpg b/doc/CN-Arch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bb4e42d597acab39013ab2447b4ac5f15cfb13b GIT binary patch literal 76127 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 Wgf28C-S1Jov>VzWCT!*Z-vj_$p+q16 literal 0 HcmV?d00001 diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml index b6a9c64..b0bbb1e 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml @@ -33,6 +33,9 @@ services: ports: - 80:80 - 443:443 +# deploy: +# placement: +# constraints: [node.hostname==dev] volumes: - "/var/run/docker.sock:/var/run/docker.sock" - "<%= traefik_datapath%>/traefik.toml:/traefik.toml" @@ -160,6 +163,9 @@ services: ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" <% } %> +# deploy: +# placement: +# constraints: [node.hostname==dev] volumes: - "<%= bitcoin_datapath %>:/.bitcoin" networks: @@ -172,9 +178,6 @@ services: # deploy: # placement: # constraints: [node.hostname==dev] -# ports: -# - "1883:1883" -# - "9001:9001" networks: - cyphernodenet restart: always diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 40bff9b..82d79ca 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -83,6 +83,26 @@ checkpycoin() { return 0 } +checkbroker() { + echo -en "\r\n\e[1;36mTesting Broker... " > /dev/console + local rc + + + echo -e "\e[1;36mBroker rocks!" > /dev/console + + return 0 +} + +checknotifier() { + echo -en "\r\n\e[1;36mTesting Notifier... " > /dev/console + local rc + + + echo -e "\e[1;36mNotifier rocks!" > /dev/console + + return 0 +} + checkots() { echo -en "\r\n\e[1;36mTesting OTSclient... " > /dev/console local rc @@ -133,12 +153,12 @@ checkservice() { while : do outcome=0 - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do echo -e " \e[0;32mVerifying \e[0;33m${container}\e[0;32m..." > /dev/console (ping -c 10 ${container} 2> /dev/null | grep "0% packet loss" > /dev/null) & eval ${container}=$! done - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do eval wait '$'${container} ; returncode=$? ; outcome=$((${outcome} + ${returncode})) eval c_${container}=${returncode} done @@ -160,7 +180,7 @@ checkservice() { # { "name": "bitcoin", "active":true }, # { "name": "lightning", "active":true }, # ] - for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + for container in gatekeeper proxy proxycron broker notifier pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do [ -n "${result}" ] && result="${result}," result="${result}{\"name\":\"${container}\",\"active\":" eval "returncode=\$c_${container}" @@ -279,6 +299,28 @@ fi finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Bitcoin error!')}" +result="${result},{\"coreFeature\":true, \"name\":\"broker\",\"working\":" +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"broker\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checkbroker + returncode=$? +else + returncode=1 +fi +finalreturncode=$((${returncode} | ${finalreturncode})) +result="${result}$(feature_status ${returncode} 'Broker error!')}" + +result="${result},{\"coreFeature\":true, \"name\":\"notifier\",\"working\":" +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"notifier\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checknotifier + returncode=$? +else + returncode=1 +fi +finalreturncode=$((${returncode} | ${finalreturncode})) +result="${result}$(feature_status ${returncode} 'Notifier error!')}" + result="${result},{\"coreFeature\":true, \"name\":\"pycoin\",\"working\":" status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"pycoin\") | .active") if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then From acaf7c73a5fa970b649a70235e9c6ad3d87d4872 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 22 May 2019 13:30:56 -0400 Subject: [PATCH 169/255] Changed arch schema --- README.md | 2 +- doc/CN-Arch-0.2.1.jpg | Bin 74189 -> 0 bytes doc/CN-Arch.jpg | Bin 0 -> 76127 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 doc/CN-Arch-0.2.1.jpg create mode 100644 doc/CN-Arch.jpg diff --git a/README.md b/README.md index ce671e8..adeda9c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It is currently in production by Bylls.com, Canada's first and largest Bitcoin p The project is in **heavy development** - we are currently looking for reviews, new features, user feedback and contributors to our roadmap. -![image](doc/CN-Arch-0.2.1.jpg) +![image](doc/CN-Arch.jpg) # Low requirements, efficient use of resources diff --git a/doc/CN-Arch-0.2.1.jpg b/doc/CN-Arch-0.2.1.jpg deleted file mode 100644 index 0163945e92ccb22aa7fe1869fb051be3d8917982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74189 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