From 830e16c12d4ebba89c71107a8f319e52d702a31d Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 12 Nov 2018 12:01:01 -0500 Subject: [PATCH 001/268] OTS Stamping support, first pass --- api_auth_docker/api.properties | 3 + api_auth_docker/tests.sh | 57 ++- cron_docker/README.md | 3 +- cron_docker/callbacks_cron | 3 +- cron_docker/env.properties | 3 +- doc/INSTALL-MANUAL-STEPS.md | 3 + docker-compose.yml | 16 + otsclient_docker/Dockerfile | 27 ++ otsclient_docker/README.md | 24 ++ otsclient_docker/env.properties | 2 + otsclient_docker/script/otsclient.sh | 83 ++++ otsclient_docker/script/requesthandler.sh | 88 ++++ otsclient_docker/script/responsetoclient.sh | 21 + otsclient_docker/script/startotsclient.sh | 6 + otsclient_docker/script/trace.sh | 15 + proxy_docker/Dockerfile | 1 + proxy_docker/app/data/watching.sql | 10 + proxy_docker/app/script/ots.sh | 203 +++++++++ proxy_docker/app/script/requesthandler.sh | 436 ++++++++++---------- proxy_docker/app/script/responsetoclient.sh | 19 + proxy_docker/app/script/watchrequest.sh | 111 +++-- proxy_docker/env.properties | 3 +- 22 files changed, 850 insertions(+), 287 deletions(-) create mode 100644 otsclient_docker/Dockerfile create mode 100644 otsclient_docker/README.md create mode 100644 otsclient_docker/env.properties create mode 100644 otsclient_docker/script/otsclient.sh create mode 100644 otsclient_docker/script/requesthandler.sh create mode 100644 otsclient_docker/script/responsetoclient.sh create mode 100644 otsclient_docker/script/startotsclient.sh create mode 100644 otsclient_docker/script/trace.sh create mode 100644 proxy_docker/app/script/ots.sh diff --git a/api_auth_docker/api.properties b/api_auth_docker/api.properties index 3ae021b97..bde99947a 100644 --- a/api_auth_docker/api.properties +++ b/api_auth_docker/api.properties @@ -20,6 +20,8 @@ action_deriveindex=spender action_derivepubpath=spender action_ln_pay=spender action_ln_newaddr=spender +action_ots_stamp=spender +action_ots_getfile=spender # Admin can do what the spender can do plus: @@ -27,3 +29,4 @@ action_ln_newaddr=spender # Should be called from inside the Swarm: action_conf=internal action_executecallbacks=internal +action_ots_backoffice=internal diff --git a/api_auth_docker/tests.sh b/api_auth_docker/tests.sh index deaf27368..25e1285ad 100644 --- a/api_auth_docker/tests.sh +++ b/api_auth_docker/tests.sh @@ -143,56 +143,68 @@ test_authorization_spender() # getbalance echo -n " Testing getbalance... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getbalance) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 130 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 135 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 430 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 435 # getnewaddress echo -n " Testing getnewaddress... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getnewaddress) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 140 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 145 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 440 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 445 # spend echo -n " Testing spend... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/spend) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 150 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 155 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 450 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 455 # addtobatch echo -n " Testing addtobatch... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/addtobatch) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 160 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 165 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 460 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 465 # batchspend echo -n " Testing batchspend... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/batchspend) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 170 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 175 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 470 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 475 # deriveindex echo -n " Testing deriveindex... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/deriveindex) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 180 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 185 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 480 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 485 # derivepubpath echo -n " Testing derivepubpath... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/derivepubpath) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 190 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 195 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 490 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 495 # ln_pay echo -n " Testing ln_pay... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_pay) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 200 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 205 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 500 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 505 # ln_newaddr echo -n " Testing ln_newaddr... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_newaddr) - [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 210 - [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 215 + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 510 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 515 + + # ots_stamp + echo -n " Testing ots_stamp... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ots_stamp) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 520 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 525 + + # ots_getfile + echo -n " Testing ots_getfile... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ots_getfile) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 530 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 535 return 0 } @@ -216,12 +228,17 @@ test_authorization_internal() # conf echo -n " Testing conf... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/conf) - [ "${rc}" -ne "403" ] && return 220 + [ "${rc}" -ne "403" ] && return 920 # executecallbacks echo -n " Testing executecallbacks... " rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/executecallbacks) - [ "${rc}" -ne "403" ] && return 230 + [ "${rc}" -ne "403" ] && return 930 + + # ots_backoffice + echo -n " Testing ots_backoffice... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ots_backoffice) + [ "${rc}" -ne "403" ] && return 940 return 0 } diff --git a/cron_docker/README.md b/cron_docker/README.md index 65af6135f..c5e56bb51 100644 --- a/cron_docker/README.md +++ b/cron_docker/README.md @@ -3,7 +3,8 @@ ## Configure your container by modifying `env.properties` file ```properties -PROXY_URL=cyphernode:8888/executecallbacks +TX_CONF_URL=cyphernode:8888/executecallbacks +OTS_URL=cyphernode:8888/ots_backoffice ``` ## Building docker image diff --git a/cron_docker/callbacks_cron b/cron_docker/callbacks_cron index 68b888bce..e6a2e204b 100644 --- a/cron_docker/callbacks_cron +++ b/cron_docker/callbacks_cron @@ -1,3 +1,4 @@ #!/bin/sh -curl ${PROXY_URL} +curl ${TX_CONF_URL} +curl ${OTS_URL} diff --git a/cron_docker/env.properties b/cron_docker/env.properties index 7c46889ad..e49eee37a 100644 --- a/cron_docker/env.properties +++ b/cron_docker/env.properties @@ -1 +1,2 @@ -PROXY_URL=cyphernode:8888/executecallbacks +TX_CONF_URL=cyphernode:8888/executecallbacks +OTS_URL=cyphernode:8888/ots_backoffice diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index decd9ffc4..4f376af21 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -38,6 +38,7 @@ vi proxy_docker/env.properties ```shell vi cron_docker/env.properties vi pycoin_docker/env.properties +vi otsclient_docker/env.properties vi api_auth_docker/env.properties ``` @@ -52,6 +53,7 @@ docker build -t authapi api_auth_docker/. docker build -t proxycronimg cron_docker/. docker build -t btcproxyimg proxy_docker/. docker build -t pycoinimg pycoin_docker/. +docker build -t otsclientimg otsclient_docker/. ``` ## Build images from Satoshi Portal's dockers repo @@ -137,6 +139,7 @@ sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f ```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 -H "Authorization: Bearer $token" -k https://localhost/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 -H "Authorization: Bearer $token" -k https://localhost/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://localhost/ots_stamp ``` ```shell diff --git a/docker-compose.yml b/docker-compose.yml index d5a652e5e..97f10fbc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,8 @@ services: - "~/btcproxydb:/proxy/db" # c-lightning looks for $HOME/.lightning/, and $HOME is set to / in the container - "~/lndata:/.lightning" + # OTS files, shared with otsclient container + - "~/otsfiles:/otsfiles" # deploy: # placement: # constraints: [node.hostname==dev] @@ -59,6 +61,20 @@ services: networks: - cyphernodenet + otsclient: + # otsclient JS + env_file: + - otsclient_docker/env.properties + image: otsclientimg +# deploy: +# placement: +# constraints: [node.hostname==dev] + volumes: + - "~/otsfiles:/otsfiles" + command: $USER /script/startotsclient.sh + networks: + - cyphernodenet + clightningnode: # c-lightning lightning network node image: clnimg diff --git a/otsclient_docker/Dockerfile b/otsclient_docker/Dockerfile new file mode 100644 index 000000000..1ae177942 --- /dev/null +++ b/otsclient_docker/Dockerfile @@ -0,0 +1,27 @@ +FROM node:11.1-alpine + +RUN apk add --update --no-cache \ + git \ + jq \ + su-exec \ + && yarn global add javascript-opentimestamps + +WORKDIR /script + +COPY script/otsclient.sh /script/otsclient.sh +COPY script/requesthandler.sh /script/requesthandler.sh +COPY script/responsetoclient.sh /script/responsetoclient.sh +COPY script/startotsclient.sh /script/startotsclient.sh +COPY script/trace.sh /script/trace.sh + +RUN chmod +x /script/startotsclient.sh /script/requesthandler.sh + +ENTRYPOINT ["su-exec"] + +# docker build -t otsclient-js . +# docker run -it --rm --name otsclient -v /home/debian/otsfiles:/otsfiles otsclient-js `id -u cyphernode`:`id -g cyphernode` ash + +# ots-cli.js stamp -d 1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 +# ots-cli.js verify -d 1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7.ots +# ots-cli.js info 1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7.ots +# ots-cli.js upgrade 1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7.ots diff --git a/otsclient_docker/README.md b/otsclient_docker/README.md new file mode 100644 index 000000000..c90bb4ea2 --- /dev/null +++ b/otsclient_docker/README.md @@ -0,0 +1,24 @@ +# Build image + +```shell +docker build -t otsclientimg . +``` + +# OTS files directory... + +```shell +mkdir -p ~/otsfiles +sudo chown -R cyphernode:debian ~/otsfiles ; sudo chmod g+ws ~/otsfiles +sudo find ~/otsfiles -type d -exec chmod 2775 {} \; ; sudo find ~/otsfiles -type f -exec chmod g+rw {} \; +``` + +# Usefull examples + +See https://github.com/opentimestamps/javascript-opentimestamps + +List SegWit addresses for path 0/24-30 for a pub32: + +```shell +curl -H "Content-Type: application/json" -d '{"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}' http://localhost:7777/derive +curl -H "Content-Type: application/json" -d '{"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}' http://localhost:7777/derive +``` diff --git a/otsclient_docker/env.properties b/otsclient_docker/env.properties new file mode 100644 index 000000000..3971c34cb --- /dev/null +++ b/otsclient_docker/env.properties @@ -0,0 +1,2 @@ +TRACING=1 +OTSCLIENT_LISTENING_PORT=6666 diff --git a/otsclient_docker/script/otsclient.sh b/otsclient_docker/script/otsclient.sh new file mode 100644 index 000000000..1861b6e77 --- /dev/null +++ b/otsclient_docker/script/otsclient.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +. ./trace.sh + +stamp() +{ + trace "Entering stamp()..." + + local hash=${1} + trace "[stamp] hash=${hash}" + + local result + local returncode + local data + + trace "[stamp] ots-cli.js stamp -d ${hash}" + result=$(cd /otsfiles && ots-cli.js stamp -d ${hash} 2>&1) + returncode=$? + trace_rc ${returncode} + trace "[stamp] result=${result}" + + # The timestamp proof '1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7.ots' has been created! + + data="{\"method\":\"stamp\",\"hash\":\"${hash}\",\"result\":\"" + + trace "[stamp] grepping..." + echo "${result}" | grep "has been created!" > /dev/null + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + # String found + data="${data}success\"}" + else + # String nor found + data="${data}error\",\"error\":\"${result}\"}" + fi + + trace "[stamp] data=${data}" + + echo "${data}" + + return ${returncode} +} + +upgrade() +{ + trace "Entering upgrade()..." + + local hash=${1} + trace "[upgrade] hash=${hash}" + + local result + local returncode + + trace "[upgrade] ots-cli.js upgrade ${hash}.ots" + result=$(cd /otsfiles && ots-cli.js upgrade ${hash}.ots 2>&1) + returncode=$? + trace_rc ${returncode} + trace "[upgrade] result=${result}" + + # Success! Timestamp complete + # Failed! Timestamp not complete + + data="{\"method\":\"upgrade\",\"hash\":\"${hash}\",\"result\":\"" + + trace "[upgrade] grepping..." + echo "${result}" | grep "Success!" > /dev/null + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + data="${data}success\"}" + else + data="${data}error\",\"error\":\"${result}\"}" + fi + + trace "[upgrade] data=${data}" + + echo "${data}" + + return ${returncode} +} diff --git a/otsclient_docker/script/requesthandler.sh b/otsclient_docker/script/requesthandler.sh new file mode 100644 index 000000000..7ad8d08f0 --- /dev/null +++ b/otsclient_docker/script/requesthandler.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# +# +# +# + +. ./otsclient.sh +. ./responsetoclient.sh +. ./trace.sh + +main() +{ + trace "Entering main()..." + + local step=0 + local cmd + local http_method + local line + local content_length + local response + local returncode + + while read line; do + line=$(echo "${line}" | tr -d '\r\n') + trace "[main] line=${line}" + + if [ "${cmd}" = "" ]; then + # First line! + # Looking for something like: + # GET /cmd/params HTTP/1.1 + # POST / HTTP/1.1 + cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1) + trace "[main] cmd=${cmd}" + http_method=$(echo "${line}" | cut -d ' ' -f1) + trace "[main] http_method=${http_method}" + if [ "${http_method}" = "GET" ]; then + step=1 + fi + fi + if [ "${line}" = "" ]; then + trace "[main] empty line" + if [ ${step} -eq 1 ]; then + trace "[main] body part finished, disconnecting" + break + else + trace "[main] headers part finished, body incoming" + step=1 + fi + fi + # line=content-length: 406 + case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) + content_length=$(echo ${line} | cut -d ':' -f2) + trace "[main] content_length=${content_length}"; + ;; + esac + if [ ${step} -eq 1 ]; then + trace "[main] step=${step}" + if [ "${http_method}" = "POST" ]; then + read -n ${content_length} line + trace "[main] line=${line}" + fi + case "${cmd}" in + stamp) + # GET http://192.168.111.152:8080/stamp/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + + response=$(stamp $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + upgrade) + # GET http://192.168.111.152:8080/upgrade/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + + response=$(upgrade $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + esac + break + fi + done + trace "[main] exiting" + return 0 +} + +export TRACING + +main +exit $? diff --git a/otsclient_docker/script/responsetoclient.sh b/otsclient_docker/script/responsetoclient.sh new file mode 100644 index 000000000..c907e766f --- /dev/null +++ b/otsclient_docker/script/responsetoclient.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +. ./trace.sh + +response_to_client() +{ + trace "Entering response_to_client()..." + + local response=${1} + local returncode=${2} + + ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" + [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" + + echo -e "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" + + # Small delay needed for the data to be processed correctly by peer + sleep 0.2s +} + +case "${0}" in *responsetoclient.sh) response_to_client $@;; esac diff --git a/otsclient_docker/script/startotsclient.sh b/otsclient_docker/script/startotsclient.sh new file mode 100644 index 000000000..589e0a870 --- /dev/null +++ b/otsclient_docker/script/startotsclient.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +export TRACING +export OTSCLIENT_LISTENING_PORT + +nc -vlkp${OTSCLIENT_LISTENING_PORT} -e ./requesthandler.sh diff --git a/otsclient_docker/script/trace.sh b/otsclient_docker/script/trace.sh new file mode 100644 index 000000000..680f3f23d --- /dev/null +++ b/otsclient_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/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index e49980ea3..54580ce73 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -12,6 +12,7 @@ COPY app/script/callbacks_job.sh ${HOME}/callbacks_job.sh COPY app/script/blockchainrpc.sh ${HOME}/blockchainrpc.sh COPY app/script/call_lightningd.sh ${HOME}/call_lightningd.sh COPY app/script/bitcoin.sh ${HOME}/bitcoin.sh +COPY app/script/ots.sh ${HOME}/ots.sh COPY app/script/requesthandler.sh ${HOME}/requesthandler.sh COPY app/script/watchrequest.sh ${HOME}/watchrequest.sh COPY app/script/walletoperations.sh ${HOME}/walletoperations.sh diff --git a/proxy_docker/app/data/watching.sql b/proxy_docker/app/data/watching.sql index 290fabf3e..0717ec4a2 100644 --- a/proxy_docker/app/data/watching.sql +++ b/proxy_docker/app/data/watching.sql @@ -53,3 +53,13 @@ CREATE TABLE recipient ( inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_recipient_address ON recipient (address); + +CREATE TABLE stamp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE, + callbackUrl TEXT, + requested INTEGER DEFAULT FALSE, + upgraded INTEGER DEFAULT FALSE, + calledback INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh new file mode 100644 index 000000000..e55cf810b --- /dev/null +++ b/proxy_docker/app/script/ots.sh @@ -0,0 +1,203 @@ +#!/bin/sh + +. ./trace.sh + +serve_ots_stamp() +{ + + trace "Entering serve_ots_stamp()..." + + local request=${1} + local hash=$(echo "${request}" | jq ".hash" | tr -d '"') + trace "[serve_ots_stamp] hash=${hash}" + local callbackUrl=$(echo "${request}" | jq ".callbackUrl" | tr -d '"') + trace "[serve_ots_stamp] callbackUrl=${callbackUrl}" + + local result + local returncode + local errorstring + + # Already requested? + local requested + requested=$(sql "SELECT requested FROM stamp WHERE hash='${hash}'") + if [ -n "${requested}" ]; then + # Hash exists in DB... + trace "[serve_ots_stamp] Hash already exists in DB." + + if [ "${requested}" -eq "1" ]; then + # Stamp already requested + trace "[serve_ots_stamp] Stamp already requested" + errorstring="Duplicate stamping request, hash already exists in DB and been OTS requested" + returncode=1 + else + errorstring=$(request_ots_stamp "${hash}") + returncode=$? + fi + else + sql "INSERT OR IGNORE INTO stamp (hash, callbackUrl) VALUES (\"${hash}\", \"${callbackUrl}\")" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq "0" ]; then + errorstring=$(request_ots_stamp "${hash}") + returncode=$? + trace_rc ${returncode} + else + trace "[serve_ots_stamp] Stamp request could not be inserted in DB" + errorstring="Stamp request could not be inserted in DB, please retry later" + returncode=1 + fi + fi + + if [ "${returncode}" -eq "0" ]; then + result="{\"method\":\"ots_stamp\",\"hash\":\"${hash}\",\"result\":\"success\"" + else + result="{\"method\":\"ots_stamp\",\"hash\":\"${hash}\",\"result\":\"error\",\"error\":\"${errorstring}\"" + fi + + trace "[serve_ots_stamp] result=${result}" + + # Output response to stdout before exiting with return code + echo "${result}" + + return ${returncode} +} + +request_ots_stamp() +{ + # Request the OTS server to stamp + + local hash=${1} + local returncode + local result + local errorstring + + trace "[request_ots_stamp] Stamping..." + result=$(curl -s ${OTSCLIENT_CONTAINER}/stamp/${hash}) + returncode=$? + trace_rc ${returncode} + trace "[request_ots_stamp] Stamping result=${result}" + + if [ "${returncode}" -eq 0 ]; then + # jq -e will have a return code of 1 if the supplied tag is null. + errorstring=$(echo "${result}" | tr '\r\n' ' ' | jq -e ".error" | tr -d '"') + if [ "$?" -eq "0" ]; then + # Error tag not null, so there's an error + + # If the error message is "Already exists" + trace "[request_ots_stamp] grepping 'already exists'..." + echo "${result}" | grep "already exists" > /dev/null + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + # "already exists" found, let's try updating DB again + trace "[request_ots_stamp] was already requested to the OTS server... let's update the DB, looks like it didn't work on first try" + sql "UPDATE stamp SET requested=1 WHERE hash='${hash}'" + errorstring="Duplicate stamping request, hash already exists in DB and been OTS requested" + returncode=1 + else + # If OTS CLIENT responded with an error, it is not down, it just can't stamp it. ABORT. + trace "[request_ots_stamp] Stamping error: ${errorstring}" + sql "DELETE FROM stamp WHERE hash='${hash}'" + returncode=1 + fi + else + trace "[request_ots_stamp] Stamping request sent successfully!" + sql "UPDATE stamp SET requested=1 WHERE hash='${hash}'" + errorstring="" + returncode=0 + fi + else + trace "[request_ots_stamp] Stamping error, will retry later: ${errorstring}" + errorstring="" + returncode=0 + fi + + echo "${errorstring}" + return ${returncode} +} + +serve_ots_backoffice() +{ + # What we want to do here: + # ======================== + # Re-request the unrequested calls to ots_stamp + # Upgrade requested calls to ots_stamp that have not been called back yet + # Call back newly upgraded stamps + + trace "Entering serve_ots_backoffice()..." + + local result + local returncode + + # Let's fetch all the incomplete stamping request + local callbacks=$(sql 'SELECT hash, callbackUrl, requested, upgraded FROM stamp WHERE NOT calledback') + trace "[serve_ots_backoffice] callbacks=${callbacks}" + + local url + local hash + local requested + local upgraded + local IFS=$'\n' + for row in ${callbacks} + do + trace "[serve_ots_backoffice] row=${row}" + hash=$(echo "${row}" | cut -d '|' -f1) + trace "[serve_ots_backoffice] hash=${hash}" + requested=$(echo "${row}" | cut -d '|' -f3) + trace "[serve_ots_backoffice] requested=${requested}" + upgraded=$(echo "${row}" | cut -d '|' -f4) + trace "[serve_ots_backoffice] upgraded=${upgraded}" + + if [ "${requested}" -ne "1" ]; then + # Re-request the unrequested calls to ots_stamp + request_ots_stamp "${hash}" + returncode=$? + else + if [ "${upgraded}" -ne "1" ]; then + # Upgrade requested calls to ots_stamp that have not been called back yet + trace "[serve_ots_backoffice] curl -s ${OTSCLIENT_CONTAINER}/upgrade/${hash}" + result=$(curl -s ${OTSCLIENT_CONTAINER}/upgrade/${hash}) + returncode=$? + trace_rc ${returncode} + trace "[serve_ots_backoffice] result=${result}" + if [ "${returncode}" -eq 0 ]; then + trace "[serve_ots_backoffice] just upgraded!" + sql "UPDATE stamp SET upgraded=1 WHERE hash=\"${hash}\"" + trace_rc $? + + upgraded=1 + fi + fi + if [ "${upgraded}" -eq "1" ]; then + trace "[serve_ots_backoffice] upgraded! Let's send the OTS file to callback..." + url=$(echo "${row}" | cut -d '|' -f2) + trace "[serve_ots_backoffice] url=${url}" + + # Call back newly upgraded stamps + curl -H "X-Forwarded-Proto: https" ${url} + returncode=$? + trace_rc ${returncode} + + if [ "${returncode}" -eq "0" ]; then + sql "UPDATE stamp SET calledback=1 WHERE hash=\"${hash}\"" + trace_rc $? + fi + fi + fi + done +} + +serve_ots_getfile() +{ + trace "Entering serve_ots_getfile()..." + + local hash=${1} + trace "[serve_ots_getfile] hash=${hash}" + + file_response_to_client "/otsfiles/" "${hash}.ots" + returncode=$? + trace_rc ${returncode} + + return ${returncode} +} diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 1605f3221..433ffbad2 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -17,216 +17,238 @@ . ./walletoperations.sh . ./bitcoin.sh . ./call_lightningd.sh +. ./ots.sh main() { - trace "Entering main()..." - - local step=0 - local cmd - local http_method - local line - local content_length - local response - local returncode - - while read line; do - line=$(echo "${line}" | tr -d '\r\n') - trace "[main] line=${line}" - - if [ "${cmd}" = "" ]; then - # First line! - # Looking for something like: - # GET /cmd/params HTTP/1.1 - # POST / HTTP/1.1 - cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1) - trace "[main] cmd=${cmd}" - http_method=$(echo "${line}" | cut -d ' ' -f1) - trace "[main] http_method=${http_method}" - if [ "${http_method}" = "GET" ]; then - step=1 - fi - fi - if [ "${line}" = "" ]; then - trace "[main] empty line" - if [ ${step} -eq 1 ]; then - trace "[main] body part finished, disconnecting" - break - else - trace "[main] headers part finished, body incoming" - step=1 - fi - fi - # line=content-length: 406 - case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) - content_length=$(echo ${line} | cut -d ':' -f2) - trace "[main] content_length=${content_length}"; - ;; - esac - if [ ${step} -eq 1 ]; then - trace "[main] step=${step}" - if [ "${http_method}" = "POST" ]; then - read -n ${content_length} line - trace "[main] line=${line}" - fi - case "${cmd}" in - watch) - # POST http://192.168.111.152:8080/watch - # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"} - - response=$(watchrequest "${line}") - response_to_client "${response}" ${?} - break - ;; - unwatch) - # curl (GET) 192.168.111.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp - - response=$(unwatchrequest "${line}") - response_to_client "${response}" ${?} - break - ;; - getactivewatches) - # curl (GET) 192.168.111.152:8080/getactivewatches - - response=$(getactivewatches) - response_to_client "${response}" ${?} - break - ;; - conf) - # curl (GET) 192.168.111.152:8080/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387 - - response=$(confirmation_request "${line}") - response_to_client "${response}" ${?} - break - ;; - getbestblockhash) - # curl (GET) http://192.168.111.152:8080/getbestblockhash - - response=$(get_best_block_hash) - response_to_client "${response}" ${?} - break - ;; - getblockinfo) - # curl (GET) http://192.168.111.152:8080/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea - - response=$(get_block_info $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) - response_to_client "${response}" ${?} - break - ;; - gettransaction) - # curl (GET) http://192.168.111.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 - - response=$(get_rawtransaction $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) - response_to_client "${response}" ${?} - break - ;; - getbestblockinfo) - # curl (GET) http://192.168.111.152:8080/getbestblockinfo - - response=$(get_best_block_info) - response_to_client "${response}" ${?} - break - ;; - executecallbacks) - # curl (GET) http://192.168.111.152:8080/executecallbacks - - manage_not_imported - manage_missed_conf - response=$(do_callbacks) - response_to_client "${response}" ${?} - break - ;; - getbalance) - # curl (GET) http://192.168.111.152:8080/getbalance - - response=$(getbalance) - response_to_client "${response}" ${?} - break - ;; - getnewaddress) - # curl (GET) http://192.168.111.152:8080/getnewaddress - - response=$(getnewaddress) - response_to_client "${response}" ${?} - break - ;; - spend) - # POST http://192.168.111.152:8080/spend - # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} - - response=$(spend "${line}") - response_to_client "${response}" ${?} - break - ;; - addtobatch) - # POST http://192.168.111.152:8080/addtobatch - # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} - - response=$(addtobatching $(echo "${line}" | jq ".address" | tr -d '"') $(echo "${line}" | jq ".amount")) - response_to_client "${response}" ${?} - break - ;; - batchspend) - # GET http://192.168.111.152:8080/batchspend - - response=$(batchspend "${line}") - response_to_client "${response}" ${?} - break - ;; - deriveindex) - # curl GET http://192.168.111.152:8080/deriveindex/25-30 - # curl GET http://192.168.111.152:8080/deriveindex/34 - - response=$(deriveindex $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) - response_to_client "${response}" ${?} - break - ;; - derivepubpath) - # POST http://192.168.111.152:8080/derivepubpath - # BODY {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"} - # BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"} - # BODY {"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"} - - response=$(send_to_pycoin "${line}") - response_to_client "${response}" ${?} - break - ;; - ln_getinfo) - # GET http://192.168.111.152:8080/ln_getinfo - - response=$(ln_getinfo) - response_to_client "${response}" ${?} - break - ;; - ln_create_invoice) - # POST http://192.168.111.152:8080/ln_create_invoice - # BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"900"} - - response=$(ln_create_invoice "${line}") - response_to_client "${response}" ${?} - break - ;; - ln_pay) - # POST http://192.168.111.152:8080/ln_pay - # BODY {"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","expected_msatoshi":"10000","expected_description":"Bitcoin Outlet order #7082"} - - response=$(ln_pay "${line}") - response_to_client "${response}" ${?} - break - ;; - ln_newaddr) - # GET http://192.168.111.152:8080/ln_newaddr - - response=$(ln_newaddr) - response_to_client "${response}" ${?} - break - ;; - esac - break - fi - done - trace "[main] exiting" - return 0 + trace "Entering main()..." + + local step=0 + local cmd + local http_method + local line + local content_length + local response + local returncode + + while read line; do + line=$(echo "${line}" | tr -d '\r\n') + trace "[main] line=${line}" + + if [ "${cmd}" = "" ]; then + # First line! + # Looking for something like: + # GET /cmd/params HTTP/1.1 + # POST / HTTP/1.1 + cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1) + trace "[main] cmd=${cmd}" + http_method=$(echo "${line}" | cut -d ' ' -f1) + trace "[main] http_method=${http_method}" + if [ "${http_method}" = "GET" ]; then + step=1 + fi + fi + if [ "${line}" = "" ]; then + trace "[main] empty line" + if [ ${step} -eq 1 ]; then + trace "[main] body part finished, disconnecting" + break + else + trace "[main] headers part finished, body incoming" + step=1 + fi + fi + # line=content-length: 406 + case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*) + content_length=$(echo ${line} | cut -d ':' -f2) + trace "[main] content_length=${content_length}"; + ;; + esac + if [ ${step} -eq 1 ]; then + trace "[main] step=${step}" + if [ "${http_method}" = "POST" ]; then + read -n ${content_length} line + trace "[main] line=${line}" + fi + case "${cmd}" in + watch) + # POST http://192.168.111.152:8080/watch + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"} + + response=$(watchrequest "${line}") + response_to_client "${response}" ${?} + break + ;; + unwatch) + # curl (GET) 192.168.111.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp + + response=$(unwatchrequest "${line}") + response_to_client "${response}" ${?} + break + ;; + getactivewatches) + # curl (GET) 192.168.111.152:8080/getactivewatches + + response=$(getactivewatches) + response_to_client "${response}" ${?} + break + ;; + conf) + # curl (GET) 192.168.111.152:8080/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387 + + response=$(confirmation_request "${line}") + response_to_client "${response}" ${?} + break + ;; + getbestblockhash) + # curl (GET) http://192.168.111.152:8080/getbestblockhash + + response=$(get_best_block_hash) + response_to_client "${response}" ${?} + break + ;; + getblockinfo) + # curl (GET) http://192.168.111.152:8080/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea + + response=$(get_block_info $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + gettransaction) + # curl (GET) http://192.168.111.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 + + response=$(get_rawtransaction $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + getbestblockinfo) + # curl (GET) http://192.168.111.152:8080/getbestblockinfo + + response=$(get_best_block_info) + response_to_client "${response}" ${?} + break + ;; + executecallbacks) + # curl (GET) http://192.168.111.152:8080/executecallbacks + + manage_not_imported + manage_missed_conf + response=$(do_callbacks) + response_to_client "${response}" ${?} + break + ;; + getbalance) + # curl (GET) http://192.168.111.152:8080/getbalance + + response=$(getbalance) + response_to_client "${response}" ${?} + break + ;; + getnewaddress) + # curl (GET) http://192.168.111.152:8080/getnewaddress + + response=$(getnewaddress) + response_to_client "${response}" ${?} + break + ;; + spend) + # POST http://192.168.111.152:8080/spend + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + + response=$(spend "${line}") + response_to_client "${response}" ${?} + break + ;; + addtobatch) + # POST http://192.168.111.152:8080/addtobatch + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + + response=$(addtobatching $(echo "${line}" | jq ".address" | tr -d '"') $(echo "${line}" | jq ".amount")) + response_to_client "${response}" ${?} + break + ;; + batchspend) + # GET http://192.168.111.152:8080/batchspend + + response=$(batchspend "${line}") + response_to_client "${response}" ${?} + break + ;; + deriveindex) + # curl GET http://192.168.111.152:8080/deriveindex/25-30 + # curl GET http://192.168.111.152:8080/deriveindex/34 + + response=$(deriveindex $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)) + response_to_client "${response}" ${?} + break + ;; + derivepubpath) + # POST http://192.168.111.152:8080/derivepubpath + # BODY {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"} + # BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"} + # BODY {"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"} + + response=$(send_to_pycoin "${line}") + response_to_client "${response}" ${?} + break + ;; + ln_getinfo) + # GET http://192.168.111.152:8080/ln_getinfo + + response=$(ln_getinfo) + response_to_client "${response}" ${?} + break + ;; + ln_create_invoice) + # POST http://192.168.111.152:8080/ln_create_invoice + # BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"900"} + + response=$(ln_create_invoice "${line}") + response_to_client "${response}" ${?} + break + ;; + ln_pay) + # POST http://192.168.111.152:8080/ln_pay + # BODY {"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","expected_msatoshi":"10000","expected_description":"Bitcoin Outlet order #7082"} + + response=$(ln_pay "${line}") + response_to_client "${response}" ${?} + break + ;; + ln_newaddr) + # GET http://192.168.111.152:8080/ln_newaddr + + response=$(ln_newaddr) + response_to_client "${response}" ${?} + break + ;; + ots_stamp) + # POST http://192.168.111.152:8080/ots_stamp + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","callbackUrl":"192.168.111.233:1111/callbackUrl"} + + response=$(serve_ots_stamp "${line}") + response_to_client "${response}" ${?} + break + ;; + ots_backoffice) + # curl (GET) http://192.168.111.152:8080/ots_upgradeandcallback + + response=$(serve_ots_backoffice) + response_to_client "${response}" ${?} + break + ;; + ots_getfile) + # curl (GET) http://192.168.111.152:8080/ots_getfile/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + + serve_ots_getfile $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3) + break + ;; + esac + break + fi + done + trace "[main] exiting" + return 0 } export NODE_RPC_URL=$BTC_NODE_RPC_URL diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh index 820439556..53f95ec27 100644 --- a/proxy_docker/app/script/responsetoclient.sh +++ b/proxy_docker/app/script/responsetoclient.sh @@ -18,4 +18,23 @@ response_to_client() sleep 0.2s } +file_response_to_client() +{ + trace "Entering bin_response_to_client()..." + + local path=${1} + local filename=${2} + local pathfile="${path}${filename}" + local returncode + + [ -r "${pathfile}" ] \ + && echo -ne "HTTP/1.1 200 OK\r\nContent-Disposition: inline; filename=\"${filename}\"\r\nContent-Length: $(stat -c'%s' ${pathfile})\r\n\r\n" \ + && cat ${pathfile} + + [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" + + # Small delay needed for the data to be processed correctly by peer + sleep 0.2s +} + case "${0}" in *responsetoclient.sh) response_to_client $@;; esac diff --git a/proxy_docker/app/script/watchrequest.sh b/proxy_docker/app/script/watchrequest.sh index e056d8cb2..9846d80b3 100644 --- a/proxy_docker/app/script/watchrequest.sh +++ b/proxy_docker/app/script/watchrequest.sh @@ -7,69 +7,68 @@ watchrequest() { - trace "Entering watchrequest()..." + trace "Entering watchrequest()..." - local returncode - local request=${1} - local address=$(echo "${request}" | jq ".address" | tr -d '"') - local cb0conf_url=$(echo "${request}" | jq ".unconfirmedCallbackURL" | tr -d '"') - local cb1conf_url=$(echo "${request}" | jq ".confirmedCallbackURL" | tr -d '"') - local imported - local inserted - local id_inserted - local result - trace "[watchrequest] Watch request on address (${address}), cb 0-conf (${cb0conf_url}), cb 1-conf (${cb1conf_url})" + local returncode + local request=${1} + local address=$(echo "${request}" | jq ".address" | tr -d '"') + local cb0conf_url=$(echo "${request}" | jq ".unconfirmedCallbackURL" | tr -d '"') + local cb1conf_url=$(echo "${request}" | jq ".confirmedCallbackURL" | tr -d '"') + local imported + local inserted + local id_inserted + local result + trace "[watchrequest] Watch request on address (${address}), cb 0-conf (${cb0conf_url}), cb 1-conf (${cb1conf_url})" - result=$(importaddress_rpc "${address}") - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -eq 0 ]; then - imported=1 - else - imported=0 - fi + result=$(importaddress_rpc "${address}") + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + imported=1 + else + imported=0 + fi -# sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})" - sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})" - returncode=$? - trace_rc ${returncode} - if [ "${returncode}" -eq 0 ]; then - inserted=1 - id_inserted=$(sql "SELECT id FROM watching WHERE address='${address}'") - trace "[watchrequest] id_inserted: ${id_inserted}" - else - inserted=0 - fi + sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})" + returncode=$? + trace_rc ${returncode} + if [ "${returncode}" -eq 0 ]; then + inserted=1 + id_inserted=$(sql "SELECT id FROM watching WHERE address='${address}'") + trace "[watchrequest] id_inserted: ${id_inserted}" + else + inserted=0 + fi - local fees2blocks - local fees6blocks - local fees36blocks - local fees144blocks - fees2blocks=$(getestimatesmartfee 2) - trace_rc $? - fees6blocks=$(getestimatesmartfee 6) - trace_rc $? - fees36blocks=$(getestimatesmartfee 36) - trace_rc $? - fees144blocks=$(getestimatesmartfee 144) - trace_rc $? + local fees2blocks + local fees6blocks + local fees36blocks + local fees144blocks + fees2blocks=$(getestimatesmartfee 2) + trace_rc $? + fees6blocks=$(getestimatesmartfee 6) + trace_rc $? + fees36blocks=$(getestimatesmartfee 36) + trace_rc $? + fees144blocks=$(getestimatesmartfee 144) + trace_rc $? - local data="{\"id\":\"${id_inserted}\", - \"event\":\"watch\", - \"imported\":\"${imported}\", - \"inserted\":\"${inserted}\", - \"address\":\"${address}\", - \"unconfirmedCallbackURL\":\"${cb0conf_url}\", - \"confirmedCallbackURL\":\"${cb1conf_url}\", - \"estimatesmartfee2blocks\":\"${fees2blocks}\", - \"estimatesmartfee6blocks\":\"${fees6blocks}\", - \"estimatesmartfee36blocks\":\"${fees36blocks}\", - \"estimatesmartfee144blocks\":\"${fees144blocks}\"}" - trace "[watchrequest] responding=${data}" + local data="{\"id\":\"${id_inserted}\", + \"event\":\"watch\", + \"imported\":\"${imported}\", + \"inserted\":\"${inserted}\", + \"address\":\"${address}\", + \"unconfirmedCallbackURL\":\"${cb0conf_url}\", + \"confirmedCallbackURL\":\"${cb1conf_url}\", + \"estimatesmartfee2blocks\":\"${fees2blocks}\", + \"estimatesmartfee6blocks\":\"${fees6blocks}\", + \"estimatesmartfee36blocks\":\"${fees36blocks}\", + \"estimatesmartfee144blocks\":\"${fees144blocks}\"}" + trace "[watchrequest] responding=${data}" - echo "${data}" + echo "${data}" - return ${returncode} + return ${returncode} } case "${0}" in *watchrequest.sh) watchrequest $@;; esac diff --git a/proxy_docker/env.properties b/proxy_docker/env.properties index 9a1f8d5cf..6de427e6e 100644 --- a/proxy_docker/env.properties +++ b/proxy_docker/env.properties @@ -12,7 +12,8 @@ DB_FILE=/proxy/db/proxydb # Pycoin container PYCOIN_CONTAINER=pycoinnode:7777 # OTS container -OTS_CONTAINER=otsnode:6666 +OTSCLIENT_CONTAINER=otsclient:6666 +OTS_FILES=/otsfiles DERIVATION_PUB32=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb DERIVATION_PATH=0/n From dfe28b17794cd7ae933186e80e40c0d53abf1d78 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 19 Nov 2018 21:39:54 -0500 Subject: [PATCH 002/268] Tweaks and versioning and nginx arm alpine --- api_auth_docker/Dockerfile | 4 +++- api_auth_docker/Dockerfile-debian | 22 +++++++++++++++++++++ api_auth_docker/auth.sh | 32 ++++++++++++++++--------------- api_auth_docker/entrypoint.sh | 2 +- api_auth_docker/tests.sh | 2 +- otsclient_docker/README.md | 8 ++------ pycoin_docker/Dockerfile | 3 +-- 7 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 api_auth_docker/Dockerfile-debian diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index c1db73b2d..1fcf5381d 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -1,4 +1,6 @@ -FROM nginx:alpine +# Does not work on ARM / Raspberry Pi + +FROM cyphernode/nginx:1.14.1-alpine RUN apk add --update --no-cache \ git \ diff --git a/api_auth_docker/Dockerfile-debian b/api_auth_docker/Dockerfile-debian new file mode 100644 index 000000000..f9ec64379 --- /dev/null +++ b/api_auth_docker/Dockerfile-debian @@ -0,0 +1,22 @@ +FROM nginx:1.14 + +RUN apt-get update \ + && apt-get install -y \ + openssl \ + spawn-fcgi \ + fcgiwrap \ + jq \ + curl + +COPY auth.sh /etc/nginx/conf.d +COPY default-ssl.conf /etc/nginx/conf.d/default.conf +COPY entrypoint.sh entrypoint.sh +COPY keys.properties /etc/nginx/conf.d +COPY api.properties /etc/nginx/conf.d +COPY trace.sh /etc/nginx/conf.d +COPY tests.sh /etc/nginx/conf.d +COPY ip-whitelist.conf /etc/nginx/conf.d + +RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh index 6915603c0..6900f71ea 100644 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -39,14 +39,14 @@ verify_sign() if [ ${exp} -gt ${current} ]; then trace "[verify_sign] Not expired, let's validate signature" local id=$(echo ${payload} | jq ".id" | tr -d '"') - trace "[verify_sign] id=${id}" + trace "[verify_sign] id=${id}" - # Check for code injection - # id will usually be an int, but can be alphanum... nothing else - case $id in (*[![:alnum:]]*|"") - trace "[verify_sign] Potential code injection, exiting" - return 1 - esac + # Check for code injection + # id will usually be an int, but can be alphanum... nothing else + case $id in (*[![:alnum:]]*|"") + trace "[verify_sign] Potential code injection, exiting" + return 1 + esac # It is so much faster to include the keys here instead of grep'ing the file for key. . ./keys.properties @@ -88,15 +88,15 @@ verify_group() local id=${1} # REQUEST_URI should look like this: /watch/2blablabla - local action=$(echo "${REQUEST_URI:1}" | cut -d '/' -f1) + local action=$(echo "${REQUEST_URI#\/}" | cut -d '/' -f1) trace "[verify_group] action=${action}" # Check for code injection # action can be alphanum... and _ and - but nothing else local actiontoinspect=$(echo "$action" | tr -d '_-') case $actiontoinspect in (*[![:alnum:]]*|"") - trace "[verify_group] Potential code injection, exiting" - return 1 + trace "[verify_group] Potential code injection, exiting" + return 1 esac # It is so much faster to include the keys here instead of grep'ing the file for key. @@ -121,15 +121,17 @@ verify_group() # $HTTP_AUTHORIZATION = Bearer +# Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwMyIsImV4cCI6MTU0MjE0OTMyNH0=.b811067cf79c7009a0a38f110a6e3bf82cc4310aa6afae75b9d915b9febf13f7 # If this is not found in header, we leave trace "[auth.sh] HTTP_AUTHORIZATION=${HTTP_AUTHORIZATION}" -if [ "${HTTP_AUTHORIZATION:0:6}" = "Bearer" ]; then - token="${HTTP_AUTHORIZATION:6}" +# /bin/sh on debian points to dash, which does not support substring in the form ${var:offset:length} +if [ "-${HTTP_AUTHORIZATION%% *}" = "-Bearer" ]; then + token="${HTTP_AUTHORIZATION#Bearer }" if [ -n "$token" ]; then - trace "[auth.sh] Valid format for authorization header" - verify_sign "${token}" - [ "$?" -eq "0" ] && return + trace "[auth.sh] Valid format for authorization header" + verify_sign "${token}" + [ "$?" -eq "0" ] && return fi fi diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh index be8f16484..fb53a01e2 100644 --- a/api_auth_docker/entrypoint.sh +++ b/api_auth_docker/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/sh -spawn-fcgi -s /var/run/fcgiwrap.socket -u nginx -g nginx -U nginx -- /usr/bin/fcgiwrap +spawn-fcgi -s /var/run/fcgiwrap.socket -u nginx -g nginx -U nginx -- `which fcgiwrap` nginx -g "daemon off;" diff --git a/api_auth_docker/tests.sh b/api_auth_docker/tests.sh index 25e1285ad..e95fd2600 100644 --- a/api_auth_docker/tests.sh +++ b/api_auth_docker/tests.sh @@ -4,7 +4,7 @@ # Replace # proxy_pass http://cyphernode:8888; # by -# proxy_pass http://tests:8888; +# proxy_pass http://cyphernode:1111; # in /etc/nginx/conf.d/default.conf to run the tests test_expiration() diff --git a/otsclient_docker/README.md b/otsclient_docker/README.md index c90bb4ea2..b45e20d54 100644 --- a/otsclient_docker/README.md +++ b/otsclient_docker/README.md @@ -14,11 +14,7 @@ sudo find ~/otsfiles -type d -exec chmod 2775 {} \; ; sudo find ~/otsfiles -type # Usefull examples -See https://github.com/opentimestamps/javascript-opentimestamps - -List SegWit addresses for path 0/24-30 for a pub32: - ```shell -curl -H "Content-Type: application/json" -d '{"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}' http://localhost:7777/derive -curl -H "Content-Type: application/json" -d '{"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}' http://localhost:7777/derive +curl http://localhost:6666/stamp/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 +curl http://localhost:6666/upgrade/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 ``` diff --git a/pycoin_docker/Dockerfile b/pycoin_docker/Dockerfile index 5e748526e..7b45efa92 100644 --- a/pycoin_docker/Dockerfile +++ b/pycoin_docker/Dockerfile @@ -1,5 +1,4 @@ -#FROM resin/raspberry-pi-alpine-python:3.6 -FROM python:3.6-alpine +FROM python:3.6-alpine3.8 ENV HOME /pycoin From 6f1d9348f9a1a0986c94646a1717f1680159de5a Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 20 Nov 2018 19:44:28 -0500 Subject: [PATCH 003/268] Added Docker registry related docs --- api_auth_docker/README.md | 22 +++++++++++++++++++++- cron_docker/README.md | 26 ++++++++++++++++++++------ docker-compose.yml | 2 +- otsclient_docker/README.md | 26 +++++++++++++++++++++----- proxy_docker/README.md | 22 ++++++++++++++++++++-- pycoin_docker/README.md | 22 +++++++++++++++++++--- 6 files changed, 102 insertions(+), 18 deletions(-) diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index c5b86dba1..a5d999080 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -2,7 +2,27 @@ So all the other containers are in the Docker Swarm and we want to expose a real HTTP/S interface to clients outside of the Swarm, that makes sense. Clients have to get an API key first. -## Build +## Pull our Cyphernode image + +```shell +docker pull cyphernode/gatekeeper:cyphernode-0.05 +``` + +## Build yourself the image + +```shell +docker build -t cyphernode/gatekeeper:cyphernode-0.05 . +``` + +## Run image + +If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: + +```shell +docker run -d --rm --name gatekeeper -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode-ssl/certs:/etc/ssl/certs" -v "~/cyphernode-ssl/private:/etc/ssl/private" --env-file env.properties cyphernode/gatekeeper:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` +``` + +## Prepare ### Create your API key and put it in keys.properties diff --git a/cron_docker/README.md b/cron_docker/README.md index c5e56bb51..a510d87d3 100644 --- a/cron_docker/README.md +++ b/cron_docker/README.md @@ -1,14 +1,28 @@ # Cyphernode CRON container -## Configure your container by modifying `env.properties` file +## Pull our Cyphernode image -```properties -TX_CONF_URL=cyphernode:8888/executecallbacks -OTS_URL=cyphernode:8888/ots_backoffice +```shell +docker pull cyphernode/proxycron:cyphernode-0.05 ``` -## Building docker image +## Build yourself the image ```shell -docker build -t proxycronimg . +docker build -t cyphernode/proxycron:cyphernode-0.05 . +``` + +## Run image + +If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: + +```shell +docker run --rm -d --network cyphernodenet --env-file env.properties cyphernode/proxycron:cyphernode-0.05 +``` + +## Configure your container by modifying `env.properties` file + +```properties +TX_CONF_URL=cyphernode:8888/executecallbacks +OTS_URL=cyphernode:8888/ots_backoffice ``` diff --git a/docker-compose.yml b/docker-compose.yml index 97f10fbc2..b46cbdc24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: # Pycoin env_file: - pycoin_docker/env.properties - image: pycoinimg + image: cyphernode/pycoin:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] diff --git a/otsclient_docker/README.md b/otsclient_docker/README.md index b45e20d54..964a2ed57 100644 --- a/otsclient_docker/README.md +++ b/otsclient_docker/README.md @@ -1,18 +1,34 @@ -# Build image +# OTS Client Cyphernode Container + +## Pull our Cyphernode image + +```shell +docker pull cyphernode/ots:cyphernode-0.05 +``` + +## Build yourself the image ```shell -docker build -t otsclientimg . +docker build -t cyphernode/ots:cyphernode-0.05 . ``` -# OTS files directory... +## OTS files directory... ```shell mkdir -p ~/otsfiles -sudo chown -R cyphernode:debian ~/otsfiles ; sudo chmod g+ws ~/otsfiles +sudo chown -R cyphernode:cyphernode ~/otsfiles ; sudo chmod g+ws ~/otsfiles sudo find ~/otsfiles -type d -exec chmod 2775 {} \; ; sudo find ~/otsfiles -type f -exec chmod g+rw {} \; ``` -# Usefull examples +## Run image + +If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: + +```shell +docker run --rm -d -p 6666:6666 --network cyphernodenet --env-file env.properties cyphernode/ots:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startotsclient.sh +``` + +## Usefull examples ```shell curl http://localhost:6666/stamp/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 diff --git a/proxy_docker/README.md b/proxy_docker/README.md index 98dea439f..bf6fac3f4 100644 --- a/proxy_docker/README.md +++ b/proxy_docker/README.md @@ -1,6 +1,24 @@ # Cyphernode Proxy -We assume you are the user pi on a Raspberry Pi. +## Pull our Cyphernode image + +```shell +docker pull cyphernode/proxy:cyphernode-0.05 +``` + +## Build yourself the image + +```shell +docker build -t cyphernode/proxy:cyphernode-0.05 . +``` + +## Run image + +If you want to run this container independently from Cyphernode: + +```shell +docker run --rm -d -p 8888:8888 --network cyphernodenet --env-file env.properties cyphernode/proxy:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startproxy.sh +``` ## Configure your container by modifying `env.properties` file @@ -45,7 +63,7 @@ docker build -t btcproxyimg . ## Create sqlite3 database path and give rights ```shell -mkdir ~/btcproxydb ; sudo chown -R cyphernode:pi ~/btcproxydb ; sudo chmod g+ws ~/btcproxydb +mkdir ~/proxydb ; sudo chown -R cyphernode:cyphernode ~/proxydb ; sudo chmod g+ws ~/proxydb ``` ## What you MUST have in your Watching Bitcoin node's bitcoin.conf file diff --git a/pycoin_docker/README.md b/pycoin_docker/README.md index 5f00b5d7e..971aec37b 100644 --- a/pycoin_docker/README.md +++ b/pycoin_docker/README.md @@ -1,10 +1,26 @@ -# Build image +# Pycoin container in Cyphernode + +## Pull our Cyphernode image + +```shell +docker pull cyphernode/pycoin:cyphernode-0.05 +``` + +## Build yourself the image + +```shell +docker build -t cyphernode/pycoin:cyphernode-0.05 . +``` + +## Run image + +If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: ```shell -docker build -t pycoinimg . +docker run --rm -d -p 7777:7777 --network cyphernodenet --env-file env.properties cyphernode/pycoin:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startpycoin.sh ``` -# Usefull examples +## Usefull examples See https://github.com/shivaenigma/pycoin From 4673b12993f4b8a81fd713f4006083525265efe2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 20 Nov 2018 20:11:07 -0500 Subject: [PATCH 004/268] ARM version of nginx on Alpine is back --- api_auth_docker/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 1fcf5381d..c1db73b2d 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -1,6 +1,4 @@ -# Does not work on ARM / Raspberry Pi - -FROM cyphernode/nginx:1.14.1-alpine +FROM nginx:alpine RUN apk add --update --no-cache \ git \ From 8198b1977a02e582d33e59f5d3c7f56dbfae04c4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 21 Nov 2018 09:32:01 -0500 Subject: [PATCH 005/268] Using registry images with new container names --- cron_docker/env.properties | 4 ++-- docker-compose.yml | 24 ++++++++++++------------ proxy_docker/env.properties | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cron_docker/env.properties b/cron_docker/env.properties index e49eee37a..a441ac313 100644 --- a/cron_docker/env.properties +++ b/cron_docker/env.properties @@ -1,2 +1,2 @@ -TX_CONF_URL=cyphernode:8888/executecallbacks -OTS_URL=cyphernode:8888/ots_backoffice +TX_CONF_URL=proxy:8888/executecallbacks +OTS_URL=proxy:8888/ots_backoffice diff --git a/docker-compose.yml b/docker-compose.yml index b46cbdc24..e898d1dfb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: "3" services: - authapi: + gatekeeper: # HTTP authentication API gate env_file: - api_auth_docker/env.properties - image: authapi + image: cyphernode/gatekeeper:cyphernode-0.05 ports: # - "80:80" - "443:443" @@ -18,11 +18,11 @@ services: networks: - cyphernodenet - cyphernode: + proxy: # Bitcoin Mini Proxy env_file: - proxy_docker/env.properties - image: btcproxyimg + image: cyphernode/proxy:cyphernode-0.05 volumes: # Variable substitutions don't work # Match with DB_PATH in proxy_docker/env.properties @@ -38,18 +38,18 @@ services: networks: - cyphernodenet - proxycronnode: + proxycron: # Async jobs env_file: - cron_docker/env.properties - image: proxycronimg + image: cyphernode/proxycron:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] networks: - cyphernodenet - pycoinnode: + pycoin: # Pycoin env_file: - pycoin_docker/env.properties @@ -65,7 +65,7 @@ services: # otsclient JS env_file: - otsclient_docker/env.properties - image: otsclientimg + image: cyphernode/ots:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] @@ -75,9 +75,9 @@ services: networks: - cyphernodenet - clightningnode: + lightning: # c-lightning lightning network node - image: clnimg + image: cyphernode/clightning:dev ports: - "9735:9735" volumes: @@ -89,9 +89,9 @@ services: networks: - cyphernodenet - btcnode: + bitcoin: # Bitcoin node - image: btcnode + image: cyphernode/bitcoin:0.17.0 # ports: # - "18333:18333" # - "29000:29000" diff --git a/proxy_docker/env.properties b/proxy_docker/env.properties index 6de427e6e..480110843 100644 --- a/proxy_docker/env.properties +++ b/proxy_docker/env.properties @@ -1,8 +1,8 @@ TRACING=1 -WATCHER_BTC_NODE_RPC_URL=btcnode:18332/wallet/watching01.dat +WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/watching01.dat WATCHER_BTC_NODE_RPC_USER=rpc_username:rpc_password WATCHER_BTC_NODE_RPC_CFG=/proxy/watcher_btcnode_curlcfg.properties -SPENDER_BTC_NODE_RPC_URL=btcnode:18332/wallet/spending01.dat +SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/spending01.dat SPENDER_BTC_NODE_RPC_USER=rpc_username:rpc_password SPENDER_BTC_NODE_RPC_CFG=/proxy/spender_btcnode_curlcfg.properties PROXY_LISTENING_PORT=8888 @@ -10,7 +10,7 @@ PROXY_LISTENING_PORT=8888 DB_PATH=/proxy/db DB_FILE=/proxy/db/proxydb # Pycoin container -PYCOIN_CONTAINER=pycoinnode:7777 +PYCOIN_CONTAINER=pycoin:7777 # OTS container OTSCLIENT_CONTAINER=otsclient:6666 OTS_FILES=/otsfiles From 2106d717d7c89e1080c4089f32b3579ba977ff42 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 21 Nov 2018 11:26:01 -0500 Subject: [PATCH 006/268] Proxy's hostname is proxy --- api_auth_docker/default-ssl.conf | 2 +- api_auth_docker/default.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index 36edeeb92..d5487d741 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -9,7 +9,7 @@ server { location / { auth_request /auth; - proxy_pass http://cyphernode:8888; + proxy_pass http://proxy:8888; } location /auth { diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index fca3c1be0..e9d02c7b3 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -6,7 +6,7 @@ server { location / { auth_request /auth; - proxy_pass http://cyphernode:8888; + proxy_pass http://proxy:8888; } location /auth { From f1b98255c090726d085a1b8818cc24c6b5bbe685 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 21 Nov 2018 11:46:18 -0500 Subject: [PATCH 007/268] Some tweaks for the new cyphernode --- doc/INSTALL-MANUAL-STEPS.md | 27 +++++++++++++++------------ docker-compose.yml | 1 + 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 4f376af21..8d51d493d 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -46,7 +46,7 @@ vi api_auth_docker/env.properties ```shell sudo useradd cyphernode -mkdir ~/btcproxydb ; sudo chown -R cyphernode:debian ~/btcproxydb ; sudo chmod g+ws ~/btcproxydb +mkdir ~/proxydb ; sudo chown -R cyphernode:cyphernode ~/proxydb ; sudo chmod g+ws ~/proxydb mkdir -p ~/cyphernode-ssl/certs ~/cyphernode-ssl/private openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode-ssl/private/key.pem -out ~/cyphernode-ssl/certs/cert.pem -days 365 docker build -t authapi api_auth_docker/. @@ -69,7 +69,7 @@ vi bitcoin.conf *Make sure testnet, rpcuser and rpcpassword have the same value as in bitcoin node's bitcoin.conf file (see below)* ```console -rpcconnect=btcnode +rpcconnect=bitcoin rpcuser=rpc_username rpcpassword=rpc_password testnet=1 @@ -80,14 +80,17 @@ rpcwallet=ln01.dat vi config mkdir ~/lndata cp config ~/lndata/ -sudo chown -R cyphernode:debian ~/lndata ; sudo chmod g+ws ~/lndata +sudo chown -R cyphernode:cyphernode ~/lndata ; sudo chmod g+ws ~/lndata sudo find ~/lndata -type d -exec chmod 2775 {} \; ; sudo find ~/lndata -type f -exec chmod g+rw {} \; docker build -t clnimg . cd ../../bitcoin-core/ mkdir ~/btcdata -sudo chown -R cyphernode:debian ~/btcdata ; sudo chmod g+ws ~/btcdata +sudo chown -R cyphernode:cyphernode ~/btcdata ; sudo chmod g+ws ~/btcdata sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f -exec chmod g+rw {} \; docker build -t btcnode . +mkdir ~/otsfiles +sudo chown -R cyphernode:cyphernode ~/otsfiles ; sudo chmod g+ws ~/otsfiles +sudo find ~/otsfiles -type d -exec chmod 2775 {} \; ; sudo find ~/otsfiles -type f -exec chmod g+rw {} \; ``` ## Mount bitcoin data volume and make sure bitcoin configuration is ok @@ -115,21 +118,21 @@ zmqpubrawtx=tcp://0.0.0.0:29000 wallet=watching01.dat wallet=spending01.dat wallet=ln01.dat -walletnotify=curl cyphernode:8888/conf/%s +walletnotify=curl proxy:8888/conf/%s ``` ## Deploy the cyphernode stack ```shell cd ~/cyphernode/ -USER=`id -u cyphernode`:`id -g cyphernode` docker stack deploy --compose-file docker-compose.yml cyphernodestack +USER=`id -u cyphernode`:`id -g cyphernode` docker stack deploy --compose-file docker-compose.yml cyphernode ``` ## Wait a few minutes and re-apply permissions ```shell -sudo chown -R cyphernode:debian ~/lndata ; sudo chmod g+ws ~/lndata -sudo chown -R cyphernode:debian ~/btcdata ; sudo chmod g+ws ~/btcdata +sudo chown -R cyphernode:cyphernode ~/lndata ; sudo chmod g+ws ~/lndata +sudo chown -R cyphernode:cyphernode ~/btcdata ; sudo chmod g+ws ~/btcdata sudo find ~/lndata -type d -exec chmod 2775 {} \; ; sudo find ~/lndata -type f -exec chmod g+rw {} \; sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f -exec chmod g+rw {} \; ``` @@ -143,8 +146,8 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech ``` ```shell -echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -docker exec -it `docker ps -q -f name=cyphernodestack_cyphernode` curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" cyphernode:8888/derivepubpath +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 ``` diff --git a/docker-compose.yml b/docker-compose.yml index e898d1dfb..1da585c87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,6 +82,7 @@ services: - "9735:9735" volumes: - "~/lndata:/.lightning" + - "~/lndata/bitcoin.conf:/.bitcoin/bitcoin.conf" # deploy: # placement: # constraints: [node.hostname==dev] From a7ea9ce93c80abf458abd48ac59a879e1906895f Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 26 Nov 2018 13:38:26 -0500 Subject: [PATCH 008/268] OTS stamping and upgrading fixes --- doc/INSTALL-MANUAL-STEPS.md | 6 ++++ doc/INSTALL.md | 7 ++++ proxy_docker/app/script/ots.sh | 25 ++++++++++---- proxy_docker/app/script/responsetoclient.sh | 38 ++++++++++----------- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 8d51d493d..5b228a490 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -145,6 +145,12 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech 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://localhost/ots_stamp ``` +If you need the authorization header to copy/paste in another tool: + +```shell +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+30))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" +``` + ```shell 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 - diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 9bd13b350..d78cd6a71 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -116,6 +116,13 @@ pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode ```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 -H "Authorization: Bearer $token" -k https://localhost/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 -H "Authorization: Bearer $token" -k https://localhost/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://localhost/ots_stamp +``` + +If you need the authorization header to copy/paste in another tool: + +```shell +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" ``` ## Test deployment from any host of the swarm diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index e55cf810b..807cb5db0 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -79,10 +79,12 @@ request_ots_stamp() if [ "${returncode}" -eq 0 ]; then # jq -e will have a return code of 1 if the supplied tag is null. - errorstring=$(echo "${result}" | tr '\r\n' ' ' | jq -e ".error" | tr -d '"') + errorstring=$(echo "${result}" | tr '\r\n' ' ' | jq -e ".error") if [ "$?" -eq "0" ]; then # Error tag not null, so there's an error + errorstring=$(echo "${errorstring}" | tr -d '"') + # If the error message is "Already exists" trace "[request_ots_stamp] grepping 'already exists'..." echo "${result}" | grep "already exists" > /dev/null @@ -161,12 +163,23 @@ serve_ots_backoffice() returncode=$? trace_rc ${returncode} trace "[serve_ots_backoffice] result=${result}" - if [ "${returncode}" -eq 0 ]; then - trace "[serve_ots_backoffice] just upgraded!" - sql "UPDATE stamp SET upgraded=1 WHERE hash=\"${hash}\"" - trace_rc $? - upgraded=1 + if [ "${returncode}" -eq 0 ]; then + # CURL success... let's see if error in response + errorstring=$(echo "${result}" | tr '\r\n' ' ' | jq -e ".error") + if [ "$?" -eq "0" ]; then + # Error tag not null, so there's an error + trace "[serve_ots_backoffice] not upgraded!" + + upgraded=0 + else + # No failure, upgraded + trace "[serve_ots_backoffice] just upgraded!" + sql "UPDATE stamp SET upgraded=1 WHERE hash=\"${hash}\"" + trace_rc $? + + upgraded=1 + fi fi fi if [ "${upgraded}" -eq "1" ]; then diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh index 53f95ec27..28121e598 100644 --- a/proxy_docker/app/script/responsetoclient.sh +++ b/proxy_docker/app/script/responsetoclient.sh @@ -4,37 +4,37 @@ response_to_client() { - trace "Entering response_to_client()..." + trace "Entering response_to_client()..." - local response=${1} - local returncode=${2} + local response=${1} + local returncode=${2} - ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" - [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" + ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" + [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" - echo -en "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" + echo -en "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" - # Small delay needed for the data to be processed correctly by peer - sleep 0.2s + # Small delay needed for the data to be processed correctly by peer + sleep 0.2s } file_response_to_client() { - trace "Entering bin_response_to_client()..." + trace "Entering file_response_to_client()..." - local path=${1} - local filename=${2} - local pathfile="${path}${filename}" - local returncode + local path=${1} + local filename=${2} + local pathfile="${path}${filename}" + local returncode - [ -r "${pathfile}" ] \ - && echo -ne "HTTP/1.1 200 OK\r\nContent-Disposition: inline; filename=\"${filename}\"\r\nContent-Length: $(stat -c'%s' ${pathfile})\r\n\r\n" \ - && cat ${pathfile} + [ -r "${pathfile}" ] \ + && echo -ne "HTTP/1.1 200 OK\r\nContent-Disposition: inline; filename=\"${filename}\"\r\nContent-Length: $(stat -c'%s' ${pathfile})\r\n\r\n" \ + && cat ${pathfile} - [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" + [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" - # Small delay needed for the data to be processed correctly by peer - sleep 0.2s + # Small delay needed for the data to be processed correctly by peer + sleep 0.2s } case "${0}" in *responsetoclient.sh) response_to_client $@;; esac From e6378db01213e81152baa2e9f8185eec2a5410ad Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 29 Nov 2018 11:54:43 -0500 Subject: [PATCH 009/268] Added inserted_id in stamp response and content-type on get OTS file --- proxy_docker/app/script/ots.sh | 23 ++++++++++++++++----- proxy_docker/app/script/responsetoclient.sh | 10 +++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index 807cb5db0..52991e48f 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -16,14 +16,23 @@ serve_ots_stamp() local result local returncode local errorstring + local id_inserted + local requested + local row # Already requested? - local requested - requested=$(sql "SELECT requested FROM stamp WHERE hash='${hash}'") - if [ -n "${requested}" ]; then + row=$(sql "SELECT id, requested FROM stamp WHERE hash='${hash}'") + trace "[serve_ots_stamp] row=${row}" + + if [ -n "${row}" ]; then # Hash exists in DB... trace "[serve_ots_stamp] Hash already exists in DB." + requested=$(echo "${row}" | cut -d '|' -f2) + trace "[serve_ots_stamp] requested=${requested}" + id_inserted=$(echo "${row}" | cut -d '|' -f1) + trace "[serve_ots_stamp] id_inserted=${id_inserted}" + if [ "${requested}" -eq "1" ]; then # Stamp already requested trace "[serve_ots_stamp] Stamp already requested" @@ -38,6 +47,8 @@ serve_ots_stamp() returncode=$? trace_rc ${returncode} if [ "${returncode}" -eq "0" ]; then + id_inserted=$(sql "SELECT id FROM stamp WHERE hash='${hash}'") + trace_rc $? errorstring=$(request_ots_stamp "${hash}") returncode=$? trace_rc ${returncode} @@ -48,10 +59,12 @@ serve_ots_stamp() fi fi + result="{\"method\":\"ots_stamp\",\"hash\":\"${hash}\",\"id\":\"${id_inserted}\",\"result\":\"" + if [ "${returncode}" -eq "0" ]; then - result="{\"method\":\"ots_stamp\",\"hash\":\"${hash}\",\"result\":\"success\"" + result="${result}success\"}" else - result="{\"method\":\"ots_stamp\",\"hash\":\"${hash}\",\"result\":\"error\",\"error\":\"${errorstring}\"" + result="${result}error\",\"error\":\"${errorstring}\"}" fi trace "[serve_ots_stamp] result=${result}" diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh index 28121e598..193d0c7e5 100644 --- a/proxy_docker/app/script/responsetoclient.sh +++ b/proxy_docker/app/script/responsetoclient.sh @@ -27,14 +27,20 @@ file_response_to_client() local pathfile="${path}${filename}" local returncode + trace "[file_response_to_client] path=${path}" + trace "[file_response_to_client] filename=${filename}" + trace "[file_response_to_client] pathfile=${pathfile}" + local file_length=$(stat -c'%s' ${pathfile}) + trace "[file_response_to_client] file_length=${file_length}" + [ -r "${pathfile}" ] \ - && echo -ne "HTTP/1.1 200 OK\r\nContent-Disposition: inline; filename=\"${filename}\"\r\nContent-Length: $(stat -c'%s' ${pathfile})\r\n\r\n" \ + && echo -ne "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Disposition: inline; filename=\"${filename}\"\r\nContent-Length: ${file_length}\r\n\r\n" \ && cat ${pathfile} [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" # Small delay needed for the data to be processed correctly by peer - sleep 0.2s + sleep 0.5s } case "${0}" in *responsetoclient.sh) response_to_client $@;; esac From d81e36f10026ab5cf543699658af68481a5f5902 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 30 Nov 2018 14:17:58 -0500 Subject: [PATCH 010/268] OTS on the client samples and a fix in comments --- clients/javascript/cyphernode-client.js | 82 ++++++++++++++-------- clients/shell/cyphernode-client.sh | 93 +++++++++++++++---------- proxy_docker/app/script/ots.sh | 2 +- 3 files changed, 109 insertions(+), 68 deletions(-) diff --git a/clients/javascript/cyphernode-client.js b/clients/javascript/cyphernode-client.js index a70ffe549..afc1eaa1b 100644 --- a/clients/javascript/cyphernode-client.js +++ b/clients/javascript/cyphernode-client.js @@ -30,48 +30,56 @@ CyphernodeClient.prototype._generateToken = function() { return token } -CyphernodeClient.prototype._post = function(url, postdata, cb) { +CyphernodeClient.prototype._post = function(url, postdata, cb, addedOptions) { let urlr = this.baseURL + url; - - HTTP.post(urlr, - { - data: postdata, - npmRequestOptions: { - strictSSL: false, - agentOptions: { - rejectUnauthorized: false - } - }, - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this._generateToken() + let httpOptions = { + data: postdata, + npmRequestOptions: { + strictSSL: false, + agentOptions: { + rejectUnauthorized: false } - }, function (err, resp) { + }, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this._generateToken() + } + } + if (addedOptions) { + Object.assign(httpOptions.npmRequestOptions, addedOptions) + } + + HTTP.post(urlr, httpOptions, + function (err, resp) { // console.log(err) // console.log(resp) - cb(err, resp.data) + cb(err, resp.data || resp.content) } ) }; -CyphernodeClient.prototype._get = function(url, cb) { +CyphernodeClient.prototype._get = function(url, cb, addedOptions) { let urlr = this.baseURL + url; - - HTTP.get(urlr, - { - npmRequestOptions: { - strictSSL: false, - agentOptions: { - rejectUnauthorized: false - } - }, - headers: { - 'Authorization': 'Bearer ' + this._generateToken() + let httpOptions = { + npmRequestOptions: { + strictSSL: false, + agentOptions: { + rejectUnauthorized: false } - }, function (err, resp) { + }, + headers: { + 'Authorization': 'Bearer ' + this._generateToken() + } + } + if (addedOptions) { + Object.assign(httpOptions.npmRequestOptions, addedOptions) + } + + HTTP.get(urlr, httpOptions, + function (err, resp) { // console.log(err) // console.log(resp) - cb(err, resp.data) + cb(err, resp.data || resp.content) } ) }; @@ -112,3 +120,17 @@ CyphernodeClient.prototype.getNewAddress = function(cbreply) { // http://192.168.122.152:8080/getnewaddress this._get('/getnewaddress', cbreply); }; + +CyphernodeClient.prototype.ots_stamp = function(hash, callbackUrl, cbreply) { + // POST https://cyphernode/ots_stamp + // BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","callbackUrl":"192.168.111.233:1111/callbackUrl"} + let data = { hash: hash, callbackUrl: callbackUrl } + this._post('/ots_stamp', data, cbreply); +}; + +CyphernodeClient.prototype.ots_getfile = function(hash, cbreply) { + // http://192.168.122.152:8080/ots_getfile/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + + // encoding: null is for HTTP get to not convert the binary data to the default encoding + this._get('/ots_getfile/' + hash, cbreply, { encoding: null }); +}; diff --git a/clients/shell/cyphernode-client.sh b/clients/shell/cyphernode-client.sh index 512c1a385..7504c7698 100644 --- a/clients/shell/cyphernode-client.sh +++ b/clients/shell/cyphernode-client.sh @@ -4,73 +4,92 @@ invoke_cyphernode() { - local action=${1} - local post=${2} - - local p64=$(echo -n "{\"id\":\"${id}\",\"exp\":$((`date +"%s"`+10))}" | base64) - local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$key" -sha256 -r | cut -sd ' ' -f1) - local token="$h64.$p64.$s" - - if [ -n "${post}" ]; then - echo $(curl -v -H "Authorization: Bearer $token" -d "${post}" -k "https://cyphernode/${action}") - return $? - else - echo $(curl -v -H "Authorization: Bearer $token" -k "https://cyphernode/${action}") - return $? - fi + local action=${1} + local post=${2} + + local p64=$(echo -n "{\"id\":\"${id}\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$key" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + if [ -n "${post}" ]; then + echo $(curl -v -H "Authorization: Bearer $token" -d "${post}" -k "https://cyphernode/${action}") + return $? + else + echo $(curl -v -H "Authorization: Bearer $token" -k "https://cyphernode/${action}") + return $? + fi } watch() { - # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.122.233:1111/callback0conf","confirmedCallbackURL":"192.168.122.233:1111/callback1conf"} - local btcaddr=${1} - local cb0conf=${2} - local cb1conf=${3} - local post="{\"address\":\"${btcaddr}\",\"unconfirmedCallbackURL\":\"${cb0conf}\",\"confirmedCallbackURL\":\"${cb1conf}\"}" + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.122.233:1111/callback0conf","confirmedCallbackURL":"192.168.122.233:1111/callback1conf"} + local btcaddr=${1} + local cb0conf=${2} + local cb1conf=${3} + local post="{\"address\":\"${btcaddr}\",\"unconfirmedCallbackURL\":\"${cb0conf}\",\"confirmedCallbackURL\":\"${cb1conf}\"}" - echo $(invoke_cyphernode "watch" ${post}) + echo $(invoke_cyphernode "watch" ${post}) } unwatch() { - # 192.168.122.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp - local btcaddr=${1} + # 192.168.122.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp + local btcaddr=${1} - echo $(invoke_cyphernode "unwatch/${btcaddr}") + echo $(invoke_cyphernode "unwatch/${btcaddr}") } getactivewatches() { - # 192.168.122.152:8080/getactivewatches - echo $(invoke_cyphernode "getactivewatches") + # 192.168.122.152:8080/getactivewatches + echo $(invoke_cyphernode "getactivewatches") } gettransaction() { - # http://192.168.122.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 - local txid=${1} + # http://192.168.122.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 + local txid=${1} - echo $(invoke_cyphernode "gettransaction/${txid}") + echo $(invoke_cyphernode "gettransaction/${txid}") } spend() { - # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} - local btcaddr=${1} - local amount=${2} - local post="{\"address\":\"${btcaddr}\",\"amount\":\"${amount}\"}" + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + local btcaddr=${1} + local amount=${2} + local post="{\"address\":\"${btcaddr}\",\"amount\":\"${amount}\"}" - echo $(invoke_cyphernode "spend" ${post}) + echo $(invoke_cyphernode "spend" ${post}) } getbalance() { - # http://192.168.122.152:8080/getbalance - echo $(invoke_cyphernode "getbalance") + # http://192.168.122.152:8080/getbalance + echo $(invoke_cyphernode "getbalance") } getnewaddress() { - # http://192.168.122.152:8080/getnewaddress - echo $(invoke_cyphernode "getnewaddress") + # http://192.168.122.152:8080/getnewaddress + echo $(invoke_cyphernode "getnewaddress") +} + +ots_stamp() +{ + # POST https://cyphernode/ots_stamp + # BODY {"hash":"1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7","callbackUrl":"192.168.111.233:1111/callbackUrl"} + local hash=${1} + local callbackUrl=${2} + local post="{\"hash\":\"${hash}\",\"callbackUrl\":\"${callbackUrl}\"}" + + echo $(invoke_cyphernode "ots_stamp" ${post}) +} + +ots_getfile() +{ + # http://192.168.122.152:8080/ots_getfile/1ddfb769eb0b8876bc570e25580e6a53afcf973362ee1ee4b54a807da2e5eed7 + local hash=${1} + + echo $(invoke_cyphernode "ots_getfile/${hash}") } diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index 52991e48f..a24ee6b68 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -196,7 +196,7 @@ serve_ots_backoffice() fi fi if [ "${upgraded}" -eq "1" ]; then - trace "[serve_ots_backoffice] upgraded! Let's send the OTS file to callback..." + trace "[serve_ots_backoffice] upgraded! Let's call the callback..." url=$(echo "${row}" | cut -d '|' -f2) trace "[serve_ots_backoffice] url=${url}" From 983ab0662014de339be5b3cca21ae96c7189ec7c Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 3 Oct 2018 22:29:23 +0200 Subject: [PATCH 011/268] added submodule for SatoshiPortal dockers --- .gitmodules | 3 +++ install/SatoshiPortal/dockers | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 install/SatoshiPortal/dockers diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..6ce87e64a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "install/SatoshiPortal/dockers"] + path = install/SatoshiPortal/dockers + url = git@github.com:SatoshiPortal/dockers.git diff --git a/install/SatoshiPortal/dockers b/install/SatoshiPortal/dockers new file mode 160000 index 000000000..094a4ef34 --- /dev/null +++ b/install/SatoshiPortal/dockers @@ -0,0 +1 @@ +Subproject commit 094a4ef34f66a106c2bca0e802dd105aa27eb71a From ac9b771bc4b21f04f50954ae17a328b096202f6b Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 3 Oct 2018 22:29:55 +0200 Subject: [PATCH 012/268] added building of needed dockerfiles --- install/script/docker.sh | 10 ++++++++++ install/script/install.sh | 17 +++++++++++++++++ install/script/trace.sh | 15 +++++++++++++++ 3 files changed, 42 insertions(+) create mode 100755 install/script/docker.sh create mode 100755 install/script/install.sh create mode 100644 install/script/trace.sh diff --git a/install/script/docker.sh b/install/script/docker.sh new file mode 100755 index 000000000..19de4e2dc --- /dev/null +++ b/install/script/docker.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +. ./trace.sh + +build_docker_image() { + + trace "building docker image: $1 with tag $2:latest" + docker build $1 -t $2:latest + +} diff --git a/install/script/install.sh b/install/script/install.sh new file mode 100755 index 000000000..a186b923b --- /dev/null +++ b/install/script/install.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +. ./trace.sh +. ./docker.sh + +trace "Updating SatoshiPortal dockers" +git submodule update --recursive --remote + +# build SatoshiPortal images +arch=x86_64 +build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode +build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg + +# build cyphernode images +build_docker_image ../../cron_docker/ proxycronimg +build_docker_image ../../proxy_docker/ btcproxyimg +build_docker_image ../../pycoin_docker/ pycoinimg diff --git a/install/script/trace.sh b/install/script/trace.sh new file mode 100644 index 000000000..34a18df18 --- /dev/null +++ b/install/script/trace.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi +} From 3a4eed516bfab6a1a45f9f1eb1bc890ed35a4834 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 3 Oct 2018 22:30:16 +0200 Subject: [PATCH 013/268] added script to trigger real install.sh --- install.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 install.sh diff --git a/install.sh b/install.sh new file mode 100755 index 000000000..bc75115b4 --- /dev/null +++ b/install.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +(cd install/script && TRACING=1 ./install.sh) \ No newline at end of file From acb777db90b5eaeb6ef041370d3fe93462cf7b73 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 4 Oct 2018 00:30:30 +0200 Subject: [PATCH 014/268] created structure for configuring services inside a common docker container so the configuration process is os-independant --- install/.dockerignore | 3 +++ install/Dockerfile | 13 +++++++++++++ install/bitcoind/script/configure.sh | 7 +++++++ install/bitcoind/script/trace.sh | 15 +++++++++++++++ install/bitcoind/templates/bitcoin.conf | 11 +++++++++++ install/script/cyphernodeconf.sh | 20 ++++++++++++++++++++ install/script/install.sh | 7 +++++++ 7 files changed, 76 insertions(+) create mode 100644 install/.dockerignore create mode 100644 install/Dockerfile create mode 100755 install/bitcoind/script/configure.sh create mode 100644 install/bitcoind/script/trace.sh create mode 100644 install/bitcoind/templates/bitcoin.conf create mode 100755 install/script/cyphernodeconf.sh diff --git a/install/.dockerignore b/install/.dockerignore new file mode 100644 index 000000000..3e95f1851 --- /dev/null +++ b/install/.dockerignore @@ -0,0 +1,3 @@ +SatoshiPortal +data +script diff --git a/install/Dockerfile b/install/Dockerfile new file mode 100644 index 000000000..8f5c30b13 --- /dev/null +++ b/install/Dockerfile @@ -0,0 +1,13 @@ +FROM alpine + +RUN apk add --update --no-cache \ + curl \ + dialog + +RUN mkdir /volume /script /data + +WORKDIR /script + +ENV TRACING=1 + +CMD ["./configure.sh"] diff --git a/install/bitcoind/script/configure.sh b/install/bitcoind/script/configure.sh new file mode 100755 index 000000000..c2f9930c0 --- /dev/null +++ b/install/bitcoind/script/configure.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# TODO: config entry for persistent volume of bitcoind + +dialog --colors --textbox trace.sh 40 80 + + diff --git a/install/bitcoind/script/trace.sh b/install/bitcoind/script/trace.sh new file mode 100644 index 000000000..34a18df18 --- /dev/null +++ b/install/bitcoind/script/trace.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi +} diff --git a/install/bitcoind/templates/bitcoin.conf b/install/bitcoind/templates/bitcoin.conf new file mode 100644 index 000000000..ca7e8ec34 --- /dev/null +++ b/install/bitcoind/templates/bitcoin.conf @@ -0,0 +1,11 @@ +# testnet +testnet=1 + +#lnd opts +txindex=1 +zmqpubrawblock=tcp://0.0.0.0:18501 +zmqpubrawtx=tcp://0.0.0.0:18502 + +# general opts +rpcuser=%RPCUSER% +rpcpassword=%RPCPASSWORD% diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh new file mode 100755 index 000000000..99dad8b25 --- /dev/null +++ b/install/script/cyphernodeconf.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +. ./trace.sh + +# this will run configure.sh of the specified package inside a +# cyphernodeconf container. This way we ensure we have the right +# environment and do not pollute the host machine with utility +# commands not needed for runtime + +cyphernodeconf_configure() { + PWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + DATA_PATH=$PWD/../data + SCRIPT_PATH=$PWD/../$1/script + VOLUME_PATH=/tmp + docker run -v $VOLUME_PATH:/volume \ + -v $DATA_PATH:/data \ + -v $SCRIPT_PATH:/script\ + --log-driver=none\ + --rm -it cyphernodeconf:latest +} \ No newline at end of file diff --git a/install/script/install.sh b/install/script/install.sh index a186b923b..b7f86fece 100755 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -2,6 +2,7 @@ . ./trace.sh . ./docker.sh +. ./cyphernodeconf.sh trace "Updating SatoshiPortal dockers" git submodule update --recursive --remote @@ -15,3 +16,9 @@ build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg build_docker_image ../../cron_docker/ proxycronimg build_docker_image ../../proxy_docker/ btcproxyimg build_docker_image ../../pycoin_docker/ pycoinimg + +# build setup docker image +build_docker_image ../ cyphernodeconf + +# configure bitcoind +cyphernodeconf_configure bitcoind \ No newline at end of file From 91aab75e39e520e9578efac68f7326cd6257c440 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 14:40:27 +0200 Subject: [PATCH 015/268] switched configuration process to yeoman generator inside a minimal docker image --- install/.dockerignore | 3 + install/Dockerfile | 28 +++-- install/bitcoind/script/configure.sh | 7 -- install/bitcoind/script/trace.sh | 15 --- install/bitcoind/templates/bitcoin.conf | 11 -- install/data/.gitkeep | 0 install/generator-cyphernode/.gitignore | 2 + .../generators/app/index.js | 113 ++++++++++++++++++ .../generators/app/templates/splash.txt | 27 +++++ install/generator-cyphernode/package.json | 29 +++++ install/insight-yo.json | 3 + install/script/cyphernodeconf.sh | 14 +-- install/script/install.sh | 35 +++--- 13 files changed, 222 insertions(+), 65 deletions(-) delete mode 100755 install/bitcoind/script/configure.sh delete mode 100644 install/bitcoind/script/trace.sh delete mode 100644 install/bitcoind/templates/bitcoin.conf create mode 100644 install/data/.gitkeep create mode 100644 install/generator-cyphernode/.gitignore create mode 100644 install/generator-cyphernode/generators/app/index.js create mode 100644 install/generator-cyphernode/generators/app/templates/splash.txt create mode 100644 install/generator-cyphernode/package.json create mode 100644 install/insight-yo.json diff --git a/install/.dockerignore b/install/.dockerignore index 3e95f1851..aad322ef1 100644 --- a/install/.dockerignore +++ b/install/.dockerignore @@ -1,3 +1,6 @@ SatoshiPortal data script +generator-cyphernode/node_modules +generator-cyphernode/package-lock +generator-cyphernode/__tests__ diff --git a/install/Dockerfile b/install/Dockerfile index 8f5c30b13..91c36852f 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,13 +1,25 @@ -FROM alpine +FROM node:alpine -RUN apk add --update --no-cache \ - curl \ - dialog +RUN apk add --update bash && rm -rf /var/cache/apk/* +RUN adduser -D -h /yo yo yo +RUN chown -R yo:yo /usr/local/lib/node_modules /usr/local/bin -RUN mkdir /volume /script /data +USER yo +RUN npm install -g yo +COPY generator-cyphernode /yo +WORKDIR /yo/generator-cyphernode +RUN npm link -WORKDIR /script +USER root +RUN mkdir -p /yo/.config/insight-nodejs -ENV TRACING=1 +# prevent "do you want to send stats"-questions for temporary yo installation +COPY insight-yo.json /yo/.config/insight-nodejs/insight-yo.json +RUN chown -R yo:yo /yo/.config + +# run in user space +USER yo +WORKDIR /yo + +CMD ["yo","cyphernode"] -CMD ["./configure.sh"] diff --git a/install/bitcoind/script/configure.sh b/install/bitcoind/script/configure.sh deleted file mode 100755 index c2f9930c0..000000000 --- a/install/bitcoind/script/configure.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# TODO: config entry for persistent volume of bitcoind - -dialog --colors --textbox trace.sh 40 80 - - diff --git a/install/bitcoind/script/trace.sh b/install/bitcoind/script/trace.sh deleted file mode 100644 index 34a18df18..000000000 --- a/install/bitcoind/script/trace.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -trace() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr - fi -} - -trace_rc() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr - fi -} diff --git a/install/bitcoind/templates/bitcoin.conf b/install/bitcoind/templates/bitcoin.conf deleted file mode 100644 index ca7e8ec34..000000000 --- a/install/bitcoind/templates/bitcoin.conf +++ /dev/null @@ -1,11 +0,0 @@ -# testnet -testnet=1 - -#lnd opts -txindex=1 -zmqpubrawblock=tcp://0.0.0.0:18501 -zmqpubrawtx=tcp://0.0.0.0:18502 - -# general opts -rpcuser=%RPCUSER% -rpcpassword=%RPCPASSWORD% diff --git a/install/data/.gitkeep b/install/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/install/generator-cyphernode/.gitignore b/install/generator-cyphernode/.gitignore new file mode 100644 index 000000000..ba2a97b57 --- /dev/null +++ b/install/generator-cyphernode/.gitignore @@ -0,0 +1,2 @@ +node_modules +coverage diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js new file mode 100644 index 000000000..1fd4e8331 --- /dev/null +++ b/install/generator-cyphernode/generators/app/index.js @@ -0,0 +1,113 @@ +'use strict'; +const Generator = require('yeoman-generator'); +const chalk = require('chalk'); +const fs = require('fs'); +const wrap = require('wordwrap')(86); + +module.exports = class extends Generator { + + constructor(args, opts) { + super(args, opts); + + this.props = { + name: 'supercollider-project', + type: 'simple', + description: '' + }; + } + + /* + prompting() { + + const prompts = [ + { + type: 'confirm', + name: 'someAnswer', + message: 'Would you like to enable this option?', + default: true + } + ]; + + return this.prompt(prompts).then(props => { + // To access props later use this.props.someAnswer; + this.props = props; + }); + } + */ + +// fountainPrompting() { + prompting() { + const splash = fs.readFileSync(this.templatePath('splash.txt')); + this.log(splash.toString()); + + var prompts = [ + { + // https://github.com/SBoudrias/Inquirer.js#question + // input, confirm, list, rawlist, expand, checkbox, password, editor + type: 'checkbox', + name: 'features', + message: wrap('What features do you want to add to your cyphernode?')+'\n', + choices: [ + { + name: 'Bitcoin full node', + value: 'bitcoin' + }, + { + name: 'Lightning node', + value: 'lightning' + }, + { + name: 'Open timestamps server', + value: 'ots' + } + + ] + }, + { + when: function(answers) { + return answers.features && + answers.features.indexOf( 'bitcoin' ) != -1; + }, + type: 'confirm', + default: false, + name: 'lightning_implementation', + message: wrap('Run bitcoin node in prune mode?')+'\n', + }, + { + when: function(answers) { + return answers.features && + answers.features.indexOf( 'lightning' ) != -1; + }, + type: 'list', + name: 'lightning_implementation', + message: wrap('What lightning implementation do you want to use?')+'\n', + choices: [ + { + name: 'C-lightning', + value: 'c-lightning' + }, + { + name: 'LND', + value: 'lnd' + } + ] + } + ]; + + return this.prompt(prompts).then(props => { + this.props = Object.assign(this.props, props); + }); + } + + writing() { + /* + this.fs.copy( + this.templatePath('dummyfile.txt'), + this.destinationPath('dummyfile.txt') + ); + */ + } + + install() { + } +}; diff --git a/install/generator-cyphernode/generators/app/templates/splash.txt b/install/generator-cyphernode/generators/app/templates/splash.txt new file mode 100644 index 000000000..4250a2ef2 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/splash.txt @@ -0,0 +1,27 @@ +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            +                                            diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json new file mode 100644 index 000000000..851db5e39 --- /dev/null +++ b/install/generator-cyphernode/package.json @@ -0,0 +1,29 @@ +{ + "name": "generator-cyphernode", + "version": "0.0.0", + "description": "", + "homepage": "", + "author": { + "name": "jash", + "email": "jash@schulterklopfer-productions.de", + "url": "" + }, + "files": [ + "generators" + ], + "main": "generators/index.js", + "keywords": [ + "cyphernode", + "yeoman-generator" + ], + "engines": { + "npm": ">= 4.0.0" + }, + "dependencies": { + "chalk": "^2.1.0", + "wordwrap": "^1.0.0", + "yeoman-generator": "^2.0.1" + }, + "repository": "git@github.com:schulterklopfer/cyphernode.git", + "license": "MIT" +} diff --git a/install/insight-yo.json b/install/insight-yo.json new file mode 100644 index 000000000..3f7916942 --- /dev/null +++ b/install/insight-yo.json @@ -0,0 +1,3 @@ +{ + "optOut": true +} \ No newline at end of file diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh index 99dad8b25..7868522d9 100755 --- a/install/script/cyphernodeconf.sh +++ b/install/script/cyphernodeconf.sh @@ -8,13 +8,11 @@ # commands not needed for runtime cyphernodeconf_configure() { - PWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - DATA_PATH=$PWD/../data - SCRIPT_PATH=$PWD/../$1/script - VOLUME_PATH=/tmp - docker run -v $VOLUME_PATH:/volume \ - -v $DATA_PATH:/data \ - -v $SCRIPT_PATH:/script\ + local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + local data_path=$current_path/../data + local docker_image="cyphernodeconf:latest" + + docker run -v $data_path:/data \ --log-driver=none\ - --rm -it cyphernodeconf:latest + --rm -it $docker_image } \ No newline at end of file diff --git a/install/script/install.sh b/install/script/install.sh index b7f86fece..1d4e8cbba 100755 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -3,22 +3,25 @@ . ./trace.sh . ./docker.sh . ./cyphernodeconf.sh +. ./config.sh -trace "Updating SatoshiPortal dockers" -git submodule update --recursive --remote - -# build SatoshiPortal images -arch=x86_64 -build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode -build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg +config_file=$1 -# build cyphernode images -build_docker_image ../../cron_docker/ proxycronimg -build_docker_image ../../proxy_docker/ btcproxyimg -build_docker_image ../../pycoin_docker/ pycoinimg - -# build setup docker image -build_docker_image ../ cyphernodeconf +trace "Updating SatoshiPortal dockers" +#git submodule update --recursive --remote +# +## build SatoshiPortal images +#local arch=x86_64 +#build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode +#build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg +# +## build cyphernode images +#build_docker_image ../../cron_docker/ proxycronimg +#build_docker_image ../../proxy_docker/ btcproxyimg +#build_docker_image ../../pycoin_docker/ pycoinimg +# +## build setup docker image +build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." -# configure bitcoind -cyphernodeconf_configure bitcoind \ No newline at end of file +# configure features of cyphernode +cyphernodeconf_configure From 61e4c6621e382bb9b6caa046dccb3c766e817d37 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 14:41:50 +0200 Subject: [PATCH 016/268] removed broken ref --- install/script/install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/install/script/install.sh b/install/script/install.sh index 1d4e8cbba..b94d2da0e 100755 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -3,7 +3,6 @@ . ./trace.sh . ./docker.sh . ./cyphernodeconf.sh -. ./config.sh config_file=$1 From 324df1804b8da6ba6f21b407a5bb55453bdcd6b5 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 16:38:52 +0200 Subject: [PATCH 017/268] some cleanup and validation tests --- .../generators/app/index.js | 191 +++++++++++------- install/generator-cyphernode/package.json | 1 + install/script/cyphernodeconf.sh | 6 +- install/script/install.sh | 2 + 4 files changed, 124 insertions(+), 76 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 1fd4e8331..f0fafbb5a 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -3,6 +3,7 @@ const Generator = require('yeoman-generator'); const chalk = require('chalk'); const fs = require('fs'); const wrap = require('wordwrap')(86); +const validator = require('validator'); module.exports = class extends Generator { @@ -10,89 +11,134 @@ module.exports = class extends Generator { super(args, opts); this.props = { - name: 'supercollider-project', - type: 'simple', - description: '' + }; } - /* - prompting() { + /* values */ - const prompts = [ - { - type: 'confirm', - name: 'someAnswer', - message: 'Would you like to enable this option?', - default: true - } - ]; + _isChecked( name, value ) { + return value=='bitcoin'; + } - return this.prompt(prompts).then(props => { - // To access props later use this.props.someAnswer; - this.props = props; - }); + _getConfirmDefault( name ) { + return true; } - */ -// fountainPrompting() { - prompting() { - const splash = fs.readFileSync(this.templatePath('splash.txt')); - this.log(splash.toString()); + _getListDefault( name ) { + return 'lnd'; + } + + _getInputDefault( name ) { + return ''; + } + + /* validators */ + _ipValidator( ip ) { + return validator.isIP((ip+"").trim()); + } + + /* filters */ + + _trimFilter( input ) { + return input.trim(); + } + + /* prompts */ + _configureFeatures() { + return [{ + // https://github.com/SBoudrias/Inquirer.js#question + // input, confirm, list, rawlist, expand, checkbox, password, editor + type: 'checkbox', + name: 'features', + message: wrap('What features do you want to add to your cyphernode?')+'\n', + choices: [ + { + name: 'Bitcoin full node', + value: 'bitcoin', + checked: this._isChecked( 'features', 'bitcoin' ) + }, + { + name: 'Lightning node', + value: 'lightning', + checked: this._isChecked( 'features', 'lightning' ) - var prompts = [ - { - // https://github.com/SBoudrias/Inquirer.js#question - // input, confirm, list, rawlist, expand, checkbox, password, editor - type: 'checkbox', - name: 'features', - message: wrap('What features do you want to add to your cyphernode?')+'\n', - choices: [ - { - name: 'Bitcoin full node', - value: 'bitcoin' - }, - { - name: 'Lightning node', - value: 'lightning' - }, - { - name: 'Open timestamps server', - value: 'ots' - } - - ] - }, - { - when: function(answers) { - return answers.features && - answers.features.indexOf( 'bitcoin' ) != -1; }, - type: 'confirm', - default: false, - name: 'lightning_implementation', - message: wrap('Run bitcoin node in prune mode?')+'\n', + { + name: 'Open timestamps server', + value: 'ots', + checked: this._isChecked( 'features', 'ots' ) + } + + ] + }]; + } + + _configureBitcoinFullNode() { + return [{ + when: function(answers) { + return answers.features && + answers.features.indexOf( 'bitcoin' ) != -1; + }, + type: 'confirm', + name: 'bitcoin_prune', + default: this._getConfirmDefault( 'bitcoin_prune' ), + message: wrap('Run bitcoin node in prune mode?')+'\n', + }, + { + when: function(answers) { + return answers.features && + answers.features.indexOf( 'bitcoin' ) != -1; }, - { - when: function(answers) { - return answers.features && - answers.features.indexOf( 'lightning' ) != -1; + type: 'input', + name: 'bitcoin_external_ip', + default: this._getInputDefault( 'bitcoin_external_ip' ), + validate: this._ipValidator, + message: wrap('What external ip does your bitcoin full node have?')+'\n', + }]; + } + + _configureLightningImplementation() { + return [{ + when: function(answers) { + return answers.features && + answers.features.indexOf( 'lightning' ) != -1; + }, + type: 'list', + name: 'lightning_implementation', + default: this._getListDefault( 'lightning_implementation' ), + message: wrap('What lightning implementation do you want to use?')+'\n', + choices: [ + { + name: 'C-lightning', + value: 'c-lightning' }, - type: 'list', - name: 'lightning_implementation', - message: wrap('What lightning implementation do you want to use?')+'\n', - choices: [ - { - name: 'C-lightning', - value: 'c-lightning' - }, - { - name: 'LND', - value: 'lnd' - } - ] - } - ]; + { + name: 'LND', + value: 'lnd' + } + ] + }]; + } + + _configureCLightning() { + return [{}]; + } + + _configureLND() { + return [{}]; + } + + prompting() { + const splash = fs.readFileSync(this.templatePath('splash.txt')); + this.log(splash.toString()); + + var prompts = + this._configureFeatures() + .concat(this._configureBitcoinFullNode()) + .concat(this._configureLightningImplementation()) + //.concat(this._configureCLightning()) + //.concat(this._configureLND()) return this.prompt(prompts).then(props => { this.props = Object.assign(this.props, props); @@ -100,6 +146,7 @@ module.exports = class extends Generator { } writing() { + console.log( JSON.stringify(this.props, null, 2)); /* this.fs.copy( this.templatePath('dummyfile.txt'), diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index 851db5e39..32d63bd3b 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "chalk": "^2.1.0", + "validator": "^10.8.0", "wordwrap": "^1.0.0", "yeoman-generator": "^2.0.1" }, diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh index 7868522d9..f5644a98a 100755 --- a/install/script/cyphernodeconf.sh +++ b/install/script/cyphernodeconf.sh @@ -9,10 +9,8 @@ cyphernodeconf_configure() { local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - local data_path=$current_path/../data - local docker_image="cyphernodeconf:latest" - docker run -v $data_path:/data \ + docker run -v $current_path/../data:/data \ --log-driver=none\ - --rm -it $docker_image + --rm -it cyphernodeconf:latest } \ No newline at end of file diff --git a/install/script/install.sh b/install/script/install.sh index b94d2da0e..a94aaef90 100755 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -24,3 +24,5 @@ build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." # configure features of cyphernode cyphernodeconf_configure + +#docker image rm cyphernodeconf:latest From b40dc6d5d52bddff831c5b29d0de6fa7e84df92c Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 17:04:45 +0200 Subject: [PATCH 018/268] props.json is written and reread at beginning of configuration process so alle former selections are preserved when reconfiguring --- .../generators/app/index.js | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index f0fafbb5a..841b4f889 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -9,28 +9,22 @@ module.exports = class extends Generator { constructor(args, opts) { super(args, opts); + if( fs.existsSync('/data/props.json') ) { + this.props = require('/data/props.json'); + } else { + this.props = {}; + } - this.props = { - - }; + console.log( this.props ); } /* values */ - _isChecked( name, value ) { - return value=='bitcoin'; - } - - _getConfirmDefault( name ) { - return true; - } - - _getListDefault( name ) { - return 'lnd'; + return this.props && this.props[name].indexOf(value) != -1 ; } - _getInputDefault( name ) { - return ''; + _getDefault( name ) { + return this.props && this.props[name]; } /* validators */ @@ -65,9 +59,14 @@ module.exports = class extends Generator { }, { - name: 'Open timestamps server', + name: 'Open timestamps client', value: 'ots', checked: this._isChecked( 'features', 'ots' ) + }, + { + name: 'Electrum server', + value: 'electrum', + checked: this._isChecked( 'features', 'ots' ) } ] @@ -82,7 +81,7 @@ module.exports = class extends Generator { }, type: 'confirm', name: 'bitcoin_prune', - default: this._getConfirmDefault( 'bitcoin_prune' ), + default: this._getDefault( 'bitcoin_prune' ), message: wrap('Run bitcoin node in prune mode?')+'\n', }, { @@ -92,7 +91,7 @@ module.exports = class extends Generator { }, type: 'input', name: 'bitcoin_external_ip', - default: this._getInputDefault( 'bitcoin_external_ip' ), + default: this._getDefault( 'bitcoin_external_ip' ), validate: this._ipValidator, message: wrap('What external ip does your bitcoin full node have?')+'\n', }]; @@ -106,7 +105,7 @@ module.exports = class extends Generator { }, type: 'list', name: 'lightning_implementation', - default: this._getListDefault( 'lightning_implementation' ), + default: this._getDefault( 'lightning_implementation' ), message: wrap('What lightning implementation do you want to use?')+'\n', choices: [ { @@ -121,6 +120,29 @@ module.exports = class extends Generator { }]; } + _configureElectrumImplementation() { + return [{ + when: function(answers) { + return answers.features && + answers.features.indexOf( 'electrum' ) != -1; + }, + type: 'list', + name: 'electrum_implementation', + default: this._getDefault( 'electrum_implementation' ), + message: wrap('What electrum implementation do you want to use?')+'\n', + choices: [ + { + name: 'Electrum personal server', + value: 'eps' + }, + { + name: 'Electrumx server', + value: 'elx' + } + ] + }]; + } + _configureCLightning() { return [{}]; } @@ -137,6 +159,7 @@ module.exports = class extends Generator { this._configureFeatures() .concat(this._configureBitcoinFullNode()) .concat(this._configureLightningImplementation()) + .concat(this._configureElectrumImplementation()) //.concat(this._configureCLightning()) //.concat(this._configureLND()) @@ -146,7 +169,7 @@ module.exports = class extends Generator { } writing() { - console.log( JSON.stringify(this.props, null, 2)); + fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); /* this.fs.copy( this.templatePath('dummyfile.txt'), From d1945b6f37cbd824cbc5b556c0e7a21e2d0a751e Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 17:09:51 +0200 Subject: [PATCH 019/268] fixed label --- install/generator-cyphernode/generators/app/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 841b4f889..d18295969 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -35,8 +35,8 @@ module.exports = class extends Generator { /* filters */ _trimFilter( input ) { - return input.trim(); - } + return (input+"").trim(); + } /* prompts */ _configureFeatures() { @@ -66,7 +66,7 @@ module.exports = class extends Generator { { name: 'Electrum server', value: 'electrum', - checked: this._isChecked( 'features', 'ots' ) + checked: this._isChecked( 'features', 'electrum' ) } ] From 574766095fab411692e50b1bdf1768c24f696cd1 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 18:25:02 +0200 Subject: [PATCH 020/268] different cyphernode modules can now be configured in seperate files. This makes adding modules easier --- .../generators/app/features.json | 19 ++ .../generators/app/features/bitcoin.js | 21 ++ .../generators/app/features/electrum.js | 23 +++ .../generators/app/features/lightning.js | 23 +++ .../generators/app/features/opentimestamps.js | 7 + .../generators/app/index.js | 186 +++++------------- 6 files changed, 142 insertions(+), 137 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/features.json create mode 100644 install/generator-cyphernode/generators/app/features/bitcoin.js create mode 100644 install/generator-cyphernode/generators/app/features/electrum.js create mode 100644 install/generator-cyphernode/generators/app/features/lightning.js create mode 100644 install/generator-cyphernode/generators/app/features/opentimestamps.js diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json new file mode 100644 index 000000000..0195ce52b --- /dev/null +++ b/install/generator-cyphernode/generators/app/features.json @@ -0,0 +1,19 @@ +[ + { + "name": "Bitcoin full node", + "value": "bitcoin" + }, + { + "name": "Lightning node", + "value": "lightning" + + }, + { + "name": "Open timestamps client", + "value": "opentimestamps" + }, + { + "name": "Electrum server", + "value": "electrum" + } +] \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/bitcoin.js b/install/generator-cyphernode/generators/app/features/bitcoin.js new file mode 100644 index 000000000..9ea5e8475 --- /dev/null +++ b/install/generator-cyphernode/generators/app/features/bitcoin.js @@ -0,0 +1,21 @@ +const featureCondition = function(props) { + return props.features && props.features.indexOf( 'bitcoin' ) != -1; +}; + +module.exports = function( utils ) { + return [{ + when: featureCondition, + type: 'confirm', + name: 'bitcoin_prune', + default: utils._getDefault( 'bitcoin_prune' ), + message: 'Run bitcoin node in prune mode?'+'\n', + }, + { + when: featureCondition, + type: 'input', + name: 'bitcoin_external_ip', + default: utils._getDefault( 'bitcoin_external_ip' ), + validate: utils._ipValidator, + message: 'What external ip does your bitcoin full node have?'+'\n', + }]; +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/electrum.js b/install/generator-cyphernode/generators/app/features/electrum.js new file mode 100644 index 000000000..b8d73f7f9 --- /dev/null +++ b/install/generator-cyphernode/generators/app/features/electrum.js @@ -0,0 +1,23 @@ +const featureCondition = function(props) { + return props.features && props.features.indexOf( 'electrum' ) != -1; +} + +module.exports = function( utils ) { + return [{ + when: featureCondition, + type: 'list', + name: 'electrum_implementation', + default: utils._getDefault( 'electrum_implementation' ), + message: 'What electrum implementation do you want to use?'+'\n', + choices: [ + { + name: 'Electrum personal server', + value: 'eps' + }, + { + name: 'Electrumx server', + value: 'elx' + } + ] + }]; +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/lightning.js b/install/generator-cyphernode/generators/app/features/lightning.js new file mode 100644 index 000000000..b6622f2ca --- /dev/null +++ b/install/generator-cyphernode/generators/app/features/lightning.js @@ -0,0 +1,23 @@ +const featureCondition = function(props) { + return props.features && props.features.indexOf( 'lightning' ) != -1; +} + +module.exports = function( utils ) { + return [{ + when: featureCondition, + type: 'list', + name: 'lightning_implementation', + default: utils._getDefault( 'lightning_implementation' ), + message: 'What lightning implementation do you want to use?'+'\n', + choices: [ + { + name: 'C-lightning', + value: 'c-lightning' + }, + { + name: 'LND', + value: 'lnd' + } + ] + }]; +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/opentimestamps.js b/install/generator-cyphernode/generators/app/features/opentimestamps.js new file mode 100644 index 000000000..6c0e7e898 --- /dev/null +++ b/install/generator-cyphernode/generators/app/features/opentimestamps.js @@ -0,0 +1,7 @@ +const featureCondition = function(props) { + return props.features && props.features.indexOf( 'opentimestamps' ) != -1; +} + +module.exports = function( utils ) { + return []; +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index d18295969..ff6d82e41 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -4,41 +4,63 @@ const chalk = require('chalk'); const fs = require('fs'); const wrap = require('wordwrap')(86); const validator = require('validator'); +const path = require("path"); +const featureChoices = require(path.join(__dirname, "features.json")); + +let featurePromptModules = []; +const normalizedPath = path.join(__dirname, "features"); +fs.readdirSync(normalizedPath).forEach(function(file) { + featurePromptModules.push(require(path.join(normalizedPath,file))); +}); module.exports = class extends Generator { constructor(args, opts) { super(args, opts); + if( fs.existsSync('/data/props.json') ) { this.props = require('/data/props.json'); } else { this.props = {}; } - console.log( this.props ); - } + this.featureChoices = featureChoices; + for( let c of this.featureChoices ) { + c.checked = this._isChecked( 'features', c.value ); + } - /* values */ - _isChecked( name, value ) { - return this.props && this.props[name].indexOf(value) != -1 ; } - _getDefault( name ) { - return this.props && this.props[name]; + prompting() { + const splash = fs.readFileSync(this.templatePath('splash.txt')); + this.log(splash.toString()); + + var prompts = this._configureFeatures() + + for( let m of featurePromptModules ) { + prompts = prompts.concat(m(this)); + } + + return this.prompt(prompts).then(props => { + this.props = Object.assign(this.props, props); + }); } - /* validators */ - _ipValidator( ip ) { - return validator.isIP((ip+"").trim()); + writing() { + fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); + /* + this.fs.copy( + this.templatePath('dummyfile.txt'), + this.destinationPath('dummyfile.txt') + ); + */ } - /* filters */ + install() { + } - _trimFilter( input ) { - return (input+"").trim(); - } + /* some utils */ - /* prompts */ _configureFeatures() { return [{ // https://github.com/SBoudrias/Inquirer.js#question @@ -46,138 +68,28 @@ module.exports = class extends Generator { type: 'checkbox', name: 'features', message: wrap('What features do you want to add to your cyphernode?')+'\n', - choices: [ - { - name: 'Bitcoin full node', - value: 'bitcoin', - checked: this._isChecked( 'features', 'bitcoin' ) - }, - { - name: 'Lightning node', - value: 'lightning', - checked: this._isChecked( 'features', 'lightning' ) - - }, - { - name: 'Open timestamps client', - value: 'ots', - checked: this._isChecked( 'features', 'ots' ) - }, - { - name: 'Electrum server', - value: 'electrum', - checked: this._isChecked( 'features', 'electrum' ) - } - - ] - }]; - } - - _configureBitcoinFullNode() { - return [{ - when: function(answers) { - return answers.features && - answers.features.indexOf( 'bitcoin' ) != -1; - }, - type: 'confirm', - name: 'bitcoin_prune', - default: this._getDefault( 'bitcoin_prune' ), - message: wrap('Run bitcoin node in prune mode?')+'\n', - }, - { - when: function(answers) { - return answers.features && - answers.features.indexOf( 'bitcoin' ) != -1; - }, - type: 'input', - name: 'bitcoin_external_ip', - default: this._getDefault( 'bitcoin_external_ip' ), - validate: this._ipValidator, - message: wrap('What external ip does your bitcoin full node have?')+'\n', - }]; - } - - _configureLightningImplementation() { - return [{ - when: function(answers) { - return answers.features && - answers.features.indexOf( 'lightning' ) != -1; - }, - type: 'list', - name: 'lightning_implementation', - default: this._getDefault( 'lightning_implementation' ), - message: wrap('What lightning implementation do you want to use?')+'\n', - choices: [ - { - name: 'C-lightning', - value: 'c-lightning' - }, - { - name: 'LND', - value: 'lnd' - } - ] + choices: this.featureChoices }]; } - - _configureElectrumImplementation() { - return [{ - when: function(answers) { - return answers.features && - answers.features.indexOf( 'electrum' ) != -1; - }, - type: 'list', - name: 'electrum_implementation', - default: this._getDefault( 'electrum_implementation' ), - message: wrap('What electrum implementation do you want to use?')+'\n', - choices: [ - { - name: 'Electrum personal server', - value: 'eps' - }, - { - name: 'Electrumx server', - value: 'elx' - } - ] - }]; + + _isChecked( name, value ) { + return this.props && this.props[name] && this.props[name].indexOf(value) != -1 ; } - _configureCLightning() { - return [{}]; + _getDefault( name ) { + return this.props && this.props[name]; } - _configureLND() { - return [{}]; + _ipValidator( ip ) { + return validator.isIP((ip+"").trim()); } - prompting() { - const splash = fs.readFileSync(this.templatePath('splash.txt')); - this.log(splash.toString()); - - var prompts = - this._configureFeatures() - .concat(this._configureBitcoinFullNode()) - .concat(this._configureLightningImplementation()) - .concat(this._configureElectrumImplementation()) - //.concat(this._configureCLightning()) - //.concat(this._configureLND()) - - return this.prompt(prompts).then(props => { - this.props = Object.assign(this.props, props); - }); + _trimFilter( input ) { + return (input+"").trim(); } - writing() { - fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); - /* - this.fs.copy( - this.templatePath('dummyfile.txt'), - this.destinationPath('dummyfile.txt') - ); - */ + _wrap(text) { + return wrap(text); } - install() { - } }; From 5176d867e84a12a3585505ebc825d715494930b4 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 18:43:14 +0200 Subject: [PATCH 021/268] added generation of environment files for later installation step --- .../generators/app/features/bitcoin.js | 43 ++++++++++------- .../generators/app/features/electrum.js | 47 +++++++++++-------- .../generators/app/features/lightning.js | 47 +++++++++++-------- .../generators/app/features/opentimestamps.js | 16 +++++-- .../generators/app/index.js | 29 ++++++------ 5 files changed, 110 insertions(+), 72 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features/bitcoin.js b/install/generator-cyphernode/generators/app/features/bitcoin.js index 9ea5e8475..1277546c3 100644 --- a/install/generator-cyphernode/generators/app/features/bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/bitcoin.js @@ -1,21 +1,30 @@ +const name = 'bitcoin'; const featureCondition = function(props) { - return props.features && props.features.indexOf( 'bitcoin' ) != -1; + return props.features && props.features.indexOf( name ) != -1; }; -module.exports = function( utils ) { - return [{ - when: featureCondition, - type: 'confirm', - name: 'bitcoin_prune', - default: utils._getDefault( 'bitcoin_prune' ), - message: 'Run bitcoin node in prune mode?'+'\n', - }, - { - when: featureCondition, - type: 'input', - name: 'bitcoin_external_ip', - default: utils._getDefault( 'bitcoin_external_ip' ), - validate: utils._ipValidator, - message: 'What external ip does your bitcoin full node have?'+'\n', - }]; +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + when: featureCondition, + type: 'confirm', + name: 'bitcoin_prune', + default: utils._getDefault( 'bitcoin_prune' ), + message: 'Run bitcoin node in prune mode?'+'\n', + }, + { + when: featureCondition, + type: 'input', + name: 'bitcoin_external_ip', + default: utils._getDefault( 'bitcoin_external_ip' ), + validate: utils._ipValidator, + message: 'What external ip does your bitcoin full node have?'+'\n', + }]; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1' + } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/electrum.js b/install/generator-cyphernode/generators/app/features/electrum.js index b8d73f7f9..137e3df2a 100644 --- a/install/generator-cyphernode/generators/app/features/electrum.js +++ b/install/generator-cyphernode/generators/app/features/electrum.js @@ -1,23 +1,32 @@ +const name = 'electrum'; const featureCondition = function(props) { - return props.features && props.features.indexOf( 'electrum' ) != -1; + return props.features && props.features.indexOf( name ) != -1; } -module.exports = function( utils ) { - return [{ - when: featureCondition, - type: 'list', - name: 'electrum_implementation', - default: utils._getDefault( 'electrum_implementation' ), - message: 'What electrum implementation do you want to use?'+'\n', - choices: [ - { - name: 'Electrum personal server', - value: 'eps' - }, - { - name: 'Electrumx server', - value: 'elx' - } - ] - }]; +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + when: featureCondition, + type: 'list', + name: 'electrum_implementation', + default: utils._getDefault( 'electrum_implementation' ), + message: 'What electrum implementation do you want to use?'+'\n', + choices: [ + { + name: 'Electrum personal server', + value: 'eps' + }, + { + name: 'Electrumx server', + value: 'elx' + } + ] + }]; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1' + } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/lightning.js b/install/generator-cyphernode/generators/app/features/lightning.js index b6622f2ca..a695231c1 100644 --- a/install/generator-cyphernode/generators/app/features/lightning.js +++ b/install/generator-cyphernode/generators/app/features/lightning.js @@ -1,23 +1,32 @@ +const name = 'lightning'; const featureCondition = function(props) { - return props.features && props.features.indexOf( 'lightning' ) != -1; + return props.features && props.features.indexOf( name ) != -1; } -module.exports = function( utils ) { - return [{ - when: featureCondition, - type: 'list', - name: 'lightning_implementation', - default: utils._getDefault( 'lightning_implementation' ), - message: 'What lightning implementation do you want to use?'+'\n', - choices: [ - { - name: 'C-lightning', - value: 'c-lightning' - }, - { - name: 'LND', - value: 'lnd' - } - ] - }]; +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + when: featureCondition, + type: 'list', + name: 'lightning_implementation', + default: utils._getDefault( 'lightning_implementation' ), + message: 'What lightning implementation do you want to use?'+'\n', + choices: [ + { + name: 'C-lightning', + value: 'c-lightning' + }, + { + name: 'LND', + value: 'lnd' + } + ] + }]; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1' + } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/opentimestamps.js b/install/generator-cyphernode/generators/app/features/opentimestamps.js index 6c0e7e898..789f319c3 100644 --- a/install/generator-cyphernode/generators/app/features/opentimestamps.js +++ b/install/generator-cyphernode/generators/app/features/opentimestamps.js @@ -1,7 +1,17 @@ + +const name = 'opentimestamps'; const featureCondition = function(props) { - return props.features && props.features.indexOf( 'opentimestamps' ) != -1; + return props.features && props.features.indexOf( name ) != -1; } -module.exports = function( utils ) { - return []; +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return []; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1'; + } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index ff6d82e41..b5fe6202a 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -35,10 +35,17 @@ module.exports = class extends Generator { const splash = fs.readFileSync(this.templatePath('splash.txt')); this.log(splash.toString()); - var prompts = this._configureFeatures() + var prompts = [{ + // https://github.com/SBoudrias/Inquirer.js#question + // input, confirm, list, rawlist, expand, checkbox, password, editor + type: 'checkbox', + name: 'features', + message: wrap('What features do you want to add to your cyphernode?')+'\n', + choices: this.featureChoices + }]; for( let m of featurePromptModules ) { - prompts = prompts.concat(m(this)); + prompts = prompts.concat(m.prompts(this)); } return this.prompt(prompts).then(props => { @@ -48,6 +55,12 @@ module.exports = class extends Generator { writing() { fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); + + for( let m of featurePromptModules ) { + const name = m.name(); + const env = m.env(); + fs.writeFileSync('/data/'+name+'.sh', env); + } /* this.fs.copy( this.templatePath('dummyfile.txt'), @@ -60,18 +73,6 @@ module.exports = class extends Generator { } /* some utils */ - - _configureFeatures() { - return [{ - // https://github.com/SBoudrias/Inquirer.js#question - // input, confirm, list, rawlist, expand, checkbox, password, editor - type: 'checkbox', - name: 'features', - message: wrap('What features do you want to add to your cyphernode?')+'\n', - choices: this.featureChoices - }]; - } - _isChecked( name, value ) { return this.props && this.props[name] && this.props[name].indexOf(value) != -1 ; } From 54f1d0461e2b73108ef7c57c12570fb5735baecf Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 18:52:37 +0200 Subject: [PATCH 022/268] fixed indentation --- .../generators/app/features/bitcoin.js | 46 +++++++++---------- install/insight-yo.json | 2 +- install/script/cyphernodeconf.sh | 8 ++-- install/script/docker.sh | 6 +-- install/script/trace.sh | 12 ++--- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features/bitcoin.js b/install/generator-cyphernode/generators/app/features/bitcoin.js index 1277546c3..3b7b959be 100644 --- a/install/generator-cyphernode/generators/app/features/bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/bitcoin.js @@ -4,27 +4,27 @@ const featureCondition = function(props) { }; module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - return [{ - when: featureCondition, - type: 'confirm', - name: 'bitcoin_prune', - default: utils._getDefault( 'bitcoin_prune' ), - message: 'Run bitcoin node in prune mode?'+'\n', - }, - { - when: featureCondition, - type: 'input', - name: 'bitcoin_external_ip', - default: utils._getDefault( 'bitcoin_external_ip' ), - validate: utils._ipValidator, - message: 'What external ip does your bitcoin full node have?'+'\n', - }]; - }, - env: function( props ) { - return 'VAR0=VALUE0\nVAR1=VALUE1' - } + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + when: featureCondition, + type: 'confirm', + name: 'bitcoin_prune', + default: utils._getDefault( 'bitcoin_prune' ), + message: 'Run bitcoin node in prune mode?'+'\n', + }, + { + when: featureCondition, + type: 'input', + name: 'bitcoin_external_ip', + default: utils._getDefault( 'bitcoin_external_ip' ), + validate: utils._ipValidator, + message: 'What external ip does your bitcoin full node have?'+'\n', + }]; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1' + } }; \ No newline at end of file diff --git a/install/insight-yo.json b/install/insight-yo.json index 3f7916942..63a715322 100644 --- a/install/insight-yo.json +++ b/install/insight-yo.json @@ -1,3 +1,3 @@ { - "optOut": true + "optOut": true } \ No newline at end of file diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh index f5644a98a..a057b0c4e 100755 --- a/install/script/cyphernodeconf.sh +++ b/install/script/cyphernodeconf.sh @@ -8,9 +8,9 @@ # commands not needed for runtime cyphernodeconf_configure() { - local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - docker run -v $current_path/../data:/data \ - --log-driver=none\ - --rm -it cyphernodeconf:latest + docker run -v $current_path/../data:/data \ + --log-driver=none\ + --rm -it cyphernodeconf:latest } \ No newline at end of file diff --git a/install/script/docker.sh b/install/script/docker.sh index 19de4e2dc..78447dfc7 100755 --- a/install/script/docker.sh +++ b/install/script/docker.sh @@ -3,8 +3,8 @@ . ./trace.sh build_docker_image() { - - trace "building docker image: $1 with tag $2:latest" - docker build $1 -t $2:latest + + trace "building docker image: $1 with tag $2:latest" + docker build $1 -t $2:latest } diff --git a/install/script/trace.sh b/install/script/trace.sh index 34a18df18..b67a0cf19 100644 --- a/install/script/trace.sh +++ b/install/script/trace.sh @@ -2,14 +2,14 @@ trace() { - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr - fi + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi } trace_rc() { - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr - fi + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi } From 90d9a8944cd80f60e025e0475a57831524a3697b Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 21:06:38 +0200 Subject: [PATCH 023/268] integrated initial feature choice into prompts modules --- .../generators/app/features/000_cyphernode.js | 32 +++++++++++++++++++ .../features/{bitcoin.js => 100_bitcoin.js} | 0 .../{lightning.js => 200_lightning.js} | 0 .../features/{electrum.js => 300_electrum.js} | 0 ...pentimestamps.js => 400_opentimestamps.js} | 0 .../generators/app/index.js | 19 ++++------- 6 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/features/000_cyphernode.js rename install/generator-cyphernode/generators/app/features/{bitcoin.js => 100_bitcoin.js} (100%) rename install/generator-cyphernode/generators/app/features/{lightning.js => 200_lightning.js} (100%) rename install/generator-cyphernode/generators/app/features/{electrum.js => 300_electrum.js} (100%) rename install/generator-cyphernode/generators/app/features/{opentimestamps.js => 400_opentimestamps.js} (100%) diff --git a/install/generator-cyphernode/generators/app/features/000_cyphernode.js b/install/generator-cyphernode/generators/app/features/000_cyphernode.js new file mode 100644 index 000000000..7ee54c664 --- /dev/null +++ b/install/generator-cyphernode/generators/app/features/000_cyphernode.js @@ -0,0 +1,32 @@ +const name = 'cyphernode'; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + // https://github.com/SBoudrias/Inquirer.js#question + // input, confirm, list, rawlist, expand, checkbox, password, editor + type: 'checkbox', + name: 'features', + message: 'What features do you want to add to your cyphernode?'+'\n', + choices: utils._featureChoices() + }, + { + type: 'confirm', + name: 'cyphernode_rocks0', + default: utils._getDefault( 'cyphernode_rocks0' ), + message: 'Does cyphernode rock?'+'\n', + }, + { + type: 'confirm', + name: 'cyphernode_rocks1', + default: utils._getDefault( 'cyphernode_rocks1' ), + message: 'Does cyphernode rock?'+'\n', + }]; + }, + env: function( props ) { + return 'VAR0=VALUE0\nVAR1=VALUE1' + } +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/bitcoin.js rename to install/generator-cyphernode/generators/app/features/100_bitcoin.js diff --git a/install/generator-cyphernode/generators/app/features/lightning.js b/install/generator-cyphernode/generators/app/features/200_lightning.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/lightning.js rename to install/generator-cyphernode/generators/app/features/200_lightning.js diff --git a/install/generator-cyphernode/generators/app/features/electrum.js b/install/generator-cyphernode/generators/app/features/300_electrum.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/electrum.js rename to install/generator-cyphernode/generators/app/features/300_electrum.js diff --git a/install/generator-cyphernode/generators/app/features/opentimestamps.js b/install/generator-cyphernode/generators/app/features/400_opentimestamps.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/opentimestamps.js rename to install/generator-cyphernode/generators/app/features/400_opentimestamps.js diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index b5fe6202a..4d742aa95 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -34,16 +34,9 @@ module.exports = class extends Generator { prompting() { const splash = fs.readFileSync(this.templatePath('splash.txt')); this.log(splash.toString()); - - var prompts = [{ - // https://github.com/SBoudrias/Inquirer.js#question - // input, confirm, list, rawlist, expand, checkbox, password, editor - type: 'checkbox', - name: 'features', - message: wrap('What features do you want to add to your cyphernode?')+'\n', - choices: this.featureChoices - }]; + let prompts = []; + for( let m of featurePromptModules ) { prompts = prompts.concat(m.prompts(this)); } @@ -57,9 +50,7 @@ module.exports = class extends Generator { fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); for( let m of featurePromptModules ) { - const name = m.name(); - const env = m.env(); - fs.writeFileSync('/data/'+name+'.sh', env); + fs.writeFileSync('/data/'+m.name(), m.env()); } /* this.fs.copy( @@ -93,4 +84,8 @@ module.exports = class extends Generator { return wrap(text); } + _featureChoices() { + return this.featureChoices; + } + }; From ee94109bd33c7cc3c49bc6edca2941f01e8b33a2 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 21:45:45 +0200 Subject: [PATCH 024/268] added extended key validator --- .../generators/app/features/000_cyphernode.js | 15 +++++---------- .../generator-cyphernode/generators/app/index.js | 11 +++++++++++ install/generator-cyphernode/package.json | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features/000_cyphernode.js b/install/generator-cyphernode/generators/app/features/000_cyphernode.js index 7ee54c664..82f69f896 100644 --- a/install/generator-cyphernode/generators/app/features/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/features/000_cyphernode.js @@ -14,16 +14,11 @@ module.exports = { choices: utils._featureChoices() }, { - type: 'confirm', - name: 'cyphernode_rocks0', - default: utils._getDefault( 'cyphernode_rocks0' ), - message: 'Does cyphernode rock?'+'\n', - }, - { - type: 'confirm', - name: 'cyphernode_rocks1', - default: utils._getDefault( 'cyphernode_rocks1' ), - message: 'Does cyphernode rock?'+'\n', + type: 'input', + name: 'cyphernode_xpub', + default: utils._getDefault( 'cyphernode_xpub' ), + message: 'What is your xpub to watch?'+'\n', + validate: utils._xkeyValidator }]; }, env: function( props ) { diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 4d742aa95..5e5df5868 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -6,6 +6,7 @@ const wrap = require('wordwrap')(86); const validator = require('validator'); const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); +const coinstring = require('coinstring'); let featurePromptModules = []; const normalizedPath = path.join(__dirname, "features"); @@ -76,6 +77,16 @@ module.exports = class extends Generator { return validator.isIP((ip+"").trim()); } + _xpubValidator( xpub ) { + try { + coinstring.decode(xpub); + } catch( e ) { + throw new Error('Invalid extended public key. Please check your input.'); + } + return true; + } + + _trimFilter( input ) { return (input+"").trim(); } diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index 32d63bd3b..634a27ac5 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "chalk": "^2.1.0", + "coinstring": "^2.3.0", "validator": "^10.8.0", "wordwrap": "^1.0.0", "yeoman-generator": "^2.0.1" From 5f3e8fd51804ab58e19ab6131a6ce72c2950adc1 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 21:48:22 +0200 Subject: [PATCH 025/268] renamed function --- 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 5e5df5868..4661a114a 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -77,7 +77,7 @@ module.exports = class extends Generator { return validator.isIP((ip+"").trim()); } - _xpubValidator( xpub ) { + _xkeyValidator( xpub ) { try { coinstring.decode(xpub); } catch( e ) { From ce8ccd2231d21d09ce3913a7cf7c0d357e8cef05 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 22:18:25 +0200 Subject: [PATCH 026/268] using provided isValid method of coinstring --- install/generator-cyphernode/generators/app/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 4661a114a..edd86113f 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -78,11 +78,11 @@ module.exports = class extends Generator { } _xkeyValidator( xpub ) { - try { - coinstring.decode(xpub); - } catch( e ) { - throw new Error('Invalid extended public key. Please check your input.'); + // TOOD: check for version + if( !coinstring.isValid( xpub ) ) { + throw new Error('Not an extended key.'); } + return true; } From 16d78de4229132a6cc6a624c5d32c79759b5644f Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 22:25:08 +0200 Subject: [PATCH 027/268] more screens --- .../generators/app/features/000_cyphernode.js | 13 +++++++++++++ .../generators/app/features/100_bitcoin.js | 8 -------- .../generators/app/features/200_lightning.js | 8 ++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features/000_cyphernode.js b/install/generator-cyphernode/generators/app/features/000_cyphernode.js index 82f69f896..301a9a4f7 100644 --- a/install/generator-cyphernode/generators/app/features/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/features/000_cyphernode.js @@ -13,6 +13,19 @@ module.exports = { message: 'What features do you want to add to your cyphernode?'+'\n', choices: utils._featureChoices() }, + { + type: 'list', + name: 'cyphernode_net', + default: utils._getDefault( 'cyphernode_net' ), + message: 'What net do you want to run on?'+'\n', + choices: [{ + name: "Testnet", + value: "testnet" + },{ + name: "Mainnet", + value: "mainnet" + }] + }, { type: 'input', name: 'cyphernode_xpub', diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js index 3b7b959be..cb93b062b 100644 --- a/install/generator-cyphernode/generators/app/features/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/100_bitcoin.js @@ -14,14 +14,6 @@ module.exports = { name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), message: 'Run bitcoin node in prune mode?'+'\n', - }, - { - when: featureCondition, - type: 'input', - name: 'bitcoin_external_ip', - default: utils._getDefault( 'bitcoin_external_ip' ), - validate: utils._ipValidator, - message: 'What external ip does your bitcoin full node have?'+'\n', }]; }, env: function( props ) { diff --git a/install/generator-cyphernode/generators/app/features/200_lightning.js b/install/generator-cyphernode/generators/app/features/200_lightning.js index a695231c1..70d8f1892 100644 --- a/install/generator-cyphernode/generators/app/features/200_lightning.js +++ b/install/generator-cyphernode/generators/app/features/200_lightning.js @@ -24,6 +24,14 @@ module.exports = { value: 'lnd' } ] + }, + { + when: featureCondition, + type: 'input', + name: 'lightning_external_ip', + default: utils._getDefault( 'lightning_external_ip' ), + validate: utils._ipValidator, + message: 'What external ip does your lightning node have?'+'\n', }]; }, env: function( props ) { From 2bd641285b72d8d83fdb72d3bfe4f3961c035ec9 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 22:51:16 +0200 Subject: [PATCH 028/268] full node is no longer a feature. Cyphernode needs an internal or external full node to be present --- .../generators/app/features.json | 4 -- .../generators/app/features/100_bitcoin.js | 57 +++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index 0195ce52b..533967df3 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -1,8 +1,4 @@ [ - { - "name": "Bitcoin full node", - "value": "bitcoin" - }, { "name": "Lightning node", "value": "lightning" diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js index cb93b062b..de7148df0 100644 --- a/install/generator-cyphernode/generators/app/features/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/100_bitcoin.js @@ -1,6 +1,11 @@ const name = 'bitcoin'; -const featureCondition = function(props) { - return props.features && props.features.indexOf( name ) != -1; + +const bitcoinExternal = function(props) { + return props.bitcoin_mode === 'external' +}; + +const bitcoinInternal = function(props) { + return props.bitcoin_mode === 'internal' }; module.exports = { @@ -8,12 +13,56 @@ module.exports = { return name; }, prompts: function( utils ) { - return [{ - when: featureCondition, + return [ + { + type: 'list', + name: 'bitcoin_mode', + default: utils._getDefault( 'bitcoin_mode' ), + message: 'Where is your bitcoin full node running?'+'\n', + choices: [ + { + name: 'Nowhere! I want cyphernode to run one.', + value: 'internal' + }, + { + name: 'I have a full node running.', + value: 'external' + } + ] + }, + { + when: bitcoinExternal, + type: 'input', + name: 'bitcoin_node_ip', + default: utils._getDefault( 'bitcoin_node_ip' ), + validate: utils._ipValidator, + message: 'What is your full node ip address?'+'\n', + }, + { + type: 'input', + name: 'bitcoin_rpcuser', + default: utils._getDefault( 'bitcoin_rpcuser' ), + message: 'Name of bitcoin rpc user?'+'\n', + }, + { + type: 'password', + name: 'bitcoin_rpcpassword', + default: utils._getDefault( 'bitcoin_rpcpassword' ), + message: 'Password of bitcoin rpc user?'+'\n', + }, + { + when: bitcoinInternal, type: 'confirm', name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), message: 'Run bitcoin node in prune mode?'+'\n', + }, + { + when: bitcoinInternal, + type: 'confirm', + name: 'bitcoin_expose', + default: utils._getDefault( 'bitcoin_expose' ), + message: 'Expose bitcoin full node outside of the docker network?'+'\n', }]; }, env: function( props ) { From e205dfa7dc67368cd813d8dde20188153fd27c35 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 6 Oct 2018 23:01:39 +0200 Subject: [PATCH 029/268] hosts in validator can now be ip address or fully qualified domain names --- .../generators/app/features/100_bitcoin.js | 2 +- .../generators/app/features/200_lightning.js | 2 +- install/generator-cyphernode/generators/app/index.js | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js index de7148df0..b9cb8785f 100644 --- a/install/generator-cyphernode/generators/app/features/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/100_bitcoin.js @@ -35,7 +35,7 @@ module.exports = { type: 'input', name: 'bitcoin_node_ip', default: utils._getDefault( 'bitcoin_node_ip' ), - validate: utils._ipValidator, + validate: utils._ipOrFQDNValidator, message: 'What is your full node ip address?'+'\n', }, { diff --git a/install/generator-cyphernode/generators/app/features/200_lightning.js b/install/generator-cyphernode/generators/app/features/200_lightning.js index 70d8f1892..580ca1734 100644 --- a/install/generator-cyphernode/generators/app/features/200_lightning.js +++ b/install/generator-cyphernode/generators/app/features/200_lightning.js @@ -30,7 +30,7 @@ module.exports = { type: 'input', name: 'lightning_external_ip', default: utils._getDefault( 'lightning_external_ip' ), - validate: utils._ipValidator, + validate: utils._ipOrFQDNValidator, message: 'What external ip does your lightning node have?'+'\n', }]; }, diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index edd86113f..19445f51c 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -73,8 +73,15 @@ module.exports = class extends Generator { return this.props && this.props[name]; } - _ipValidator( ip ) { - return validator.isIP((ip+"").trim()); + _ipOrFQDNValidator( host ) { + host = (host+"").trim(); + + if( !(validator.isIP(host) || + validator.isFQDN(host)) ) { + throw new Error( 'No IP address or fully qualified domain name' ) + } + + return true; } _xkeyValidator( xpub ) { From 2b6cb1d8c3db685c9eff642abf40c59e1c5c2b86 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 00:06:43 +0200 Subject: [PATCH 030/268] changed workdir to data --- install/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/Dockerfile b/install/Dockerfile index 91c36852f..e6daf05a3 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -19,7 +19,7 @@ RUN chown -R yo:yo /yo/.config # run in user space USER yo -WORKDIR /yo +WORKDIR /data CMD ["yo","cyphernode"] From d96d1138147b443a9d37f58ddd10448a4b2d77e5 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 00:10:48 +0200 Subject: [PATCH 031/268] added simple config file template support --- .../generators/app/features/000_cyphernode.js | 3 +++ .../generators/app/features/100_bitcoin.js | 3 +++ .../generators/app/features/200_lightning.js | 10 +++++++ .../generators/app/features/300_electrum.js | 3 +++ .../app/features/400_opentimestamps.js | 3 +++ .../generators/app/index.js | 25 ++++++++++------- .../app/templates/bitcoin/bitcoin.conf | 20 ++++++++++++++ .../templates/lightning/c-lightning/config | 4 +++ .../app/templates/lightning/lnd/lnd.conf | 27 +++++++++++++++++++ 9 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf create mode 100644 install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config create mode 100644 install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf diff --git a/install/generator-cyphernode/generators/app/features/000_cyphernode.js b/install/generator-cyphernode/generators/app/features/000_cyphernode.js index 301a9a4f7..d861437e7 100644 --- a/install/generator-cyphernode/generators/app/features/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/features/000_cyphernode.js @@ -36,5 +36,8 @@ module.exports = { }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1' + }, + templates: function( props ) { + return []; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js index b9cb8785f..71ff5b419 100644 --- a/install/generator-cyphernode/generators/app/features/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/100_bitcoin.js @@ -67,5 +67,8 @@ module.exports = { }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1' + }, + templates: function( props ) { + return ['bitcoin.conf'] } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/200_lightning.js b/install/generator-cyphernode/generators/app/features/200_lightning.js index 580ca1734..768fddf83 100644 --- a/install/generator-cyphernode/generators/app/features/200_lightning.js +++ b/install/generator-cyphernode/generators/app/features/200_lightning.js @@ -1,8 +1,15 @@ +const path = require('path'); + const name = 'lightning'; const featureCondition = function(props) { return props.features && props.features.indexOf( name ) != -1; } +const templates = { + 'lnd': [ path.join('lnd','lnd.conf') ], + 'c-lightning': [ path.join('c-lightning','config') ] +} + module.exports = { name: function() { return name; @@ -36,5 +43,8 @@ module.exports = { }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1' + }, + templates: function( props ) { + return templates[props.lightning_implementation] } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/300_electrum.js b/install/generator-cyphernode/generators/app/features/300_electrum.js index 137e3df2a..e0b7deb29 100644 --- a/install/generator-cyphernode/generators/app/features/300_electrum.js +++ b/install/generator-cyphernode/generators/app/features/300_electrum.js @@ -28,5 +28,8 @@ module.exports = { }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1' + }, + templates: function( props ) { + return []; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/400_opentimestamps.js b/install/generator-cyphernode/generators/app/features/400_opentimestamps.js index 789f319c3..fba60f2e8 100644 --- a/install/generator-cyphernode/generators/app/features/400_opentimestamps.js +++ b/install/generator-cyphernode/generators/app/features/400_opentimestamps.js @@ -13,5 +13,8 @@ module.exports = { }, env: function( props ) { return 'VAR0=VALUE0\nVAR1=VALUE1'; + }, + templates: function( props ) { + return []; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 19445f51c..3582907f4 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -19,8 +19,8 @@ module.exports = class extends Generator { constructor(args, opts) { super(args, opts); - if( fs.existsSync('/data/props.json') ) { - this.props = require('/data/props.json'); + if( fs.existsSync(this.destinationPath('props.json')) ) { + this.props = require(this.destinationPath('props.json')); } else { this.props = {}; } @@ -48,17 +48,22 @@ module.exports = class extends Generator { } writing() { - fs.writeFileSync('/data/props.json', JSON.stringify(this.props, null, 2)); + fs.writeFileSync(this.destinationPath('props.json'), JSON.stringify(this.props, null, 2)); for( let m of featurePromptModules ) { - fs.writeFileSync('/data/'+m.name(), m.env()); + const name = m.name(); + fs.writeFileSync(this.destinationPath(name+'.properties'), m.env()); + + for( let t of m.templates(this.props) ) { + const p = path.join(name,t); + this.fs.copyTpl( + this.templatePath(p), + this.destinationPath(p), + this.props + ); + } + } - /* - this.fs.copy( - this.templatePath('dummyfile.txt'), - this.destinationPath('dummyfile.txt') - ); - */ } install() { diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf new file mode 100644 index 000000000..44c6232a4 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -0,0 +1,20 @@ +<% if (cyphernode_net === 'testnet') { %> +# testnet +testnet=1 +<% } %> + +<% if (lightning_implementation === 'lnd') { %> +#lnd opts +txindex=1 +zmqpubrawblock=tcp://0.0.0.0:18501 +zmqpubrawtx=tcp://0.0.0.0:18502 +<% } %> + +#tor +#proxy=127.0.0.1:9050 +#listen=1 + +rpcconnect=btcnode +rpcuser=<%= bitcoin_rpcuser %> +rpcpassword=<%= bitcoin_rpcpassword %> +rpcwallet=ln01.dat diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config new file mode 100644 index 000000000..77c25c0d6 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config @@ -0,0 +1,4 @@ +alias=SatoshiPortal01 +rgb=008000 +#port=9735 +network=testnet diff --git a/install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf b/install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf new file mode 100644 index 000000000..071ea40c5 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/lightning/lnd/lnd.conf @@ -0,0 +1,27 @@ +[Application Options] +debuglevel=info +maxpendingchannels=10 +externalip=88.198.55.131 +color=#a111ff +alias=SatoshiPortal01 +rpclisten=0.0.0.0:10009 +tlsextraip=lnd +tlsextradomain=lnd + +[Bitcoin] +bitcoin.active=1 +bitcoin.node=bitcoind +bitcoin.mainnet=1 + +[Bitcoind] +bitcoind.rpcuser=<%= bitcoin_rpcuser %> +bitcoind.rpcpass=<%= bitcoin_rpcpassword %> +bitcoind.zmqpubrawblock=tcp://bitcoin:18501 +bitcoind.zmqpubrawtx=tcp://bitcoin:18502 +#bitcoind.zmqpath=tcp://bitcoin:18501 +bitcoind.rpchost=bitcoin + +[autopilot] +autopilot.active=1 +autopilot.maxchannels=5 +autopilot.allocation=0.6 \ No newline at end of file From b1a7204aef110ece32716f87af7d44381fd7b9a9 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 16:33:49 +0200 Subject: [PATCH 032/268] moved all properties from config files into environment --- proxy_docker/app/script/sendtobitcoinnode.sh | 10 +++++----- proxy_docker/app/script/utils.sh | 17 ----------------- 2 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 proxy_docker/app/script/utils.sh diff --git a/proxy_docker/app/script/sendtobitcoinnode.sh b/proxy_docker/app/script/sendtobitcoinnode.sh index daf66efb9..ea92adab8 100644 --- a/proxy_docker/app/script/sendtobitcoinnode.sh +++ b/proxy_docker/app/script/sendtobitcoinnode.sh @@ -5,7 +5,7 @@ send_to_watcher_node() { trace "Entering send_to_watcher_node()..." - send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} watcher_btcnode_curlcfg.properties $@ + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_NODE_RPC_USER} $@ local returncode=$? trace_rc ${returncode} return ${returncode} @@ -14,7 +14,7 @@ send_to_watcher_node() send_to_spender_node() { trace "Entering send_to_spender_node()..." - send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} spender_btcnode_curlcfg.properties $@ + send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_NODE_RPC_USER} $@ local returncode=$? trace_rc ${returncode} return ${returncode} @@ -27,11 +27,11 @@ send_to_bitcoin_node() local result local errorstring local node_url=${1} - local configfile=${2} + local user=${2} local data=${3} - trace "[send_to_bitcoin_node] curl -s --config ${configfile} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" - result=$(curl -s --config ${configfile} -H "Content-Type: application/json" -d "${data}" ${node_url}) + trace "[send_to_bitcoin_node] curl -s --user ${user} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" + result=$(curl -s --user ${user} -H "Content-Type: application/json" -d "${data}" ${node_url}) returncode=$? trace_rc ${returncode} trace "[send_to_bitcoin_node] result=${result}" diff --git a/proxy_docker/app/script/utils.sh b/proxy_docker/app/script/utils.sh deleted file mode 100644 index 7c739e872..000000000 --- a/proxy_docker/app/script/utils.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -. ./trace.sh - -get_prop() -{ - trace "Entering get_prop()..." - - local property=${1} - trace "[get_prop] property=${property}" - - local value=$(grep "${property}" config.properties | cut -d'=' -f2) - - trace "[get_prop] value=${value}" - - echo ${value} -} From 93fcdc0f330be968fb75b16ae94add60d02cd412 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 18:45:06 +0200 Subject: [PATCH 033/268] curl needs to use config files for credentials so they do not show up in ps. We create these config files now at container startup --- proxy_docker/app/script/sendtobitcoinnode.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proxy_docker/app/script/sendtobitcoinnode.sh b/proxy_docker/app/script/sendtobitcoinnode.sh index ea92adab8..a1ddd0a2b 100644 --- a/proxy_docker/app/script/sendtobitcoinnode.sh +++ b/proxy_docker/app/script/sendtobitcoinnode.sh @@ -5,7 +5,7 @@ send_to_watcher_node() { trace "Entering send_to_watcher_node()..." - send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_NODE_RPC_USER} $@ + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_BTC_NODE_RPC_CFG} $@ local returncode=$? trace_rc ${returncode} return ${returncode} @@ -14,7 +14,7 @@ send_to_watcher_node() send_to_spender_node() { trace "Entering send_to_spender_node()..." - send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_NODE_RPC_USER} $@ + send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_BTC_NODE_RPC_CFG} $@ local returncode=$? trace_rc ${returncode} return ${returncode} @@ -27,11 +27,11 @@ send_to_bitcoin_node() local result local errorstring local node_url=${1} - local user=${2} + local config=${2} local data=${3} trace "[send_to_bitcoin_node] curl -s --user ${user} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}" - result=$(curl -s --user ${user} -H "Content-Type: application/json" -d "${data}" ${node_url}) + result=$(curl -s --config ${config} -H "Content-Type: application/json" -d "${data}" ${node_url}) returncode=$? trace_rc ${returncode} trace "[send_to_bitcoin_node] result=${result}" From 65cce8b65268dcb535d1e8639258dd9c3edc5330 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 18:53:08 +0200 Subject: [PATCH 034/268] exported needed variable --- proxy_docker/app/script/sendtobitcoinnode.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy_docker/app/script/sendtobitcoinnode.sh b/proxy_docker/app/script/sendtobitcoinnode.sh index a1ddd0a2b..3a9814a93 100644 --- a/proxy_docker/app/script/sendtobitcoinnode.sh +++ b/proxy_docker/app/script/sendtobitcoinnode.sh @@ -5,7 +5,7 @@ send_to_watcher_node() { trace "Entering send_to_watcher_node()..." - send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_BTC_NODE_RPC_CFG} $@ + send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} ${WATCHER_NODE_RPC_CFG} $@ local returncode=$? trace_rc ${returncode} return ${returncode} @@ -14,7 +14,7 @@ send_to_watcher_node() send_to_spender_node() { trace "Entering send_to_spender_node()..." - send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_BTC_NODE_RPC_CFG} $@ + send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} ${SPENDER_NODE_RPC_CFG} $@ local returncode=$? trace_rc ${returncode} return ${returncode} From cce9c1397a26c5f7244cd58ace99dc03d1e123c0 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 19:39:25 +0200 Subject: [PATCH 035/268] bump --- .../{000_cyphernode.js => 000_proxy.js} | 22 +++++++++++-------- .../generators/app/features/100_bitcoin.js | 7 ++++++ .../generators/app/features/200_lightning.js | 3 --- .../generators/app/features/300_electrum.js | 3 --- .../app/features/400_opentimestamps.js | 3 --- .../generators/app/index.js | 15 ++++++------- .../app/templates/bitcoin/bitcoin.conf | 10 +++++++-- .../app/templates/proxy/env.properties | 19 ++++++++++++++++ install/script/install.sh | 2 +- 9 files changed, 55 insertions(+), 29 deletions(-) rename install/generator-cyphernode/generators/app/features/{000_cyphernode.js => 000_proxy.js} (66%) create mode 100644 install/generator-cyphernode/generators/app/templates/proxy/env.properties diff --git a/install/generator-cyphernode/generators/app/features/000_cyphernode.js b/install/generator-cyphernode/generators/app/features/000_proxy.js similarity index 66% rename from install/generator-cyphernode/generators/app/features/000_cyphernode.js rename to install/generator-cyphernode/generators/app/features/000_proxy.js index d861437e7..a0142a1c9 100644 --- a/install/generator-cyphernode/generators/app/features/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/features/000_proxy.js @@ -1,4 +1,4 @@ -const name = 'cyphernode'; +const name = 'proxy'; module.exports = { name: function() { @@ -15,8 +15,8 @@ module.exports = { }, { type: 'list', - name: 'cyphernode_net', - default: utils._getDefault( 'cyphernode_net' ), + name: 'net', + default: utils._getDefault( 'net' ), message: 'What net do you want to run on?'+'\n', choices: [{ name: "Testnet", @@ -28,16 +28,20 @@ module.exports = { }, { type: 'input', - name: 'cyphernode_xpub', - default: utils._getDefault( 'cyphernode_xpub' ), + name: 'xpub', + default: utils._getDefault( 'xpub' ), message: 'What is your xpub to watch?'+'\n', validate: utils._xkeyValidator + }, + { + type: 'input', + name: 'derivation_path', + default: utils._getDefault( 'derivation_path' ), + message: 'What is your address derivation path?'+'\n', + validate: utils._derivationPathValidator }]; }, - env: function( props ) { - return 'VAR0=VALUE0\nVAR1=VALUE1' - }, templates: function( props ) { - return []; + return [ 'env.properties' ]; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/features/100_bitcoin.js index 71ff5b419..a18e156a2 100644 --- a/install/generator-cyphernode/generators/app/features/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/features/100_bitcoin.js @@ -63,6 +63,13 @@ module.exports = { name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), message: 'Expose bitcoin full node outside of the docker network?'+'\n', + }, + { + when: bitcoinInternal, + type: 'input', + name: 'bitcoin_uacomment', + default: utils._getDefault( 'bitcoin_uacomment' ), + message: 'Any UA comment?'+'\n', }]; }, env: function( props ) { diff --git a/install/generator-cyphernode/generators/app/features/200_lightning.js b/install/generator-cyphernode/generators/app/features/200_lightning.js index 768fddf83..60a2617a5 100644 --- a/install/generator-cyphernode/generators/app/features/200_lightning.js +++ b/install/generator-cyphernode/generators/app/features/200_lightning.js @@ -41,9 +41,6 @@ module.exports = { message: 'What external ip does your lightning node have?'+'\n', }]; }, - env: function( props ) { - return 'VAR0=VALUE0\nVAR1=VALUE1' - }, templates: function( props ) { return templates[props.lightning_implementation] } diff --git a/install/generator-cyphernode/generators/app/features/300_electrum.js b/install/generator-cyphernode/generators/app/features/300_electrum.js index e0b7deb29..c374918f7 100644 --- a/install/generator-cyphernode/generators/app/features/300_electrum.js +++ b/install/generator-cyphernode/generators/app/features/300_electrum.js @@ -26,9 +26,6 @@ module.exports = { ] }]; }, - env: function( props ) { - return 'VAR0=VALUE0\nVAR1=VALUE1' - }, templates: function( props ) { return []; } diff --git a/install/generator-cyphernode/generators/app/features/400_opentimestamps.js b/install/generator-cyphernode/generators/app/features/400_opentimestamps.js index fba60f2e8..238c45ef2 100644 --- a/install/generator-cyphernode/generators/app/features/400_opentimestamps.js +++ b/install/generator-cyphernode/generators/app/features/400_opentimestamps.js @@ -11,9 +11,6 @@ module.exports = { prompts: function( utils ) { return []; }, - env: function( props ) { - return 'VAR0=VALUE0\nVAR1=VALUE1'; - }, templates: function( props ) { return []; } diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 3582907f4..63cdc9be8 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -22,7 +22,9 @@ module.exports = class extends Generator { if( fs.existsSync(this.destinationPath('props.json')) ) { this.props = require(this.destinationPath('props.json')); } else { - this.props = {}; + this.props = { + 'derivation_path': '0/n' + }; } this.featureChoices = featureChoices; @@ -51,9 +53,7 @@ module.exports = class extends Generator { fs.writeFileSync(this.destinationPath('props.json'), JSON.stringify(this.props, null, 2)); for( let m of featurePromptModules ) { - const name = m.name(); - fs.writeFileSync(this.destinationPath(name+'.properties'), m.env()); - + const name = m.name(); for( let t of m.templates(this.props) ) { const p = path.join(name,t); this.fs.copyTpl( @@ -62,7 +62,6 @@ module.exports = class extends Generator { this.props ); } - } } @@ -80,12 +79,10 @@ module.exports = class extends Generator { _ipOrFQDNValidator( host ) { host = (host+"").trim(); - if( !(validator.isIP(host) || validator.isFQDN(host)) ) { throw new Error( 'No IP address or fully qualified domain name' ) } - return true; } @@ -94,10 +91,12 @@ module.exports = class extends Generator { if( !coinstring.isValid( xpub ) ) { throw new Error('Not an extended key.'); } - return true; } + _derivationPathValidator( path ) { + return true; + } _trimFilter( input ) { return (input+"").trim(); diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 44c6232a4..715f3723c 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -1,4 +1,4 @@ -<% if (cyphernode_net === 'testnet') { %> +<% if (net === 'testnet') { %> # testnet testnet=1 <% } %> @@ -14,7 +14,13 @@ zmqpubrawtx=tcp://0.0.0.0:18502 #proxy=127.0.0.1:9050 #listen=1 -rpcconnect=btcnode +rpcconnect=bitcoin rpcuser=<%= bitcoin_rpcuser %> rpcpassword=<%= bitcoin_rpcpassword %> + +# why? rpcwallet=ln01.dat + +<% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> +uacomment=<%= bitcoin_uacomment %> +<% } %> \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/proxy/env.properties b/install/generator-cyphernode/generators/app/templates/proxy/env.properties new file mode 100644 index 000000000..5ed7afa25 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/proxy/env.properties @@ -0,0 +1,19 @@ +TRACING=1 +WATCHER_BTC_NODE_RPC_URL=btcnode:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat +WATCHER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %> +WATCHER_BTC_NODE_RPC_CFG=/proxyuser/watcher_btcnode_curlcfg.properties +SPENDER_BTC_NODE_RPC_URL=btcnode:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat +SPENDER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %> +SPENDER_BTC_NODE_RPC_CFG=/proxyuser/sender_btcnode_curlcfg.properties +PROXY_LISTENING_PORT=8888 +# Variable substitutions don't work +DB_PATH=/proxyuser/db +DB_FILE=/proxyuser/db/proxydb +# Pycoin container +PYCOIN_CONTAINER=pycoinnode:7777 +# OTS container +OTS_CONTAINER=otsnode:6666 + +DERIVATION_PUB32=<%= xpub %> +DERIVATION_PATH=<%= derivation_path %> +WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %> diff --git a/install/script/install.sh b/install/script/install.sh index a94aaef90..b1979b228 100755 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -16,7 +16,7 @@ trace "Updating SatoshiPortal dockers" # ## build cyphernode images #build_docker_image ../../cron_docker/ proxycronimg -#build_docker_image ../../proxy_docker/ btcproxyimg +build_docker_image ../../proxy_docker/ btcproxyimg #build_docker_image ../../pycoin_docker/ pycoinimg # ## build setup docker image From a1a435705db8486b71df0cd1913f4d4085cf2fe8 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 19:47:01 +0200 Subject: [PATCH 036/268] more bitcoin.conf options --- .../generators/app/templates/bitcoin/bitcoin.conf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 715f3723c..72e311514 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -14,12 +14,18 @@ zmqpubrawtx=tcp://0.0.0.0:18502 #proxy=127.0.0.1:9050 #listen=1 +maxmempool=64 +dbcache=64 + rpcconnect=bitcoin rpcuser=<%= bitcoin_rpcuser %> rpcpassword=<%= bitcoin_rpcpassword %> -# why? -rpcwallet=ln01.dat +wallet=watching01.dat +wallet=spending01.dat +wallet=ln01.dat + +walletnotify=curl cyphernode:8888/conf/%s <% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> uacomment=<%= bitcoin_uacomment %> From fcdb72f291141b826a8d05c22fd4ca6b0be0d539 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:07:52 +0200 Subject: [PATCH 037/268] renamed features to prompters --- install/generator-cyphernode/generators/app/index.js | 2 +- .../generators/app/{features => prompters}/000_proxy.js | 0 .../generators/app/{features => prompters}/100_bitcoin.js | 0 .../generators/app/{features => prompters}/200_lightning.js | 0 .../generators/app/{features => prompters}/300_electrum.js | 0 .../app/{features => prompters}/400_opentimestamps.js | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename install/generator-cyphernode/generators/app/{features => prompters}/000_proxy.js (100%) rename install/generator-cyphernode/generators/app/{features => prompters}/100_bitcoin.js (100%) rename install/generator-cyphernode/generators/app/{features => prompters}/200_lightning.js (100%) rename install/generator-cyphernode/generators/app/{features => prompters}/300_electrum.js (100%) rename install/generator-cyphernode/generators/app/{features => prompters}/400_opentimestamps.js (100%) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 63cdc9be8..a09e6e6f9 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -9,7 +9,7 @@ const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); let featurePromptModules = []; -const normalizedPath = path.join(__dirname, "features"); +const normalizedPath = path.join(__dirname, "prompters"); fs.readdirSync(normalizedPath).forEach(function(file) { featurePromptModules.push(require(path.join(normalizedPath,file))); }); diff --git a/install/generator-cyphernode/generators/app/features/000_proxy.js b/install/generator-cyphernode/generators/app/prompters/000_proxy.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/000_proxy.js rename to install/generator-cyphernode/generators/app/prompters/000_proxy.js diff --git a/install/generator-cyphernode/generators/app/features/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/100_bitcoin.js rename to install/generator-cyphernode/generators/app/prompters/100_bitcoin.js diff --git a/install/generator-cyphernode/generators/app/features/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/200_lightning.js rename to install/generator-cyphernode/generators/app/prompters/200_lightning.js diff --git a/install/generator-cyphernode/generators/app/features/300_electrum.js b/install/generator-cyphernode/generators/app/prompters/300_electrum.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/300_electrum.js rename to install/generator-cyphernode/generators/app/prompters/300_electrum.js diff --git a/install/generator-cyphernode/generators/app/features/400_opentimestamps.js b/install/generator-cyphernode/generators/app/prompters/400_opentimestamps.js similarity index 100% rename from install/generator-cyphernode/generators/app/features/400_opentimestamps.js rename to install/generator-cyphernode/generators/app/prompters/400_opentimestamps.js From f5781e2f03bdfe867a48e16df2b84aa1e6a0ed8d Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:08:20 +0200 Subject: [PATCH 038/268] added installation mode selection --- .../generators/app/prompters/999_installer | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 install/generator-cyphernode/generators/app/prompters/999_installer diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer new file mode 100644 index 000000000..6edd36bd1 --- /dev/null +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -0,0 +1,49 @@ +const name = 'installer'; +const chalk = require('chalk'); + +const installerDocker = function(props) { + return props.installer === 'docker' +}; + +const installerLunanode = function(props) { + return props.installer === 'lunanode' +}; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + return [{ + type: 'list', + name: 'installer', + default: utils._getDefault( 'installer' ), + message: chalk.red('Where do you want to install cyphernode?')+'\n', + choices: [{ + name: "Local docker", + value: "docker" + },{ + name: "Lunanode", + value: "lunanode" + }] + }, + { + when: installerDocker, + type: 'confirm', + name: 'installer_confirm_docker', + default: utils._getDefault( 'installer_confirm_docker' ), + message: 'Docker?! Really?'+'\n' + }, + { + when: installerLunanode, + type: 'confirm', + name: 'installer_confirm_docker', + default: utils._getDefault( 'installer_confirm_docker' ), + message: 'Lunanode?! No wayyyy!'+'\n' + } + ]; + }, + templates: function( props ) { + return []; + } +}; \ No newline at end of file From 73d99e1e2860c414f4412b6d2dc30778eb5677e4 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:09:10 +0200 Subject: [PATCH 039/268] copy pasta! --- .../generators/app/prompters/999_installer | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer index 6edd36bd1..472a3d6a6 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -37,8 +37,8 @@ module.exports = { { when: installerLunanode, type: 'confirm', - name: 'installer_confirm_docker', - default: utils._getDefault( 'installer_confirm_docker' ), + name: 'installer_confirm_lunanode', + default: utils._getDefault( 'installer_confirm_lunanode' ), message: 'Lunanode?! No wayyyy!'+'\n' } ]; From 48af084cb05fde83e5d9dd4987bbc2a112ea41dd Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:33:44 +0200 Subject: [PATCH 040/268] renamed variable --- install/generator-cyphernode/generators/app/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index a09e6e6f9..5067a06fa 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -8,10 +8,10 @@ const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); -let featurePromptModules = []; +let prompters = []; const normalizedPath = path.join(__dirname, "prompters"); fs.readdirSync(normalizedPath).forEach(function(file) { - featurePromptModules.push(require(path.join(normalizedPath,file))); + prompters.push(require(path.join(normalizedPath,file))); }); module.exports = class extends Generator { @@ -40,7 +40,7 @@ module.exports = class extends Generator { let prompts = []; - for( let m of featurePromptModules ) { + for( let m of prompters ) { prompts = prompts.concat(m.prompts(this)); } @@ -52,7 +52,7 @@ module.exports = class extends Generator { writing() { fs.writeFileSync(this.destinationPath('props.json'), JSON.stringify(this.props, null, 2)); - for( let m of featurePromptModules ) { + for( let m of prompters ) { const name = m.name(); for( let t of m.templates(this.props) ) { const p = path.join(name,t); From 875a2c7afd37e5967849664389ab89933010a59e Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:34:03 +0200 Subject: [PATCH 041/268] added default installer --- install/generator-cyphernode/generators/app/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5067a06fa..f56882d64 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -23,7 +23,8 @@ module.exports = class extends Generator { this.props = require(this.destinationPath('props.json')); } else { this.props = { - 'derivation_path': '0/n' + 'derivation_path': '0/n', + 'installer': 'docker' }; } From 9c614fedfb51640dae81d40b014b1885409d99fd Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:34:17 +0200 Subject: [PATCH 042/268] renamed options --- .../generators/app/prompters/999_installer | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer index 472a3d6a6..a6a84813f 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -20,10 +20,10 @@ module.exports = { default: utils._getDefault( 'installer' ), message: chalk.red('Where do you want to install cyphernode?')+'\n', choices: [{ - name: "Local docker", + name: "LDocker", value: "docker" },{ - name: "Lunanode", + name: "Lunanode (not implemented)", value: "lunanode" }] }, From ffa92932260b5c4bfd4e60a5ecd573e4ed996c18 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 7 Oct 2018 23:34:59 +0200 Subject: [PATCH 043/268] fixed typo --- .../generator-cyphernode/generators/app/prompters/999_installer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer index a6a84813f..5e9f2eda0 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -20,7 +20,7 @@ module.exports = { default: utils._getDefault( 'installer' ), message: chalk.red('Where do you want to install cyphernode?')+'\n', choices: [{ - name: "LDocker", + name: "Docker", value: "docker" },{ name: "Lunanode (not implemented)", From 7479433f4aed50cf70a5bfe708e7a3b356da76a1 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 11:02:11 +0200 Subject: [PATCH 044/268] did some script reorg --- install.sh | 3 --- install/script/configure.sh | 25 +++++++++++++++++++++++++ install/script/cyphernodeconf.sh | 4 ---- install/script/docker.sh | 4 ---- install/script/install.sh | 31 +++---------------------------- install/script/setup.sh | 11 +++++++++++ install/script/trace.sh | 2 -- setup.sh | 3 +++ 8 files changed, 42 insertions(+), 41 deletions(-) delete mode 100755 install.sh create mode 100644 install/script/configure.sh mode change 100755 => 100644 install/script/cyphernodeconf.sh mode change 100755 => 100644 install/script/docker.sh mode change 100755 => 100644 install/script/install.sh create mode 100755 install/script/setup.sh create mode 100755 setup.sh diff --git a/install.sh b/install.sh deleted file mode 100755 index bc75115b4..000000000 --- a/install.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -(cd install/script && TRACING=1 ./install.sh) \ No newline at end of file diff --git a/install/script/configure.sh b/install/script/configure.sh new file mode 100644 index 000000000..9d335c5cd --- /dev/null +++ b/install/script/configure.sh @@ -0,0 +1,25 @@ +. ./docker.sh +. ./cyphernodeconf.sh + +configure() { + trace "Updating SatoshiPortal dockers" + #git submodule update --recursive --remote + # + ## build SatoshiPortal images + #local arch=x86_64 + #build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode + #build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg + # + ## build cyphernode images + #build_docker_image ../../cron_docker/ proxycronimg + build_docker_image ../../proxy_docker/ btcproxyimg + #build_docker_image ../../pycoin_docker/ pycoinimg + # + ## build setup docker image + build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." + + # configure features of cyphernode + cyphernodeconf_configure + + #docker image rm cyphernodeconf:latest +} diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh old mode 100755 new mode 100644 index a057b0c4e..036cb117b --- a/install/script/cyphernodeconf.sh +++ b/install/script/cyphernodeconf.sh @@ -1,7 +1,3 @@ -#!/bin/sh - -. ./trace.sh - # this will run configure.sh of the specified package inside a # cyphernodeconf container. This way we ensure we have the right # environment and do not pollute the host machine with utility diff --git a/install/script/docker.sh b/install/script/docker.sh old mode 100755 new mode 100644 index 78447dfc7..4d30004b0 --- a/install/script/docker.sh +++ b/install/script/docker.sh @@ -1,7 +1,3 @@ -#!/bin/sh - -. ./trace.sh - build_docker_image() { trace "building docker image: $1 with tag $2:latest" diff --git a/install/script/install.sh b/install/script/install.sh old mode 100755 new mode 100644 index b1979b228..eac781dce --- a/install/script/install.sh +++ b/install/script/install.sh @@ -1,28 +1,3 @@ -#!/bin/sh - -. ./trace.sh -. ./docker.sh -. ./cyphernodeconf.sh - -config_file=$1 - -trace "Updating SatoshiPortal dockers" -#git submodule update --recursive --remote -# -## build SatoshiPortal images -#local arch=x86_64 -#build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode -#build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg -# -## build cyphernode images -#build_docker_image ../../cron_docker/ proxycronimg -build_docker_image ../../proxy_docker/ btcproxyimg -#build_docker_image ../../pycoin_docker/ pycoinimg -# -## build setup docker image -build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." - -# configure features of cyphernode -cyphernodeconf_configure - -#docker image rm cyphernodeconf:latest +install() { + echo "Installation phase not implemented yet" +} \ No newline at end of file diff --git a/install/script/setup.sh b/install/script/setup.sh new file mode 100755 index 000000000..806a90ef5 --- /dev/null +++ b/install/script/setup.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +. ./trace.sh +. ./configure.sh +. ./install.sh + +echo "Starting configuration phase" +configure + +echo "Starting installation phase" +install \ No newline at end of file diff --git a/install/script/trace.sh b/install/script/trace.sh index b67a0cf19..c4a6e81fd 100644 --- a/install/script/trace.sh +++ b/install/script/trace.sh @@ -1,5 +1,3 @@ -#!/bin/sh - trace() { if [ -n "${TRACING}" ]; then diff --git a/setup.sh b/setup.sh new file mode 100755 index 000000000..c8831d7cd --- /dev/null +++ b/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +(cd install/script && TRACING=1 ./setup.sh) \ No newline at end of file From ff23c419bcb5e960a6172627413eeb695793938d Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:09:40 +0200 Subject: [PATCH 045/268] bump :-( --- install/.gitignore | 1 + .../generators/app/prompters/000_proxy.js | 2 +- .../generators/app/prompters/999_installer | 16 +++- .../app/templates/installer/config.sh | 5 + .../templates/installer/docker-compose.yaml | 93 +++++++++++++++++++ .../app/templates/proxy/env.properties | 19 ---- install/script/configure.sh | 20 +--- install/script/cyphernodeconf.sh | 12 --- install/script/docker.sh | 4 +- install/script/install.sh | 12 ++- install/script/install_docker.sh | 34 +++++++ install/script/install_lunanode.sh | 3 + install/script/setup.sh | 30 +++++- setup.sh | 2 +- 14 files changed, 193 insertions(+), 60 deletions(-) create mode 100644 install/.gitignore create mode 100644 install/generator-cyphernode/generators/app/templates/installer/config.sh create mode 100644 install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml delete mode 100644 install/generator-cyphernode/generators/app/templates/proxy/env.properties delete mode 100644 install/script/cyphernodeconf.sh create mode 100644 install/script/install_docker.sh create mode 100644 install/script/install_lunanode.sh diff --git a/install/.gitignore b/install/.gitignore new file mode 100644 index 000000000..6320cd248 --- /dev/null +++ b/install/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/000_proxy.js b/install/generator-cyphernode/generators/app/prompters/000_proxy.js index a0142a1c9..70d695454 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_proxy.js +++ b/install/generator-cyphernode/generators/app/prompters/000_proxy.js @@ -42,6 +42,6 @@ module.exports = { }]; }, templates: function( props ) { - return [ 'env.properties' ]; + return []; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer index 5e9f2eda0..59bd2158b 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -16,15 +16,20 @@ module.exports = { prompts: function( utils ) { return [{ type: 'list', - name: 'installer', - default: utils._getDefault( 'installer' ), + name: 'installer_mode', + default: utils._getDefault( 'installer_mode' ), message: chalk.red('Where do you want to install cyphernode?')+'\n', choices: [{ name: "Docker", value: "docker" - },{ + }, + { name: "Lunanode (not implemented)", value: "lunanode" + }, + { + name: "No installation. Just create config files", + value: "none" }] }, { @@ -44,6 +49,9 @@ module.exports = { ]; }, templates: function( props ) { - return []; + if( props.installer_mode === 'docker' ) { + return ['config.sh','docker-compose.yaml']; + } + return ['config.sh']; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh new file mode 100644 index 000000000..555817fcc --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -0,0 +1,5 @@ +INSTALLER_MODE=<%= installer_mode %> +BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> +FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> +FEATURE_OPENTIMESTAMPS=<%= (features.indexOf('opentimestamps') != -1)?'true':'false' %> +FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml new file mode 100644 index 000000000..0f1df011e --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml @@ -0,0 +1,93 @@ +version: "3" + +services: + proxy: + # Bitcoin Mini Proxy + env: + "TRACING":"1" + "WATCHER_BTC_NODE_RPC_URL":"bitcoin:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat" + "WATCHER_BTC_NODE_RPC_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + "WATCHER_BTC_NODE_RPC_CFG":"/proxyuser/watcher_btcnode_curlcfg.properties" + "SPENDER_BTC_NODE_RPC_URL":"bitcoin:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat" + "SPENDER_BTC_NODE_RPC_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + "SPENDER_BTC_NODE_RPC_CFG":"/proxyuser/sender_btcnode_curlcfg.properties" + "PROXY_LISTENING_PORT":"8888" + "DB_PATH":"/proxyuser/db" + "DB_FILE":"/proxyuser/db/proxydb" + "PYCOIN_CONTAINER":"pycoin:7777" + "OTS_CONTAINER":"opentimestamps:6666" + "DERIVATION_PUB32":"<%= xpub %>" + "DERIVATION_PATH":"<%= derivation_path %>" + "WATCHER_BTC_NODE_PRUNED":"<%= bitcoin_prune?'true':'false' %>" + image: cyphernode/proxy +# ports: +# - "8888:8888" + volumes: + # Variable substitutions don't work + # Match with DB_PATH in proxy_docker/env.properties + - "~/btcproxydb:/proxyuser/db" + - "~/.lightning:/proxyuser/.lightning" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + + proxycron: + # Async jobs + env: + "PROXY_URL":"proxy:8888/executecallbacks" + image: cyphernode/proxycron +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + + pycoin: + # Pycoin + image: cyphernode/pycoin + env: + "TRACING":"1" + "PYCOIN_LISTENING_PORT":"7777" +# ports: +# - "7777:7777" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet +<% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> + lightning: + # c-lightning lightning network node + image: cyphernode/clightning + ports: + - "9735:9735" + volumes: + - "~/.lightning:/lnuser/.lightning" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet +<% } else if( features.indexOf('lightning') !== -1 && lightning_implementation === 'lnd' ) { %> +# TODO: add lnd support +<% } %> +<% if( bitcoin_mode === 'internal' ) { %> + bitcoin: + # Bitcoin node + image: cyphernode/bitcoin +# ports: +# - "18333:18333" +# - "18332:18332" +# - "29000:29000" +# - "8333:8333" +# - "8332:8332" + volumes: + - "~/.bitcoin:/bitcoinuser/.bitcoin" + networks: + - cyphernodenet +<% } %> +networks: + cyphernodenet: + external: true diff --git a/install/generator-cyphernode/generators/app/templates/proxy/env.properties b/install/generator-cyphernode/generators/app/templates/proxy/env.properties deleted file mode 100644 index 5ed7afa25..000000000 --- a/install/generator-cyphernode/generators/app/templates/proxy/env.properties +++ /dev/null @@ -1,19 +0,0 @@ -TRACING=1 -WATCHER_BTC_NODE_RPC_URL=btcnode:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat -WATCHER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %> -WATCHER_BTC_NODE_RPC_CFG=/proxyuser/watcher_btcnode_curlcfg.properties -SPENDER_BTC_NODE_RPC_URL=btcnode:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat -SPENDER_BTC_NODE_RPC_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %> -SPENDER_BTC_NODE_RPC_CFG=/proxyuser/sender_btcnode_curlcfg.properties -PROXY_LISTENING_PORT=8888 -# Variable substitutions don't work -DB_PATH=/proxyuser/db -DB_FILE=/proxyuser/db/proxydb -# Pycoin container -PYCOIN_CONTAINER=pycoinnode:7777 -# OTS container -OTS_CONTAINER=otsnode:6666 - -DERIVATION_PUB32=<%= xpub %> -DERIVATION_PATH=<%= derivation_path %> -WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %> diff --git a/install/script/configure.sh b/install/script/configure.sh index 9d335c5cd..d284a55a9 100644 --- a/install/script/configure.sh +++ b/install/script/configure.sh @@ -1,25 +1,13 @@ -. ./docker.sh -. ./cyphernodeconf.sh configure() { - trace "Updating SatoshiPortal dockers" - #git submodule update --recursive --remote - # - ## build SatoshiPortal images - #local arch=x86_64 - #build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core btcnode - #build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning clnimg - # - ## build cyphernode images - #build_docker_image ../../cron_docker/ proxycronimg - build_docker_image ../../proxy_docker/ btcproxyimg - #build_docker_image ../../pycoin_docker/ pycoinimg - # + local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ## build setup docker image build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." # configure features of cyphernode - cyphernodeconf_configure + docker run -v $current_path/../data:/data \ + --log-driver=none\ + --rm -it cyphernodeconf:latest #docker image rm cyphernodeconf:latest } diff --git a/install/script/cyphernodeconf.sh b/install/script/cyphernodeconf.sh deleted file mode 100644 index 036cb117b..000000000 --- a/install/script/cyphernodeconf.sh +++ /dev/null @@ -1,12 +0,0 @@ -# this will run configure.sh of the specified package inside a -# cyphernodeconf container. This way we ensure we have the right -# environment and do not pollute the host machine with utility -# commands not needed for runtime - -cyphernodeconf_configure() { - local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - - docker run -v $current_path/../data:/data \ - --log-driver=none\ - --rm -it cyphernodeconf:latest -} \ No newline at end of file diff --git a/install/script/docker.sh b/install/script/docker.sh index 4d30004b0..70ff26472 100644 --- a/install/script/docker.sh +++ b/install/script/docker.sh @@ -1,6 +1,6 @@ build_docker_image() { - trace "building docker image: $1 with tag $2:latest" - docker build $1 -t $2:latest + trace "building docker image: $2:latest" + docker build -q $1 -t $2:latest > /dev/null } diff --git a/install/script/install.sh b/install/script/install.sh index eac781dce..b4a662a9a 100644 --- a/install/script/install.sh +++ b/install/script/install.sh @@ -1,3 +1,13 @@ +. ./install_docker.sh +. ./install_lunanode.sh + install() { - echo "Installation phase not implemented yet" + . ../data/installer/config.sh + if [[ ''$INSTALLER_MODE == 'none' ]]; then + echo "Skipping installation phase" + elif [[ ''$INSTALLER_MODE == 'docker' ]]; then + install_docker + elif [[ ''$INSTALLER_MODE == 'lunanode' ]]; then + install_lunanode + fi } \ No newline at end of file diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh new file mode 100644 index 000000000..f13eb4a6d --- /dev/null +++ b/install/script/install_docker.sh @@ -0,0 +1,34 @@ +install_docker() { + + echo + + if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then + trace "Updating SatoshiPortal repos" + git submodule update --recursive --remote + fi + + # build SatoshiPortal images + local arch=$(uname -m) #x86_64 + + if [[ $BITCOIN_INTERNAL == true ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin + fi + + if [[ $FEATURE_LIGHTNING == true ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning + fi + + if [[ $FEATURE_OPENTIMESTAMPS == true ]]; then + trace "Opentimestamps support not implemented" + fi + + + # build cyphernode images + trace "Creating cyphernode dockers" + build_docker_image ../../proxy_docker/ cyphernode/proxy + build_docker_image ../../cron_docker/ cyphernode/proxycron + build_docker_image ../../pycoin_docker/ cyphernode/pycoin + + trace "Creating cyphernode network" + docker network create cyphernodenet > /dev/null 2>&1 +} \ No newline at end of file diff --git a/install/script/install_lunanode.sh b/install/script/install_lunanode.sh new file mode 100644 index 000000000..b2d708eef --- /dev/null +++ b/install/script/install_lunanode.sh @@ -0,0 +1,3 @@ +install_lunanode() { + trace "Lunanode installation not implemented" +} \ No newline at end of file diff --git a/install/script/setup.sh b/install/script/setup.sh index 806a90ef5..44f0699a0 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -3,9 +3,31 @@ . ./trace.sh . ./configure.sh . ./install.sh +. ./docker.sh -echo "Starting configuration phase" -configure +CONFIGURE=0 +INSTALL=0 -echo "Starting installation phase" -install \ No newline at end of file +while getopts ":ci" opt; do + case $opt in + c) + CONFIGURE=1 + ;; + i) + INSTALL=1 + ;; + \?) + echo "Invalid option: -$OPTARG. Use -c to configure and -i to install" >&2 + ;; + esac +done + +if [[ $CONFIGURE == 1 ]]; then + echo "Starting configuration phase" + configure +fi + +if [[ $INSTALL == 1 ]]; then + echo "Starting installation phase" + install +fi diff --git a/setup.sh b/setup.sh index c8831d7cd..3d88e5519 100755 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,3 @@ #!/bin/sh -(cd install/script && TRACING=1 ./setup.sh) \ No newline at end of file +(cd install/script && TRACING=1 ./setup.sh $@) \ No newline at end of file From 549c762ee32e2280ca823b1250c915a7d4474155 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:14:14 +0200 Subject: [PATCH 046/268] quick help --- install/script/setup.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/install/script/setup.sh b/install/script/setup.sh index 44f0699a0..c7c39a304 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -22,12 +22,16 @@ while getopts ":ci" opt; do esac done -if [[ $CONFIGURE == 1 ]]; then - echo "Starting configuration phase" - configure -fi +if [[ $CONFIGURE == 0 && $INSTALL == 0 ]]; then + echo "Please use -c to configure, -i to install and -ci to do both" +else + if [[ $CONFIGURE == 1 ]]; then + trace "Starting configuration phase" + configure + fi -if [[ $INSTALL == 1 ]]; then - echo "Starting installation phase" - install + if [[ $INSTALL == 1 ]]; then + trace "Starting installation phase" + install + fi fi From d94a8fef8ddc91aeae4929e679b218542dea68c4 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:26:15 +0200 Subject: [PATCH 047/268] renamed opentimestamps feature to otsclient --- install/generator-cyphernode/generators/app/features.json | 2 +- .../app/prompters/{400_opentimestamps.js => 400_otsclient.js} | 2 +- .../generators/app/templates/installer/config.sh | 2 +- .../generators/app/templates/installer/docker-compose.yaml | 2 +- install/script/install_docker.sh | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename install/generator-cyphernode/generators/app/prompters/{400_opentimestamps.js => 400_otsclient.js} (90%) diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index 533967df3..56ede64a9 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -6,7 +6,7 @@ }, { "name": "Open timestamps client", - "value": "opentimestamps" + "value": "otsclient" }, { "name": "Electrum server", diff --git a/install/generator-cyphernode/generators/app/prompters/400_opentimestamps.js b/install/generator-cyphernode/generators/app/prompters/400_otsclient.js similarity index 90% rename from install/generator-cyphernode/generators/app/prompters/400_opentimestamps.js rename to install/generator-cyphernode/generators/app/prompters/400_otsclient.js index 238c45ef2..80cd80cc8 100644 --- a/install/generator-cyphernode/generators/app/prompters/400_opentimestamps.js +++ b/install/generator-cyphernode/generators/app/prompters/400_otsclient.js @@ -1,5 +1,5 @@ -const name = 'opentimestamps'; +const name = 'otsclient'; const featureCondition = function(props) { return props.features && props.features.indexOf( name ) != -1; } diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 555817fcc..3079604a3 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,5 +1,5 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> -FEATURE_OPENTIMESTAMPS=<%= (features.indexOf('opentimestamps') != -1)?'true':'false' %> +FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml index 0f1df011e..4801ac86f 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml +++ b/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml @@ -15,7 +15,7 @@ services: "DB_PATH":"/proxyuser/db" "DB_FILE":"/proxyuser/db/proxydb" "PYCOIN_CONTAINER":"pycoin:7777" - "OTS_CONTAINER":"opentimestamps:6666" + "OTS_CONTAINER":"otsclient:6666" "DERIVATION_PUB32":"<%= xpub %>" "DERIVATION_PATH":"<%= derivation_path %>" "WATCHER_BTC_NODE_PRUNED":"<%= bitcoin_prune?'true':'false' %>" diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index f13eb4a6d..f4938dbc6 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -18,8 +18,8 @@ install_docker() { build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning fi - if [[ $FEATURE_OPENTIMESTAMPS == true ]]; then - trace "Opentimestamps support not implemented" + if [[ $FEATURE_OTSCLIENT == true ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/ots/otsclient cyphernode/otsclient fi From a87c2e6e147ba8818f3fb33208c0cca09d2ebd3a Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:26:45 +0200 Subject: [PATCH 048/268] added lightning_implementation to installer config --- .../generators/app/templates/installer/config.sh | 1 + install/script/install_docker.sh | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 3079604a3..1500e9638 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -3,3 +3,4 @@ BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> +LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> \ No newline at end of file diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index f4938dbc6..a3f933657 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -15,7 +15,11 @@ install_docker() { fi if [[ $FEATURE_LIGHTNING == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning + if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning + elif [[ $LIGHTNING_IMPLEMENTATION == "lnd" ]]; then + trace "lnd is not supported right now" + fi fi if [[ $FEATURE_OTSCLIENT == true ]]; then From 1bb52a402408656f620cf312f4b27a56b33645c9 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:29:46 +0200 Subject: [PATCH 049/268] more verbosity --- install/script/install_docker.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index a3f933657..4b66de537 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -5,6 +5,8 @@ install_docker() { if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then trace "Updating SatoshiPortal repos" git submodule update --recursive --remote + trace "Creating SatoshiPortal images" + fi # build SatoshiPortal images @@ -28,7 +30,7 @@ install_docker() { # build cyphernode images - trace "Creating cyphernode dockers" + trace "Creating cyphernode images" build_docker_image ../../proxy_docker/ cyphernode/proxy build_docker_image ../../cron_docker/ cyphernode/proxycron build_docker_image ../../pycoin_docker/ cyphernode/pycoin From 5bec6232c64d2fcb4afdb067c52fd98e08140fc8 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:39:17 +0200 Subject: [PATCH 050/268] moved docker lib --- install/script/install_docker.sh | 5 +++-- install/script/setup.sh | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index 4b66de537..5268ec263 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -1,3 +1,5 @@ +. ./docker.sh + install_docker() { echo @@ -9,8 +11,7 @@ install_docker() { fi - # build SatoshiPortal images - local arch=$(uname -m) #x86_64 + local arch=$(uname -m) # TODO: is this correct for every host if [[ $BITCOIN_INTERNAL == true ]]; then build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin diff --git a/install/script/setup.sh b/install/script/setup.sh index c7c39a304..d48451fbd 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -3,7 +3,6 @@ . ./trace.sh . ./configure.sh . ./install.sh -. ./docker.sh CONFIGURE=0 INSTALL=0 From ed61e556a517fe77812d3fccbfdb531d8b879383 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:45:05 +0200 Subject: [PATCH 051/268] files for installers are now located inside their directories --- .../generators/app/prompters/999_installer | 6 ++++-- .../templates/installer/{ => docker}/docker-compose.yaml | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename install/generator-cyphernode/generators/app/templates/installer/{ => docker}/docker-compose.yaml (100%) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer index 59bd2158b..be849635d 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer @@ -1,6 +1,8 @@ -const name = 'installer'; +const path = require('path'); const chalk = require('chalk'); +const name = 'installer'; + const installerDocker = function(props) { return props.installer === 'docker' }; @@ -50,7 +52,7 @@ module.exports = { }, templates: function( props ) { if( props.installer_mode === 'docker' ) { - return ['config.sh','docker-compose.yaml']; + return ['config.sh', path.join('docker', 'docker-compose.yaml')]; } return ['config.sh']; } diff --git a/install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml b/install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml similarity index 100% rename from install/generator-cyphernode/generators/app/templates/installer/docker-compose.yaml rename to install/generator-cyphernode/generators/app/templates/installer/docker/docker-compose.yaml From 5762474ab5c865e36ccc1c023ae518889bf1a1c9 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 14:56:39 +0200 Subject: [PATCH 052/268] template support for external full node --- .../app/templates/installer/docker/docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4801ac86f..b0bb12d74 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 @@ -5,10 +5,10 @@ services: # Bitcoin Mini Proxy env: "TRACING":"1" - "WATCHER_BTC_NODE_RPC_URL":"bitcoin:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat" + "WATCHER_BTC_NODE_RPC_URL":"<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/watching01.dat" "WATCHER_BTC_NODE_RPC_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" "WATCHER_BTC_NODE_RPC_CFG":"/proxyuser/watcher_btcnode_curlcfg.properties" - "SPENDER_BTC_NODE_RPC_URL":"bitcoin:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat" + "SPENDER_BTC_NODE_RPC_URL":"<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %>:<%= (net === 'mainnet')?'8332':'18332' %>/wallet/spending01.dat" "SPENDER_BTC_NODE_RPC_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" "SPENDER_BTC_NODE_RPC_CFG":"/proxyuser/sender_btcnode_curlcfg.properties" "PROXY_LISTENING_PORT":"8888" From 88044e86d986815a1e44f15c93e86d52dc5a51da Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:11:25 +0200 Subject: [PATCH 053/268] fixed missing extension --- .../app/prompters/{999_installer => 999_installer.js} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename install/generator-cyphernode/generators/app/prompters/{999_installer => 999_installer.js} (81%) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer b/install/generator-cyphernode/generators/app/prompters/999_installer.js similarity index 81% rename from install/generator-cyphernode/generators/app/prompters/999_installer rename to install/generator-cyphernode/generators/app/prompters/999_installer.js index be849635d..fbe83a80d 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -35,11 +35,13 @@ module.exports = { }] }, { - when: installerDocker, + when: function(props) { + return (installerDocker(props) && props.bitcoin_mode === 'internal') + }, type: 'confirm', - name: 'installer_confirm_docker', - default: utils._getDefault( 'installer_confirm_docker' ), - message: 'Docker?! Really?'+'\n' + name: 'bitcoin_expose', + default: utils._getDefault( 'bitcoin_expose' ), + message: 'Expose bitcoin full node outside of the docker network?'+'\n', }, { when: installerLunanode, From ba354478fd52fcaf8b71b1037688114b713a8976 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:12:44 +0200 Subject: [PATCH 054/268] removed exose prompt from bitcoin prompter --- .../generators/app/prompters/100_bitcoin.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index a18e156a2..bd5e6d4c3 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -1,5 +1,4 @@ const name = 'bitcoin'; - const bitcoinExternal = function(props) { return props.bitcoin_mode === 'external' }; @@ -57,13 +56,6 @@ module.exports = { default: utils._getDefault( 'bitcoin_prune' ), message: 'Run bitcoin node in prune mode?'+'\n', }, - { - when: bitcoinInternal, - type: 'confirm', - name: 'bitcoin_expose', - default: utils._getDefault( 'bitcoin_expose' ), - message: 'Expose bitcoin full node outside of the docker network?'+'\n', - }, { when: bitcoinInternal, type: 'input', From 9b979ed4eb960edeb3201b330fde80d21434bf55 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:24:13 +0200 Subject: [PATCH 055/268] added "expose" option to installer and template --- .../generators/app/prompters/999_installer.js | 19 ++++++++++++------- .../installer/docker/docker-compose.yaml | 10 ++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index fbe83a80d..0488fcd71 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -4,11 +4,19 @@ const chalk = require('chalk'); const name = 'installer'; const installerDocker = function(props) { - return props.installer === 'docker' + return props.installer_mode === 'docker' +}; + +const installerDocker_bitcoinInternal = function(props) { + return props.installer_mode === 'docker' && props.bitcoin_mode === 'internal' +}; + +const installerDocker_bitcoinExternal = function(props) { + return props.installer_mode === 'docker' && props.bitcoin_mode === 'external' }; const installerLunanode = function(props) { - return props.installer === 'lunanode' + return props.installer_mode === 'lunanode' }; module.exports = { @@ -35,9 +43,7 @@ module.exports = { }] }, { - when: function(props) { - return (installerDocker(props) && props.bitcoin_mode === 'internal') - }, + when: installerDocker_bitcoinInternal, type: 'confirm', name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), @@ -49,8 +55,7 @@ module.exports = { name: 'installer_confirm_lunanode', default: utils._getDefault( 'installer_confirm_lunanode' ), message: 'Lunanode?! No wayyyy!'+'\n' - } - ]; + }]; }, templates: function( props ) { if( props.installer_mode === 'docker' ) { 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 b0bb12d74..84a62db39 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 @@ -77,12 +77,10 @@ services: bitcoin: # Bitcoin node image: cyphernode/bitcoin -# ports: -# - "18333:18333" -# - "18332:18332" -# - "29000:29000" -# - "8333:8333" -# - "8332:8332" +<% if( bitcoin_expose ) { %> + ports: + - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" +<% } %> volumes: - "~/.bitcoin:/bitcoinuser/.bitcoin" networks: From 92ee841edc267e4ee8ab77c97b31ab2a30993bd8 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:38:56 +0200 Subject: [PATCH 056/268] added bitcoin and lightning volume prompts and template entries --- .../generators/app/index.js | 5 ++++ .../generators/app/prompters/999_installer.js | 26 ++++++++++++------- .../installer/docker/docker-compose.yaml | 4 +-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index f56882d64..6fa5982c5 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -95,6 +95,11 @@ module.exports = class extends Generator { return true; } + _pathValidator( p ) { + + return true; + } + _derivationPathValidator( path ) { return true; } diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 0488fcd71..08cff7035 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -7,14 +7,6 @@ const installerDocker = function(props) { return props.installer_mode === 'docker' }; -const installerDocker_bitcoinInternal = function(props) { - return props.installer_mode === 'docker' && props.bitcoin_mode === 'internal' -}; - -const installerDocker_bitcoinExternal = function(props) { - return props.installer_mode === 'docker' && props.bitcoin_mode === 'external' -}; - const installerLunanode = function(props) { return props.installer_mode === 'lunanode' }; @@ -43,12 +35,28 @@ module.exports = { }] }, { - when: installerDocker_bitcoinInternal, + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, type: 'confirm', name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), message: 'Expose bitcoin full node outside of the docker network?'+'\n', }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, + type: 'input', + name: 'bitcoin_datapath', + default: utils._getDefault( 'bitcoin_datapath' ), + validate: utils._pathValidator, + message: 'Where is your blockchain data?'+'\n', + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, + type: 'input', + name: 'lightning_datapath', + default: utils._getDefault( 'lightning_datapath' ), + validate: utils._pathValidator, + message: 'Where is your lightning node data?'+'\n', + }, { when: installerLunanode, type: 'confirm', 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 84a62db39..f74cfc46d 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 @@ -64,7 +64,7 @@ services: ports: - "9735:9735" volumes: - - "~/.lightning:/lnuser/.lightning" + - "<%= lightning_datapath%>:/lnuser/.lightning" # deploy: # placement: # constraints: [node.hostname==dev] @@ -82,7 +82,7 @@ services: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" <% } %> volumes: - - "~/.bitcoin:/bitcoinuser/.bitcoin" + - "<%= bitcoin_datapath%>:/bitcoinuser/.bitcoin" networks: - cyphernodenet <% } %> From 0532e79a756a6c88f9b769d45f83619475924903 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:58:09 +0200 Subject: [PATCH 057/268] removed lnd option --- .../generators/app/prompters/200_lightning.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 60a2617a5..8553a6f3a 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -25,11 +25,13 @@ module.exports = { { name: 'C-lightning', value: 'c-lightning' - }, + } + /*, { name: 'LND', value: 'lnd' } + */ ] }, { From 8c805e4834b65bdafb147e13d88381601862d597 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:58:40 +0200 Subject: [PATCH 058/268] fixed docker-compose template --- .../installer/docker/docker-compose.yaml | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) 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 f74cfc46d..78f2d070f 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 @@ -3,30 +3,30 @@ version: "3" services: proxy: # Bitcoin Mini Proxy - env: - "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_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - "WATCHER_BTC_NODE_RPC_CFG":"/proxyuser/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_USER":"<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - "SPENDER_BTC_NODE_RPC_CFG":"/proxyuser/sender_btcnode_curlcfg.properties" - "PROXY_LISTENING_PORT":"8888" - "DB_PATH":"/proxyuser/db" - "DB_FILE":"/proxyuser/db/proxydb" - "PYCOIN_CONTAINER":"pycoin:7777" - "OTS_CONTAINER":"otsclient:6666" - "DERIVATION_PUB32":"<%= xpub %>" - "DERIVATION_PATH":"<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED":"<%= bitcoin_prune?'true':'false' %>" + 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_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + - "WATCHER_BTC_NODE_RPC_CFG=/proxyuser/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_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" + - "SPENDER_BTC_NODE_RPC_CFG=/proxyuser/sender_btcnode_curlcfg.properties" + - "PROXY_LISTENING_PORT=8888" + - "DB_PATH=/proxyuser/db" + - "DB_FILE=/proxyuser/db/proxydb" + - "PYCOIN_CONTAINER=pycoin:7777" + - "OTS_CONTAINER=otsclient:6666" + - "DERIVATION_PUB32=<%= xpub %>" + - "DERIVATION_PATH=<%= derivation_path %>" + - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" image: cyphernode/proxy # ports: # - "8888:8888" volumes: # Variable substitutions don't work # Match with DB_PATH in proxy_docker/env.properties - - "~/btcproxydb:/proxyuser/db" - - "~/.lightning:/proxyuser/.lightning" + - "<%= proxy_datapath %>:/proxyuser/db" + - "<%= lightning_datapath %>:/proxyuser/.lightning" # deploy: # placement: # constraints: [node.hostname==dev] @@ -35,8 +35,8 @@ services: proxycron: # Async jobs - env: - "PROXY_URL":"proxy:8888/executecallbacks" + environment: + - "PROXY_URL=proxy:8888/executecallbacks" image: cyphernode/proxycron # deploy: # placement: @@ -47,9 +47,9 @@ services: pycoin: # Pycoin image: cyphernode/pycoin - env: - "TRACING":"1" - "PYCOIN_LISTENING_PORT":"7777" + environment: + - "TRACING=1" + - "PYCOIN_LISTENING_PORT=7777" # ports: # - "7777:7777" # deploy: From 886b2f7d8c308c5e15286acd0b8afc1833af13f5 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 15:58:56 +0200 Subject: [PATCH 059/268] added prompt for proxy_data --- .../generators/app/prompters/999_installer.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 08cff7035..56a45ff83 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -35,11 +35,12 @@ module.exports = { }] }, { - when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, - type: 'confirm', - name: 'bitcoin_expose', - default: utils._getDefault( 'bitcoin_expose' ), - message: 'Expose bitcoin full node outside of the docker network?'+'\n', + when: installerDocker, + type: 'input', + name: 'proxy_datapath', + default: utils._getDefault( 'proxy_datapath' ), + validate: utils._pathValidator, + message: 'Where to store your proxy db?'+'\n', }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, @@ -57,6 +58,13 @@ module.exports = { validate: utils._pathValidator, message: 'Where is your lightning node data?'+'\n', }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, + type: 'confirm', + name: 'bitcoin_expose', + default: utils._getDefault( 'bitcoin_expose' ), + message: 'Expose bitcoin full node outside of the docker network?'+'\n', + }, { when: installerLunanode, type: 'confirm', From a7ccd08a2f4688f56b4c3e4e5ffccfea50e0014f Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 16:20:34 +0200 Subject: [PATCH 060/268] added recreate feature to skip all prompts and recreate installation configuration based on previsous selection in props.json --- install/Dockerfile | 2 +- install/generator-cyphernode/generators/app/index.js | 8 ++++++++ install/script/configure.sh | 10 +++++++--- install/script/setup.sh | 12 ++++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/install/Dockerfile b/install/Dockerfile index e6daf05a3..0a7691e0d 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -21,5 +21,5 @@ RUN chown -R yo:yo /yo/.config USER yo WORKDIR /data -CMD ["yo","cyphernode"] +ENTRYPOINT ["yo","cyphernode"] diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 6fa5982c5..591a7c7b3 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -19,6 +19,10 @@ module.exports = class extends Generator { constructor(args, opts) { super(args, opts); + if( args.indexOf('recreate') !== -1 ) { + this.recreate = true; + } + if( fs.existsSync(this.destinationPath('props.json')) ) { this.props = require(this.destinationPath('props.json')); } else { @@ -36,6 +40,10 @@ module.exports = class extends Generator { } prompting() { + if( this.recreate ) { + // no prompts + return; + } const splash = fs.readFileSync(this.templatePath('splash.txt')); this.log(splash.toString()); diff --git a/install/script/configure.sh b/install/script/configure.sh index d284a55a9..15811dfb5 100644 --- a/install/script/configure.sh +++ b/install/script/configure.sh @@ -2,12 +2,16 @@ configure() { local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ## build setup docker image + local recreate="" + + if [[ $1 == 1 ]]; then + recreate="recreate" + fi + build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." # configure features of cyphernode docker run -v $current_path/../data:/data \ --log-driver=none\ - --rm -it cyphernodeconf:latest - - #docker image rm cyphernodeconf:latest + --rm -it cyphernodeconf:latest $recreate } diff --git a/install/script/setup.sh b/install/script/setup.sh index d48451fbd..4082994a8 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -6,9 +6,13 @@ CONFIGURE=0 INSTALL=0 +RECREATE=0 -while getopts ":ci" opt; do +while getopts ":cir" opt; do case $opt in + r) + RECREATE=1 + ;; c) CONFIGURE=1 ;; @@ -21,12 +25,12 @@ while getopts ":ci" opt; do esac done -if [[ $CONFIGURE == 0 && $INSTALL == 0 ]]; then - echo "Please use -c to configure, -i to install and -ci to do both" +if [[ $CONFIGURE == 0 && $INSTALL == 0 && RECREATE == 0 ]]; then + echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." else if [[ $CONFIGURE == 1 ]]; then trace "Starting configuration phase" - configure + configure $RECREATE fi if [[ $INSTALL == 1 ]]; then From deabadd2f33edb1a361249950a79def059c20d6b Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 16:21:34 +0200 Subject: [PATCH 061/268] removed unneeded code --- .../app/templates/installer/docker/docker-compose.yaml | 7 ------- 1 file changed, 7 deletions(-) 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 78f2d070f..38be4e6df 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 @@ -23,8 +23,6 @@ services: # ports: # - "8888:8888" volumes: - # Variable substitutions don't work - # Match with DB_PATH in proxy_docker/env.properties - "<%= proxy_datapath %>:/proxyuser/db" - "<%= lightning_datapath %>:/proxyuser/.lightning" # deploy: @@ -34,7 +32,6 @@ services: - cyphernodenet proxycron: - # Async jobs environment: - "PROXY_URL=proxy:8888/executecallbacks" image: cyphernode/proxycron @@ -59,7 +56,6 @@ services: - cyphernodenet <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: - # c-lightning lightning network node image: cyphernode/clightning ports: - "9735:9735" @@ -70,12 +66,9 @@ services: # constraints: [node.hostname==dev] networks: - cyphernodenet -<% } else if( features.indexOf('lightning') !== -1 && lightning_implementation === 'lnd' ) { %> -# TODO: add lnd support <% } %> <% if( bitcoin_mode === 'internal' ) { %> bitcoin: - # Bitcoin node image: cyphernode/bitcoin <% if( bitcoin_expose ) { %> ports: From 153a7b5d3642e05400dd5ece27c53e1152e0ba91 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 16:31:41 +0200 Subject: [PATCH 062/268] removed option from installer prompts --- .../generators/app/prompters/999_installer.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 56a45ff83..b0c654491 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -28,10 +28,6 @@ module.exports = { { name: "Lunanode (not implemented)", value: "lunanode" - }, - { - name: "No installation. Just create config files", - value: "none" }] }, { From b314ee5b05cb69d63d43b9c268b66b0893e9dff5 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 18:10:48 +0200 Subject: [PATCH 063/268] better readability --- .../generators/app/prompters/000_proxy.js | 18 +++++++++++---- .../generators/app/prompters/100_bitcoin.js | 23 ++++++++++++++----- .../generators/app/prompters/200_lightning.js | 16 ++++++++++--- .../generators/app/prompters/300_electrum.js | 15 ++++++++++-- .../generators/app/prompters/400_otsclient.js | 12 +++++++++- .../generators/app/prompters/999_installer.js | 20 +++++++++++----- 6 files changed, 82 insertions(+), 22 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_proxy.js b/install/generator-cyphernode/generators/app/prompters/000_proxy.js index 70d695454..380445804 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_proxy.js +++ b/install/generator-cyphernode/generators/app/prompters/000_proxy.js @@ -1,5 +1,15 @@ +const chalk = require('chalk'); + const name = 'proxy'; +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + module.exports = { name: function() { return name; @@ -10,14 +20,14 @@ module.exports = { // input, confirm, list, rawlist, expand, checkbox, password, editor type: 'checkbox', name: 'features', - message: 'What features do you want to add to your cyphernode?'+'\n', + message: prefix()+'What features do you want to add to your cyphernode?'+'\n', choices: utils._featureChoices() }, { type: 'list', name: 'net', default: utils._getDefault( 'net' ), - message: 'What net do you want to run on?'+'\n', + message: prefix()+'What net do you want to run on?'+'\n', choices: [{ name: "Testnet", value: "testnet" @@ -30,14 +40,14 @@ module.exports = { type: 'input', name: 'xpub', default: utils._getDefault( 'xpub' ), - message: 'What is your xpub to watch?'+'\n', + message: prefix()+'What is your xpub to watch?'+'\n', validate: utils._xkeyValidator }, { type: 'input', name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), - message: 'What is your address derivation path?'+'\n', + message: prefix()+'What is your address derivation path?'+'\n', validate: utils._derivationPathValidator }]; }, diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index bd5e6d4c3..97fe60644 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -1,4 +1,15 @@ +const chalk = require('chalk'); + const name = 'bitcoin'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + const bitcoinExternal = function(props) { return props.bitcoin_mode === 'external' }; @@ -17,7 +28,7 @@ module.exports = { type: 'list', name: 'bitcoin_mode', default: utils._getDefault( 'bitcoin_mode' ), - message: 'Where is your bitcoin full node running?'+'\n', + message: prefix()+'Where is your bitcoin full node running?'+'\n', choices: [ { name: 'Nowhere! I want cyphernode to run one.', @@ -35,33 +46,33 @@ module.exports = { name: 'bitcoin_node_ip', default: utils._getDefault( 'bitcoin_node_ip' ), validate: utils._ipOrFQDNValidator, - message: 'What is your full node ip address?'+'\n', + message: prefix()+'What is your full node ip address?'+'\n', }, { type: 'input', name: 'bitcoin_rpcuser', default: utils._getDefault( 'bitcoin_rpcuser' ), - message: 'Name of bitcoin rpc user?'+'\n', + message: prefix()+'Name of bitcoin rpc user?'+'\n', }, { type: 'password', name: 'bitcoin_rpcpassword', default: utils._getDefault( 'bitcoin_rpcpassword' ), - message: 'Password of bitcoin rpc user?'+'\n', + message: prefix()+'Password of bitcoin rpc user?'+'\n', }, { when: bitcoinInternal, type: 'confirm', name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), - message: 'Run bitcoin node in prune mode?'+'\n', + message: prefix()+'Run bitcoin node in prune mode?'+'\n', }, { when: bitcoinInternal, type: 'input', name: 'bitcoin_uacomment', default: utils._getDefault( 'bitcoin_uacomment' ), - message: 'Any UA comment?'+'\n', + message: prefix()+'Any UA comment?'+'\n', }]; }, env: function( props ) { diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 8553a6f3a..c4d8dba8a 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -1,14 +1,24 @@ const path = require('path'); +const chalk = require('chalk'); const name = 'lightning'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + const featureCondition = function(props) { return props.features && props.features.indexOf( name ) != -1; -} +}; const templates = { 'lnd': [ path.join('lnd','lnd.conf') ], 'c-lightning': [ path.join('c-lightning','config') ] -} +}; module.exports = { name: function() { @@ -20,7 +30,7 @@ module.exports = { type: 'list', name: 'lightning_implementation', default: utils._getDefault( 'lightning_implementation' ), - message: 'What lightning implementation do you want to use?'+'\n', + message: prefix()+'What lightning implementation do you want to use?'+'\n', choices: [ { name: 'C-lightning', diff --git a/install/generator-cyphernode/generators/app/prompters/300_electrum.js b/install/generator-cyphernode/generators/app/prompters/300_electrum.js index c374918f7..e5949c56d 100644 --- a/install/generator-cyphernode/generators/app/prompters/300_electrum.js +++ b/install/generator-cyphernode/generators/app/prompters/300_electrum.js @@ -1,7 +1,18 @@ +const chalk = require('chalk'); + const name = 'electrum'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + const featureCondition = function(props) { return props.features && props.features.indexOf( name ) != -1; -} +}; module.exports = { name: function() { @@ -13,7 +24,7 @@ module.exports = { type: 'list', name: 'electrum_implementation', default: utils._getDefault( 'electrum_implementation' ), - message: 'What electrum implementation do you want to use?'+'\n', + message: prefix()+'What electrum implementation do you want to use?'+'\n', choices: [ { name: 'Electrum personal server', diff --git a/install/generator-cyphernode/generators/app/prompters/400_otsclient.js b/install/generator-cyphernode/generators/app/prompters/400_otsclient.js index 80cd80cc8..ad78afd3a 100644 --- a/install/generator-cyphernode/generators/app/prompters/400_otsclient.js +++ b/install/generator-cyphernode/generators/app/prompters/400_otsclient.js @@ -1,8 +1,18 @@ +const chalk = require('chalk'); const name = 'otsclient'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + const featureCondition = function(props) { return props.features && props.features.indexOf( name ) != -1; -} +}; module.exports = { name: function() { diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index b0c654491..68ab366b9 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -3,6 +3,14 @@ const chalk = require('chalk'); const name = 'installer'; +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.green(capitalise(name)+': '); +}; + const installerDocker = function(props) { return props.installer_mode === 'docker' }; @@ -20,7 +28,7 @@ module.exports = { type: 'list', name: 'installer_mode', default: utils._getDefault( 'installer_mode' ), - message: chalk.red('Where do you want to install cyphernode?')+'\n', + message: prefix()+chalk.red('Where do you want to install cyphernode?')+'\n', choices: [{ name: "Docker", value: "docker" @@ -36,7 +44,7 @@ module.exports = { name: 'proxy_datapath', default: utils._getDefault( 'proxy_datapath' ), validate: utils._pathValidator, - message: 'Where to store your proxy db?'+'\n', + message: prefix()+'Where to store your proxy db?'+'\n', }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, @@ -44,7 +52,7 @@ module.exports = { name: 'bitcoin_datapath', default: utils._getDefault( 'bitcoin_datapath' ), validate: utils._pathValidator, - message: 'Where is your blockchain data?'+'\n', + message: prefix()+'Where is your blockchain data?'+'\n', }, { when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, @@ -52,21 +60,21 @@ module.exports = { name: 'lightning_datapath', default: utils._getDefault( 'lightning_datapath' ), validate: utils._pathValidator, - message: 'Where is your lightning node data?'+'\n', + message: prefix()+'Where is your lightning node data?'+'\n', }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, type: 'confirm', name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), - message: 'Expose bitcoin full node outside of the docker network?'+'\n', + message: prefix()+'Expose bitcoin full node outside of the docker network?'+'\n', }, { when: installerLunanode, type: 'confirm', name: 'installer_confirm_lunanode', default: utils._getDefault( 'installer_confirm_lunanode' ), - message: 'Lunanode?! No wayyyy!'+'\n' + message: prefix()+'Lunanode?! No wayyyy!'+'\n' }]; }, templates: function( props ) { From 6f31f18e53fc69fbb1ca445fd32e8dfff1474e74 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 18:11:23 +0200 Subject: [PATCH 064/268] added nodecolor and nodename to lightning config prompts --- .../generators/app/index.js | 15 ++++++++++++++- .../generators/app/prompters/200_lightning.js | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 591a7c7b3..ff169a5c7 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -104,7 +104,6 @@ module.exports = class extends Generator { } _pathValidator( p ) { - return true; } @@ -112,6 +111,20 @@ module.exports = class extends Generator { return true; } + _colorValidator(color) { + if( !validator.isHexColor(color) ) { + throw new Error('Not a hex color.'); + } + return true; + } + + _notEmptyValidator( path ) { + if( !path ) { + throw new Error('Please enter something'); + } + return true; + } + _trimFilter( input ) { return (input+"").trim(); } diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index c4d8dba8a..948b394ec 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -50,7 +50,23 @@ module.exports = { name: 'lightning_external_ip', default: utils._getDefault( 'lightning_external_ip' ), validate: utils._ipOrFQDNValidator, - message: 'What external ip does your lightning node have?'+'\n', + message: prefix()+'What external ip does your lightning node have?'+'\n', + }, + { + when: featureCondition, + type: 'input', + name: 'lightning_nodename', + default: utils._getDefault( 'lightning_nodename' ), + validate: utils._notEmptyValidator, + message: prefix()+'What name has your lightning node?'+'\n', + }, + { + when: featureCondition, + type: 'input', + name: 'lightning_nodecolor', + default: utils._getDefault( 'lightning_nodecolor' ), + validate: utils._colorValidator, + message: prefix()+'What color has your lightning node?'+'\n', }]; }, templates: function( props ) { From 75e26d9f8fbef0d9fa2f8e299ccffee778fe725c Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 18:12:09 +0200 Subject: [PATCH 065/268] added datapaths to config --- .../generators/app/templates/installer/config.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 1500e9638..0d593b1fd 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -3,4 +3,7 @@ BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> -LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> \ No newline at end of file +LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> +BITCOIN_DATAPATH=<%= bitcoin_datapath %> +LIGHTNING_DATAPAT=<%= lightning_datapath %> +PROXY_DATAPAT=<%= proxy_datapath %> \ No newline at end of file From 8c8f15022f4de60388fc1f8bcb7cca7db4833598 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 18:12:24 +0200 Subject: [PATCH 066/268] better configs --- .../generators/app/templates/bitcoin/bitcoin.conf | 7 ++----- .../app/templates/lightning/c-lightning/config | 15 +++++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 72e311514..12324bfbc 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -3,12 +3,9 @@ testnet=1 <% } %> -<% if (lightning_implementation === 'lnd') { %> -#lnd opts txindex=1 zmqpubrawblock=tcp://0.0.0.0:18501 zmqpubrawtx=tcp://0.0.0.0:18502 -<% } %> #tor #proxy=127.0.0.1:9050 @@ -17,15 +14,15 @@ zmqpubrawtx=tcp://0.0.0.0:18502 maxmempool=64 dbcache=64 -rpcconnect=bitcoin rpcuser=<%= bitcoin_rpcuser %> rpcpassword=<%= bitcoin_rpcpassword %> +rpcallowip=10.0.0.0/24 wallet=watching01.dat wallet=spending01.dat wallet=ln01.dat -walletnotify=curl cyphernode:8888/conf/%s +walletnotify=curl proxy:8888/conf/%s <% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> uacomment=<%= bitcoin_uacomment %> diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config index 77c25c0d6..e4759bc27 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config @@ -1,4 +1,11 @@ -alias=SatoshiPortal01 -rgb=008000 -#port=9735 -network=testnet +<% if (net === 'testnet') { %> +# testnet +testnet=1 +<% } %> +alias=<%= lightning_nodename %> +rgb=<%= lightning_nodecolor %> +addr=<%= lightning_external_ip %> +rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> +rpcuser=<%= bitcoin_rpcuser %> +rpcpassword=<%= bitcoin_rpcpassword %> +rpcwallet=ln01.dat From ba2d2a66f70eef67ee39e467e7aea7d46634d268 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 19:40:44 +0200 Subject: [PATCH 067/268] removed # from color validation --- 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 ff169a5c7..043201f37 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -112,7 +112,7 @@ module.exports = class extends Generator { } _colorValidator(color) { - if( !validator.isHexColor(color) ) { + if( !validator.isHexadecimal(color) ) { throw new Error('Not a hex color.'); } return true; From 8ea61de06f007aaa66d35aa6cd2a90a4f64826e8 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 19:48:42 +0200 Subject: [PATCH 068/268] bitcoin.conf allow rpc from everywhere inside the docker network. --- .../generators/app/templates/bitcoin/bitcoin.conf | 5 ++++- 1 file changed, 4 insertions(+), 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 12324bfbc..2b670ae96 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -16,7 +16,10 @@ dbcache=64 rpcuser=<%= bitcoin_rpcuser %> rpcpassword=<%= bitcoin_rpcpassword %> -rpcallowip=10.0.0.0/24 + +# ATTENTION: VERY DANGEROUS OUTSIDE THE DOCKER NETWORK +rpcallowip=0.0.0.0/0 +server=1 wallet=watching01.dat wallet=spending01.dat From 13f51daff71295ca576d4349490f55393f83c98f Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 19:48:55 +0200 Subject: [PATCH 069/268] fixed typo --- .../generators/app/templates/installer/config.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 0d593b1fd..ff12667c6 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -5,5 +5,5 @@ FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> -LIGHTNING_DATAPAT=<%= lightning_datapath %> -PROXY_DATAPAT=<%= proxy_datapath %> \ No newline at end of file +LIGHTNING_DATAPATH=<%= lightning_datapath %> +PROXY_DATAPATH=<%= proxy_datapath %> \ No newline at end of file From 0c4127f5e33e7c13dca9fa3fd34efd3b42c813bf Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 19:50:33 +0200 Subject: [PATCH 070/268] removed some config entries --- .../app/templates/lightning/c-lightning/config | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config index e4759bc27..79aa05e18 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config @@ -1,11 +1,11 @@ <% if (net === 'testnet') { %> # testnet -testnet=1 +network=testnet +<% } else if (net === 'mainnet') { %> +network=bitcoin <% } %> alias=<%= lightning_nodename %> rgb=<%= lightning_nodecolor %> -addr=<%= lightning_external_ip %> -rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> -rpcuser=<%= bitcoin_rpcuser %> -rpcpassword=<%= bitcoin_rpcpassword %> -rpcwallet=ln01.dat +bitcoin-rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> +bitcoin-rpcuser=<%= bitcoin_rpcuser %> +bitcoin-rpcpassword=<%= bitcoin_rpcpassword %> From c17b0e9630ce1f157eca8cff7bff3e0852bb3b46 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 19:51:17 +0200 Subject: [PATCH 071/268] added file copying so installation can be run with docker stack or docker-compose --- install/script/install_docker.sh | 47 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index 5268ec263..82e0acb11 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -2,7 +2,8 @@ install_docker() { - echo + local sourceDataPath=../data + local topLevel=../.. if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then trace "Updating SatoshiPortal repos" @@ -15,13 +16,35 @@ install_docker() { if [[ $BITCOIN_INTERNAL == true ]]; then build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin + if [ ! -d $BITCOIN_DATAPATH ]; then + trace "Creating $BITCOIN_DATAPATH" + mkdir -p $BITCOIN_DATAPATH + fi + + if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then + trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" + cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") + fi + + trace "Copying bitcoin core node config" + cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH fi if [[ $FEATURE_LIGHTNING == true ]]; then if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning - elif [[ $LIGHTNING_IMPLEMENTATION == "lnd" ]]; then - trace "lnd is not supported right now" + if [ ! -d $LIGHTNING_DATAPATH ]; then + trace "Creating $LIGHTNING_DATAPATH" + mkdir -p $LIGHTNING_DATAPATH + fi + + if [[ -f $LIGHTNING_DATAPATH/config ]]; then + trace "Creating backup of $LIGHTNING_DATAPATH/config" + cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") + fi + + trace "Copying c-lightning config" + cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH fi fi @@ -33,9 +56,27 @@ install_docker() { # build cyphernode images trace "Creating cyphernode images" build_docker_image ../../proxy_docker/ cyphernode/proxy + if [ ! -d $PROXY_DATAPATH ]; then + trace "Creating $PROXY_DATAPATH" + mkdir -p $PROXY_DATAPATH + fi build_docker_image ../../cron_docker/ cyphernode/proxycron build_docker_image ../../pycoin_docker/ cyphernode/pycoin trace "Creating cyphernode network" docker network create cyphernodenet > /dev/null 2>&1 + + if [[ -f $topLevel/docker-compose.yaml ]]; then + trace "Creating backup of docker-compose.yaml" + cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") + fi + + trace "Copying docker-compose.yaml to top level" + cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + + echo "+------------------------------------------+" + echo "| to start cyphernode run: |" + echo "| docker-compose -f docker-compose.yaml up |" + echo "+------------------------------------------+" + } \ No newline at end of file From 80b47e6d17f65746434af23b6ed1e8a0fe6cd9b2 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 20:47:09 +0200 Subject: [PATCH 072/268] added devmode --- .../templates/installer/docker/docker-compose.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 38be4e6df..28b9f1cf2 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 @@ -20,8 +20,10 @@ services: - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" image: cyphernode/proxy -# ports: -# - "8888:8888" +<% if ( devmode ) { %> + ports: + - "8888:8888" +<% } %> volumes: - "<%= proxy_datapath %>:/proxyuser/db" - "<%= lightning_datapath %>:/proxyuser/.lightning" @@ -47,8 +49,10 @@ services: environment: - "TRACING=1" - "PYCOIN_LISTENING_PORT=7777" -# ports: -# - "7777:7777" +<% if ( devmode ) { %> + ports: + - "7777:7777" +<% } %> # deploy: # placement: # constraints: [node.hostname==dev] From 79f7d9a837fd7ca81d429288a236fe11a6891c84 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 20:47:33 +0200 Subject: [PATCH 073/268] fixed indenting --- install/script/configure.sh | 18 +++--- install/script/install_docker.sh | 98 +++++++++++++++--------------- install/script/install_lunanode.sh | 2 +- install/script/setup.sh | 28 ++++----- 4 files changed, 73 insertions(+), 73 deletions(-) diff --git a/install/script/configure.sh b/install/script/configure.sh index 15811dfb5..c290f95ce 100644 --- a/install/script/configure.sh +++ b/install/script/configure.sh @@ -1,17 +1,17 @@ configure() { - local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - ## build setup docker image - local recreate="" + local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + ## build setup docker image + local recreate="" - if [[ $1 == 1 ]]; then - recreate="recreate" - fi + if [[ $1 == 1 ]]; then + recreate="recreate" + fi - build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." + build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." - # configure features of cyphernode - docker run -v $current_path/../data:/data \ + # configure features of cyphernode + docker run -v $current_path/../data:/data \ --log-driver=none\ --rm -it cyphernodeconf:latest $recreate } diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index 82e0acb11..24c01365d 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -2,54 +2,54 @@ install_docker() { - local sourceDataPath=../data - local topLevel=../.. + local sourceDataPath=../data + local topLevel=../.. - if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then - trace "Updating SatoshiPortal repos" - git submodule update --recursive --remote - trace "Creating SatoshiPortal images" + if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then + trace "Updating SatoshiPortal repos" + git submodule update --recursive --remote + trace "Creating SatoshiPortal images" - fi + fi local arch=$(uname -m) # TODO: is this correct for every host if [[ $BITCOIN_INTERNAL == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin - if [ ! -d $BITCOIN_DATAPATH ]; then - trace "Creating $BITCOIN_DATAPATH" - mkdir -p $BITCOIN_DATAPATH - fi - - if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then - trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" - cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") - fi - - trace "Copying bitcoin core node config" - cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH + build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin + if [ ! -d $BITCOIN_DATAPATH ]; then + trace "Creating $BITCOIN_DATAPATH" + mkdir -p $BITCOIN_DATAPATH + fi + + if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then + trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" + cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") + fi + + trace "Copying bitcoin core node config" + cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH fi if [[ $FEATURE_LIGHTNING == true ]]; then - if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning - if [ ! -d $LIGHTNING_DATAPATH ]; then - trace "Creating $LIGHTNING_DATAPATH" - mkdir -p $LIGHTNING_DATAPATH - fi - - if [[ -f $LIGHTNING_DATAPATH/config ]]; then - trace "Creating backup of $LIGHTNING_DATAPATH/config" - cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") - fi - - trace "Copying c-lightning config" - cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH - fi + if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning + if [ ! -d $LIGHTNING_DATAPATH ]; then + trace "Creating $LIGHTNING_DATAPATH" + mkdir -p $LIGHTNING_DATAPATH + fi + + if [[ -f $LIGHTNING_DATAPATH/config ]]; then + trace "Creating backup of $LIGHTNING_DATAPATH/config" + cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") + fi + + trace "Copying c-lightning config" + cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH + fi fi - if [[ $FEATURE_OTSCLIENT == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/ots/otsclient cyphernode/otsclient + if [[ $FEATURE_OTSCLIENT == true ]]; then + build_docker_image ../SatoshiPortal/dockers/$arch/ots/otsclient cyphernode/otsclient fi @@ -57,9 +57,9 @@ install_docker() { trace "Creating cyphernode images" build_docker_image ../../proxy_docker/ cyphernode/proxy if [ ! -d $PROXY_DATAPATH ]; then - trace "Creating $PROXY_DATAPATH" - mkdir -p $PROXY_DATAPATH - fi + trace "Creating $PROXY_DATAPATH" + mkdir -p $PROXY_DATAPATH + fi build_docker_image ../../cron_docker/ cyphernode/proxycron build_docker_image ../../pycoin_docker/ cyphernode/pycoin @@ -67,16 +67,16 @@ install_docker() { docker network create cyphernodenet > /dev/null 2>&1 if [[ -f $topLevel/docker-compose.yaml ]]; then - trace "Creating backup of docker-compose.yaml" - cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") - fi + trace "Creating backup of docker-compose.yaml" + cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") + fi - trace "Copying docker-compose.yaml to top level" - cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + trace "Copying docker-compose.yaml to top level" + cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - echo "+------------------------------------------+" - echo "| to start cyphernode run: |" - echo "| docker-compose -f docker-compose.yaml up |" - echo "+------------------------------------------+" + echo "+------------------------------------------+" + echo "| to start cyphernode run: |" + echo "| docker-compose -f docker-compose.yaml up |" + echo "+------------------------------------------+" } \ No newline at end of file diff --git a/install/script/install_lunanode.sh b/install/script/install_lunanode.sh index b2d708eef..09dfec598 100644 --- a/install/script/install_lunanode.sh +++ b/install/script/install_lunanode.sh @@ -1,3 +1,3 @@ install_lunanode() { - trace "Lunanode installation not implemented" + trace "Lunanode installation not implemented" } \ No newline at end of file diff --git a/install/script/setup.sh b/install/script/setup.sh index 4082994a8..283df165d 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -10,14 +10,14 @@ RECREATE=0 while getopts ":cir" opt; do case $opt in - r) - RECREATE=1 - ;; + r) + RECREATE=1 + ;; c) - CONFIGURE=1 + CONFIGURE=1 ;; i) - INSTALL=1 + INSTALL=1 ;; \?) echo "Invalid option: -$OPTARG. Use -c to configure and -i to install" >&2 @@ -26,15 +26,15 @@ while getopts ":cir" opt; do done if [[ $CONFIGURE == 0 && $INSTALL == 0 && RECREATE == 0 ]]; then - echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." + echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." else - if [[ $CONFIGURE == 1 ]]; then - trace "Starting configuration phase" - configure $RECREATE - fi + if [[ $CONFIGURE == 1 ]]; then + trace "Starting configuration phase" + configure $RECREATE + fi - if [[ $INSTALL == 1 ]]; then - trace "Starting installation phase" - install - fi + if [[ $INSTALL == 1 ]]; then + trace "Starting installation phase" + install + fi fi From 5877f13ddd6381e65f412b82711408f13571cd94 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 22:31:03 +0200 Subject: [PATCH 074/268] added compat mapping for rpi --- install/script/install_docker.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index 24c01365d..d613669ad 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -12,10 +12,16 @@ install_docker() { fi - local arch=$(uname -m) # TODO: is this correct for every host + local archpath=$(uname -m) + + # compat mode for SatoshiPortal repo + # TODO: add more mappings? + if [[ $archpath == 'armv7l' ]]; then + archpath="rpi" + fi if [[ $BITCOIN_INTERNAL == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/bitcoin-core cyphernode/bitcoin + build_docker_image ../SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin if [ ! -d $BITCOIN_DATAPATH ]; then trace "Creating $BITCOIN_DATAPATH" mkdir -p $BITCOIN_DATAPATH @@ -32,7 +38,7 @@ install_docker() { if [[ $FEATURE_LIGHTNING == true ]]; then if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/LN/c-lightning cyphernode/clightning + build_docker_image ../SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning if [ ! -d $LIGHTNING_DATAPATH ]; then trace "Creating $LIGHTNING_DATAPATH" mkdir -p $LIGHTNING_DATAPATH @@ -49,7 +55,7 @@ install_docker() { fi if [[ $FEATURE_OTSCLIENT == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$arch/ots/otsclient cyphernode/otsclient + build_docker_image ../SatoshiPortal/dockers/$archpath/ots/otsclient cyphernode/otsclient fi From 54ba926e60b6335e6808d97b0b188927028ec444 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 22:49:51 +0200 Subject: [PATCH 075/268] making sure devmode exists --- install/generator-cyphernode/generators/app/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 043201f37..f52b5802e 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -28,10 +28,13 @@ module.exports = class extends Generator { } else { this.props = { 'derivation_path': '0/n', - 'installer': 'docker' + 'installer': 'docker', + 'devmode': false }; } + this.props.devmode = this.props.devmode || false; + this.featureChoices = featureChoices; for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); From 74bf407ad9a37e493dc5e9539c0df144ccab8ef8 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 23:13:46 +0200 Subject: [PATCH 076/268] fixed typo --- install/script/install_docker.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index d613669ad..3f3e6ec5b 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -5,14 +5,14 @@ install_docker() { local sourceDataPath=../data local topLevel=../.. - if [[ $BITCOIN_INTERAL == true || $FEATURE_LIGHTNING == true ]]; then + if [[ $BITCOIN_INTERNAL == true || $FEATURE_LIGHTNING == true ]]; then trace "Updating SatoshiPortal repos" git submodule update --recursive --remote trace "Creating SatoshiPortal images" fi - - local archpath=$(uname -m) + + local archpath=$(uname -m) # compat mode for SatoshiPortal repo # TODO: add more mappings? From b285c2ded6a58c8db4d57a84e7360c986775d2ec Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 23:16:06 +0200 Subject: [PATCH 077/268] using https for submodule --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 6ce87e64a..4bc31477d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "install/SatoshiPortal/dockers"] path = install/SatoshiPortal/dockers - url = git@github.com:SatoshiPortal/dockers.git + url = https://github.com/SatoshiPortal/dockers.git From b67db12ef05064729e5f2af29c2b8205397ebb9c Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 23:47:13 +0200 Subject: [PATCH 078/268] docker build command can now take Dockerfile as argument --- install/script/docker.sh | 8 +++++++- install/script/install_docker.sh | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/install/script/docker.sh b/install/script/docker.sh index 70ff26472..51f85cf5b 100644 --- a/install/script/docker.sh +++ b/install/script/docker.sh @@ -1,6 +1,12 @@ build_docker_image() { + local dockerfile="Dockerfile" + + if [[ ""$3 != "" ]]; then + dockerfile=$3 + fi + trace "building docker image: $2:latest" - docker build -q $1 -t $2:latest > /dev/null + docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null } diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index 3f3e6ec5b..d420519b2 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -38,7 +38,12 @@ install_docker() { if [[ $FEATURE_LIGHTNING == true ]]; then if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then - build_docker_image ../SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning + local dockerfile="Dockerfile" + if [[ $archpath == "rpi" ]]; then + dockerfile="Dockerfile-alpine" + fi + + build_docker_image ../SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile if [ ! -d $LIGHTNING_DATAPATH ]; then trace "Creating $LIGHTNING_DATAPATH" mkdir -p $LIGHTNING_DATAPATH From 265ed2732fb138d0bfab6777bdf2e82c32bf6d29 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 8 Oct 2018 23:48:37 +0200 Subject: [PATCH 079/268] we need bash --- install/script/setup.sh | 2 +- setup.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/script/setup.sh b/install/script/setup.sh index 283df165d..38b5f6280 100755 --- a/install/script/setup.sh +++ b/install/script/setup.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash . ./trace.sh . ./configure.sh diff --git a/setup.sh b/setup.sh index 3d88e5519..1018ba43d 100755 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,3 @@ -#!/bin/sh +#!/bin/bash (cd install/script && TRACING=1 ./setup.sh $@) \ No newline at end of file From 360a65d9da3a2563adc6488198f6df489a92bd16 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 9 Oct 2018 09:01:07 +0200 Subject: [PATCH 080/268] docker containers restart on crash and reboot now --- .../app/templates/installer/docker/docker-compose.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 28b9f1cf2..770030212 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 @@ -32,7 +32,7 @@ services: # constraints: [node.hostname==dev] networks: - cyphernodenet - + restart: always proxycron: environment: - "PROXY_URL=proxy:8888/executecallbacks" @@ -42,7 +42,7 @@ services: # constraints: [node.hostname==dev] networks: - cyphernodenet - + restart: always pycoin: # Pycoin image: cyphernode/pycoin @@ -58,6 +58,7 @@ services: # constraints: [node.hostname==dev] networks: - cyphernodenet + restart: always <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: image: cyphernode/clightning @@ -70,6 +71,7 @@ services: # constraints: [node.hostname==dev] networks: - cyphernodenet + restart: always <% } %> <% if( bitcoin_mode === 'internal' ) { %> bitcoin: @@ -82,6 +84,7 @@ services: - "<%= bitcoin_datapath%>:/bitcoinuser/.bitcoin" networks: - cyphernodenet + restart: always <% } %> networks: cyphernodenet: From 2108930fb985940805f0c57efd1ab1dc254bacd2 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 9 Oct 2018 21:42:08 +0200 Subject: [PATCH 081/268] config tool now runs with user permissions so we don't run into trouble on linux --- install/Dockerfile | 39 ++++++++++++++++++++++--------------- install/script/configure.sh | 3 ++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/install/Dockerfile b/install/Dockerfile index 0a7691e0d..73dc362a3 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,25 +1,32 @@ +FROM alpine as builder + +RUN set -x\ + && apk add --no-cache\ + gcc\ + make\ + git\ + musl-dev + +RUN git clone https://github.com/ncopa/su-exec.git /su-exec + +WORKDIR /su-exec +RUN make +RUN strip su-exec + FROM node:alpine +COPY --from=builder /su-exec/su-exec /sbin/ + RUN apk add --update bash && rm -rf /var/cache/apk/* -RUN adduser -D -h /yo yo yo -RUN chown -R yo:yo /usr/local/lib/node_modules /usr/local/bin +RUN mkdir -p /app +RUN mkdir /.config +RUN chmod a+rwx /.config -USER yo RUN npm install -g yo -COPY generator-cyphernode /yo -WORKDIR /yo/generator-cyphernode +COPY generator-cyphernode /app +WORKDIR /app/generator-cyphernode RUN npm link -USER root -RUN mkdir -p /yo/.config/insight-nodejs - -# prevent "do you want to send stats"-questions for temporary yo installation -COPY insight-yo.json /yo/.config/insight-nodejs/insight-yo.json -RUN chown -R yo:yo /yo/.config - -# run in user space -USER yo WORKDIR /data -ENTRYPOINT ["yo","cyphernode"] - +ENTRYPOINT ["/sbin/su-exec"] diff --git a/install/script/configure.sh b/install/script/configure.sh index c290f95ce..d490efb7e 100644 --- a/install/script/configure.sh +++ b/install/script/configure.sh @@ -13,5 +13,6 @@ configure() { # configure features of cyphernode docker run -v $current_path/../data:/data \ --log-driver=none\ - --rm -it cyphernodeconf:latest $recreate + --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate } + From 78beb9d5cc873a1f4cfbb8a08c25fea7349dc802 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 10 Oct 2018 00:50:55 +0200 Subject: [PATCH 082/268] cyphernode proxy is now able to write files to user owned directories using su-exec --- .../installer/docker/docker-compose.yaml | 13 +++++++------ proxy_docker/Dockerfile | 15 +++++++++++++++ proxy_docker/app/script/startproxy.sh | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) 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 770030212..e02b50de3 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,26 +7,27 @@ services: - "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_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - - "WATCHER_BTC_NODE_RPC_CFG=/proxyuser/watcher_btcnode_curlcfg.properties" + - "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_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - - "SPENDER_BTC_NODE_RPC_CFG=/proxyuser/sender_btcnode_curlcfg.properties" + - "SPENDER_BTC_NODE_RPC_CFG=/tmp/sender_btcnode_curlcfg.properties" - "PROXY_LISTENING_PORT=8888" - - "DB_PATH=/proxyuser/db" - - "DB_FILE=/proxyuser/db/proxydb" + - "DB_PATH=/app/db" + - "DB_FILE=/app/db/proxydb" - "PYCOIN_CONTAINER=pycoin:7777" - "OTS_CONTAINER=otsclient:6666" - "DERIVATION_PUB32=<%= xpub %>" - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" image: cyphernode/proxy + command: "$USER /app/startproxy.sh" <% if ( devmode ) { %> ports: - "8888:8888" <% } %> volumes: - - "<%= proxy_datapath %>:/proxyuser/db" - - "<%= lightning_datapath %>:/proxyuser/.lightning" + - "<%= proxy_datapath %>:/app/db" + - "<%= lightning_datapath %>:/app/.lightning" # deploy: # placement: # constraints: [node.hostname==dev] diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index 54580ce73..82ed519b2 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -1,3 +1,18 @@ +FROM alpine as builder + +RUN set -x\ + && apk add --no-cache\ + gcc\ + make\ + git\ + musl-dev + +RUN git clone https://github.com/ncopa/su-exec.git /su-exec + +WORKDIR /su-exec +RUN make +RUN strip su-exec + FROM alpine ENV HOME /proxy diff --git a/proxy_docker/app/script/startproxy.sh b/proxy_docker/app/script/startproxy.sh index b6e65d044..123796966 100644 --- a/proxy_docker/app/script/startproxy.sh +++ b/proxy_docker/app/script/startproxy.sh @@ -31,7 +31,7 @@ createCurlConfig() { } if [ ! -e ${DB_FILE} ]; then - echo "DB not found, creating..." 1>&2 + echo "DB not found, creating..." cat watching.sql | sqlite3 $DB_FILE fi From 0af6c5fd1ffc5560bf206f65091aad794b1dfe0e Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 10 Oct 2018 21:36:01 +0200 Subject: [PATCH 083/268] reverted som changes in the proxy. --- .../installer/docker/docker-compose.yaml | 1 - proxy_docker/Dockerfile | 16 +--------------- 2 files changed, 1 insertion(+), 16 deletions(-) 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 e02b50de3..7a17c365d 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 @@ -20,7 +20,6 @@ services: - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" image: cyphernode/proxy - command: "$USER /app/startproxy.sh" <% if ( devmode ) { %> ports: - "8888:8888" diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index 82ed519b2..9714944ec 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -1,18 +1,3 @@ -FROM alpine as builder - -RUN set -x\ - && apk add --no-cache\ - gcc\ - make\ - git\ - musl-dev - -RUN git clone https://github.com/ncopa/su-exec.git /su-exec - -WORKDIR /su-exec -RUN make -RUN strip su-exec - FROM alpine ENV HOME /proxy @@ -56,3 +41,4 @@ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ VOLUME ["${HOME}/db", "${HOME}/.lightning"] ENTRYPOINT ["su-exec"] + From 5f6855010336d131dd3aa13316af171f50333397 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 10 Oct 2018 21:36:32 +0200 Subject: [PATCH 084/268] using su-exec from apk --- install/Dockerfile | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/install/Dockerfile b/install/Dockerfile index 73dc362a3..98f944f86 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,23 +1,6 @@ -FROM alpine as builder - -RUN set -x\ - && apk add --no-cache\ - gcc\ - make\ - git\ - musl-dev - -RUN git clone https://github.com/ncopa/su-exec.git /su-exec - -WORKDIR /su-exec -RUN make -RUN strip su-exec - FROM node:alpine -COPY --from=builder /su-exec/su-exec /sbin/ - -RUN apk add --update bash && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec && rm -rf /var/cache/apk/* RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config @@ -30,3 +13,4 @@ RUN npm link WORKDIR /data ENTRYPOINT ["/sbin/su-exec"] +RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true From 3c48feca860c6be9a1da1167db22590840e449b3 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 10 Oct 2018 21:36:53 +0200 Subject: [PATCH 085/268] unused --- install/insight-yo.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 install/insight-yo.json diff --git a/install/insight-yo.json b/install/insight-yo.json deleted file mode 100644 index 63a715322..000000000 --- a/install/insight-yo.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "optOut": true -} \ No newline at end of file From 47638b5e724f2981f1d976b869a586d96531615e Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 11 Oct 2018 20:51:21 +0200 Subject: [PATCH 086/268] just some docs --- setup.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 1018ba43d..0494da286 100755 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,15 @@ #!/bin/bash -(cd install/script && TRACING=1 ./setup.sh $@) \ No newline at end of file +(cd install/script && TRACING=1 ./setup.sh $@) + +# Execute this on a freshly install ubuntu luna node +# curl -fsSL get.docker.com -o get-docker.sh +# sh get-docker.sh +# sudo usermod -aG docker $USER +# logout and relogin +# git clone --branch features/install --recursive https://github.com/schulterklopfer/cyphernode.git +# sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +# sudo chmod +x /usr/local/bin/docker-compose +# cd cyphermode +# ./setup.sh -ci +# docker-compose -f docker-compose.yaml up [-d] \ No newline at end of file From 06204873b5948494dedc3d681955d49280dc0968 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 11 Oct 2018 21:00:55 +0200 Subject: [PATCH 087/268] meh -.- --- setup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 0494da286..a0964df3a 100755 --- a/setup.sh +++ b/setup.sh @@ -2,14 +2,14 @@ (cd install/script && TRACING=1 ./setup.sh $@) -# Execute this on a freshly install ubuntu luna node +### Execute this on a freshly install ubuntu luna node # curl -fsSL get.docker.com -o get-docker.sh # sh get-docker.sh # sudo usermod -aG docker $USER -# logout and relogin +## >>logout and relogin<< # git clone --branch features/install --recursive https://github.com/schulterklopfer/cyphernode.git # sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # sudo chmod +x /usr/local/bin/docker-compose -# cd cyphermode +# cd cyphernode # ./setup.sh -ci # docker-compose -f docker-compose.yaml up [-d] \ No newline at end of file From 3d6b0eb1fe2c9d1dcfafc9501930cd212ebd0970 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 19:46:10 +0200 Subject: [PATCH 088/268] added ua comment validator --- install/generator-cyphernode/generators/app/index.js | 9 +++++++++ .../generators/app/prompters/100_bitcoin.js | 1 + 2 files changed, 10 insertions(+) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index f52b5802e..300dd9461 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -8,6 +8,8 @@ const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); +const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\?@]+$/ + let prompters = []; const normalizedPath = path.join(__dirname, "prompters"); fs.readdirSync(normalizedPath).forEach(function(file) { @@ -128,6 +130,13 @@ module.exports = class extends Generator { return true; } + _UACommentValidator( comment ) { + if( !uaCommentRegexp.test( comment ) ) { + throw new Error('Unsafe characters in UA comment. Please use only a-z, A-Z, 0-9, SPACE and .,:_?@'); + } + return true; + } + _trimFilter( input ) { return (input+"").trim(); } diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index 97fe60644..bdbe44ec1 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -73,6 +73,7 @@ module.exports = { name: 'bitcoin_uacomment', default: utils._getDefault( 'bitcoin_uacomment' ), message: prefix()+'Any UA comment?'+'\n', + validate: utils._UACommentValidator }]; }, env: function( props ) { From 4193bd59844e8988e13e615d092774197e05c5f6 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 19:46:39 +0200 Subject: [PATCH 089/268] added trim filters to all inputs to avoid leading and trailing spaces --- .../generators/app/prompters/000_proxy.js | 2 ++ .../generators/app/prompters/100_bitcoin.js | 4 ++++ .../generators/app/prompters/200_lightning.js | 3 +++ .../generators/app/prompters/999_installer.js | 3 +++ 4 files changed, 12 insertions(+) diff --git a/install/generator-cyphernode/generators/app/prompters/000_proxy.js b/install/generator-cyphernode/generators/app/prompters/000_proxy.js index 380445804..6563e5849 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_proxy.js +++ b/install/generator-cyphernode/generators/app/prompters/000_proxy.js @@ -41,6 +41,7 @@ module.exports = { name: 'xpub', default: utils._getDefault( 'xpub' ), message: prefix()+'What is your xpub to watch?'+'\n', + filter: utils._trimFilter, validate: utils._xkeyValidator }, { @@ -48,6 +49,7 @@ module.exports = { name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), message: prefix()+'What is your address derivation path?'+'\n', + filter: utils._trimFilter, validate: utils._derivationPathValidator }]; }, diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index bdbe44ec1..4ed34126f 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -45,6 +45,7 @@ module.exports = { type: 'input', name: 'bitcoin_node_ip', default: utils._getDefault( 'bitcoin_node_ip' ), + filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, message: prefix()+'What is your full node ip address?'+'\n', }, @@ -53,12 +54,14 @@ module.exports = { name: 'bitcoin_rpcuser', default: utils._getDefault( 'bitcoin_rpcuser' ), message: prefix()+'Name of bitcoin rpc user?'+'\n', + filter: utils._trimFilter, }, { type: 'password', name: 'bitcoin_rpcpassword', default: utils._getDefault( 'bitcoin_rpcpassword' ), message: prefix()+'Password of bitcoin rpc user?'+'\n', + filter: utils._trimFilter, }, { when: bitcoinInternal, @@ -73,6 +76,7 @@ module.exports = { name: 'bitcoin_uacomment', default: utils._getDefault( 'bitcoin_uacomment' ), message: prefix()+'Any UA comment?'+'\n', + filter: utils._trimFilter, validate: utils._UACommentValidator }]; }, diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 948b394ec..f4de49e77 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -49,6 +49,7 @@ module.exports = { type: 'input', name: 'lightning_external_ip', default: utils._getDefault( 'lightning_external_ip' ), + filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, message: prefix()+'What external ip does your lightning node have?'+'\n', }, @@ -57,6 +58,7 @@ module.exports = { type: 'input', name: 'lightning_nodename', default: utils._getDefault( 'lightning_nodename' ), + filter: utils._trimFilter, validate: utils._notEmptyValidator, message: prefix()+'What name has your lightning node?'+'\n', }, @@ -65,6 +67,7 @@ module.exports = { type: 'input', name: 'lightning_nodecolor', default: utils._getDefault( 'lightning_nodecolor' ), + filter: utils._trimFilter, validate: utils._colorValidator, message: prefix()+'What color has your lightning node?'+'\n', }]; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 68ab366b9..2325139f3 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -43,6 +43,7 @@ module.exports = { type: 'input', name: 'proxy_datapath', default: utils._getDefault( 'proxy_datapath' ), + filter: utils._trimFilter, validate: utils._pathValidator, message: prefix()+'Where to store your proxy db?'+'\n', }, @@ -51,6 +52,7 @@ module.exports = { type: 'input', name: 'bitcoin_datapath', default: utils._getDefault( 'bitcoin_datapath' ), + filter: utils._trimFilter, validate: utils._pathValidator, message: prefix()+'Where is your blockchain data?'+'\n', }, @@ -59,6 +61,7 @@ module.exports = { type: 'input', name: 'lightning_datapath', default: utils._getDefault( 'lightning_datapath' ), + filter: utils._trimFilter, validate: utils._pathValidator, message: prefix()+'Where is your lightning node data?'+'\n', }, From ca73143b0f3432356ed89187a0be46c98b73a42d Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 22:21:46 +0200 Subject: [PATCH 090/268] more chars for ua comment --- 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 300dd9461..1a92700b3 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -8,7 +8,7 @@ const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); -const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\?@]+$/ +const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars let prompters = []; const normalizedPath = path.join(__dirname, "prompters"); From eeb96fa7ccb3a01bb02657d84a2f5ea9c181ecc3 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 22:22:33 +0200 Subject: [PATCH 091/268] support for su-exec containers --- .../installer/docker/docker-compose.yaml | 18 +++++++++++------- install/script/install_docker.sh | 8 ++++---- 2 files changed, 15 insertions(+), 11 deletions(-) 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 7a17c365d..3cccc573d 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 @@ -2,6 +2,7 @@ version: "3" services: proxy: + command: $USER ./startproxy.sh # Bitcoin Mini Proxy environment: - "TRACING=1" @@ -10,10 +11,10 @@ services: - "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_USER=<%= bitcoin_rpcuser %>:<%= bitcoin_rpcpassword %>" - - "SPENDER_BTC_NODE_RPC_CFG=/tmp/sender_btcnode_curlcfg.properties" + - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" - "PROXY_LISTENING_PORT=8888" - - "DB_PATH=/app/db" - - "DB_FILE=/app/db/proxydb" + - "DB_PATH=/proxy/db" + - "DB_FILE=/proxy/db/proxydb" - "PYCOIN_CONTAINER=pycoin:7777" - "OTS_CONTAINER=otsclient:6666" - "DERIVATION_PUB32=<%= xpub %>" @@ -25,8 +26,8 @@ services: - "8888:8888" <% } %> volumes: - - "<%= proxy_datapath %>:/app/db" - - "<%= lightning_datapath %>:/app/.lightning" + - "<%= proxy_datapath %>:/proxy/db" + - "<%= lightning_datapath %>:/proxy/.lightning" # deploy: # placement: # constraints: [node.hostname==dev] @@ -45,6 +46,7 @@ services: restart: always pycoin: # Pycoin + command: $USER ./startpycoin.sh image: cyphernode/pycoin environment: - "TRACING=1" @@ -61,11 +63,12 @@ services: restart: always <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: + command: $USER lightningd image: cyphernode/clightning ports: - "9735:9735" volumes: - - "<%= lightning_datapath%>:/lnuser/.lightning" + - "<%= lightning_datapath%>:/.lightning" # deploy: # placement: # constraints: [node.hostname==dev] @@ -75,13 +78,14 @@ services: <% } %> <% if( bitcoin_mode === 'internal' ) { %> bitcoin: + command: $USER bitcoind image: cyphernode/bitcoin <% if( bitcoin_expose ) { %> ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" <% } %> volumes: - - "<%= bitcoin_datapath%>:/bitcoinuser/.bitcoin" + - "<%= bitcoin_datapath%>:/.bitcoin" networks: - cyphernodenet restart: always diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh index d420519b2..cb5dc8b7b 100644 --- a/install/script/install_docker.sh +++ b/install/script/install_docker.sh @@ -85,9 +85,9 @@ install_docker() { trace "Copying docker-compose.yaml to top level" cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - echo "+------------------------------------------+" - echo "| to start cyphernode run: |" - echo "| docker-compose -f docker-compose.yaml up |" - echo "+------------------------------------------+" + echo "+---------------------------------------------------------------+" + echo "| to start cyphernode run: |" + echo '| USER=`id -u`:`id -g` docker-compose -f docker-compose.yaml up |' + echo "+---------------------------------------------------------------+" } \ No newline at end of file From ac8f5f3b65d440611a3d20a39f442da685723f15 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 22:22:52 +0200 Subject: [PATCH 092/268] latest version of satoshiportal dockers --- install/SatoshiPortal/dockers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/SatoshiPortal/dockers b/install/SatoshiPortal/dockers index 094a4ef34..93ed4a3dc 160000 --- a/install/SatoshiPortal/dockers +++ b/install/SatoshiPortal/dockers @@ -1 +1 @@ -Subproject commit 094a4ef34f66a106c2bca0e802dd105aa27eb71a +Subproject commit 93ed4a3dc3b24d7fef32b60ea1a820fc82e1580b From 1f20036790761956697bbe05b634cac4b398c8c3 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 23:36:31 +0200 Subject: [PATCH 093/268] refactored setup.sh and split it into build.sh and setup.sh --- build.sh | 58 +++++++++++ dist/config.json.sample | 27 +++++ dist/setup.sh | 160 +++++++++++++++++++++++++++++ install/script/configure.sh | 18 ---- install/script/docker.sh | 12 --- install/script/install.sh | 13 --- install/script/install_docker.sh | 93 ----------------- install/script/install_lunanode.sh | 3 - install/script/setup.sh | 40 -------- install/script/trace.sh | 13 --- setup.sh | 15 --- 11 files changed, 245 insertions(+), 207 deletions(-) create mode 100755 build.sh create mode 100644 dist/config.json.sample create mode 100755 dist/setup.sh delete mode 100644 install/script/configure.sh delete mode 100644 install/script/docker.sh delete mode 100644 install/script/install.sh delete mode 100644 install/script/install_docker.sh delete mode 100644 install/script/install_lunanode.sh delete mode 100755 install/script/setup.sh delete mode 100644 install/script/trace.sh delete mode 100755 setup.sh diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..c441e2a81 --- /dev/null +++ b/build.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +TRACING=1 + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi +} + + +build_docker_image() { + + local dockerfile="Dockerfile" + + if [[ ""$3 != "" ]]; then + dockerfile=$3 + fi + + trace "building docker image: $2:latest" + docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null + +} + +build_docker_images() { + trace "Updating SatoshiPortal repos" + git submodule update --recursive --remote + + local archpath=$(uname -m) + + # compat mode for SatoshiPortal repo + # TODO: add more mappings? + if [[ $archpath == 'armv7l' ]]; then + archpath="rpi" + fi + + trace "Creating SatoshiPortal images" + build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin + build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile + build_docker_image install/SatoshiPortal/dockers/$archpath/ots/otsclient cyphernode/otsclient + + trace "Creating cyphernode images" + build_docker_image proxy_docker/ cyphernode/proxy + build_docker_image cron_docker/ cyphernode/proxycron + build_docker_image pycoin_docker/ cyphernode/pycoin + +} + +build_docker_images + diff --git a/dist/config.json.sample b/dist/config.json.sample new file mode 100644 index 000000000..28446e69c --- /dev/null +++ b/dist/config.json.sample @@ -0,0 +1,27 @@ +{ + "derivation_path": "0/n", + "installer": "docker", + "features": [ + "lightning", + "otsclient", + "electrum" + ], + "net": "testnet", + "xpub": "upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb", + "bitcoin_mode": "internal", + "bitcoin_rpcuser": "user", + "bitcoin_rpcpassword": "password", + "bitcoin_prune": false, + "bitcoin_uacomment": "", + "lightning_implementation": "c-lightning", + "lightning_external_ip": "clightning.nodes.com", + "lightning_nodename": "SatoshiPortal", + "lightning_nodecolor": "ff00ff", + "electrum_implementation": "eps", + "installer_mode": "docker", + "proxy_datapath": "/tmp/p", + "bitcoin_datapath": "/tmp/b", + "lightning_datapath": "/tmp/l", + "bitcoin_expose": false, + "devmode": true +} \ No newline at end of file diff --git a/dist/setup.sh b/dist/setup.sh new file mode 100755 index 000000000..3278c783d --- /dev/null +++ b/dist/setup.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +### Execute this on a freshly install ubuntu luna node +# curl -fsSL get.docker.com -o get-docker.sh +# sh get-docker.sh +# sudo usermod -aG docker $USER +## >>logout and relogin<< +# git clone --branch features/install --recursive https://github.com/schulterklopfer/cyphernode.git +# sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +# sudo chmod +x /usr/local/bin/docker-compose +# cd cyphernode +# ./setup.sh -ci +# docker-compose -f docker-compose.yaml up [-d] + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi +} + +configure() { + local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + ## build setup docker image + local recreate="" + + if [[ $1 == 1 ]]; then + recreate="recreate" + fi + + clear && echo "Thinking..." + + # configure features of cyphernode + docker run -v $current_path:/data \ + --log-driver=none\ + --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate + +} + +install_docker() { + + local sourceDataPath=./ + local topLevel=./ + + if [[ $BITCOIN_INTERNAL == true ]]; then + if [ ! -d $BITCOIN_DATAPATH ]; then + trace "Creating $BITCOIN_DATAPATH" + mkdir -p $BITCOIN_DATAPATH + fi + + if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then + trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" + cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") + fi + + trace "Copying bitcoin core node config" + cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH + fi + + if [[ $FEATURE_LIGHTNING == true ]]; then + if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then + local dockerfile="Dockerfile" + if [[ $archpath == "rpi" ]]; then + dockerfile="Dockerfile-alpine" + fi + if [ ! -d $LIGHTNING_DATAPATH ]; then + trace "Creating $LIGHTNING_DATAPATH" + mkdir -p $LIGHTNING_DATAPATH + fi + + if [[ -f $LIGHTNING_DATAPATH/config ]]; then + trace "Creating backup of $LIGHTNING_DATAPATH/config" + cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") + fi + + trace "Copying c-lightning config" + cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH + fi + fi + + if [[ $FEATURE_OTSCLIENT == true ]]; then + trace "opentimestamps not supported yet." + fi + + # build cyphernode images + if [ ! -d $PROXY_DATAPATH ]; then + trace "Creating $PROXY_DATAPATH" + mkdir -p $PROXY_DATAPATH + fi + trace "Creating cyphernode network" + docker network create cyphernodenet > /dev/null 2>&1 + + if [[ -f $topLevel/docker-compose.yaml ]]; then + trace "Creating backup of docker-compose.yaml" + cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") + fi + + trace "Copying docker-compose.yaml to top level" + cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + + echo "+---------------------------------------------------------------+" + echo "| to start cyphernode run: |" + echo '| USER=`id -u`:`id -g` docker-compose -f docker-compose.yaml up |' + echo "+---------------------------------------------------------------+" + +} + +install() { + . installer/config.sh + if [[ ''$INSTALLER_MODE == 'none' ]]; then + echo "Skipping installation phase" + elif [[ ''$INSTALLER_MODE == 'docker' ]]; then + install_docker + fi +} + + +CONFIGURE=0 +INSTALL=0 +RECREATE=0 +TRACING=1 + +while getopts ":cir" opt; do + case $opt in + r) + RECREATE=1 + ;; + c) + CONFIGURE=1 + ;; + i) + INSTALL=1 + ;; + \?) + echo "Invalid option: -$OPTARG. Use -c to configure and -i to install" >&2 + ;; + esac +done + +if [[ $CONFIGURE == 0 && $INSTALL == 0 && RECREATE == 0 ]]; then + echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." +else + if [[ $CONFIGURE == 1 ]]; then + trace "Starting configuration phase" + configure $RECREATE + fi + + if [[ $INSTALL == 1 ]]; then + trace "Starting installation phase" + install + fi +fi + diff --git a/install/script/configure.sh b/install/script/configure.sh deleted file mode 100644 index d490efb7e..000000000 --- a/install/script/configure.sh +++ /dev/null @@ -1,18 +0,0 @@ - -configure() { - local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - ## build setup docker image - local recreate="" - - if [[ $1 == 1 ]]; then - recreate="recreate" - fi - - build_docker_image ../ cyphernodeconf && clear && echo "Thinking..." - - # configure features of cyphernode - docker run -v $current_path/../data:/data \ - --log-driver=none\ - --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate -} - diff --git a/install/script/docker.sh b/install/script/docker.sh deleted file mode 100644 index 51f85cf5b..000000000 --- a/install/script/docker.sh +++ /dev/null @@ -1,12 +0,0 @@ -build_docker_image() { - - local dockerfile="Dockerfile" - - if [[ ""$3 != "" ]]; then - dockerfile=$3 - fi - - trace "building docker image: $2:latest" - docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null - -} diff --git a/install/script/install.sh b/install/script/install.sh deleted file mode 100644 index b4a662a9a..000000000 --- a/install/script/install.sh +++ /dev/null @@ -1,13 +0,0 @@ -. ./install_docker.sh -. ./install_lunanode.sh - -install() { - . ../data/installer/config.sh - if [[ ''$INSTALLER_MODE == 'none' ]]; then - echo "Skipping installation phase" - elif [[ ''$INSTALLER_MODE == 'docker' ]]; then - install_docker - elif [[ ''$INSTALLER_MODE == 'lunanode' ]]; then - install_lunanode - fi -} \ No newline at end of file diff --git a/install/script/install_docker.sh b/install/script/install_docker.sh deleted file mode 100644 index cb5dc8b7b..000000000 --- a/install/script/install_docker.sh +++ /dev/null @@ -1,93 +0,0 @@ -. ./docker.sh - -install_docker() { - - local sourceDataPath=../data - local topLevel=../.. - - if [[ $BITCOIN_INTERNAL == true || $FEATURE_LIGHTNING == true ]]; then - trace "Updating SatoshiPortal repos" - git submodule update --recursive --remote - trace "Creating SatoshiPortal images" - - fi - - local archpath=$(uname -m) - - # compat mode for SatoshiPortal repo - # TODO: add more mappings? - if [[ $archpath == 'armv7l' ]]; then - archpath="rpi" - fi - - if [[ $BITCOIN_INTERNAL == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin - if [ ! -d $BITCOIN_DATAPATH ]; then - trace "Creating $BITCOIN_DATAPATH" - mkdir -p $BITCOIN_DATAPATH - fi - - if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then - trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" - cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") - fi - - trace "Copying bitcoin core node config" - cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH - fi - - if [[ $FEATURE_LIGHTNING == true ]]; then - if [[ $LIGHTNING_IMPLEMENTATION == "c-lightning" ]]; then - local dockerfile="Dockerfile" - if [[ $archpath == "rpi" ]]; then - dockerfile="Dockerfile-alpine" - fi - - build_docker_image ../SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile - if [ ! -d $LIGHTNING_DATAPATH ]; then - trace "Creating $LIGHTNING_DATAPATH" - mkdir -p $LIGHTNING_DATAPATH - fi - - if [[ -f $LIGHTNING_DATAPATH/config ]]; then - trace "Creating backup of $LIGHTNING_DATAPATH/config" - cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") - fi - - trace "Copying c-lightning config" - cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH - fi - fi - - if [[ $FEATURE_OTSCLIENT == true ]]; then - build_docker_image ../SatoshiPortal/dockers/$archpath/ots/otsclient cyphernode/otsclient - fi - - - # build cyphernode images - trace "Creating cyphernode images" - build_docker_image ../../proxy_docker/ cyphernode/proxy - if [ ! -d $PROXY_DATAPATH ]; then - trace "Creating $PROXY_DATAPATH" - mkdir -p $PROXY_DATAPATH - fi - build_docker_image ../../cron_docker/ cyphernode/proxycron - build_docker_image ../../pycoin_docker/ cyphernode/pycoin - - trace "Creating cyphernode network" - docker network create cyphernodenet > /dev/null 2>&1 - - if [[ -f $topLevel/docker-compose.yaml ]]; then - trace "Creating backup of docker-compose.yaml" - cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") - fi - - trace "Copying docker-compose.yaml to top level" - cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - - echo "+---------------------------------------------------------------+" - echo "| to start cyphernode run: |" - echo '| USER=`id -u`:`id -g` docker-compose -f docker-compose.yaml up |' - echo "+---------------------------------------------------------------+" - -} \ No newline at end of file diff --git a/install/script/install_lunanode.sh b/install/script/install_lunanode.sh deleted file mode 100644 index 09dfec598..000000000 --- a/install/script/install_lunanode.sh +++ /dev/null @@ -1,3 +0,0 @@ -install_lunanode() { - trace "Lunanode installation not implemented" -} \ No newline at end of file diff --git a/install/script/setup.sh b/install/script/setup.sh deleted file mode 100755 index 38b5f6280..000000000 --- a/install/script/setup.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -. ./trace.sh -. ./configure.sh -. ./install.sh - -CONFIGURE=0 -INSTALL=0 -RECREATE=0 - -while getopts ":cir" opt; do - case $opt in - r) - RECREATE=1 - ;; - c) - CONFIGURE=1 - ;; - i) - INSTALL=1 - ;; - \?) - echo "Invalid option: -$OPTARG. Use -c to configure and -i to install" >&2 - ;; - esac -done - -if [[ $CONFIGURE == 0 && $INSTALL == 0 && RECREATE == 0 ]]; then - echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." -else - if [[ $CONFIGURE == 1 ]]; then - trace "Starting configuration phase" - configure $RECREATE - fi - - if [[ $INSTALL == 1 ]]; then - trace "Starting installation phase" - install - fi -fi diff --git a/install/script/trace.sh b/install/script/trace.sh deleted file mode 100644 index c4a6e81fd..000000000 --- a/install/script/trace.sh +++ /dev/null @@ -1,13 +0,0 @@ -trace() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr - fi -} - -trace_rc() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr - fi -} diff --git a/setup.sh b/setup.sh deleted file mode 100755 index a0964df3a..000000000 --- a/setup.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -(cd install/script && TRACING=1 ./setup.sh $@) - -### Execute this on a freshly install ubuntu luna node -# curl -fsSL get.docker.com -o get-docker.sh -# sh get-docker.sh -# sudo usermod -aG docker $USER -## >>logout and relogin<< -# git clone --branch features/install --recursive https://github.com/schulterklopfer/cyphernode.git -# sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -# sudo chmod +x /usr/local/bin/docker-compose -# cd cyphernode -# ./setup.sh -ci -# docker-compose -f docker-compose.yaml up [-d] \ No newline at end of file From ddc8b0c6a40618679d5264adc2bd1e24f9d4d884 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 23:36:52 +0200 Subject: [PATCH 094/268] renamed props.json to config.json --- install/generator-cyphernode/generators/app/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 1a92700b3..ba91bbd17 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -25,8 +25,8 @@ module.exports = class extends Generator { this.recreate = true; } - if( fs.existsSync(this.destinationPath('props.json')) ) { - this.props = require(this.destinationPath('props.json')); + if( fs.existsSync(this.destinationPath('config.json')) ) { + this.props = require(this.destinationPath('config.json')); } else { this.props = { 'derivation_path': '0/n', @@ -64,7 +64,7 @@ module.exports = class extends Generator { } writing() { - fs.writeFileSync(this.destinationPath('props.json'), JSON.stringify(this.props, null, 2)); + fs.writeFileSync(this.destinationPath('config.json'), JSON.stringify(this.props, null, 2)); for( let m of prompters ) { const name = m.name(); From 360cbafddb622d2cf0dbc26537a6f6773c341bef Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 23:49:12 +0200 Subject: [PATCH 095/268] forgot to build config tool --- build.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.sh b/build.sh index c441e2a81..aac332804 100755 --- a/build.sh +++ b/build.sh @@ -42,6 +42,9 @@ build_docker_images() { archpath="rpi" fi + trace "Creating cyphernodeconf image" + build_docker_image install/ cyphernodeconf + trace "Creating SatoshiPortal images" build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile From 8e603957fc85b4e3733191d80c7e8d5195eba261 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 23:49:51 +0200 Subject: [PATCH 096/268] removed lunanode option --- .../generators/app/prompters/999_installer.js | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 2325139f3..7db8baa10 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -32,11 +32,13 @@ module.exports = { choices: [{ name: "Docker", value: "docker" - }, + } + /*, { name: "Lunanode (not implemented)", value: "lunanode" - }] + }*/ + ] }, { when: installerDocker, @@ -73,11 +75,20 @@ module.exports = { message: prefix()+'Expose bitcoin full node outside of the docker network?'+'\n', }, { - when: installerLunanode, - type: 'confirm', - name: 'installer_confirm_lunanode', - default: utils._getDefault( 'installer_confirm_lunanode' ), - message: prefix()+'Lunanode?! No wayyyy!'+'\n' + when: installerDocker, + type: 'list', + name: 'docker_mode', + default: utils._getDefault( 'docker_mode' ), + message: prefix()+'What docker mode: docker swarm or docker-compose?'+'\n', + choices: [{ + name: "docker swarm", + value: "swarm" + }, + { + name: "docker-compose", + value: "compose" + } + ] }]; }, templates: function( props ) { From 54b2b6972cb452fa6bd08e3de1dfa3e94e2771d6 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 12 Oct 2018 23:50:10 +0200 Subject: [PATCH 097/268] docker_mode is now written to installer config.sh --- .../generators/app/templates/installer/config.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index ff12667c6..a06e79017 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -6,4 +6,5 @@ FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> -PROXY_DATAPATH=<%= proxy_datapath %> \ No newline at end of file +PROXY_DATAPATH=<%= proxy_datapath %> +DOCKER_MODE=<%= docker_mode %> From 338efaa0ad895a7de2e6cf25ea991da5533c7b25 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 12:31:31 +0200 Subject: [PATCH 098/268] added animated splash screen --- .../generators/app/index.js | 44 ++++++++++++++++--- .../generators/app/splash/01.png.txt | 25 +++++++++++ .../generators/app/splash/02.png.txt | 25 +++++++++++ .../generators/app/splash/03.png.txt | 25 +++++++++++ .../generators/app/splash/04.png.txt | 25 +++++++++++ .../generators/app/splash/05.png.txt | 25 +++++++++++ .../generators/app/splash/06.png.txt | 25 +++++++++++ .../generators/app/splash/07.png.txt | 25 +++++++++++ .../generators/app/splash/08.png.txt | 25 +++++++++++ .../generators/app/splash/09.png.txt | 25 +++++++++++ .../generators/app/splash/10.png.txt | 25 +++++++++++ .../generators/app/splash/11.png.txt | 25 +++++++++++ .../generators/app/splash/12.png.txt | 25 +++++++++++ .../generators/app/splash/13.png.txt | 25 +++++++++++ .../generators/app/splash/14.png.txt | 25 +++++++++++ .../generators/app/splash/15.png.txt | 25 +++++++++++ .../generators/app/templates/splash.txt | 27 ------------ 17 files changed, 412 insertions(+), 34 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/splash/01.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/02.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/03.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/04.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/05.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/06.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/07.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/08.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/09.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/10.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/11.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/12.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/13.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/14.png.txt create mode 100644 install/generator-cyphernode/generators/app/splash/15.png.txt delete mode 100644 install/generator-cyphernode/generators/app/templates/splash.txt diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index ba91bbd17..3a3e5d144 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -10,12 +10,43 @@ const coinstring = require('coinstring'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars +const reset = '\u001B[u'; +const clear = '\u001Bc'; + + let prompters = []; -const normalizedPath = path.join(__dirname, "prompters"); -fs.readdirSync(normalizedPath).forEach(function(file) { - prompters.push(require(path.join(normalizedPath,file))); +fs.readdirSync(path.join(__dirname, "prompters")).forEach(function(file) { + prompters.push(require(path.join(__dirname, "prompters",file))); }); +const sleep = function(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const splash = async function() { + let frames = []; + fs.readdirSync(path.join(__dirname,'splash')).forEach(function(file) { + frames.push(fs.readFileSync(path.join(__dirname,'splash',file))); + }); + + process.stdout.write(clear); + + process.stdout.write(reset); + process.stdout.write(frames[0]); + + await sleep(400); + + for( let frame of frames ) { + process.stdout.write(reset); + process.stdout.write(frame.toString()); + await sleep(33); + } + + await sleep(400); + + process.stdout.write('\n'); +} + module.exports = class extends Generator { constructor(args, opts) { @@ -44,16 +75,15 @@ module.exports = class extends Generator { } - prompting() { + async prompting() { if( this.recreate ) { // no prompts return; } - const splash = fs.readFileSync(this.templatePath('splash.txt')); - this.log(splash.toString()); + + await splash(); let prompts = []; - for( let m of prompters ) { prompts = prompts.concat(m.prompts(this)); } diff --git a/install/generator-cyphernode/generators/app/splash/01.png.txt b/install/generator-cyphernode/generators/app/splash/01.png.txt new file mode 100644 index 000000000..74e17cb1e --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/01.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/02.png.txt b/install/generator-cyphernode/generators/app/splash/02.png.txt new file mode 100644 index 000000000..dd52cfdfd --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/02.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/03.png.txt b/install/generator-cyphernode/generators/app/splash/03.png.txt new file mode 100644 index 000000000..02f1d969b --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/03.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/04.png.txt b/install/generator-cyphernode/generators/app/splash/04.png.txt new file mode 100644 index 000000000..33c66f4d5 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/04.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/05.png.txt b/install/generator-cyphernode/generators/app/splash/05.png.txt new file mode 100644 index 000000000..162026e7d --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/05.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/06.png.txt b/install/generator-cyphernode/generators/app/splash/06.png.txt new file mode 100644 index 000000000..dd23868f3 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/06.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/07.png.txt b/install/generator-cyphernode/generators/app/splash/07.png.txt new file mode 100644 index 000000000..93be9b3a4 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/07.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/08.png.txt b/install/generator-cyphernode/generators/app/splash/08.png.txt new file mode 100644 index 000000000..f8990e043 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/08.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/09.png.txt b/install/generator-cyphernode/generators/app/splash/09.png.txt new file mode 100644 index 000000000..e662e1b16 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/09.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/10.png.txt b/install/generator-cyphernode/generators/app/splash/10.png.txt new file mode 100644 index 000000000..a56b5c73e --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/10.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/11.png.txt b/install/generator-cyphernode/generators/app/splash/11.png.txt new file mode 100644 index 000000000..386643d27 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/11.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/12.png.txt b/install/generator-cyphernode/generators/app/splash/12.png.txt new file mode 100644 index 000000000..2d261aba0 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/12.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/13.png.txt b/install/generator-cyphernode/generators/app/splash/13.png.txt new file mode 100644 index 000000000..ae6ffa77a --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/13.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/14.png.txt b/install/generator-cyphernode/generators/app/splash/14.png.txt new file mode 100644 index 000000000..7228c81c5 --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/14.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/splash/15.png.txt b/install/generator-cyphernode/generators/app/splash/15.png.txt new file mode 100644 index 000000000..74e17cb1e --- /dev/null +++ b/install/generator-cyphernode/generators/app/splash/15.png.txt @@ -0,0 +1,25 @@ +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          +                                          diff --git a/install/generator-cyphernode/generators/app/templates/splash.txt b/install/generator-cyphernode/generators/app/templates/splash.txt deleted file mode 100644 index 4250a2ef2..000000000 --- a/install/generator-cyphernode/generators/app/templates/splash.txt +++ /dev/null @@ -1,27 +0,0 @@ -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            -                                            From 568124f80c83ce5c1804bd3c5df97ca6770cca2b Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 12:45:23 +0200 Subject: [PATCH 099/268] we need bash --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index aac332804..89461db5d 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash TRACING=1 From c8a5e04d3a604875a23b22678b7d8b18cd1c3327 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 12:56:03 +0200 Subject: [PATCH 100/268] renamed proxy prompter to cyphernode --- .../app/prompters/{000_proxy.js => 000_cyphernode.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename install/generator-cyphernode/generators/app/prompters/{000_proxy.js => 000_cyphernode.js} (98%) diff --git a/install/generator-cyphernode/generators/app/prompters/000_proxy.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js similarity index 98% rename from install/generator-cyphernode/generators/app/prompters/000_proxy.js rename to install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index 6563e5849..c283348c9 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_proxy.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -1,6 +1,6 @@ const chalk = require('chalk'); -const name = 'proxy'; +const name = 'cyphernode'; const capitalise = function( txt ) { return txt.charAt(0).toUpperCase() + txt.substr(1); From 4b25263cd407e3f0bc6fc582edd3bad5fb78872f Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 13:12:54 +0200 Subject: [PATCH 101/268] default mode is -ci now --- dist/setup.sh | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 3278c783d..cd30f7684 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -127,7 +127,7 @@ INSTALL=0 RECREATE=0 TRACING=1 -while getopts ":cir" opt; do +while getopts ":cirh" opt; do case $opt in r) RECREATE=1 @@ -138,23 +138,28 @@ while getopts ":cir" opt; do i) INSTALL=1 ;; + h) + echo "Use -c to configure and -i to install or -r to recreate from config.json." >&2 + exit + ;; \?) - echo "Invalid option: -$OPTARG. Use -c to configure and -i to install" >&2 + echo "Invalid option: -$OPTARG. Use -c to configure and -i to install or -r to recreate from config.json." >&2 ;; esac done -if [[ $CONFIGURE == 0 && $INSTALL == 0 && RECREATE == 0 ]]; then - echo "Please use -c to configure, -i to install and -ci to do both. Use -r to recreate config files." -else - if [[ $CONFIGURE == 1 ]]; then - trace "Starting configuration phase" - configure $RECREATE - fi +if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then + CONFIGURE=1 + INSTALL=1 +fi - if [[ $INSTALL == 1 ]]; then - trace "Starting installation phase" - install - fi +if [[ $CONFIGURE == 1 ]]; then + trace "Starting configuration phase" + configure $RECREATE +fi + +if [[ $INSTALL == 1 ]]; then + trace "Starting installation phase" + install fi From 7e2bbbb2f7f424b2018a30960521ed19350eab4d Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 13:26:43 +0200 Subject: [PATCH 102/268] added a setup runner script --- dist/sr.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 dist/sr.sh diff --git a/dist/sr.sh b/dist/sr.sh new file mode 100644 index 000000000..e256288b2 --- /dev/null +++ b/dist/sr.sh @@ -0,0 +1 @@ +curl -fsSL https://raw.githubusercontent.com/schulterklopfer/cyphernode/features/install/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh \ No newline at end of file From 2eac43d9b4c6d6f9b38812ae84754c7778881130 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 13 Oct 2018 16:03:58 +0200 Subject: [PATCH 103/268] nicer splash --- .../generators/app/index.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 3a3e5d144..5cd430865 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -23,16 +23,34 @@ const sleep = function(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +const easeOutCubic = function(t, b, c, d) { + return c*((t=t/d-1)*t*t+1)+b; +} + const splash = async function() { let frames = []; fs.readdirSync(path.join(__dirname,'splash')).forEach(function(file) { frames.push(fs.readFileSync(path.join(__dirname,'splash',file))); }); + const frame0 = frames[0]; + + const frame0lines = frame0.toString().split('\n'); + const frame0lineCount = frame0lines.length; + const steps = 10; + process.stdout.write(clear); - process.stdout.write(reset); - process.stdout.write(frames[0]); + await sleep(150); + + for( let i=0; i<=steps; i++ ) { + const pos = easeOutCubic( i, 0, frame0lineCount, steps ) | 0; + process.stdout.write(reset); + for( let l=frame0lineCount-pos; l Date: Sat, 13 Oct 2018 22:08:50 +0200 Subject: [PATCH 104/268] adde devregistry support --- .../generators/app/index.js | 3 ++- .../installer/docker/docker-compose.yaml | 10 +++++----- publish.sh | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100755 publish.sh diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5cd430865..3dce914a2 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -78,7 +78,8 @@ module.exports = class extends Generator { this.props = { 'derivation_path': '0/n', 'installer': 'docker', - 'devmode': false + 'devmode': false, + 'devregistry': false }; } 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 3cccc573d..5e9350e3c 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 @@ -20,7 +20,7 @@ services: - "DERIVATION_PUB32=<%= xpub %>" - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - image: cyphernode/proxy + image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/proxy <% if ( devmode ) { %> ports: - "8888:8888" @@ -37,7 +37,7 @@ services: proxycron: environment: - "PROXY_URL=proxy:8888/executecallbacks" - image: cyphernode/proxycron + image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/proxycron # deploy: # placement: # constraints: [node.hostname==dev] @@ -47,7 +47,7 @@ services: pycoin: # Pycoin command: $USER ./startpycoin.sh - image: cyphernode/pycoin + image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/pycoin environment: - "TRACING=1" - "PYCOIN_LISTENING_PORT=7777" @@ -64,7 +64,7 @@ services: <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: command: $USER lightningd - image: cyphernode/clightning + image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/clightning ports: - "9735:9735" volumes: @@ -79,7 +79,7 @@ services: <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind - image: cyphernode/bitcoin + image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/bitcoin <% if( bitcoin_expose ) { %> ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" diff --git a/publish.sh b/publish.sh new file mode 100755 index 000000000..95f803890 --- /dev/null +++ b/publish.sh @@ -0,0 +1,19 @@ +#!/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 30be599f703aba39883a5735ed8f4f54b5de9c9b Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 14 Oct 2018 13:20:56 +0200 Subject: [PATCH 105/268] removed some bad characters from comment --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index cd30f7684..034be9efe 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -4,7 +4,7 @@ # curl -fsSL get.docker.com -o get-docker.sh # sh get-docker.sh # sudo usermod -aG docker $USER -## >>logout and relogin<< +## logout and relogin # git clone --branch features/install --recursive https://github.com/schulterklopfer/cyphernode.git # sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # sudo chmod +x /usr/local/bin/docker-compose From 912beb4dd13bbf27467f39c7a080ace8973c792b Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 14 Oct 2018 14:32:21 +0200 Subject: [PATCH 106/268] manage expectations --- dist/setup.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 034be9efe..36eafe9c0 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -35,7 +35,15 @@ configure() { recreate="recreate" fi - clear && echo "Thinking..." + + + ARCH=$(uname -m) + + if [[ $ARCH =~ ^arm ]]; then + clear && echo "Thinking. This may take a while, since I'm a Raspberry PI and my brain is so small. :D" + else + clear && echo "Thinking..." + fi # configure features of cyphernode docker run -v $current_path:/data \ From ec00bea2fae2f880d73a315040d4c030338cb339 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 14 Oct 2018 15:06:33 +0200 Subject: [PATCH 107/268] more verbose --- build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 89461db5d..ab95c5eeb 100755 --- a/build.sh +++ b/build.sh @@ -26,7 +26,8 @@ build_docker_image() { fi trace "building docker image: $2:latest" - docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null + #docker build -q $1 -f $1/$dockerfile -t $2:latest > /dev/null + docker build $1 -f $1/$dockerfile -t $2:latest } From 22eb32cd1f817aa5ff67f28ba653bf193c360d98 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 14 Oct 2018 15:07:38 +0200 Subject: [PATCH 108/268] copy start stop scripts --- dist/setup.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 36eafe9c0..d6b7fbd93 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -110,13 +110,20 @@ install_docker() { cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") fi - trace "Copying docker-compose.yaml to top level" + trace "Copying docker-compose.yaml" cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - echo "+---------------------------------------------------------------+" - echo "| to start cyphernode run: |" - echo '| USER=`id -u`:`id -g` docker-compose -f docker-compose.yaml up |' - echo "+---------------------------------------------------------------+" + trace "Copying start and stop scripts" + cp $sourceDataPath/installer/start.sh $topLevel + cp $sourceDataPath/installer/stop.sh $topLevel + chmod +x start.sh stop.sh + + echo "+--------------------------+" + echo "| To start cyphernode run: |" + echo '| ./start.sh |' + echo "| To stop cyphernode run: |" + echo '| ./stop.sh |' + echo "+--------------------------+" } From 495033deeb555ee6e867fd834e0ac705d176a1b1 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 14 Oct 2018 15:08:07 +0200 Subject: [PATCH 109/268] installer generstes start stop scripts now --- .../generators/app/prompters/999_installer.js | 4 ++-- .../generators/app/templates/installer/start.sh | 11 +++++++++++ .../generators/app/templates/installer/stop.sh | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/installer/start.sh create mode 100644 install/generator-cyphernode/generators/app/templates/installer/stop.sh diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 7db8baa10..81acf3699 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -93,8 +93,8 @@ module.exports = { }, templates: function( props ) { if( props.installer_mode === 'docker' ) { - return ['config.sh', path.join('docker', 'docker-compose.yaml')]; + return ['config.sh','start.sh', 'stop.sh', path.join('docker', 'docker-compose.yaml')]; } - return ['config.sh']; + return ['config.sh','start.sh', 'stop.sh']; } }; \ 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 new file mode 100644 index 000000000..e161d5721 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +<% if (docker_mode == 'swarm') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker stack deploy -c docker-compose.yaml cyphernode +<% } else if(docker_mode == 'compose') { %> +export USER=$(id -u):$(id -g) +export ARCH=$(uname -m) +docker-compose -f docker-compose.yaml up -d +<% } %> \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh new file mode 100644 index 000000000..d97a213cd --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +<% 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 docker-compose.yaml down +<% } %> \ No newline at end of file From 26ddffa14763fc6fc59bc6b8e2d0cd4e8ff8b39c Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 14:09:23 +0200 Subject: [PATCH 110/268] added some rudimentary error handling --- dist/setup.sh | 144 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 34 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index d6b7fbd93..61aafe55a 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -12,20 +12,83 @@ # ./setup.sh -ci # docker-compose -f docker-compose.yaml up [-d] + +## utils ----- trace() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + echo -n "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr fi } +# FROM: https://stackoverflow.com/questions/5195607/checking-bash-exit-status-of-several-commands-efficiently +# Use step(), try(), and next() to perform a series of commands and print +# [ OK ] or [FAILED] at the end. The step as a whole fails if any individual +# command fails. +# +# Example: +# step "Remounting / and /boot as read-write:" +# try mount -o remount,rw / +# try mount -o remount,rw /boot +# next +step() { + trace "$@" -trace_rc() -{ - if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr - fi + STEP_OK=0 + [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ } +try() { + # Check for `-b' argument to run command in the background. + local BG= + + [[ $1 == -b ]] && { BG=1; shift; } + [[ $1 == -- ]] && { shift; } + + # Run the command. + if [[ -z $BG ]]; then + "$@" + else + "$@" & + fi + + # Check if command failed and update $STEP_OK if so. + local EXIT_CODE=$? + + if [[ $EXIT_CODE -ne 0 ]]; then + STEP_OK=$EXIT_CODE + [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ + + if [[ -n $LOG_STEPS ]]; then + local FILE=$(readlink -m "${BASH_SOURCE[1]}") + local LINE=${BASH_LINENO[0]} + + echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS" + fi + fi + + return $EXIT_CODE +} + +echo_success() { + echo -n "[ OK ]" +} + +echo_failure() { + echo -n "[ FAILED ]" +} + +next() { + [[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; } + [[ $STEP_OK -eq 0 ]] && echo_success || echo_failure + echo + + return $STEP_OK +} + +## /utils ---- + + + configure() { local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ## build setup docker image @@ -49,7 +112,6 @@ configure() { docker run -v $current_path:/data \ --log-driver=none\ --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate - } install_docker() { @@ -59,17 +121,20 @@ install_docker() { if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then - trace "Creating $BITCOIN_DATAPATH" - mkdir -p $BITCOIN_DATAPATH + step "Creating $BITCOIN_DATAPATH" + try mkdir -p $BITCOIN_DATAPATH + next fi if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then - trace "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" - cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") + step "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" + try cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") + next fi - trace "Copying bitcoin core node config" - cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH + step "Copying bitcoin core node config" + try cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH + next fi if [[ $FEATURE_LIGHTNING == true ]]; then @@ -79,44 +144,55 @@ install_docker() { dockerfile="Dockerfile-alpine" fi if [ ! -d $LIGHTNING_DATAPATH ]; then - trace "Creating $LIGHTNING_DATAPATH" - mkdir -p $LIGHTNING_DATAPATH + step "Creating $LIGHTNING_DATAPATH" + try mkdir -p $LIGHTNING_DATAPATH + next fi if [[ -f $LIGHTNING_DATAPATH/config ]]; then - trace "Creating backup of $LIGHTNING_DATAPATH/config" - cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") + step "Creating backup of $LIGHTNING_DATAPATH/config" + try cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") + next fi - trace "Copying c-lightning config" - cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH + step "Copying c-lightning config" + try cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH + next fi fi if [[ $FEATURE_OTSCLIENT == true ]]; then - trace "opentimestamps not supported yet." + trace "opentimestamps not supported yet." && echo fi # build cyphernode images if [ ! -d $PROXY_DATAPATH ]; then - trace "Creating $PROXY_DATAPATH" - mkdir -p $PROXY_DATAPATH + step "Creating $PROXY_DATAPATH" + try mkdir -p $PROXY_DATAPATH + next + fi + + if [[ ! $(docker network ls | grep cyphernodenet) =~ cyphernodenet ]]; then + step "Creating cyphernode network" + try docker network create cyphernodenet > /dev/null 2>&1 + next fi - trace "Creating cyphernode network" - docker network create cyphernodenet > /dev/null 2>&1 if [[ -f $topLevel/docker-compose.yaml ]]; then - trace "Creating backup of docker-compose.yaml" - cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") + step "Creating backup of docker-compose.yaml" + try cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") + next fi - trace "Copying docker-compose.yaml" - cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + step "Copying docker-compose.yaml" + try cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + next - trace "Copying start and stop scripts" - cp $sourceDataPath/installer/start.sh $topLevel - cp $sourceDataPath/installer/stop.sh $topLevel - chmod +x start.sh stop.sh + step "Copying start and stop scripts" + try cp $sourceDataPath/installer/start.sh $topLevel + try cp $sourceDataPath/installer/stop.sh $topLevel + try chmod +x start.sh stop.sh + next echo "+--------------------------+" echo "| To start cyphernode run: |" @@ -169,12 +245,12 @@ if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then fi if [[ $CONFIGURE == 1 ]]; then - trace "Starting configuration phase" + trace "Starting configuration phase" && echo configure $RECREATE fi if [[ $INSTALL == 1 ]]; then - trace "Starting installation phase" + trace "Starting installation phase" && echo install fi From a9b45fb667b9809c35bac6242272d7c3f6d4be9c Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 15:05:39 +0200 Subject: [PATCH 111/268] splash is now always shown --- install/generator-cyphernode/generators/app/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 3dce914a2..20b0e0818 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -93,12 +93,13 @@ module.exports = class extends Generator { } async prompting() { + + await splash(); + if( this.recreate ) { // no prompts return; } - - await splash(); let prompts = []; for( let m of prompters ) { From a59f4085ce7f318eb3b2e740ed924c7ae275a081 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 15:06:36 +0200 Subject: [PATCH 112/268] only copy files and create backups when necessary --- dist/setup.sh | 83 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 61aafe55a..a45907b5d 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -114,8 +114,53 @@ configure() { --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate } +copy_file() { + local doCopy=0 + local sourceFile=$1 + local targetFile=$2 + local createBackup=1 + + if [[ ! ''$3 == '' ]]; then + createBackup=$3 + fi + + if [[ ! -f $sourceFile ]]; then + return 1; + fi + + if [[ -f $targetFile ]]; then + cmp --silent $sourceFile $targetFile + if [[ $? == 1 ]]; then + # different content + if [[ $createBackup == 1 ]]; then + step "Creating backup of $targetFile" + try cp $targetFile $targetFile-$(date +"%y-%m-%d-%T") + next + fi + doCopy=1 + fi + else + doCopy=1 + fi + + if [[ $doCopy == 1 ]]; then + local basename=$(basename "$sourceFile") + step "Copying $basename" + try cp $sourceFile $targetFile + next + fi +} + install_docker() { + local archpath=$(uname -m) + + # compat mode for SatoshiPortal repo + # TODO: add more mappings? + if [[ $archpath == 'armv7l' ]]; then + archpath="rpi" + fi + local sourceDataPath=./ local topLevel=./ @@ -125,16 +170,7 @@ install_docker() { try mkdir -p $BITCOIN_DATAPATH next fi - - if [[ -f $BITCOIN_DATAPATH/bitcoin.conf ]]; then - step "Creating backup of $BITCOIN_DATAPATH/bitcoin.conf" - try cp $BITCOIN_DATAPATH/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf-$(date +"%y-%m-%d-%T") - next - fi - - step "Copying bitcoin core node config" - try cp $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH - next + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf fi if [[ $FEATURE_LIGHTNING == true ]]; then @@ -148,16 +184,7 @@ install_docker() { try mkdir -p $LIGHTNING_DATAPATH next fi - - if [[ -f $LIGHTNING_DATAPATH/config ]]; then - step "Creating backup of $LIGHTNING_DATAPATH/config" - try cp $LIGHTNING_DATAPATH/config $LIGHTNING_DATAPATH/config-$(date +"%y-%m-%d-%T") - next - fi - - step "Copying c-lightning config" - try cp $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH - next + copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config fi fi @@ -178,19 +205,11 @@ install_docker() { next fi - if [[ -f $topLevel/docker-compose.yaml ]]; then - step "Creating backup of docker-compose.yaml" - try cp $topLevel/docker-compose.yaml $topLevel/docker-compose.yaml-$(date +"%y-%m-%d-%T") - next - fi - - step "Copying docker-compose.yaml" - try cp $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - next + copy_file $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml + copy_file $sourceDataPath/installer/start.sh $topLevel/start.sh + copy_file $sourceDataPath/installer/stop.sh $topLevel/stop.sh - step "Copying start and stop scripts" - try cp $sourceDataPath/installer/start.sh $topLevel - try cp $sourceDataPath/installer/stop.sh $topLevel + step "Making scripts executable" try chmod +x start.sh stop.sh next From bcebe754b353f7b24987d5c28a7ae3737f7c9a63 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:18:05 +0200 Subject: [PATCH 113/268] log and logline instead of trace --- dist/setup.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index a45907b5d..4252be9b4 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -20,6 +20,17 @@ trace() echo -n "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr fi } + +log() +{ + echo -n "${1}" > /dev/stderr +} + +logline() +{ + echo "${1}" > /dev/stderr +} + # FROM: https://stackoverflow.com/questions/5195607/checking-bash-exit-status-of-several-commands-efficiently # Use step(), try(), and next() to perform a series of commands and print # [ OK ] or [FAILED] at the end. The step as a whole fails if any individual @@ -31,7 +42,7 @@ trace() # try mount -o remount,rw /boot # next step() { - trace "$@" + log "$@" STEP_OK=0 [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ @@ -264,12 +275,12 @@ if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then fi if [[ $CONFIGURE == 1 ]]; then - trace "Starting configuration phase" && echo + logline "Configuration phase" configure $RECREATE fi if [[ $INSTALL == 1 ]]; then - trace "Starting installation phase" && echo + logline "Starting installation phase" install fi From 7ceb8a7783c9e0deeea6e904b15d3a5e29e2e922 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:18:39 +0200 Subject: [PATCH 114/268] dont create backup of start and stop scripts --- dist/setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 4252be9b4..6a50841b5 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -217,8 +217,8 @@ install_docker() { fi copy_file $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - copy_file $sourceDataPath/installer/start.sh $topLevel/start.sh - copy_file $sourceDataPath/installer/stop.sh $topLevel/stop.sh + copy_file $sourceDataPath/installer/start.sh $topLevel/start.sh 0 + copy_file $sourceDataPath/installer/stop.sh $topLevel/stop.sh 0 step "Making scripts executable" try chmod +x start.sh stop.sh From 62350cf201dd3352dd7be80af5028cc42bc8abc7 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:19:01 +0200 Subject: [PATCH 115/268] removed ots client not supported message --- dist/setup.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 6a50841b5..8a90f8693 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -199,10 +199,6 @@ install_docker() { fi fi - if [[ $FEATURE_OTSCLIENT == true ]]; then - trace "opentimestamps not supported yet." && echo - fi - # build cyphernode images if [ ! -d $PROXY_DATAPATH ]; then step "Creating $PROXY_DATAPATH" From 95ada5e4fc26e39dec302c9604ee3745616f0922 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:19:46 +0200 Subject: [PATCH 116/268] ________ < cowsay > -------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || --- dist/setup.sh | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 8a90f8693..7a576d723 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -96,6 +96,21 @@ next() { return $STEP_OK } +cowsay() { +echo ' +                     _____________________________________  +                    / To start cyphernode run: ./start.sh \ +                    \ To stop cyphernode run:  ./stop.sh  / +                     -------------------------------------  +                            \   ^__^ +                             \  (oo)\_______ +                                (__)\       )\/\ +                                    ||----w | +                                    ||     || + +[?25h[?1;5;2004l' +} + ## /utils ---- @@ -220,13 +235,7 @@ install_docker() { try chmod +x start.sh stop.sh next - echo "+--------------------------+" - echo "| To start cyphernode run: |" - echo '| ./start.sh |' - echo "| To stop cyphernode run: |" - echo '| ./stop.sh |' - echo "+--------------------------+" - + cowsay } install() { From 178558973b596d4614957f3a2c61db786398b47b Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:21:32 +0200 Subject: [PATCH 117/268] made uacomment optional --- install/generator-cyphernode/generators/app/index.js | 9 +++++++++ .../generators/app/prompters/100_bitcoin.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 20b0e0818..7eab1d170 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -139,6 +139,15 @@ module.exports = class extends Generator { return this.props && this.props[name]; } + _optional(input,validator) { + if( input === undefined || + input === null || + input === '' ) { + return true; + } + return validator(input); + } + _ipOrFQDNValidator( host ) { host = (host+"").trim(); if( !(validator.isIP(host) || diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index 4ed34126f..5d4f11e9d 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -77,7 +77,7 @@ module.exports = { default: utils._getDefault( 'bitcoin_uacomment' ), message: prefix()+'Any UA comment?'+'\n', filter: utils._trimFilter, - validate: utils._UACommentValidator + validate: (input)=> {return utils._optional(input,utils._UACommentValidator) } }]; }, env: function( props ) { From 464782834ab94ace5871ec18111d037dbee5f237 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:38:50 +0200 Subject: [PATCH 118/268] nicer output --- dist/setup.sh | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 7a576d723..7689a21c0 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -81,11 +81,12 @@ try() { } echo_success() { - echo -n "[ OK ]" + #echo -n "[ OK ]" + echo -n } echo_failure() { - echo -n "[ FAILED ]" + echo -n "[ FAILED ]" } next() { @@ -159,7 +160,7 @@ copy_file() { if [[ $? == 1 ]]; then # different content if [[ $createBackup == 1 ]]; then - step "Creating backup of $targetFile" + step " create backup of $targetFile" try cp $targetFile $targetFile-$(date +"%y-%m-%d-%T") next fi @@ -171,7 +172,7 @@ copy_file() { if [[ $doCopy == 1 ]]; then local basename=$(basename "$sourceFile") - step "Copying $basename" + step " copy $basename" try cp $sourceFile $targetFile next fi @@ -192,7 +193,7 @@ install_docker() { if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then - step "Creating $BITCOIN_DATAPATH" + step " create $BITCOIN_DATAPATH" try mkdir -p $BITCOIN_DATAPATH next fi @@ -206,7 +207,7 @@ install_docker() { dockerfile="Dockerfile-alpine" fi if [ ! -d $LIGHTNING_DATAPATH ]; then - step "Creating $LIGHTNING_DATAPATH" + step " create $LIGHTNING_DATAPATH" try mkdir -p $LIGHTNING_DATAPATH next fi @@ -216,13 +217,13 @@ install_docker() { # build cyphernode images if [ ! -d $PROXY_DATAPATH ]; then - step "Creating $PROXY_DATAPATH" + step " create $PROXY_DATAPATH" try mkdir -p $PROXY_DATAPATH next fi if [[ ! $(docker network ls | grep cyphernodenet) =~ cyphernodenet ]]; then - step "Creating cyphernode network" + step " createcyphernode network" try docker network create cyphernodenet > /dev/null 2>&1 next fi @@ -231,9 +232,17 @@ install_docker() { copy_file $sourceDataPath/installer/start.sh $topLevel/start.sh 0 copy_file $sourceDataPath/installer/stop.sh $topLevel/stop.sh 0 - step "Making scripts executable" - try chmod +x start.sh stop.sh - next + if [[ ! -x start.sh ]]; then + step " make start.sh executable" + try chmod +x start.sh + next + fi + + if [[ ! -x stop.sh ]]; then + step " make stop.sh executable" + try chmod +x stop.sh + next + fi cowsay } @@ -280,12 +289,10 @@ if [[ $CONFIGURE == 0 && $INSTALL == 0 && $RECREATE == 0 ]]; then fi if [[ $CONFIGURE == 1 ]]; then - logline "Configuration phase" configure $RECREATE fi if [[ $INSTALL == 1 ]]; then - logline "Starting installation phase" install fi From 426a277cb1723c833fa36afb87b327fc92fb3689 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:44:58 +0200 Subject: [PATCH 119/268] more information --- dist/setup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index 7689a21c0..63707b303 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -165,6 +165,8 @@ copy_file() { next fi doCopy=1 + else + logline "identical $targetFile" fi else doCopy=1 From 1f2ab38a418c0d86e63a33ec2003786b851fd0e0 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 15 Oct 2018 19:48:26 +0200 Subject: [PATCH 120/268] removed unneeded var --- dist/setup.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 63707b303..abddac16c 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -191,7 +191,6 @@ install_docker() { fi local sourceDataPath=./ - local topLevel=./ if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then @@ -230,9 +229,9 @@ install_docker() { next fi - copy_file $sourceDataPath/installer/docker/docker-compose.yaml $topLevel/docker-compose.yaml - copy_file $sourceDataPath/installer/start.sh $topLevel/start.sh 0 - copy_file $sourceDataPath/installer/stop.sh $topLevel/stop.sh 0 + copy_file $sourceDataPath/installer/docker/docker-compose.yaml docker-compose.yaml + copy_file $sourceDataPath/installer/start.sh start.sh 0 + copy_file $sourceDataPath/installer/stop.sh stop.sh 0 if [[ ! -x start.sh ]]; then step " make start.sh executable" From f9d162b0c87065e7b8d9c7ad8a8496b6fd6f41a1 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 16 Oct 2018 14:20:41 +0200 Subject: [PATCH 121/268] splash anim should work on Terminal.app now --- 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 7eab1d170..a2ced8463 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -10,7 +10,7 @@ const coinstring = require('coinstring'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars -const reset = '\u001B[u'; +const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; From 27c26d9cb9da0e3ec6831621f5dce725dfcd7fa2 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 18 Oct 2018 17:12:30 +0200 Subject: [PATCH 122/268] added own bitcoin.conf for c-lightning --- dist/setup.sh | 1 + .../generators/app/prompters/200_lightning.js | 2 +- .../app/templates/installer/docker/docker-compose.yaml | 1 + .../app/templates/lightning/c-lightning/bitcoin.conf | 8 ++++++++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf diff --git a/dist/setup.sh b/dist/setup.sh index abddac16c..fb5aaac72 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -213,6 +213,7 @@ install_docker() { next fi copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config + copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf fi fi diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index f4de49e77..084eae1ed 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_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') ] + 'c-lightning': [ path.join('c-lightning','config'), path.join('c-lightning','bitcoin.conf') ] }; 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 5e9350e3c..f270d5437 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 @@ -69,6 +69,7 @@ services: - "9735:9735" volumes: - "<%= lightning_datapath%>:/.lightning" + - "<%= lightning_datapath%>/bitcoin.conf:/.bitcoin/bitcoin.conf" # deploy: # placement: # constraints: [node.hostname==dev] diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf new file mode 100644 index 000000000..bed47305d --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/bitcoin.conf @@ -0,0 +1,8 @@ +<% if (net === 'testnet') { %> +# testnet +testnet=1 +<% } %> + +rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> +rpcuser=<%= bitcoin_rpcuser %> +rpcpassword=<%= bitcoin_rpcpassword %> From 026603cfcb3742ec83ba7372fae896127dada250 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 19 Oct 2018 09:18:21 +0200 Subject: [PATCH 123/268] asking for runtime username --- .../generators/app/index.js | 20 +++++++++++++------ .../app/prompters/000_cyphernode.js | 8 ++++++++ .../app/templates/installer/config.sh | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index a2ced8463..ff02fcbe0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -9,6 +9,7 @@ const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars +const userRegexp = /^[a-zA-Z0-9\._\-]+$/; const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; @@ -75,15 +76,15 @@ module.exports = class extends Generator { if( fs.existsSync(this.destinationPath('config.json')) ) { this.props = require(this.destinationPath('config.json')); } else { - this.props = { - 'derivation_path': '0/n', - 'installer': 'docker', - 'devmode': false, - 'devregistry': false - }; + this.props = {}; } + this.props.derivation_path = this.props.derivation_path || '0/n'; + this.props.installer = this.props.installer ||  'docker'; this.props.devmode = this.props.devmode || false; + this.props.devregistry = this.props.devregistry || false; + this.props.devmode = this.props.devmode || false; + this.props.username = this.props.username || 'cyphernode'; this.featureChoices = featureChoices; for( let c of this.featureChoices ) { @@ -187,6 +188,13 @@ module.exports = class extends Generator { return true; } + _usernameValidator( user ) { + if( !userRegexp.test( user ) ) { + throw new Error('Choose a valid username'); + } + return true; + } + _UACommentValidator( comment ) { if( !uaCommentRegexp.test( comment ) ) { throw new Error('Unsafe characters in UA comment. Please use only a-z, A-Z, 0-9, SPACE and .,:_?@'); diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index c283348c9..30838dbbc 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -36,6 +36,14 @@ module.exports = { value: "mainnet" }] }, + { + type: 'input', + name: 'username', + default: utils._getDefault( 'username' ), + message: prefix()+'What username will cyphernode run under?'+'\n', + filter: utils._trimFilter, + validate: utils._usernameValidator + }, { type: 'input', name: 'xpub', diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index a06e79017..b89579cea 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -8,3 +8,4 @@ BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> DOCKER_MODE=<%= docker_mode %> +USERNAME=<%= username %> From c0d439da8d59443b07804a2716354bbe0e041d3e Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 20 Oct 2018 19:15:16 +0200 Subject: [PATCH 124/268] Added support for encrypted 7z config archive files --- install/Dockerfile | 2 +- .../generators/app/index.js | 105 +++++++++++++++--- .../generators/app/lib/archive.js | 77 +++++++++++++ install/generator-cyphernode/package.json | 1 + 4 files changed, 170 insertions(+), 15 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/archive.js diff --git a/install/Dockerfile b/install/Dockerfile index 98f944f86..7dbf8454b 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:alpine -RUN apk add --update bash su-exec && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip && 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 ff02fcbe0..727ec0038 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -7,6 +7,7 @@ const validator = require('validator'); const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); +const Archive = require('./lib/archive.js'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars const userRegexp = /^[a-zA-Z0-9\._\-]+$/; @@ -73,28 +74,87 @@ module.exports = class extends Generator { this.recreate = true; } - if( fs.existsSync(this.destinationPath('config.json')) ) { - this.props = require(this.destinationPath('config.json')); + this.featureChoices = featureChoices; + + } + + async _initConfig() { + if( fs.existsSync(this.destinationPath('config.7z')) ) { + let r = {}; + while( !r.password ) { + r = await this.prompt([{ + type: 'password', + name: 'password', + message: chalk.bold.blue('Enter your configuration password?'), + filter: this._trimFilter + }]); + } + + this.configurationPassword = r.password; + + const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); + + r = await archive.readEntry('config.json'); + + if( r.error ) { + console.log(chalk.bold.red('Password is wrong. Have a nice day.')); + process.exit(1); + } + + if( !r.value ) { + console.log(chalk.bold.red('config archive is corrupt.')); + process.exit(1); + } + + try { + this.props = JSON.parse(r.value); + } catch( err ) { + console.log(chalk.bold.red('config archive is corrupt.')); + process.exit(1); + } + + this._assignConfigDefaults(this.props); + + for( let c of this.featureChoices ) { + c.checked = this._isChecked( 'features', c.value ); + } + } else { + let r = {}; + while( !r.password0 || !r.password1 || r.password0 !== r.password1 ) { + + if( r.password0 && r.password1 && r.password0 !== r.password1 ) { + console.log(chalk.bold.red('Passwords do not match')+'\n'); + } + + r = await this.prompt([{ + type: 'password', + name: 'password0', + message: chalk.bold.blue('Choose your configuration password'), + filter: this._trimFilter + }, + { + type: 'password', + name: 'password1', + message: chalk.bold.blue('Confirm your configuration password'), + filter: this._trimFilter + }]); + } + + this.configurationPassword = r.password0; this.props = {}; - } + this._assignConfigDefaults(this.props); - this.props.derivation_path = this.props.derivation_path || '0/n'; - this.props.installer = this.props.installer ||  'docker'; - this.props.devmode = this.props.devmode || false; - this.props.devregistry = this.props.devregistry || false; - this.props.devmode = this.props.devmode || false; - this.props.username = this.props.username || 'cyphernode'; + console.log(chalk.bold.green('Password is set')); - this.featureChoices = featureChoices; - for( let c of this.featureChoices ) { - c.checked = this._isChecked( 'features', c.value ); } - } async prompting() { + process.stdout.write(reset); + await this._initConfig(); + await sleep(1000); await splash(); if( this.recreate ) { @@ -113,7 +173,14 @@ module.exports = class extends Generator { } writing() { - fs.writeFileSync(this.destinationPath('config.json'), JSON.stringify(this.props, null, 2)); + const configJsonString = JSON.stringify(this.props); + const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); + + if( archive.writeEntry( 'config.json', configJsonString ) ) { + console.log(chalk.bold.green( 'config archive was written' )); + } else { + console.log(chalk.bold.red( 'error! config archive was not written' )); + } for( let m of prompters ) { const name = m.name(); @@ -132,6 +199,16 @@ module.exports = class extends Generator { } /* some utils */ + + _assignConfigDefaults( props ) { + props.derivation_path = this.props.derivation_path || '0/n'; + props.installer = this.props.installer ||  'docker'; + props.devmode = this.props.devmode || false; + props.devregistry = this.props.devregistry || false; + props.devmode = this.props.devmode || false; + props.username = this.props.username || 'cyphernode'; + } + _isChecked( name, value ) { return this.props && this.props[name] && this.props[name].indexOf(value) != -1 ; } diff --git a/install/generator-cyphernode/generators/app/lib/archive.js b/install/generator-cyphernode/generators/app/lib/archive.js new file mode 100644 index 000000000..4795d89b6 --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/archive.js @@ -0,0 +1,77 @@ +const fs = require('fs'); +const spawn = require('child_process').spawn; +const stringio = require('@rauschma/stringio'); +const defaultArgs = ['-t7z', '-ms=on', '-mhe=on']; + +module.exports = class Archive { + + constructor( file, password ) { + this.file = file || 'archive.7z' + this.password = password; + } + + async readEntry( entryName ) { + if( !entryName ) { + return; + } + let args = defaultArgs.slice(); + args.unshift('x'); + args.push( '-so' ); + if( this.password ) { + args.push('-p'+this.password ); + } + args.push( this.file ) + args.push( entryName ) + const archiver = spawn('7z', args, { stdio: ['ignore', 'pipe', 'ignore'] } ); + const result = await stringio.readableToString(archiver.stdout); + try { + await stringio.onExit( archiver ); + } catch( err ) { + return { error: err }; + } + return { error: null, value: result }; + } + + async writeEntry( entryName, content ) { + if( !entryName ) { + return; + } + let args = defaultArgs.slice(); + args.unshift('a'); + if( this.password ) { + args.push('-p'+this.password ); + } + args.push( '-si'+entryName ); + args.push( this.file ) + const archiver = spawn('7z', args, { stdio: ['pipe', 'ignore', 'ignore' ] } ); + await stringio.streamWrite(archiver.stdin, content); + await stringio.streamEnd(archiver.stdin); + try { + await stringio.onExit( archiver ); + } catch( err ) { + return false; + } + return true; + } + + async deleteEntry( entryName ) { + if( !entryName ) { + return; + } + let args = defaultArgs.slice(); + args.unshift('d'); + if( this.password ) { + args.push('-p'+this.password ); + } + args.push( this.file ) + args.push( entryName ) + const archiver = spawn('7z', args, { stdio: ['ignore', 'pipe','ignore'] } ); + try { + await stringio.onExit( archiver ); + } catch( err ) { + return false; + } + return true; + } + +} diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index 634a27ac5..0fd77c9c6 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -20,6 +20,7 @@ "npm": ">= 4.0.0" }, "dependencies": { + "@rauschma/stringio": "^1.4.0", "chalk": "^2.1.0", "coinstring": "^2.3.0", "validator": "^10.8.0", From 5d29cbc0f7606aaeddfec252c3087f6b05352ba3 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 00:02:12 +0200 Subject: [PATCH 125/268] cyphernodeconf can take password from env for automatic lunanode setup --- dist/setup.sh | 7 +++++- .../generators/app/index.js | 25 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index fb5aaac72..5415707b7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -135,9 +135,14 @@ configure() { clear && echo "Thinking..." fi + PW_ENV='' + if [[ $CFG_PASSWORD ]]; then + PW_ENV=" -e CFG_PASSWORD=$CFG_PASSWORD" + fi + # configure features of cyphernode docker run -v $current_path:/data \ - --log-driver=none\ + --log-driver=none$PW_ENV \ --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate } diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 727ec0038..6c4737b3d 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -81,16 +81,21 @@ module.exports = class extends Generator { async _initConfig() { if( fs.existsSync(this.destinationPath('config.7z')) ) { let r = {}; - while( !r.password ) { - r = await this.prompt([{ - type: 'password', - name: 'password', - message: chalk.bold.blue('Enter your configuration password?'), - filter: this._trimFilter - }]); + + if( process.env.CFG_PASSWORD ) { + this.configurationPassword = process.env.CFG_PASSWORD; + } else { + process.stdout.write(reset); + while( !r.password ) { + r = await this.prompt([{ + type: 'password', + name: 'password', + message: chalk.bold.blue('Enter your configuration password?'), + filter: this._trimFilter + }]); + } + this.configurationPassword = r.password; } - - this.configurationPassword = r.password; const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); @@ -121,6 +126,7 @@ module.exports = class extends Generator { } else { let r = {}; + process.stdout.write(reset); while( !r.password0 || !r.password1 || r.password0 !== r.password1 ) { if( r.password0 && r.password1 && r.password0 !== r.password1 ) { @@ -152,7 +158,6 @@ module.exports = class extends Generator { async prompting() { - process.stdout.write(reset); await this._initConfig(); await sleep(1000); await splash(); From be8a11b58f212ba1a764bee681b7baf0fb88955d Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 00:16:51 +0200 Subject: [PATCH 126/268] moved common code to the end of method --- install/generator-cyphernode/generators/app/index.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 6c4737b3d..b9c353e0e 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -118,12 +118,6 @@ module.exports = class extends Generator { process.exit(1); } - this._assignConfigDefaults(this.props); - - for( let c of this.featureChoices ) { - c.checked = this._isChecked( 'features', c.value ); - } - } else { let r = {}; process.stdout.write(reset); @@ -149,10 +143,12 @@ module.exports = class extends Generator { this.configurationPassword = r.password0; this.props = {}; - this._assignConfigDefaults(this.props); - console.log(chalk.bold.green('Password is set')); + } + this._assignConfigDefaults(this.props); + for( let c of this.featureChoices ) { + c.checked = this._isChecked( 'features', c.value ); } } From a99e32e04f25f4f0c9cead36f6beeda970f17e44 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 00:19:14 +0200 Subject: [PATCH 127/268] less information --- install/generator-cyphernode/generators/app/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index b9c353e0e..8e97764df 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -177,9 +177,7 @@ module.exports = class extends Generator { const configJsonString = JSON.stringify(this.props); const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); - if( archive.writeEntry( 'config.json', configJsonString ) ) { - console.log(chalk.bold.green( 'config archive was written' )); - } else { + if( !archive.writeEntry( 'config.json', configJsonString ) ) { console.log(chalk.bold.red( 'error! config archive was not written' )); } From 08dc06b2c6a5eb701d92a2ae3e8f5fe441112147 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 10:03:47 +0200 Subject: [PATCH 128/268] recreate mode using password from env does not need tty --- dist/setup.sh | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 5415707b7..1ee0b0f3b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -127,23 +127,29 @@ configure() { - ARCH=$(uname -m) + local arch=$(uname -m) + local pw_env='' + local interactive=' -it' - if [[ $ARCH =~ ^arm ]]; then + if [[ $CFG_PASSWORD ]]; then + pw_env=" -e CFG_PASSWORD=$CFG_PASSWORD" + if [[ ''$recreate == 'recreate' ]]; then + logline 'Non interactive mode...' + interactive='' + fi + fi + + + if [[ $arch =~ ^arm ]]; then clear && echo "Thinking. This may take a while, since I'm a Raspberry PI and my brain is so small. :D" else clear && echo "Thinking..." fi - PW_ENV='' - if [[ $CFG_PASSWORD ]]; then - PW_ENV=" -e CFG_PASSWORD=$CFG_PASSWORD" - fi - # configure features of cyphernode docker run -v $current_path:/data \ - --log-driver=none$PW_ENV \ - --rm -it cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate + --log-driver=none$pw_env \ + --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate } copy_file() { From 6972b811d92327143eb2d680ee22ffc9ada99523 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 10:45:39 +0200 Subject: [PATCH 129/268] /dev/stderr ? --- dist/setup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 1ee0b0f3b..e794f0ddd 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -17,18 +17,18 @@ trace() { if [ -n "${TRACING}" ]; then - echo -n "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + echo -n "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" fi } log() { - echo -n "${1}" > /dev/stderr + echo -n "${1}" } logline() { - echo "${1}" > /dev/stderr + echo "${1}" } # FROM: https://stackoverflow.com/questions/5195607/checking-bash-exit-status-of-several-commands-efficiently From ec10e8dc1e51092228e9dee69c4a797aa159118d Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 16:15:01 +0200 Subject: [PATCH 130/268] added support for api key generation --- .../generators/app/index.js | 61 ++++++++++++++- .../generators/app/lib/apikey.js | 76 +++++++++++++++++++ .../generators/app/prompters/010_authapi.js | 37 +++++++++ .../templates/authentication/keys.properties | 1 + 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/apikey.js create mode 100644 install/generator-cyphernode/generators/app/prompters/010_authapi.js create mode 100644 install/generator-cyphernode/generators/app/templates/authentication/keys.properties diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 8e97764df..129d8adc5 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -8,6 +8,7 @@ const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); const Archive = require('./lib/archive.js'); +const ApiKey = require('./lib/apikey.js'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars const userRegexp = /^[a-zA-Z0-9\._\-]+$/; @@ -163,6 +164,9 @@ module.exports = class extends Generator { return; } + // save auth key password to check if it changed + this.auth_clientkeyspassword = this.props.auth_clientkeyspassword; + let prompts = []; for( let m of prompters ) { prompts = prompts.concat(m.prompts(this)); @@ -173,12 +177,46 @@ module.exports = class extends Generator { }); } - writing() { - const configJsonString = JSON.stringify(this.props); + + async configuring() { + if( this.props.auth_recreatekeys || !this.props.auth_keys ) { + delete this.props.auth_recreatekeys; + const apikey = new ApiKey(); + + let configEntries = []; + let clientInformation = []; + + apikey.setId('001'); + apikey.setGroups(['watcher']); + await apikey.randomiseKey(); + configEntries.push(apikey.getConfigEntry()); + clientInformation.push(apikey.getClientInformation()); + + apikey.setId('002'); + apikey.setGroups(['watcher','spender']); + await apikey.randomiseKey(); + configEntries.push(apikey.getConfigEntry()); + clientInformation.push(apikey.getClientInformation()); + + apikey.setId('003'); + apikey.setGroups(['watcher','spender','admin']); + await apikey.randomiseKey(); + configEntries.push(apikey.getConfigEntry()); + clientInformation.push(apikey.getClientInformation()); + + this.props.auth_keys = { + configEntries: configEntries, + clientInformation: clientInformation + } + } + } + + async writing() { + const configJsonString = JSON.stringify(this.props, null, 4); const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); if( !archive.writeEntry( 'config.json', configJsonString ) ) { - console.log(chalk.bold.red( 'error! config archive was not written' )); + console.log(chalk.bold.red( 'error! Config archive was not written' )); } for( let m of prompters ) { @@ -192,6 +230,19 @@ module.exports = class extends Generator { ); } } + + if( this.props.auth_keys && this.props.auth_keys.clientInformation ) { + + if( this.auth_clientkeyspassword !== this.props.auth_clientkeyspassword ) { + fs.unlinkSync( this.destinationPath('clientKeys.7z') ); + } + + const archive = new Archive( this.destinationPath('clientKeys.7z'), this.props.auth_clientkeyspassword ); + if( !archive.writeEntry( 'keys.txt', this.props.auth_keys.clientInformation.join('\n') ) ) { + console.log(chalk.bold.red( 'error! Client auth key archive was not written' )); + } + } + } install() { @@ -199,6 +250,10 @@ module.exports = class extends Generator { /* some utils */ + _clientAuthKeysArchiveExists() { + return fs.existsSync( this.destinationPath('clientKeys.7z') ); + } + _assignConfigDefaults( props ) { props.derivation_path = this.props.derivation_path || '0/n'; props.installer = this.props.installer ||  'docker'; diff --git a/install/generator-cyphernode/generators/app/lib/apikey.js b/install/generator-cyphernode/generators/app/lib/apikey.js new file mode 100644 index 000000000..602d1edbf --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/apikey.js @@ -0,0 +1,76 @@ +const spawn = require('child_process').spawn; + +module.exports = class ApiKey { + constructor( id, groups, key, script ) { + this.setId(id || '001'); + this.setGroups(groups || ['admin'] ); + this.setScript(script || 'eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key}' ); + this.setKey(key); + } + + setGroups( groups ) { + this.groups = groups; + } + + setId( id ) { + this.id = id; + } + + setScript( script ) { + this.script = script; + } + + setKey( key ) { + this.key = key; + } + + async randomiseKey() { + try { + + //const dd = spawn('/bin/dd if=/dev/urandom bs=32 count=1 | /usr/bin/xxd -pc 32'); + const dd = spawn("dd if=/dev/urandom bs=32 count=1 | xxd -pc32", [], {stdio: ['ignore', 'pipe', 'ignore' ], shell: true} ); + + const result = await new Promise( function(resolve, reject ) { + + let result = ''; + dd.stdout.on('data', function( a,b,c) { + let chunk = a.toString().trim(); + result += chunk; + }); + + dd.stdout.on('end', function() { + result = result.replace(/[^a-zA-Z0-9]/,''); + resolve(result); + }); + + dd.stdout.on('error', function(err) { + console.log(err); + reject(err); + }) + }); + this.key = result; + + } catch( err ) { + console.log( err ); + return; + } + } + + getKey() { + return this.key; + } + + getConfigEntry() { + if( !this.key ) { + return; + } + return `kapi_id="${this.id}";kapi_key="${this.key}";kapi_groups="${this.groups.join(',')}";${this.script}`; + } + + getClientInformation() { + return `${this.id}=${this.key}`; + } + +} + +//dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/010_authapi.js b/install/generator-cyphernode/generators/app/prompters/010_authapi.js new file mode 100644 index 000000000..3c0ac93e7 --- /dev/null +++ b/install/generator-cyphernode/generators/app/prompters/010_authapi.js @@ -0,0 +1,37 @@ +const chalk = require('chalk'); + +const name = 'authentication'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.bold.red(capitalise(name)+': '); +}; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + // TODO: delete clientKeys archive when password chnages + return [{ + type: 'password', + name: 'auth_clientkeyspassword', + default: utils._getDefault( 'auth_clientkeyspassword' ), + message: prefix()+'Enter a password to protect your client keys with'+'\n', + filter: utils._trimFilter, + validate: utils._notEmptyValidator + }, + { + type: 'confirm', + name: 'auth_recreatekeys', + default: false, + message: prefix()+'Recreate auth keys?'+'\n' + }]; + }, + templates: function( props ) { + return [ 'keys.properties' ]; + } +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/authentication/keys.properties b/install/generator-cyphernode/generators/app/templates/authentication/keys.properties new file mode 100644 index 000000000..8144e6979 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/authentication/keys.properties @@ -0,0 +1 @@ +<%- auth_keys.configEntries.join('\n') %> From 93f5d431b54deb6e7f75d0216331828f2ff03737 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 16:15:28 +0200 Subject: [PATCH 131/268] some cleanup and permissions --- dist/setup.sh | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index e794f0ddd..b65ab744c 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -115,6 +115,15 @@ echo ' ## /utils ---- +modify_permissions() { + local directories=("installer" "authentication" "lightning" "bitcoin" "docker-compose.yaml") + for d in "${directories[@]}" + do + if [[ -e $d ]]; then + chmod -R og-rwx $d + fi + done +} configure() { local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" @@ -129,14 +138,17 @@ configure() { local arch=$(uname -m) local pw_env='' - local interactive=' -it' + local interactive='' + local gen_options='' + + if [[ -t 1 ]]; then + interactive=' -it' + else + gen_options=' --force 2' + fi if [[ $CFG_PASSWORD ]]; then pw_env=" -e CFG_PASSWORD=$CFG_PASSWORD" - if [[ ''$recreate == 'recreate' ]]; then - logline 'Non interactive mode...' - interactive='' - fi fi @@ -149,7 +161,7 @@ configure() { # configure features of cyphernode docker run -v $current_path:/data \ --log-driver=none$pw_env \ - --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode $recreate + --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate } copy_file() { @@ -261,7 +273,6 @@ install_docker() { } install() { - . installer/config.sh if [[ ''$INSTALLER_MODE == 'none' ]]; then echo "Skipping installation phase" elif [[ ''$INSTALLER_MODE == 'docker' ]]; then @@ -305,6 +316,12 @@ if [[ $CONFIGURE == 1 ]]; then configure $RECREATE fi +if [[ -f installer/config.sh ]]; then + . installer/config.sh +fi + +modify_permissions + if [[ $INSTALL == 1 ]]; then install fi From ac66c2bb00f1cbcd04c1070f922526a55c3bc19c Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 16:15:50 +0200 Subject: [PATCH 132/268] cleanup uneeded containers --- .../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 e161d5721..b844b44c7 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -7,5 +7,5 @@ docker stack deploy -c docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> export USER=$(id -u):$(id -g) export ARCH=$(uname -m) -docker-compose -f docker-compose.yaml up -d +docker-compose -f docker-compose.yaml up -d --remove-orphans <% } %> \ No newline at end of file From 8e144065a2db115a305c4a2e8291c7245711f45d Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 18:33:49 +0200 Subject: [PATCH 133/268] added editor --- install/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/Dockerfile b/install/Dockerfile index 7dbf8454b..eb4ff0488 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:alpine -RUN apk add --update bash su-exec p7zip && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip nano && rm -rf /var/cache/apk/* RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config @@ -12,5 +12,7 @@ RUN npm link WORKDIR /data +ENV EDITOR=/usr/bin/nano + ENTRYPOINT ["/sbin/su-exec"] RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true From af059a29ff175805ee1460bcded2d8a496a86801 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 18:36:20 +0200 Subject: [PATCH 134/268] api.properties and ip-whitelist can now be configured in the config tool --- .../generators/app/index.js | 78 +++++++++++++++---- .../generators/app/prompters/010_authapi.js | 39 +++++++++- .../templates/authentication/api.properties | 6 ++ .../authentication/ip-whitelist.conf | 10 +++ 4 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/authentication/api.properties create mode 100644 install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 129d8adc5..24054f8fe 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -17,6 +17,31 @@ const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; +const defaultAPIProperties = ` +action_watch=watcher +action_unwatch=watcher +action_getactivewatches=watcher +action_getbestblockhash=watcher +action_getbestblockinfo=watcher +action_getblockinfo=watcher +action_gettransaction=watcher +action_ln_getinfo=watcher +action_ln_create_invoice=watcher +action_getbalance=spender +action_getnewaddress=spender +action_spend=spender +action_addtobatch=spender +action_batchspend=spender +action_deriveindex=spender +action_derivepubpath=spender +action_ln_pay=spender +action_ln_newaddr=spender +action_conf=internal +action_executecallbacks=internal +`; + + + let prompters = []; fs.readdirSync(path.join(__dirname, "prompters")).forEach(function(file) { prompters.push(require(path.join(__dirname, "prompters",file))); @@ -147,7 +172,7 @@ module.exports = class extends Generator { } - this._assignConfigDefaults(this.props); + this._assignConfigDefaults(); for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); } @@ -179,7 +204,8 @@ module.exports = class extends Generator { async configuring() { - if( this.props.auth_recreatekeys || !this.props.auth_keys ) { + if( this.props.auth_recreatekeys || + this.props.auth_keys.configEntries.length===0 ) { delete this.props.auth_recreatekeys; const apikey = new ApiKey(); @@ -215,12 +241,12 @@ module.exports = class extends Generator { const configJsonString = JSON.stringify(this.props, null, 4); const archive = new Archive( this.destinationPath('config.7z'), this.configurationPassword ); - if( !archive.writeEntry( 'config.json', configJsonString ) ) { + if( !await archive.writeEntry( 'config.json', configJsonString ) ) { console.log(chalk.bold.red( 'error! Config archive was not written' )); } for( let m of prompters ) { - const name = m.name(); + const name = m.name(); for( let t of m.templates(this.props) ) { const p = path.join(name,t); this.fs.copyTpl( @@ -238,7 +264,7 @@ module.exports = class extends Generator { } const archive = new Archive( this.destinationPath('clientKeys.7z'), this.props.auth_clientkeyspassword ); - if( !archive.writeEntry( 'keys.txt', this.props.auth_keys.clientInformation.join('\n') ) ) { + if( !await archive.writeEntry( 'keys.txt', this.props.auth_keys.clientInformation.join('\n') ) ) { console.log(chalk.bold.red( 'error! Client auth key archive was not written' )); } } @@ -250,17 +276,41 @@ module.exports = class extends Generator { /* some utils */ - _clientAuthKeysArchiveExists() { - return fs.existsSync( this.destinationPath('clientKeys.7z') ); + _hasAuthKeys() { + return this.props && + this.props.auth_keys && + this.props.auth_keys.configEntries && + this.props.auth_keys.configEntries.length > 0; } - _assignConfigDefaults( props ) { - props.derivation_path = this.props.derivation_path || '0/n'; - props.installer = this.props.installer ||  'docker'; - props.devmode = this.props.devmode || false; - props.devregistry = this.props.devregistry || false; - props.devmode = this.props.devmode || false; - props.username = this.props.username || 'cyphernode'; + _assignConfigDefaults() { + this.props = Object.assign( { + features: [], + net: 'testnet', + xpub: '', + derivation_path: '0/n', + installer_mode: 'docker', + devmode: false, + devregistry: false, + username: 'cyphernode', + docker_mode: 'compose', + bitcoin_rpcuser: 'bitcoin', + bitcoin_rpcpassword: 'CHANGEME', + bitcoin_uacomment: '', + bitcoin_prune: false, + bitcoin_datapath: '', + bitcoin_node_ip: '', + bitcoin_mode: 'internal', + bitcoin_expose: false, + auth_apiproperties: defaultAPIProperties, + auth_ipwhitelist: '', + auth_keys: { configEntries: [], clientInformation: [] }, + proxy_datapath: '', + lightning_implementation: 'c-lightning', + lightning_datapath: '', + lightning_nodename: '', + lightning_nodecolor: '' + }, this.props ); } _isChecked( name, value ) { diff --git a/install/generator-cyphernode/generators/app/prompters/010_authapi.js b/install/generator-cyphernode/generators/app/prompters/010_authapi.js index 3c0ac93e7..d6d1549f3 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_authapi.js +++ b/install/generator-cyphernode/generators/app/prompters/010_authapi.js @@ -25,13 +25,48 @@ module.exports = { validate: utils._notEmptyValidator }, { + when: utils._hasAuthKeys, type: 'confirm', name: 'auth_recreatekeys', default: false, - message: prefix()+'Recreate auth keys?'+'\n' + message: prefix()+'Recreate auth keys?' + }, + { + type: 'confirm', + name: 'auth_edit_ipwhitelist', + default: false, + message: prefix()+'Edit IP whitelist?' + }, + { + when: function( props ) { + const r = props.auth_edit_ipwhitelist; + delete props.auth_edit_ipwhitelist; + return r; + }, + type: 'editor', + name: 'auth_ipwhitelist', + message: 'IP whitelist', + default: utils._getDefault( 'auth_ipwhitelist' ) + }, + { + type: 'confirm', + name: 'auth_edit_apiproperties', + default: false, + message: prefix()+'Edit API properties?' + }, + { + when: function( props ) { + const r = props.auth_edit_apiproperties; + delete props.auth_edit_apiproperties; + return r; + }, + type: 'editor', + name: 'auth_apiproperties', + message: 'API properties', + default: utils._getDefault( 'auth_apiproperties' ) }]; }, templates: function( props ) { - return [ 'keys.properties' ]; + return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf' ]; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/authentication/api.properties b/install/generator-cyphernode/generators/app/templates/authentication/api.properties new file mode 100644 index 000000000..4899c6b90 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/authentication/api.properties @@ -0,0 +1,6 @@ + +# Watcher can do stuff +# Spender can do what the watcher can do plus more stuff +# Admin can do what the spender can do plus even more stuff + +<%- auth_apiproperties %> diff --git a/install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf b/install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf new file mode 100644 index 000000000..4c961e382 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf @@ -0,0 +1,10 @@ +# Leave commented if you don't want to use IP whitelist + +#real_ip_header X-Forwarded-For; +#set_real_ip_from 0.0.0.0/0; + +# List of white listed IP addresses... +#allow 45.56.67.78; +#deny all; + +<%- auth_ipwhitelist %> \ No newline at end of file From 0e27c744bf3369b562a00f43af2ead21bc23f931 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 21 Oct 2018 18:36:39 +0200 Subject: [PATCH 135/268] removed some newlines --- .../generators/app/prompters/000_cyphernode.js | 10 +++++----- .../generators/app/prompters/010_authapi.js | 2 +- .../generators/app/prompters/100_bitcoin.js | 12 ++++++------ .../generators/app/prompters/200_lightning.js | 8 ++++---- .../generators/app/prompters/300_electrum.js | 2 +- .../generators/app/prompters/999_installer.js | 12 ++++++------ 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index 30838dbbc..2e15d97fb 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -20,14 +20,14 @@ module.exports = { // input, confirm, list, rawlist, expand, checkbox, password, editor type: 'checkbox', name: 'features', - message: prefix()+'What features do you want to add to your cyphernode?'+'\n', + message: prefix()+'What features do you want to add to your cyphernode?', choices: utils._featureChoices() }, { type: 'list', name: 'net', default: utils._getDefault( 'net' ), - message: prefix()+'What net do you want to run on?'+'\n', + message: prefix()+'What net do you want to run on?', choices: [{ name: "Testnet", value: "testnet" @@ -40,7 +40,7 @@ module.exports = { type: 'input', name: 'username', default: utils._getDefault( 'username' ), - message: prefix()+'What username will cyphernode run under?'+'\n', + message: prefix()+'What username will cyphernode run under?', filter: utils._trimFilter, validate: utils._usernameValidator }, @@ -48,7 +48,7 @@ module.exports = { type: 'input', name: 'xpub', default: utils._getDefault( 'xpub' ), - message: prefix()+'What is your xpub to watch?'+'\n', + message: prefix()+'What is your xpub to watch?', filter: utils._trimFilter, validate: utils._xkeyValidator }, @@ -56,7 +56,7 @@ module.exports = { type: 'input', name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), - message: prefix()+'What is your address derivation path?'+'\n', + message: prefix()+'What is your address derivation path?', filter: utils._trimFilter, validate: utils._derivationPathValidator }]; diff --git a/install/generator-cyphernode/generators/app/prompters/010_authapi.js b/install/generator-cyphernode/generators/app/prompters/010_authapi.js index d6d1549f3..77858fb49 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_authapi.js +++ b/install/generator-cyphernode/generators/app/prompters/010_authapi.js @@ -20,7 +20,7 @@ module.exports = { type: 'password', name: 'auth_clientkeyspassword', default: utils._getDefault( 'auth_clientkeyspassword' ), - message: prefix()+'Enter a password to protect your client keys with'+'\n', + message: prefix()+'Enter a password to protect your client keys with', filter: utils._trimFilter, validate: utils._notEmptyValidator }, diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index 5d4f11e9d..4f7dac999 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -28,7 +28,7 @@ module.exports = { type: 'list', name: 'bitcoin_mode', default: utils._getDefault( 'bitcoin_mode' ), - message: prefix()+'Where is your bitcoin full node running?'+'\n', + message: prefix()+'Where is your bitcoin full node running?', choices: [ { name: 'Nowhere! I want cyphernode to run one.', @@ -47,20 +47,20 @@ module.exports = { default: utils._getDefault( 'bitcoin_node_ip' ), filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, - message: prefix()+'What is your full node ip address?'+'\n', + message: prefix()+'What is your full node ip address?', }, { type: 'input', name: 'bitcoin_rpcuser', default: utils._getDefault( 'bitcoin_rpcuser' ), - message: prefix()+'Name of bitcoin rpc user?'+'\n', + message: prefix()+'Name of bitcoin rpc user?', filter: utils._trimFilter, }, { type: 'password', name: 'bitcoin_rpcpassword', default: utils._getDefault( 'bitcoin_rpcpassword' ), - message: prefix()+'Password of bitcoin rpc user?'+'\n', + message: prefix()+'Password of bitcoin rpc user?', filter: utils._trimFilter, }, { @@ -68,14 +68,14 @@ module.exports = { type: 'confirm', name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), - message: prefix()+'Run bitcoin node in prune mode?'+'\n', + message: prefix()+'Run bitcoin node in prune mode?', }, { when: bitcoinInternal, type: 'input', name: 'bitcoin_uacomment', default: utils._getDefault( 'bitcoin_uacomment' ), - message: prefix()+'Any UA comment?'+'\n', + message: prefix()+'Any UA comment?', filter: utils._trimFilter, validate: (input)=> {return utils._optional(input,utils._UACommentValidator) } }]; diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 084eae1ed..2d9e792e5 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -30,7 +30,7 @@ module.exports = { type: 'list', name: 'lightning_implementation', default: utils._getDefault( 'lightning_implementation' ), - message: prefix()+'What lightning implementation do you want to use?'+'\n', + message: prefix()+'What lightning implementation do you want to use?', choices: [ { name: 'C-lightning', @@ -51,7 +51,7 @@ module.exports = { default: utils._getDefault( 'lightning_external_ip' ), filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, - message: prefix()+'What external ip does your lightning node have?'+'\n', + message: prefix()+'What external ip does your lightning node have?', }, { when: featureCondition, @@ -60,7 +60,7 @@ module.exports = { default: utils._getDefault( 'lightning_nodename' ), filter: utils._trimFilter, validate: utils._notEmptyValidator, - message: prefix()+'What name has your lightning node?'+'\n', + message: prefix()+'What name has your lightning node?', }, { when: featureCondition, @@ -69,7 +69,7 @@ module.exports = { default: utils._getDefault( 'lightning_nodecolor' ), filter: utils._trimFilter, validate: utils._colorValidator, - message: prefix()+'What color has your lightning node?'+'\n', + message: prefix()+'What color has your lightning node?', }]; }, templates: function( props ) { diff --git a/install/generator-cyphernode/generators/app/prompters/300_electrum.js b/install/generator-cyphernode/generators/app/prompters/300_electrum.js index e5949c56d..400075dc1 100644 --- a/install/generator-cyphernode/generators/app/prompters/300_electrum.js +++ b/install/generator-cyphernode/generators/app/prompters/300_electrum.js @@ -24,7 +24,7 @@ module.exports = { type: 'list', name: 'electrum_implementation', default: utils._getDefault( 'electrum_implementation' ), - message: prefix()+'What electrum implementation do you want to use?'+'\n', + message: prefix()+'What electrum implementation do you want to use?', choices: [ { name: 'Electrum personal server', diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 81acf3699..92efa0bff 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -28,7 +28,7 @@ module.exports = { type: 'list', name: 'installer_mode', default: utils._getDefault( 'installer_mode' ), - message: prefix()+chalk.red('Where do you want to install cyphernode?')+'\n', + message: prefix()+chalk.red('Where do you want to install cyphernode?'), choices: [{ name: "Docker", value: "docker" @@ -47,7 +47,7 @@ module.exports = { default: utils._getDefault( 'proxy_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where to store your proxy db?'+'\n', + message: prefix()+'Where to store your proxy db?', }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, @@ -56,7 +56,7 @@ module.exports = { default: utils._getDefault( 'bitcoin_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your blockchain data?'+'\n', + message: prefix()+'Where is your blockchain data?', }, { when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, @@ -65,21 +65,21 @@ module.exports = { default: utils._getDefault( 'lightning_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your lightning node data?'+'\n', + message: prefix()+'Where is your lightning node data?', }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, type: 'confirm', name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), - message: prefix()+'Expose bitcoin full node outside of the docker network?'+'\n', + message: prefix()+'Expose bitcoin full node outside of the docker network?', }, { when: installerDocker, type: 'list', name: 'docker_mode', default: utils._getDefault( 'docker_mode' ), - message: prefix()+'What docker mode: docker swarm or docker-compose?'+'\n', + message: prefix()+'What docker mode: docker swarm or docker-compose?', choices: [{ name: "docker swarm", value: "swarm" From 1ffb836386e60b42bc7cfd2120b5cb62480d7808 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 22 Oct 2018 00:23:57 +0200 Subject: [PATCH 136/268] added optional help module --- .../generators/app/index.js | 36 +++++++--- .../generators/app/lib/help.js | 69 +++++++++++++++++++ .../app/prompters/000_cyphernode.js | 10 +-- .../generators/app/prompters/010_authapi.js | 12 ++-- .../generators/app/prompters/100_bitcoin.js | 14 ++-- .../generators/app/prompters/200_lightning.js | 8 +-- .../generators/app/prompters/300_electrum.js | 2 +- .../generators/app/prompters/999_installer.js | 10 +-- install/generator-cyphernode/package.json | 2 +- 9 files changed, 125 insertions(+), 38 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/help.js diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 24054f8fe..49a5fc6b0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -2,13 +2,13 @@ const Generator = require('yeoman-generator'); const chalk = require('chalk'); const fs = require('fs'); -const wrap = require('wordwrap')(86); const validator = require('validator'); const path = require("path"); const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); +const help = require('./lib/help.js'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars const userRegexp = /^[a-zA-Z0-9\._\-]+$/; @@ -40,7 +40,9 @@ action_conf=internal action_executecallbacks=internal `; - +const prefix = function() { + return chalk.green('Cyphernode')+': '; +}; let prompters = []; fs.readdirSync(path.join(__dirname, "prompters")).forEach(function(file) { @@ -116,7 +118,7 @@ module.exports = class extends Generator { r = await this.prompt([{ type: 'password', name: 'password', - message: chalk.bold.blue('Enter your configuration password?'), + message: prefix()+chalk.bold.blue('Enter your configuration password?'), filter: this._trimFilter }]); } @@ -156,13 +158,13 @@ module.exports = class extends Generator { r = await this.prompt([{ type: 'password', name: 'password0', - message: chalk.bold.blue('Choose your configuration password'), + message: prefix()+chalk.bold.blue('Choose your configuration password'), filter: this._trimFilter }, { type: 'password', name: 'password1', - message: chalk.bold.blue('Confirm your configuration password'), + message: prefix()+chalk.bold.blue('Confirm your configuration password'), filter: this._trimFilter }]); } @@ -192,6 +194,15 @@ module.exports = class extends Generator { // save auth key password to check if it changed this.auth_clientkeyspassword = this.props.auth_clientkeyspassword; + let r = await this.prompt([{ + type: 'confirm', + name: 'enablehelp', + message: prefix()+'Enable help?', + default: this._getDefault( 'enablehelp' ), + }]); + + this.props.enablehelp = r.enablehelp; + let prompts = []; for( let m of prompters ) { prompts = prompts.concat(m.prompts(this)); @@ -286,6 +297,7 @@ module.exports = class extends Generator { _assignConfigDefaults() { this.props = Object.assign( { features: [], + enablehelp: true, net: 'testnet', xpub: '', derivation_path: '0/n', @@ -387,12 +399,18 @@ module.exports = class extends Generator { return (input+"").trim(); } - _wrap(text) { - return wrap(text); - } - _featureChoices() { return this.featureChoices; } + _getHelp( topic ) { + if( !this.props.enablehelp ) + return ''; + const helpText = help.text( topic ); + if( !helpText ||helpText === '' ) { + return ''; + } + return "\n\n"+helpText+"\n\n"; + } + }; diff --git a/install/generator-cyphernode/generators/app/lib/help.js b/install/generator-cyphernode/generators/app/lib/help.js new file mode 100644 index 000000000..4d70ea603 --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/help.js @@ -0,0 +1,69 @@ +const chalk = require('chalk'); +const wrap = require('wrap-ansi'); + +module.exports = { + text: function( topic ) { + let r=wrap('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam', 82); + switch( topic ) { + case 'features': + break; + case 'net': + break; + case 'username': + break; + case 'xpub': + break; + case 'derivation_path': + break; + case 'auth_clientkeyspassword': + break; + case 'auth_recreatekeys': + break; + case 'auth_edit_ipwhitelist': + break; + case 'auth_ipwhitelist': + break; + case 'auth_edit_apiproperties': + break; + case 'auth_apiproperties': + break; + case 'bitcoin_mode': + break; + case 'bitcoin_node_ip': + break; + case 'bitcoin_rpcuser': + break; + case 'bitcoin_rpcpassword': + break; + case 'bitcoin_prune': + break; + case 'bitcoin_uacomment': + break; + case 'lightning_implementation': + break; + case 'lightning_external_ip': + break; + case 'lightning_nodename': + break; + case 'lightning_nodecolor': + break; + case 'electrum_implementation': + break; + case 'proxy_datapath': + break; + case 'bitcoin_datapath': + break; + case 'lightning_datapath': + break; + case 'bitcoin_expose': + break; + case 'docker_mode': + break; + } + return r; + } +} + + + + \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index 2e15d97fb..d412855b6 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -20,14 +20,14 @@ module.exports = { // input, confirm, list, rawlist, expand, checkbox, password, editor type: 'checkbox', name: 'features', - message: prefix()+'What features do you want to add to your cyphernode?', + message: prefix()+'What features do you want to add to your cyphernode?'+utils._getHelp('features'), choices: utils._featureChoices() }, { type: 'list', name: 'net', default: utils._getDefault( 'net' ), - message: prefix()+'What net do you want to run on?', + message: prefix()+'What net do you want to run on?'+utils._getHelp('net'), choices: [{ name: "Testnet", value: "testnet" @@ -40,7 +40,7 @@ module.exports = { type: 'input', name: 'username', default: utils._getDefault( 'username' ), - message: prefix()+'What username will cyphernode run under?', + message: prefix()+'What username will cyphernode run under?'+utils._getHelp('username'), filter: utils._trimFilter, validate: utils._usernameValidator }, @@ -48,7 +48,7 @@ module.exports = { type: 'input', name: 'xpub', default: utils._getDefault( 'xpub' ), - message: prefix()+'What is your xpub to watch?', + message: prefix()+'What is your xpub to watch?'+utils._getHelp('xpub'), filter: utils._trimFilter, validate: utils._xkeyValidator }, @@ -56,7 +56,7 @@ module.exports = { type: 'input', name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), - message: prefix()+'What is your address derivation path?', + message: prefix()+'What is your address derivation path?'+utils._getHelp('derivation_path'), filter: utils._trimFilter, validate: utils._derivationPathValidator }]; diff --git a/install/generator-cyphernode/generators/app/prompters/010_authapi.js b/install/generator-cyphernode/generators/app/prompters/010_authapi.js index 77858fb49..499dd52d7 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_authapi.js +++ b/install/generator-cyphernode/generators/app/prompters/010_authapi.js @@ -20,7 +20,7 @@ module.exports = { type: 'password', name: 'auth_clientkeyspassword', default: utils._getDefault( 'auth_clientkeyspassword' ), - message: prefix()+'Enter a password to protect your client keys with', + message: prefix()+'Enter a password to protect your client keys with'+utils._getHelp('auth_clientkeyspassword'), filter: utils._trimFilter, validate: utils._notEmptyValidator }, @@ -29,13 +29,13 @@ module.exports = { type: 'confirm', name: 'auth_recreatekeys', default: false, - message: prefix()+'Recreate auth keys?' + message: prefix()+'Recreate auth keys?'+utils._getHelp('auth_recreatekeys') }, { type: 'confirm', name: 'auth_edit_ipwhitelist', default: false, - message: prefix()+'Edit IP whitelist?' + message: prefix()+'Edit IP whitelist?'+utils._getHelp('auth_edit_ipwhitelist') }, { when: function( props ) { @@ -45,14 +45,14 @@ module.exports = { }, type: 'editor', name: 'auth_ipwhitelist', - message: 'IP whitelist', + message: prefix()+'IP whitelist'+utils._getHelp('auth_ipwhitelist'), default: utils._getDefault( 'auth_ipwhitelist' ) }, { type: 'confirm', name: 'auth_edit_apiproperties', default: false, - message: prefix()+'Edit API properties?' + message: prefix()+'Edit API properties?'+utils._getHelp('auth_edit_apiproperties') }, { when: function( props ) { @@ -62,7 +62,7 @@ module.exports = { }, type: 'editor', name: 'auth_apiproperties', - message: 'API properties', + message: prefix()+'API properties'+utils._getHelp('auth_apiproperties'), default: utils._getDefault( 'auth_apiproperties' ) }]; }, diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index 4f7dac999..0f1d1d521 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -28,7 +28,7 @@ module.exports = { type: 'list', name: 'bitcoin_mode', default: utils._getDefault( 'bitcoin_mode' ), - message: prefix()+'Where is your bitcoin full node running?', + message: prefix()+'Where is your bitcoin full node running?'+utils._getHelp('bitcoin_mode'), choices: [ { name: 'Nowhere! I want cyphernode to run one.', @@ -47,20 +47,20 @@ module.exports = { default: utils._getDefault( 'bitcoin_node_ip' ), filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, - message: prefix()+'What is your full node ip address?', + message: prefix()+'What is your full node ip address?'+utils._getHelp('bitcoin_node_ip'), }, { type: 'input', name: 'bitcoin_rpcuser', default: utils._getDefault( 'bitcoin_rpcuser' ), - message: prefix()+'Name of bitcoin rpc user?', + message: prefix()+'Name of bitcoin rpc user?'+utils._getHelp('bitcoin_rpcuser'), filter: utils._trimFilter, }, { type: 'password', name: 'bitcoin_rpcpassword', default: utils._getDefault( 'bitcoin_rpcpassword' ), - message: prefix()+'Password of bitcoin rpc user?', + message: prefix()+'Password of bitcoin rpc user?'+utils._getHelp('bitcoin_rpcpassword'), filter: utils._trimFilter, }, { @@ -68,14 +68,14 @@ module.exports = { type: 'confirm', name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), - message: prefix()+'Run bitcoin node in prune mode?', - }, + message: prefix()+'Run bitcoin node in prune mode?'+utils._getHelp('bitcoin_prune'), + }, // TODO: ask for size of prune { when: bitcoinInternal, type: 'input', name: 'bitcoin_uacomment', default: utils._getDefault( 'bitcoin_uacomment' ), - message: prefix()+'Any UA comment?', + message: prefix()+'Any UA comment?'+utils._getHelp('bitcoin_uacomment'), filter: utils._trimFilter, validate: (input)=> {return utils._optional(input,utils._UACommentValidator) } }]; diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 2d9e792e5..256b4b755 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -30,7 +30,7 @@ module.exports = { type: 'list', name: 'lightning_implementation', default: utils._getDefault( 'lightning_implementation' ), - message: prefix()+'What lightning implementation do you want to use?', + message: prefix()+'What lightning implementation do you want to use?'+utils._getHelp('lightning_implementation'), choices: [ { name: 'C-lightning', @@ -51,7 +51,7 @@ module.exports = { default: utils._getDefault( 'lightning_external_ip' ), filter: utils._trimFilter, validate: utils._ipOrFQDNValidator, - message: prefix()+'What external ip does your lightning node have?', + message: prefix()+'What external ip does your lightning node have?'+utils._getHelp('lightning_external_ip'), }, { when: featureCondition, @@ -60,7 +60,7 @@ module.exports = { default: utils._getDefault( 'lightning_nodename' ), filter: utils._trimFilter, validate: utils._notEmptyValidator, - message: prefix()+'What name has your lightning node?', + message: prefix()+'What name has your lightning node?'+utils._getHelp('lightning_nodename'), }, { when: featureCondition, @@ -69,7 +69,7 @@ module.exports = { default: utils._getDefault( 'lightning_nodecolor' ), filter: utils._trimFilter, validate: utils._colorValidator, - message: prefix()+'What color has your lightning node?', + message: prefix()+'What color has your lightning node?'+utils._getHelp('lightning_nodecolor'), }]; }, templates: function( props ) { diff --git a/install/generator-cyphernode/generators/app/prompters/300_electrum.js b/install/generator-cyphernode/generators/app/prompters/300_electrum.js index 400075dc1..1bf6dbcb8 100644 --- a/install/generator-cyphernode/generators/app/prompters/300_electrum.js +++ b/install/generator-cyphernode/generators/app/prompters/300_electrum.js @@ -24,7 +24,7 @@ module.exports = { type: 'list', name: 'electrum_implementation', default: utils._getDefault( 'electrum_implementation' ), - message: prefix()+'What electrum implementation do you want to use?', + message: prefix()+'What electrum implementation do you want to use?'+utils._getHelp('electrum_implementation'), choices: [ { name: 'Electrum personal server', diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 92efa0bff..7f4d7f285 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -47,7 +47,7 @@ module.exports = { default: utils._getDefault( 'proxy_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where to store your proxy db?', + message: prefix()+'Where to store your proxy db?'+utils._getHelp('proxy_datapath'), }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, @@ -56,7 +56,7 @@ module.exports = { default: utils._getDefault( 'bitcoin_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your blockchain data?', + message: prefix()+'Where is your blockchain data?'+utils._getHelp('bitcoin_datapath'), }, { when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, @@ -65,21 +65,21 @@ module.exports = { default: utils._getDefault( 'lightning_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your lightning node data?', + message: prefix()+'Where is your lightning node data?'+utils._getHelp('lightning_datapath'), }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, type: 'confirm', name: 'bitcoin_expose', default: utils._getDefault( 'bitcoin_expose' ), - message: prefix()+'Expose bitcoin full node outside of the docker network?', + message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils._getHelp('bitcoin_expose'), }, { when: installerDocker, type: 'list', name: 'docker_mode', default: utils._getDefault( 'docker_mode' ), - message: prefix()+'What docker mode: docker swarm or docker-compose?', + message: prefix()+'What docker mode: docker swarm or docker-compose?'+utils._getHelp('docker_mode'), choices: [{ name: "docker swarm", value: "swarm" diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index 0fd77c9c6..01ac1dc2b 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -24,7 +24,7 @@ "chalk": "^2.1.0", "coinstring": "^2.3.0", "validator": "^10.8.0", - "wordwrap": "^1.0.0", + "wrap-ansi": "^4.0.0", "yeoman-generator": "^2.0.1" }, "repository": "git@github.com:schulterklopfer/cyphernode.git", From 7391a29d37554a4e255a8867cf6ffd516cb1a652 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 24 Oct 2018 00:37:38 +0200 Subject: [PATCH 137/268] added gatekeeper to build script --- build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sh b/build.sh index ab95c5eeb..cf3068ead 100755 --- a/build.sh +++ b/build.sh @@ -52,6 +52,7 @@ build_docker_images() { build_docker_image install/SatoshiPortal/dockers/$archpath/ots/otsclient cyphernode/otsclient trace "Creating cyphernode images" + build_docker_image api_auth_docker/ cyphernode/gatekeeper build_docker_image proxy_docker/ cyphernode/proxy build_docker_image cron_docker/ cyphernode/proxycron build_docker_image pycoin_docker/ cyphernode/pycoin From 209d46453c313afa7559c664c2af23140e79db20 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 24 Oct 2018 00:39:26 +0200 Subject: [PATCH 138/268] removed config files from Dockerfile --- api_auth_docker/Dockerfile | 3 --- api_auth_docker/default-ssl.conf | 2 +- api_auth_docker/default.conf | 2 +- api_auth_docker/ip-whitelist.conf | 8 -------- api_auth_docker/keys.properties | 7 ------- 5 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 api_auth_docker/ip-whitelist.conf delete mode 100644 api_auth_docker/keys.properties diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index c1db73b2d..2060b1f96 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -11,11 +11,8 @@ RUN apk add --update --no-cache \ COPY auth.sh /etc/nginx/conf.d COPY default-ssl.conf /etc/nginx/conf.d/default.conf COPY entrypoint.sh entrypoint.sh -COPY keys.properties /etc/nginx/conf.d -COPY api.properties /etc/nginx/conf.d COPY trace.sh /etc/nginx/conf.d COPY tests.sh /etc/nginx/conf.d -COPY ip-whitelist.conf /etc/nginx/conf.d RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index d5487d741..4427e2434 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -2,7 +2,7 @@ server { listen 443 ssl; server_name localhost; - include /etc/nginx/conf.d/ip-whitelist.conf; + #include /etc/nginx/conf.d/ip-whitelist.conf; ssl_certificate /etc/ssl/certs/cert.pem; ssl_certificate_key /etc/ssl/private/key.pem; diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index e9d02c7b3..b3da9a144 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -2,7 +2,7 @@ server { listen 80; server_name localhost; - include /etc/nginx/conf.d/ip-whitelist.conf; + #include /etc/nginx/conf.d/ip-whitelist.conf; location / { auth_request /auth; diff --git a/api_auth_docker/ip-whitelist.conf b/api_auth_docker/ip-whitelist.conf deleted file mode 100644 index aa6e2c435..000000000 --- a/api_auth_docker/ip-whitelist.conf +++ /dev/null @@ -1,8 +0,0 @@ -# Leave commented if you don't want to use IP whitelist - -#real_ip_header X-Forwarded-For; -#set_real_ip_from 0.0.0.0/0; - -# List of white listed IP addresses... -#allow 45.56.67.78; -#deny all; diff --git a/api_auth_docker/keys.properties b/api_auth_docker/keys.properties deleted file mode 100644 index 1a35c4963..000000000 --- a/api_auth_docker/keys.properties +++ /dev/null @@ -1,7 +0,0 @@ -#kappiid="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact -kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} -kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} From 8bf849a980274a11496e9a4338c0266ffd783219 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 24 Oct 2018 00:39:59 +0200 Subject: [PATCH 139/268] more checks and file copying --- dist/setup.sh | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index b65ab744c..ef7a6be34 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -116,7 +116,7 @@ echo ' modify_permissions() { - local directories=("installer" "authentication" "lightning" "bitcoin" "docker-compose.yaml") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml") for d in "${directories[@]}" do if [[ -e $d ]]; then @@ -215,13 +215,33 @@ install_docker() { local sourceDataPath=./ + if [ ! -d $GATEKEEPER_DATAPATH ]; then + step " create $GATEKEEPER_DATAPATH" + try mkdir -p $GATEKEEPER_DATAPATH + next + fi + + if [ -d $GATEKEEPER_DATAPATH ]; then + copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties + copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties + copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf + fi + + if [ ! -d $PROXY_DATAPATH ]; then + step " create $PROXY_DATAPATH" + try mkdir -p $PROXY_DATAPATH + next + fi + if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then step " create $BITCOIN_DATAPATH" try mkdir -p $BITCOIN_DATAPATH next fi - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf + if [ -d $BITCOIN_DATAPATH ]; then + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf + fi fi if [[ $FEATURE_LIGHTNING == true ]]; then @@ -235,20 +255,15 @@ install_docker() { try mkdir -p $LIGHTNING_DATAPATH next fi - copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config - copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf + if [ -d $LIGHTNING_DATAPATH ]; then + copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config + copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf + fi fi fi - # build cyphernode images - if [ ! -d $PROXY_DATAPATH ]; then - step " create $PROXY_DATAPATH" - try mkdir -p $PROXY_DATAPATH - next - fi - if [[ ! $(docker network ls | grep cyphernodenet) =~ cyphernodenet ]]; then - step " createcyphernode network" + step " create cyphernode network" try docker network create cyphernodenet > /dev/null 2>&1 next fi From abd7cfb714577f7fc7be514b9bd762e9bffb2b5d Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 24 Oct 2018 00:43:20 +0200 Subject: [PATCH 140/268] added small help text system with simple formatting --- .../generators/app/help.json | 36 ++++++++++++++++ .../generators/app/index.js | 21 ++++++--- .../generators/app/lib/html2ansi.js | 43 +++++++++++++++++++ install/generator-cyphernode/package.json | 1 + 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/help.json create mode 100644 install/generator-cyphernode/generators/app/lib/html2ansi.js diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json new file mode 100644 index 000000000..a2ece9fb3 --- /dev/null +++ b/install/generator-cyphernode/generators/app/help.json @@ -0,0 +1,36 @@ +{ + "features": "", + "net": "", + "username": "", + "xpub": "", + "derivation_path": "", + "gatekeeper_clientkeyspassword": "", + "gatekeeper_recreatekeys": "", + "gatekeeper_edit_ipwhitelist": "", + "gatekeeper_ipwhitelist": "", + "gatekeeper_edit_apiproperties": "", + "gatekeeper_apiproperties": "", + "bitcoin_mode": "", + "bitcoin_node_ip": "", + "bitcoin_rpcuser": "", + "bitcoin_rpcpassword": "", + "bitcoin_prune": "", + "bitcoin_uacomment": "", + "lightning_implementation": "", + "lightning_external_ip": "", + "lightning_nodename": "", + "lightning_nodecolor": "", + "electrum_implementation": "", + "proxy_datapath": "", + "bitcoin_datapath": "", + "lightning_datapath": "", + "bitcoin_expose": "", + "docker_mode": "", + "__default__": "Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam" +} + + + + + + \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 49a5fc6b0..622b837b0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -1,18 +1,18 @@ 'use strict'; const Generator = require('yeoman-generator'); const chalk = require('chalk'); +const wrap = require('wrap-ansi'); +const html2ansi = require('./lib/html2ansi.js'); const fs = require('fs'); const validator = require('validator'); const path = require("path"); -const featureChoices = require(path.join(__dirname, "features.json")); const coinstring = require('coinstring'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); -const help = require('./lib/help.js'); +const featureChoices = require('./features.json'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars const userRegexp = /^[a-zA-Z0-9\._\-]+$/; - const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; @@ -203,6 +203,10 @@ module.exports = class extends Generator { this.props.enablehelp = r.enablehelp; + if( this.props.enablehelp ) { + this.help = require('./help.json'); + } + let prompts = []; for( let m of prompters ) { prompts = prompts.concat(m.prompts(this)); @@ -404,13 +408,18 @@ module.exports = class extends Generator { } _getHelp( topic ) { - if( !this.props.enablehelp ) + if( !this.props.enablehelp || !this.help ) { return ''; - const helpText = help.text( topic ); + } + + // TODO: remove default later: + const helpText = this.help[topic] || this.help['__default__']; + if( !helpText ||helpText === '' ) { return ''; } - return "\n\n"+helpText+"\n\n"; + + return "\n\n"+wrap( html2ansi(helpText),82 )+"\n\n"; } }; diff --git a/install/generator-cyphernode/generators/app/lib/html2ansi.js b/install/generator-cyphernode/generators/app/lib/html2ansi.js new file mode 100644 index 000000000..44f24eef0 --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/html2ansi.js @@ -0,0 +1,43 @@ +const parse5 = require('parse5'); +const chalk = require('chalk'); + +const options = { + scriptingEnabled: false +} + +const convert = function(data){ + + // recursively flatten + let v = data.childNodes && data.childNodes.length? + data.childNodes.map(d=> convert(d)).join(''): + data.value?data.value:''; + + switch(data.tagName){ + case 'br': + v += '\n' + break + case 'font': + if( data.attrs && data.attrs.length ) { + for( let attr of data.attrs ) { + if( attr.name === 'color' && /^#[a-f0-9]{6}$/.test(attr.value) ) { + v = chalk.hex(attr.value)(v); + } + if( attr.name === 'bold' && attr.value === 'true' ) { + v = chalk.bold(v); + } + if( attr.name === 'italic' && attr.value === 'true' ) { + v = chalk.italic(v); + } + if( attr.name === 'strikethrough' && attr.value === 'true' ) { + v = chalk.strikethrough(v); + } + } + } + break; + } + return v; +} + +module.exports = function(html){ + return convert(parse5.parseFragment(html, options)); +} diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index 01ac1dc2b..a5c047d48 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -23,6 +23,7 @@ "@rauschma/stringio": "^1.4.0", "chalk": "^2.1.0", "coinstring": "^2.3.0", + "parse5": "^5.1.0", "validator": "^10.8.0", "wrap-ansi": "^4.0.0", "yeoman-generator": "^2.0.1" From 71c7aa717069751da0247f25ff7473835d5c3f0c Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 24 Oct 2018 00:46:06 +0200 Subject: [PATCH 141/268] renamed authentication to gatekeeper. Added config templates and entry in docker-compose template --- .../generators/app/index.js | 35 +++++---- .../generators/app/lib/help.js | 69 ------------------ .../generators/app/prompters/010_authapi.js | 72 ------------------- .../app/prompters/010_gatekeeper.js | 72 +++++++++++++++++++ .../generators/app/prompters/999_installer.js | 9 +++ .../templates/authentication/keys.properties | 1 - .../api.properties | 2 +- .../ip-whitelist.conf | 2 +- .../app/templates/gatekeeper/keys.properties | 1 + .../app/templates/installer/config.sh | 1 + .../installer/docker/docker-compose.yaml | 19 +++++ 11 files changed, 121 insertions(+), 162 deletions(-) delete mode 100644 install/generator-cyphernode/generators/app/lib/help.js delete mode 100644 install/generator-cyphernode/generators/app/prompters/010_authapi.js create mode 100644 install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js delete mode 100644 install/generator-cyphernode/generators/app/templates/authentication/keys.properties rename install/generator-cyphernode/generators/app/templates/{authentication => gatekeeper}/api.properties (81%) rename install/generator-cyphernode/generators/app/templates/{authentication => gatekeeper}/ip-whitelist.conf (86%) create mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 622b837b0..1a4bf0eba 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -1,4 +1,3 @@ -'use strict'; const Generator = require('yeoman-generator'); const chalk = require('chalk'); const wrap = require('wrap-ansi'); @@ -191,8 +190,8 @@ module.exports = class extends Generator { return; } - // save auth key password to check if it changed - this.auth_clientkeyspassword = this.props.auth_clientkeyspassword; + // save gatekeeper key password to check if it changed + this.gatekeeper_clientkeyspassword = this.props.gatekeeper_clientkeyspassword; let r = await this.prompt([{ type: 'confirm', @@ -219,9 +218,9 @@ module.exports = class extends Generator { async configuring() { - if( this.props.auth_recreatekeys || - this.props.auth_keys.configEntries.length===0 ) { - delete this.props.auth_recreatekeys; + if( this.props.gatekeeper_recreatekeys || + this.props.gatekeeper_keys.configEntries.length===0 ) { + delete this.props.gatekeeper_recreatekeys; const apikey = new ApiKey(); let configEntries = []; @@ -245,7 +244,7 @@ module.exports = class extends Generator { configEntries.push(apikey.getConfigEntry()); clientInformation.push(apikey.getClientInformation()); - this.props.auth_keys = { + this.props.gatekeeper_keys = { configEntries: configEntries, clientInformation: clientInformation } @@ -272,15 +271,15 @@ module.exports = class extends Generator { } } - if( this.props.auth_keys && this.props.auth_keys.clientInformation ) { + if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { - if( this.auth_clientkeyspassword !== this.props.auth_clientkeyspassword ) { + if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword ) { fs.unlinkSync( this.destinationPath('clientKeys.7z') ); } - const archive = new Archive( this.destinationPath('clientKeys.7z'), this.props.auth_clientkeyspassword ); - if( !await archive.writeEntry( 'keys.txt', this.props.auth_keys.clientInformation.join('\n') ) ) { - console.log(chalk.bold.red( 'error! Client auth key archive was not written' )); + const archive = new Archive( this.destinationPath('clientKeys.7z'), this.props.gatekeeper_clientkeyspassword ); + if( !await archive.writeEntry( 'keys.txt', this.props.gatekeeper_keys.clientInformation.join('\n') ) ) { + console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); } } @@ -293,9 +292,9 @@ module.exports = class extends Generator { _hasAuthKeys() { return this.props && - this.props.auth_keys && - this.props.auth_keys.configEntries && - this.props.auth_keys.configEntries.length > 0; + this.props.gatekeeper_keys && + this.props.gatekeeper_keys.configEntries && + this.props.gatekeeper_keys.configEntries.length > 0; } _assignConfigDefaults() { @@ -318,9 +317,9 @@ module.exports = class extends Generator { bitcoin_node_ip: '', bitcoin_mode: 'internal', bitcoin_expose: false, - auth_apiproperties: defaultAPIProperties, - auth_ipwhitelist: '', - auth_keys: { configEntries: [], clientInformation: [] }, + gatekeeper_apiproperties: defaultAPIProperties, + gatekeeper_ipwhitelist: '', + gatekeeper_keys: { configEntries: [], clientInformation: [] }, proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_datapath: '', diff --git a/install/generator-cyphernode/generators/app/lib/help.js b/install/generator-cyphernode/generators/app/lib/help.js deleted file mode 100644 index 4d70ea603..000000000 --- a/install/generator-cyphernode/generators/app/lib/help.js +++ /dev/null @@ -1,69 +0,0 @@ -const chalk = require('chalk'); -const wrap = require('wrap-ansi'); - -module.exports = { - text: function( topic ) { - let r=wrap('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam', 82); - switch( topic ) { - case 'features': - break; - case 'net': - break; - case 'username': - break; - case 'xpub': - break; - case 'derivation_path': - break; - case 'auth_clientkeyspassword': - break; - case 'auth_recreatekeys': - break; - case 'auth_edit_ipwhitelist': - break; - case 'auth_ipwhitelist': - break; - case 'auth_edit_apiproperties': - break; - case 'auth_apiproperties': - break; - case 'bitcoin_mode': - break; - case 'bitcoin_node_ip': - break; - case 'bitcoin_rpcuser': - break; - case 'bitcoin_rpcpassword': - break; - case 'bitcoin_prune': - break; - case 'bitcoin_uacomment': - break; - case 'lightning_implementation': - break; - case 'lightning_external_ip': - break; - case 'lightning_nodename': - break; - case 'lightning_nodecolor': - break; - case 'electrum_implementation': - break; - case 'proxy_datapath': - break; - case 'bitcoin_datapath': - break; - case 'lightning_datapath': - break; - case 'bitcoin_expose': - break; - case 'docker_mode': - break; - } - return r; - } -} - - - - \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/010_authapi.js b/install/generator-cyphernode/generators/app/prompters/010_authapi.js deleted file mode 100644 index 499dd52d7..000000000 --- a/install/generator-cyphernode/generators/app/prompters/010_authapi.js +++ /dev/null @@ -1,72 +0,0 @@ -const chalk = require('chalk'); - -const name = 'authentication'; - -const capitalise = function( txt ) { - return txt.charAt(0).toUpperCase() + txt.substr(1); -}; - -const prefix = function() { - return chalk.bold.red(capitalise(name)+': '); -}; - -module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - // TODO: delete clientKeys archive when password chnages - return [{ - type: 'password', - name: 'auth_clientkeyspassword', - default: utils._getDefault( 'auth_clientkeyspassword' ), - message: prefix()+'Enter a password to protect your client keys with'+utils._getHelp('auth_clientkeyspassword'), - filter: utils._trimFilter, - validate: utils._notEmptyValidator - }, - { - when: utils._hasAuthKeys, - type: 'confirm', - name: 'auth_recreatekeys', - default: false, - message: prefix()+'Recreate auth keys?'+utils._getHelp('auth_recreatekeys') - }, - { - type: 'confirm', - name: 'auth_edit_ipwhitelist', - default: false, - message: prefix()+'Edit IP whitelist?'+utils._getHelp('auth_edit_ipwhitelist') - }, - { - when: function( props ) { - const r = props.auth_edit_ipwhitelist; - delete props.auth_edit_ipwhitelist; - return r; - }, - type: 'editor', - name: 'auth_ipwhitelist', - message: prefix()+'IP whitelist'+utils._getHelp('auth_ipwhitelist'), - default: utils._getDefault( 'auth_ipwhitelist' ) - }, - { - type: 'confirm', - name: 'auth_edit_apiproperties', - default: false, - message: prefix()+'Edit API properties?'+utils._getHelp('auth_edit_apiproperties') - }, - { - when: function( props ) { - const r = props.auth_edit_apiproperties; - delete props.auth_edit_apiproperties; - return r; - }, - type: 'editor', - name: 'auth_apiproperties', - message: prefix()+'API properties'+utils._getHelp('auth_apiproperties'), - default: utils._getDefault( 'auth_apiproperties' ) - }]; - }, - templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf' ]; - } -}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js new file mode 100644 index 000000000..3f0beb705 --- /dev/null +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -0,0 +1,72 @@ +const chalk = require('chalk'); + +const name = 'gatekeeper'; + +const capitalise = function( txt ) { + return txt.charAt(0).toUpperCase() + txt.substr(1); +}; + +const prefix = function() { + return chalk.bold.red(capitalise(name)+': '); +}; + +module.exports = { + name: function() { + return name; + }, + prompts: function( utils ) { + // TODO: delete clientKeys archive when password chnages + return [{ + type: 'password', + name: 'gatekeeper_clientkeyspassword', + default: utils._getDefault( 'gatekeeper_clientkeyspassword' ), + message: prefix()+'Enter a password to protect your client keys with'+utils._getHelp('gatekeeper_clientkeyspassword'), + filter: utils._trimFilter, + validate: utils._notEmptyValidator + }, + { + when: utils._hasAuthKeys, + type: 'confirm', + name: 'gatekeeper_recreatekeys', + default: false, + message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') + }, + { + type: 'confirm', + name: 'gatekeeper_edit_ipwhitelist', + default: false, + message: prefix()+'Edit IP whitelist?'+utils._getHelp('gatekeeper_edit_ipwhitelist') + }, + { + when: function( props ) { + const r = props.gatekeeper_edit_ipwhitelist; + delete props.gatekeeper_edit_ipwhitelist; + return r; + }, + type: 'editor', + name: 'gatekeeper_ipwhitelist', + message: utils._getHelp('gatekeeper_ipwhitelist')||' ', + default: utils._getDefault( 'gatekeeper_ipwhitelist' ) + }, + { + 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' ) + }]; + }, + templates: function( props ) { + return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf' ]; + } +}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 7f4d7f285..355fbb82c 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -40,6 +40,15 @@ module.exports = { }*/ ] }, + { + when: installerDocker, + type: 'input', + name: 'gatekeeper_datapath', + default: utils._getDefault( 'gatekeeper_datapath' ), + filter: utils._trimFilter, + validate: utils._pathValidator, + message: prefix()+'Where to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), + }, { when: installerDocker, type: 'input', diff --git a/install/generator-cyphernode/generators/app/templates/authentication/keys.properties b/install/generator-cyphernode/generators/app/templates/authentication/keys.properties deleted file mode 100644 index 8144e6979..000000000 --- a/install/generator-cyphernode/generators/app/templates/authentication/keys.properties +++ /dev/null @@ -1 +0,0 @@ -<%- auth_keys.configEntries.join('\n') %> diff --git a/install/generator-cyphernode/generators/app/templates/authentication/api.properties b/install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties similarity index 81% rename from install/generator-cyphernode/generators/app/templates/authentication/api.properties rename to install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties index 4899c6b90..aa952a141 100644 --- a/install/generator-cyphernode/generators/app/templates/authentication/api.properties +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/api.properties @@ -3,4 +3,4 @@ # Spender can do what the watcher can do plus more stuff # Admin can do what the spender can do plus even more stuff -<%- auth_apiproperties %> +<%- gatekeeper_apiproperties %> diff --git a/install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf b/install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf similarity index 86% rename from install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf rename to install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf index 4c961e382..f26314c77 100644 --- a/install/generator-cyphernode/generators/app/templates/authentication/ip-whitelist.conf +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf @@ -7,4 +7,4 @@ #allow 45.56.67.78; #deny all; -<%- auth_ipwhitelist %> \ No newline at end of file +<%- gatekeeper_ipwhitelist %> \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties b/install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties new file mode 100644 index 000000000..dc4c8c8aa --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/keys.properties @@ -0,0 +1 @@ +<%- gatekeeper_keys.configEntries.join('\n') %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index b89579cea..e4cb587ea 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -7,5 +7,6 @@ LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> +GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> USERNAME=<%= username %> 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 f270d5437..be3a87b51 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 @@ -1,6 +1,25 @@ version: "3" services: + gatekeeper: + # HTTP authentication API gate + environment: + - "TRACING=1" + image: cyphernode/gatekeeper + ports: + - "443:443" + volumes: + - "<%= gatekeeper_datapath %>/certs:/etc/ssl/certs" + - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" + - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties" + - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" + +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always proxy: command: $USER ./startproxy.sh # Bitcoin Mini Proxy From 86eee3ab33bd58c1ebadd41bff86d8c93b787c55 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 29 Oct 2018 23:06:09 +0100 Subject: [PATCH 142/268] is RUN_AS_USER is set, some commands are executed with sudo --- dist/setup.sh | 85 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index ef7a6be34..35a9fba20 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -120,11 +120,32 @@ modify_permissions() { for d in "${directories[@]}" do if [[ -e $d ]]; then - chmod -R og-rwx $d + step " modify permissions: $d" + try chmod -R og-rwx $d + next fi done } +modify_owner() { + if [[ ! ''$RUN_AS_USER == '' ]]; then + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) + for d in "${directories[@]}" + do + if [[ -e $d ]]; then + step " modify owner \"$RUN_AS_USER\": $d " + if [[ $(id -u) == 0 ]]; then + try chown -R $user $d + else + try sudo chown -R $user $d + fi + next + fi + done + fi +} + configure() { local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ## build setup docker image @@ -168,8 +189,13 @@ copy_file() { local doCopy=0 local sourceFile=$1 local targetFile=$2 + local sudo='' local createBackup=1 + if [[ $4 == 1 ]]; then + sudo='sudo ' + fi + if [[ ! ''$3 == '' ]]; then createBackup=$3 fi @@ -179,12 +205,12 @@ copy_file() { fi if [[ -f $targetFile ]]; then - cmp --silent $sourceFile $targetFile + ${sudo}cmp --silent $sourceFile $targetFile if [[ $? == 1 ]]; then # different content if [[ $createBackup == 1 ]]; then - step " create backup of $targetFile" - try cp $targetFile $targetFile-$(date +"%y-%m-%d-%T") + step " create backup of $targetFile " + try ${sudo}cp $targetFile $targetFile-$(date +"%y-%m-%d-%T") next fi doCopy=1 @@ -197,14 +223,47 @@ copy_file() { if [[ $doCopy == 1 ]]; then local basename=$(basename "$sourceFile") - step " copy $basename" - try cp $sourceFile $targetFile + step " copy $basename " + try ${sudo}cp $sourceFile $targetFile next fi } +create_user() { + #check if user exists + if [[ ! ''$RUN_AS_USER == '' ]]; then + local OS=$(uname -s) + + if [[ $OS == 'Darwin' ]]; then + echo "Automatic user creation not supported on OSX." + echo "Please create the user \"$RUN_AS_USER\" by hand." + else + if [[ ! $RUN_AS_USER ]]; then + echo "No runtime user. Aborting" + exit 1 + fi + + id -u $RUN_AS_USER > /dev/null 2>&1 + if [[ $? == 1 ]]; then + step " create user $RUN_AS_USER " + if [[ $(id -u) == 0 ]]; then + try useradd $RUN_AS_USER + else + try sudo useradd $RUN_AS_USER + fi + next + fi + fi + fi +} + install_docker() { + local sudo=0 + + if [[ ! ''$RUN_AS_USER == '' ]]; then + sudo=1 + fi local archpath=$(uname -m) # compat mode for SatoshiPortal repo @@ -222,9 +281,9 @@ install_docker() { fi if [ -d $GATEKEEPER_DATAPATH ]; then - copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties - copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties - copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf + copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 ${sudo} + copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 ${sudo} + copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf 1 ${sudo} fi if [ ! -d $PROXY_DATAPATH ]; then @@ -240,7 +299,7 @@ install_docker() { next fi if [ -d $BITCOIN_DATAPATH ]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} fi fi @@ -256,8 +315,8 @@ install_docker() { next fi if [ -d $LIGHTNING_DATAPATH ]; then - copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config - copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf + copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 ${sudo} + copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 ${sudo} fi fi fi @@ -284,6 +343,8 @@ install_docker() { next fi + create_user + modify_owner cowsay } From 2cf840ff1a62b81235318bf8860d7be87f7b090e Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 29 Oct 2018 23:06:44 +0100 Subject: [PATCH 143/268] added run_as_user prompts --- .../generators/app/prompters/000_cyphernode.js | 9 +++++++++ .../generators/app/templates/installer/config.sh | 2 +- .../generators/app/templates/installer/start.sh | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index d412855b6..622888099 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -37,6 +37,15 @@ module.exports = { }] }, { + type: 'confirm', + name: 'run_as_different_user', + default: utils._getDefault( 'run_as_different_user' ), + message: prefix()+'Run as different user?'+utils._getHelp('gatekeeper_edit_ipwhitelist') + }, + { + when: function( props ) { + return props.run_as_different_user; + }, type: 'input', name: 'username', default: utils._getDefault( 'username' ), diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index e4cb587ea..79b2ed7b3 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -9,4 +9,4 @@ LIGHTNING_DATAPATH=<%= lightning_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> -USERNAME=<%= username %> +RUN_AS_USER=<%= run_as_different_user?username:'' %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index b844b44c7..0cec47afc 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -1,11 +1,11 @@ #!/bin/sh -<% if (docker_mode == 'swarm') { %> -export USER=$(id -u):$(id -g) +# run as user <%= username %> +export USER=$(id -u <%= run_as_different_user?username:'' %>):$(id -g <%= run_as_different_user?username:'' %>) export ARCH=$(uname -m) + +<% if (docker_mode == 'swarm') { %> docker stack deploy -c docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> -export USER=$(id -u):$(id -g) -export ARCH=$(uname -m) docker-compose -f docker-compose.yaml up -d --remove-orphans <% } %> \ No newline at end of file From 7a8d72673b5da4994678e2732f0a3eada617136f Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 1 Nov 2018 18:33:27 +0100 Subject: [PATCH 144/268] run as user now works for gatekeeper. All keys and certs can be mode rw for the user which is used to run cyphernode. --- api_auth_docker/Dockerfile | 4 +++- api_auth_docker/entrypoint.sh | 18 ++++++++++++++++-- .../installer/docker/docker-compose.yaml | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 2060b1f96..33f0f877f 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -1,12 +1,14 @@ FROM nginx:alpine RUN apk add --update --no-cache \ + bash \ git \ openssl \ fcgiwrap \ spawn-fcgi \ curl \ - jq + jq \ + su-exec COPY auth.sh /etc/nginx/conf.d COPY default-ssl.conf /etc/nginx/conf.d/default.conf diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh index fb53a01e2..6c4c968ab 100644 --- a/api_auth_docker/entrypoint.sh +++ b/api_auth_docker/entrypoint.sh @@ -1,5 +1,19 @@ -#!/bin/sh +#!/bin/bash -spawn-fcgi -s /var/run/fcgiwrap.socket -u nginx -g nginx -U nginx -- `which fcgiwrap` +user='nginx' +if [[ $1 ]]; then + IFS=':' read -ra arr <<< "$1" + + if [[ ${arr[0]} ]]; then + user=${arr[0]}; + fi + +fi + +# create files with -rw-rw---- +# this will allow /var/run/fcgiwrap.socket to be accessed rw for group +su -c "umask 0006" $user + +spawn-fcgi -M 0660 -s /var/run/fcgiwrap.socket -u $user -g nginx -U $user -- `which fcgiwrap` nginx -g "daemon off;" 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 be3a87b51..4eae67563 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 @@ -13,6 +13,7 @@ services: - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties" - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" + command: $USER # deploy: # placement: From 1da3984e972322de0711a421fcc58b2b8b211eb7 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 1 Nov 2018 18:50:24 +0100 Subject: [PATCH 145/268] added default for run_as_different_user key --- install/generator-cyphernode/generators/app/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 1a4bf0eba..b67858ae3 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -307,6 +307,7 @@ module.exports = class extends Generator { installer_mode: 'docker', devmode: false, devregistry: false, + run_as_different_user: false, username: 'cyphernode', docker_mode: 'compose', bitcoin_rpcuser: 'bitcoin', From 91f8dc2346a430139999f053b23fa72dd1c1be43 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 1 Nov 2018 19:02:53 +0100 Subject: [PATCH 146/268] config tool now uses user which it runs under as default user for cyphernode, when run_as_user is not selected --- dist/setup.sh | 1 + install/generator-cyphernode/generators/app/index.js | 1 + .../generators/app/templates/installer/start.sh | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 35a9fba20..9ed8b9d6d 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -181,6 +181,7 @@ configure() { # configure features of cyphernode docker run -v $current_path:/data \ + -e DEFAULT_USER=$USER \ --log-driver=none$pw_env \ --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate } diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index b67858ae3..57574e438 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -327,6 +327,7 @@ module.exports = class extends Generator { lightning_nodename: '', lightning_nodecolor: '' }, this.props ); + this.props.default_username = process.env.DEFAULT_USER || ''; } _isChecked( name, value ) { diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 0cec47afc..189451e60 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -1,7 +1,7 @@ #!/bin/sh # run as user <%= username %> -export USER=$(id -u <%= run_as_different_user?username:'' %>):$(id -g <%= run_as_different_user?username:'' %>) +export USER=$(id -u <%= run_as_different_user?username:default_username %>):$(id -g <%= run_as_different_user?username:default_username %>) export ARCH=$(uname -m) <% if (docker_mode == 'swarm') { %> From 914f4fa69e853c6f1921ac9e7bfb0258ce1c7b5e Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 1 Nov 2018 19:41:07 +0100 Subject: [PATCH 147/268] only remove file if it actually exists :/ --- install/generator-cyphernode/generators/app/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 57574e438..bdf822bf6 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -273,7 +273,8 @@ module.exports = class extends Generator { if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { - if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword ) { + if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword && + fs.existsSync(this.destinationPath('clientKeys.7z')) ) { fs.unlinkSync( this.destinationPath('clientKeys.7z') ); } From d35b2766ab45a210035a750157dda5a9d646f0a6 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 2 Nov 2018 20:48:28 +0100 Subject: [PATCH 148/268] added creation of gatekeeper certs. cert is also added to clientKeys.7z to be used with curl or some other web browser. Using this cert on the client will prevent man in the middle attacks --- dist/setup.sh | 12 +++- install/Dockerfile | 2 +- .../generators/app/index.js | 39 ++++++++++++- .../generators/app/lib/cert.js | 56 +++++++++++++++++++ .../app/prompters/010_gatekeeper.js | 9 ++- .../app/templates/gatekeeper/cert.pem | 1 + .../app/templates/gatekeeper/key.pem | 1 + 7 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/cert.js create mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem create mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem diff --git a/dist/setup.sh b/dist/setup.sh index 9ed8b9d6d..8fca0c648 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -116,7 +116,7 @@ echo ' modify_permissions() { - local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml $BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") for d in "${directories[@]}" do if [[ -e $d ]]; then @@ -282,9 +282,19 @@ install_docker() { fi if [ -d $GATEKEEPER_DATAPATH ]; then + if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then + mkdir $GATEKEEPER_DATAPATH/certs + fi + + if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then + mkdir $GATEKEEPER_DATAPATH/private + fi + copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 ${sudo} copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 ${sudo} copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf 1 ${sudo} + copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 ${sudo} + copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 ${sudo} fi if [ ! -d $PROXY_DATAPATH ]; then diff --git a/install/Dockerfile b/install/Dockerfile index eb4ff0488..182f6d6cd 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:alpine -RUN apk add --update bash su-exec p7zip nano && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip openssl && 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 bdf822bf6..00c0aed5d 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -8,6 +8,7 @@ const path = require("path"); const coinstring = require('coinstring'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); +const Cert = require('./lib/cert.js'); const featureChoices = require('./features.json'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars @@ -220,7 +221,6 @@ module.exports = class extends Generator { async configuring() { if( this.props.gatekeeper_recreatekeys || this.props.gatekeeper_keys.configEntries.length===0 ) { - delete this.props.gatekeeper_recreatekeys; const apikey = new ApiKey(); let configEntries = []; @@ -248,7 +248,29 @@ module.exports = class extends Generator { configEntries: configEntries, clientInformation: clientInformation } - } + } + + if( this.props.gatekeeper_recreatecert || + !this.props.gatekeeper_sslcert || + !this.props.gatekeeper_sslkey ) { + const cert = new Cert(); + console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); + try { + const result = await cert.create(); + if( result.code === 0 ) { + this.props.gatekeeper_sslkey = result.key.toString(); + this.props.gatekeeper_sslcert = result.cert.toString(); + } else { + console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); + } + } catch( err ) { + console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); + } + } + + delete this.props.gatekeeper_recreatecert; + delete this.props.gatekeeper_recreatekeys; + } async writing() { @@ -282,6 +304,9 @@ module.exports = class extends Generator { if( !await archive.writeEntry( 'keys.txt', this.props.gatekeeper_keys.clientInformation.join('\n') ) ) { console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); } + if( !await archive.writeEntry( 'cacert.pem', this.props.gatekeeper_sslcert ) ) { + console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); + } } } @@ -298,6 +323,12 @@ module.exports = class extends Generator { this.props.gatekeeper_keys.configEntries.length > 0; } + _hasCert() { + return this.props && + this.props.gatekeeper_sslkey && + this.props.gatekeeper_sslcert + } + _assignConfigDefaults() { this.props = Object.assign( { features: [], @@ -326,7 +357,9 @@ module.exports = class extends Generator { lightning_implementation: 'c-lightning', lightning_datapath: '', lightning_nodename: '', - lightning_nodecolor: '' + lightning_nodecolor: '', + gatekeeper_sslcert: '', + gatekeeper_sslkey: '' }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; } diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js new file mode 100644 index 000000000..7108dabdd --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -0,0 +1,56 @@ +const fs = require('fs'); +const spawn = require('child_process').spawn; +const defaultArgs = ['req', '-x509', '-newkey', 'rsa:4096', '-nodes']; +const path = require('path'); +const tmp = require('tmp'); + +module.exports = class Cert { + + constructor( options ) { + options = options || {}; + this.args = options.args || { subj: '/CN=localhost', days: 3650 }; + } + + async create() { + + let args = defaultArgs.slice(); + + const certFileTmp = tmp.fileSync(); + const keyFileTmp = tmp.fileSync(); + + args.push( '-out' ); + args.push( certFileTmp.name ); + args.push( '-keyout' ); + args.push( keyFileTmp.name ); + + for( let k in this.args ) { + args.push( '-'+k); + args.push( this.args[k] ); + } + + const openssl = spawn('openssl', args, { stdio: ['ignore','ignore','ignore'] } ); + + let code = await new Promise( function(resolve, reject) { + openssl.on('exit', (code) => { + resolve(code); + }); + }); + + const cert = fs.readFileSync( certFileTmp.name ); + const key = fs.readFileSync( keyFileTmp.name ); + + certFileTmp.removeCallback(); + keyFileTmp.removeCallback(); + + return { + code: code, + key: key, + cert: cert + } + } + + getFullPath() { + return path.join( this.folder, this.filename ); + } + +} diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 3f0beb705..ad6cabb85 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -31,6 +31,13 @@ module.exports = { default: false, message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') }, + { + when: utils._hasCert, + type: 'confirm', + name: 'gatekeeper_recreatecert', + default: false, + message: prefix()+'Recreate gatekeeper ssl cert?'+utils._getHelp('gatekeeper_recreatecert') + }, { type: 'confirm', name: 'gatekeeper_edit_ipwhitelist', @@ -67,6 +74,6 @@ module.exports = { }]; }, templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf' ]; + return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf', 'cert.pem', 'key.pem' ]; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem b/install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem new file mode 100644 index 000000000..a1bab17be --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/cert.pem @@ -0,0 +1 @@ +<%- gatekeeper_sslcert %> \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem b/install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem new file mode 100644 index 000000000..affc9b422 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/key.pem @@ -0,0 +1 @@ +<%- gatekeeper_sslkey %> \ No newline at end of file From e7cbbfd0d29c584b06fd7889c0ef4c4b84d7cc87 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 2 Nov 2018 21:15:35 +0100 Subject: [PATCH 149/268] renamed clientKeys.7z to client.7z --- install/generator-cyphernode/generators/app/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 00c0aed5d..e1ad745c0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -296,11 +296,11 @@ module.exports = class extends Generator { if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { if( this.gatekeeper_clientkeyspassword !== this.props.gatekeeper_clientkeyspassword && - fs.existsSync(this.destinationPath('clientKeys.7z')) ) { - fs.unlinkSync( this.destinationPath('clientKeys.7z') ); + fs.existsSync(this.destinationPath('client.7z')) ) { + fs.unlinkSync( this.destinationPath('client.7z') ); } - const archive = new Archive( this.destinationPath('clientKeys.7z'), this.props.gatekeeper_clientkeyspassword ); + const archive = new Archive( this.destinationPath('client.7z'), this.props.gatekeeper_clientkeyspassword ); if( !await archive.writeEntry( 'keys.txt', this.props.gatekeeper_keys.clientInformation.join('\n') ) ) { console.log(chalk.bold.red( 'error! Client gatekeeper key archive was not written' )); } From 653c69f5bf4a64920ec6b9686223ffba782c1190 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:27:04 +0100 Subject: [PATCH 150/268] added cleanup prompt --- install/generator-cyphernode/generators/app/index.js | 3 ++- .../generators/app/prompters/999_installer.js | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index e1ad745c0..52c492960 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -359,7 +359,8 @@ module.exports = class extends Generator { lightning_nodename: '', lightning_nodecolor: '', gatekeeper_sslcert: '', - gatekeeper_sslkey: '' + gatekeeper_sslkey: '', + installer_cleanup: false }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; } diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 355fbb82c..19701f30b 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -96,8 +96,13 @@ module.exports = { { name: "docker-compose", value: "compose" - } - ] + }] + }, + { + type: 'confirm', + name: 'installer_cleanup', + default: utils._getDefault( 'installer_cleanup' ), + message: prefix()+'Cleanup installer after installation?'+utils._getHelp('installer_cleanup'), }]; }, templates: function( props ) { From 516b80ebd7f943e2ffac7da3cb522f1c6d1c9736 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:27:55 +0100 Subject: [PATCH 151/268] fixed recreation of keys and certs --- .../generators/app/index.js | 13 ------------- .../generators/app/prompters/010_gatekeeper.js | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 52c492960..b03d58f0a 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -316,19 +316,6 @@ module.exports = class extends Generator { /* some utils */ - _hasAuthKeys() { - return this.props && - this.props.gatekeeper_keys && - this.props.gatekeeper_keys.configEntries && - this.props.gatekeeper_keys.configEntries.length > 0; - } - - _hasCert() { - return this.props && - this.props.gatekeeper_sslkey && - this.props.gatekeeper_sslcert - } - _assignConfigDefaults() { this.props = Object.assign( { features: [], diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index ad6cabb85..77e96db2a 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -10,6 +10,19 @@ const prefix = function() { return chalk.bold.red(capitalise(name)+': '); }; +const hasAuthKeys = function( props ) { + return props && + props.gatekeeper_keys && + props.gatekeeper_keys.configEntries && + props.gatekeeper_keys.configEntries.length > 0; +} + +const hasCert = function( props ) { + return props && + props.gatekeeper_sslkey && + props.gatekeeper_sslcert +} + module.exports = { name: function() { return name; @@ -25,14 +38,14 @@ module.exports = { validate: utils._notEmptyValidator }, { - when: utils._hasAuthKeys, + when: function() { return hasAuthKeys( utils.props ); }, type: 'confirm', name: 'gatekeeper_recreatekeys', default: false, message: prefix()+'Recreate gatekeeper keys?'+utils._getHelp('gatekeeper_recreatekeys') }, { - when: utils._hasCert, + when: function() { return hasCert( utils.props ); }, type: 'confirm', name: 'gatekeeper_recreatecert', default: false, From aa645fc709b838bbf7e32eeb7b74e76f9ad51113 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:28:19 +0100 Subject: [PATCH 152/268] added cleanup var to config.sh --- .../generators/app/templates/installer/config.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 79b2ed7b3..028131f09 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -10,3 +10,4 @@ PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> +CLEANUP=<%= installer_cleanup?'true':'false' %> From 210d9aa5bf6866bd8a85e962a85b3d6f0464d680 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:32:08 +0100 Subject: [PATCH 153/268] removed opentimestamps feature --- .../generators/app/features.json | 4 --- .../generators/app/prompters/400_otsclient.js | 27 ------------------- .../app/templates/installer/config.sh | 1 - 3 files changed, 32 deletions(-) delete mode 100644 install/generator-cyphernode/generators/app/prompters/400_otsclient.js diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index 56ede64a9..bfe2e2fa4 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -4,10 +4,6 @@ "value": "lightning" }, - { - "name": "Open timestamps client", - "value": "otsclient" - }, { "name": "Electrum server", "value": "electrum" diff --git a/install/generator-cyphernode/generators/app/prompters/400_otsclient.js b/install/generator-cyphernode/generators/app/prompters/400_otsclient.js deleted file mode 100644 index ad78afd3a..000000000 --- a/install/generator-cyphernode/generators/app/prompters/400_otsclient.js +++ /dev/null @@ -1,27 +0,0 @@ -const chalk = require('chalk'); - -const name = 'otsclient'; - -const capitalise = function( txt ) { - return txt.charAt(0).toUpperCase() + txt.substr(1); -}; - -const prefix = function() { - return chalk.green(capitalise(name)+': '); -}; - -const featureCondition = function(props) { - return props.features && props.features.indexOf( name ) != -1; -}; - -module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - return []; - }, - templates: function( props ) { - return []; - } -}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 028131f09..79c3aa707 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,7 +1,6 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> -FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> From 283d4cf2b9c5b462d6d06e4229c470697441241a Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:32:58 +0100 Subject: [PATCH 154/268] removed electrum feature --- .../generators/app/features.json | 5 --- .../generators/app/prompters/300_electrum.js | 43 ------------------- .../app/templates/installer/config.sh | 1 - 3 files changed, 49 deletions(-) delete mode 100644 install/generator-cyphernode/generators/app/prompters/300_electrum.js diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index bfe2e2fa4..e0145ec9e 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -2,10 +2,5 @@ { "name": "Lightning node", "value": "lightning" - - }, - { - "name": "Electrum server", - "value": "electrum" } ] \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/prompters/300_electrum.js b/install/generator-cyphernode/generators/app/prompters/300_electrum.js deleted file mode 100644 index 1bf6dbcb8..000000000 --- a/install/generator-cyphernode/generators/app/prompters/300_electrum.js +++ /dev/null @@ -1,43 +0,0 @@ -const chalk = require('chalk'); - -const name = 'electrum'; - -const capitalise = function( txt ) { - return txt.charAt(0).toUpperCase() + txt.substr(1); -}; - -const prefix = function() { - return chalk.green(capitalise(name)+': '); -}; - -const featureCondition = function(props) { - return props.features && props.features.indexOf( name ) != -1; -}; - -module.exports = { - name: function() { - return name; - }, - prompts: function( utils ) { - return [{ - when: featureCondition, - type: 'list', - name: 'electrum_implementation', - default: utils._getDefault( 'electrum_implementation' ), - message: prefix()+'What electrum implementation do you want to use?'+utils._getHelp('electrum_implementation'), - choices: [ - { - name: 'Electrum personal server', - value: 'eps' - }, - { - name: 'Electrumx server', - value: 'elx' - } - ] - }]; - }, - templates: function( props ) { - return []; - } -}; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 79c3aa707..4a3c4d710 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,7 +1,6 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> -FEATURE_ELECTRUM=<%= (features.indexOf('electrum') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> From f4d6036a17a33d1d3ec5357816d83910978ac59f Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:34:33 +0100 Subject: [PATCH 155/268] removed question for lightning implementation, since there is only c-lightning support --- .../generators/app/prompters/200_lightning.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 256b4b755..5d758a4e3 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -25,7 +25,9 @@ module.exports = { return name; }, prompts: function( utils ) { - return [{ + return [ + /* + { when: featureCondition, type: 'list', name: 'lightning_implementation', @@ -35,15 +37,15 @@ module.exports = { { name: 'C-lightning', value: 'c-lightning' - } - /*, + }, { name: 'LND', value: 'lnd' } - */ + ] }, + */ { when: featureCondition, type: 'input', From 175d008f10146f4dd99e7445a9dd8d119fcf0fbf Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:50:44 +0100 Subject: [PATCH 156/268] removed ip whitelist option --- .../app/prompters/010_gatekeeper.js | 19 +------------------ .../templates/gatekeeper/ip-whitelist.conf | 10 ---------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 77e96db2a..4be7a2841 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -51,23 +51,6 @@ module.exports = { default: false, message: prefix()+'Recreate gatekeeper ssl cert?'+utils._getHelp('gatekeeper_recreatecert') }, - { - type: 'confirm', - name: 'gatekeeper_edit_ipwhitelist', - default: false, - message: prefix()+'Edit IP whitelist?'+utils._getHelp('gatekeeper_edit_ipwhitelist') - }, - { - when: function( props ) { - const r = props.gatekeeper_edit_ipwhitelist; - delete props.gatekeeper_edit_ipwhitelist; - return r; - }, - type: 'editor', - name: 'gatekeeper_ipwhitelist', - message: utils._getHelp('gatekeeper_ipwhitelist')||' ', - default: utils._getDefault( 'gatekeeper_ipwhitelist' ) - }, { type: 'confirm', name: 'gatekeeper_edit_apiproperties', @@ -87,6 +70,6 @@ module.exports = { }]; }, templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'ip-whitelist.conf', 'cert.pem', 'key.pem' ]; + return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem' ]; } }; \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf b/install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf deleted file mode 100644 index f26314c77..000000000 --- a/install/generator-cyphernode/generators/app/templates/gatekeeper/ip-whitelist.conf +++ /dev/null @@ -1,10 +0,0 @@ -# Leave commented if you don't want to use IP whitelist - -#real_ip_header X-Forwarded-For; -#set_real_ip_from 0.0.0.0/0; - -# List of white listed IP addresses... -#allow 45.56.67.78; -#deny all; - -<%- gatekeeper_ipwhitelist %> \ No newline at end of file From e8f4775fa74da50c069a89490a9e7451bd84abce Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 12:52:01 +0100 Subject: [PATCH 157/268] added option to expose lightning node port --- install/generator-cyphernode/generators/app/index.js | 1 + .../generators/app/prompters/999_installer.js | 7 +++++++ .../app/templates/installer/docker/docker-compose.yaml | 3 +++ 3 files changed, 11 insertions(+) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index b03d58f0a..54fb28f06 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -337,6 +337,7 @@ module.exports = class extends Generator { bitcoin_node_ip: '', bitcoin_mode: 'internal', bitcoin_expose: false, + lightning_expose: false, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 19701f30b..a844e2a26 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -83,6 +83,13 @@ module.exports = { default: utils._getDefault( 'bitcoin_expose' ), message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils._getHelp('bitcoin_expose'), }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, + type: 'confirm', + name: 'lightning_expose', + default: utils._getDefault( 'lightning_expose' ), + message: prefix()+'Expose lightning node outside of the docker network?'+utils._getHelp('lightning_expose'), + }, { when: installerDocker, type: 'list', 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 4eae67563..4d702d21d 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 @@ -85,8 +85,11 @@ services: lightning: command: $USER lightningd image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/clightning + +<% if( lightning_expose ) { %> ports: - "9735:9735" +<% } %> volumes: - "<%= lightning_datapath%>:/.lightning" - "<%= lightning_datapath%>/bitcoin.conf:/.bitcoin/bitcoin.conf" From f9eb3062d269afe2052f7fafb1195c80bf5af001 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 13:14:31 +0100 Subject: [PATCH 158/268] cyphernodenet is now created fitting to the docker mode (compose, swarm) --- dist/setup.sh | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 8fca0c648..3327de70f 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -332,10 +332,30 @@ install_docker() { fi fi - if [[ ! $(docker network ls | grep cyphernodenet) =~ cyphernodenet ]]; then - step " create cyphernode network" - try docker network create cyphernodenet > /dev/null 2>&1 - next + local net_entry=$(docker network ls | grep cyphernodenet); + + if [[ $net_entry =~ 'cyphernodenet' ]]; then + if [[ $net_entry =~ 'local' && $DOCKER_MODE == 'swarm' ]]; then + step " recreate cyphernode network" + try docker network rm cyphernodenet > /dev/null 2>&1 + try docker network create -d overlay cyphernodenet > /dev/null 2>&1 + next + elif [[ $net_entry =~ 'swarm' && $DOCKER_MODE == 'compose' ]]; then + step " recreate cyphernode network" + try docker network rm cyphernodenet > /dev/null 2>&1 + try docker network create cyphernodenet > /dev/null 2>&1 + next + fi + else + if [[ $DOCKER_MODE == 'swarm' ]]; then + step " create cyphernode network" + try docker network create -d overlay cyphernodenet > /dev/null 2>&1 + next + elif [[ $DOCKER_MODE == 'compose' ]]; then + step " create cyphernode network" + try docker network create cyphernodenet > /dev/null 2>&1 + next + fi fi copy_file $sourceDataPath/installer/docker/docker-compose.yaml docker-compose.yaml From 3df176a4df3b72493fdebe56316411425ad69a3b Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 17:23:34 +0100 Subject: [PATCH 159/268] changing mode of db file to +rw for owner and -rw for group and others --- proxy_docker/app/script/startproxy.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proxy_docker/app/script/startproxy.sh b/proxy_docker/app/script/startproxy.sh index 123796966..ab367cccb 100644 --- a/proxy_docker/app/script/startproxy.sh +++ b/proxy_docker/app/script/startproxy.sh @@ -35,6 +35,8 @@ if [ ! -e ${DB_FILE} ]; then cat watching.sql | sqlite3 $DB_FILE fi +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} From 51235d2e0671e0c388af22093d8f8bda9acc5f96 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 17:39:17 +0100 Subject: [PATCH 160/268] gatekeeper prompts now ask for confirmation of the keys archive password --- .../app/prompters/010_gatekeeper.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 4be7a2841..ef2dd84b0 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -23,6 +23,8 @@ const hasCert = function( props ) { props.gatekeeper_sslcert } +let password = ''; + module.exports = { name: function() { return name; @@ -37,6 +39,24 @@ 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()+'Config 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() { return hasAuthKeys( utils.props ); }, type: 'confirm', From 887ea909f3cd72da1bd3a18ad522886ac4ebb307 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 17:51:12 +0100 Subject: [PATCH 161/268] added image cleanup --- dist/setup.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index 3327de70f..2975d96ec 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -427,6 +427,12 @@ if [[ -f installer/config.sh ]]; then . installer/config.sh fi +if [[ $CLEANUP && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then + step " clean cyphernodeconf image" + try docker image rm cyphernodeconf > /dev/null 2>&1 + next +fi + modify_permissions if [[ $INSTALL == 1 ]]; then From 7f936436f4c8dcdee2f9929b05a548757abd3bcb Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 19:00:30 +0100 Subject: [PATCH 162/268] whoops! :D --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 2975d96ec..35701f3d7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -427,7 +427,7 @@ if [[ -f installer/config.sh ]]; then . installer/config.sh fi -if [[ $CLEANUP && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then +if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then step " clean cyphernodeconf image" try docker image rm cyphernodeconf > /dev/null 2>&1 next From 6cbb12aa3aa0d339605dc264014261fddc766663 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 3 Nov 2018 19:09:29 +0100 Subject: [PATCH 163/268] added proper pruning --- .../generators/app/index.js | 1 + .../generators/app/prompters/100_bitcoin.js | 20 +++++++++++++++++++ .../app/templates/bitcoin/bitcoin.conf | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 54fb28f06..c7c147259 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -333,6 +333,7 @@ module.exports = class extends Generator { bitcoin_rpcpassword: 'CHANGEME', bitcoin_uacomment: '', bitcoin_prune: false, + bitcoin_prune_size: 550, bitcoin_datapath: '', bitcoin_node_ip: '', bitcoin_mode: 'internal', diff --git a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js index 0f1d1d521..62d786289 100644 --- a/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js +++ b/install/generator-cyphernode/generators/app/prompters/100_bitcoin.js @@ -18,6 +18,10 @@ const bitcoinInternal = function(props) { return props.bitcoin_mode === 'internal' }; +const bitcoinInternalAndPrune = function(props) { + return bitcoinInternal(props) && props.bitcoin_prune; +}; + module.exports = { name: function() { return name; @@ -69,6 +73,22 @@ module.exports = { name: 'bitcoin_prune', default: utils._getDefault( 'bitcoin_prune' ), message: prefix()+'Run bitcoin node in prune mode?'+utils._getHelp('bitcoin_prune'), + }, + { + when: bitcoinInternalAndPrune, + type: 'input', + name: 'bitcoin_prune_size', + default: utils._getDefault( 'bitcoin_prune_size' ), + message: prefix()+'What is the maximum size of your blockchain data in megabytes?'+utils._getHelp('bitcoin_prune_size'), + validate: function( input ) { + if( ! /^\d+$/.test(input) ) { + throw new Error( "Not a number"); + } + if( input < 550 ) { + throw new Error( "At least 550 is required"); + } + return true; + } }, // TODO: ask for size of prune { when: bitcoinInternal, diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 2b670ae96..7fc76512b 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -3,7 +3,12 @@ testnet=1 <% } %> +<% if (bitcoin_prune) { %> +prune=<%= bitcoin_prune_size || 550 %> +<% } else { %> txindex=1 +<% } %> + zmqpubrawblock=tcp://0.0.0.0:18501 zmqpubrawtx=tcp://0.0.0.0:18502 From ff291c4a0e093c820c52a6ff7c573f4e6ee0a71f Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 01:52:17 +0100 Subject: [PATCH 164/268] added some sanity checks before copying bitcoin.conf --- dist/setup.sh | 122 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 35701f3d7..dc038e68a 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -216,7 +216,7 @@ copy_file() { fi doCopy=1 else - logline "identical $targetFile" + logline "identical $sourceFile == $targetFile" fi else doCopy=1 @@ -224,7 +224,7 @@ copy_file() { if [[ $doCopy == 1 ]]; then local basename=$(basename "$sourceFile") - step " copy $basename " + step " copy $sourceFile => $targetFile " try ${sudo}cp $sourceFile $targetFile next fi @@ -258,6 +258,84 @@ create_user() { fi } + +process_bitcoinconf() { + + local bitcoinconf=$1 + + # grep for prune entry and delete all whitespaces + local pruneEntry=$(grep -e ^prune $bitcoinconf | tr -d '[:space:]') + local txindexEntry=$(grep -e ^txindex $bitcoinconf | tr -d '[:space:]') + local testnetEntry=$(grep -e ^testnet $bitcoinconf | tr -d '[:space:]') + local regtestEntry=$(grep -e ^regtest $bitcoinconf | tr -d '[:space:]') + + local prune=0 + local txindex=0 + local testnet=0 + local regtest=0 + + if [[ $pruneEntry =~ ^prune && ! $pruneEntry == 'prune=0' ]]; then + prune=1 + fi + + if [[ $txindexEntry =~ ^txindex && ! $txindexEntry == 'txindex=0' ]]; then + txindex=1 + fi + + if [[ $testnetEntry =~ ^testnet && ! $testnetEntry == 'testnet=0' ]]; then + testnet=1 + fi + + if [[ $regtestEntry =~ ^regtest && ! $regtestEntry == 'regtest=0' ]]; then + regtest=1 + fi + # prune & txindex: 3 + # !prune & txindex: 2 + # prune & !txindex: 1 + # !prune & !txindex: 0 + echo $(($prune|$txindex<<1|$testnet<<2|$regtest<<3)) +} + + +compare_bitcoinconf() { + + local new_bitcoinconf=$1 + local old_bitcoinconf=$2 + + local old_config=$(process_bitcoinconf $old_bitcoinconf ) + local new_config=$(process_bitcoinconf $new_bitcoinconf ) + + local old_prune=$(($old_config&1)) + local old_txindex=$((($old_config>>1)&1)) + local old_testnet=$((($old_config>>2)&1)) + local old_regtest=$((($old_config>>3)&1)) + local new_prune=$(($new_config&1)) + local new_txindex=$((($new_config>>1)&1)) + local new_testnet=$((($new_config>>2)&1)) + local new_regtest=$((($new_config>>3)&1)) + + local status + + if [[ $new_prune == 1 && $old_prune == 0 ]]; then + # warn about data loss + # ask for user permission + status='dataloss' + fi + + if [[ $new_txindex == 1 && $old_txindex == 0 ]]; then + # warn about reindexing + status='reindex' + fi + + if [[ ! $new_testnet == $old_testnet || ! $new_regtest == $old_regtest ]]; then + # warn about reindexing + status='incompatible' + fi + + echo $status + +} + install_docker() { local sudo=0 @@ -273,7 +351,7 @@ install_docker() { archpath="rpi" fi - local sourceDataPath=./ + local sourceDataPath=. if [ ! -d $GATEKEEPER_DATAPATH ]; then step " create $GATEKEEPER_DATAPATH" @@ -310,7 +388,37 @@ install_docker() { next fi if [ -d $BITCOIN_DATAPATH ]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} + + local cmpStatus=$(compare_bitcoinconf $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) + + if [[ $cmpStatus == 'dataloss' ]]; then + if [[ $ALWAYSYES == 1 ]]; then + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} + else + while true; do + echo " Really copy bitcoin.conf with pruning option?" + read -p " This will discard some blockchain data. (yn) " yn + case $yn in + [Yy]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo}; break;; + [Nn]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 ${sudo} + echo " Your cyphernode installation is most likely broken." + echo " Please check bitcoin.conf.cyphernode on how to repair it manually."; + break;; + * ) echo "Please answer yes or no.";; + esac + done + fi + elif [[ $cmpStatus == 'incompatible' ]]; then + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 ${sudo} + echo " Blockchain data is not compatible, due to misconfigured nets." + echo " Your cyphernode installation is most likely broken." + echo " Please check bitcoin.conf.cyphernode on how to repair it manually." + else + if [[ $cmpStatus == 'reindex' ]]; then + echo " Warning Reindexing will take some time." + fi + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} + fi fi fi @@ -392,8 +500,9 @@ CONFIGURE=0 INSTALL=0 RECREATE=0 TRACING=1 +ALWAYSYES=0 -while getopts ":cirh" opt; do +while getopts ":cirhy" opt; do case $opt in r) RECREATE=1 @@ -404,6 +513,9 @@ while getopts ":cirh" opt; do i) INSTALL=1 ;; + y) + ALWAYSYES=1 + ;; h) echo "Use -c to configure and -i to install or -r to recreate from config.json." >&2 exit From e1f04213fcf8110353f36580719119a8c29e6834 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 15:02:09 +0100 Subject: [PATCH 165/268] added some sanity checks before running the actual install --- dist/setup.sh | 137 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index dc038e68a..062ef2af7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -128,7 +128,7 @@ modify_permissions() { } modify_owner() { - if [[ ! ''$RUN_AS_USER == '' ]]; then + if [[ ! $RUN_AS_USER == $USER ]]; then local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) for d in "${directories[@]}" @@ -232,15 +232,15 @@ copy_file() { create_user() { #check if user exists - if [[ ! ''$RUN_AS_USER == '' ]]; then + if [[ ! $RUN_AS_USER == $USER ]]; then local OS=$(uname -s) if [[ $OS == 'Darwin' ]]; then - echo "Automatic user creation not supported on OSX." - echo "Please create the user \"$RUN_AS_USER\" by hand." + echo " Automatic user creation not supported on OSX." + echo " Please create the user \"$RUN_AS_USER\" by hand." else if [[ ! $RUN_AS_USER ]]; then - echo "No runtime user. Aborting" + echo " No runtime user. Aborting" exit 1 fi @@ -258,7 +258,6 @@ create_user() { fi } - process_bitcoinconf() { local bitcoinconf=$1 @@ -338,11 +337,6 @@ compare_bitcoinconf() { install_docker() { - local sudo=0 - - if [[ ! ''$RUN_AS_USER == '' ]]; then - sudo=1 - fi local archpath=$(uname -m) # compat mode for SatoshiPortal repo @@ -368,11 +362,11 @@ install_docker() { mkdir $GATEKEEPER_DATAPATH/private fi - copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 ${sudo} - copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 ${sudo} - copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf 1 ${sudo} - copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 ${sudo} - copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 ${sudo} + copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED + copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED + copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf 1 $SUDO_REQUIRED + copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED + copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED fi if [ ! -d $PROXY_DATAPATH ]; then @@ -393,14 +387,14 @@ install_docker() { if [[ $cmpStatus == 'dataloss' ]]; then if [[ $ALWAYSYES == 1 ]]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED else while true; do echo " Really copy bitcoin.conf with pruning option?" read -p " This will discard some blockchain data. (yn) " yn case $yn in - [Yy]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo}; break;; - [Nn]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 ${sudo} + [Yy]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED; break;; + [Nn]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually."; break;; @@ -409,7 +403,7 @@ install_docker() { done fi elif [[ $cmpStatus == 'incompatible' ]]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 ${sudo} + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED echo " Blockchain data is not compatible, due to misconfigured nets." echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually." @@ -417,7 +411,7 @@ install_docker() { if [[ $cmpStatus == 'reindex' ]]; then echo " Warning Reindexing will take some time." fi - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 ${sudo} + copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED fi fi fi @@ -434,8 +428,8 @@ install_docker() { next fi if [ -d $LIGHTNING_DATAPATH ]; then - copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 ${sudo} - copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 ${sudo} + copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED + copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED fi fi fi @@ -487,6 +481,91 @@ install_docker() { cowsay } +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 status=0 + for d in "${directories[@]}" + do + if [[ -e $d ]]; then + # is it mine and does it have rw ? + # don't care about group rights + if [[ ! -r $d || ! -w $d ]]; then + status=1 + break; + fi + # TODO: does parent exist and do we have rw on that? + fi + done + echo $status +} + +check_is_sudoer() { + echo " check Cyphernode installer has determined that it needs sudo to continue." + echo " Let's verify that you have sudo rights..." + sudo echo " Yes! You have what it takes to run cyphernode." + + if [[ $? == 1 ]]; then + echo " AARGH! Mein Leben..." + return 1 + fi +} + +check_bitcoind() { + echo 0 +} + +sanity_checks() { + + echo " check requirements." + + if [[ ''$RUN_AS_USER == '' ]]; then + RUN_AS_USER=$USER + fi + + local sudo=0 + local sudo_reason + + if [[ ! ''$RUN_AS_USER == ''$USER ]]; then + sudo=1 + sudo_reason='user' + fi + + if [[ $sudo == 0 ]]; then + # we still don't need sudo. Let's check access to directories + sudo=$(check_directory_owner) + sudo_reason='directories' + fi + + if [[ $sudo == 1 ]]; then + check_is_sudoer + if [[ $?==1 ]]; then + echo " To fix this, either ask your administrator to add you to the sudo group" + if [[ $sudo_reason == 'user' ]]; then + echo " or do not use the 'run as different user' option." + fi + 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 status=0 + for d in "${directories[@]}" + do + if [[ -e $d ]]; then + echo " $d" + fi + done + + fi + exit + else + SUDO_REQUIRED=1 + fi + else + echo " check everything seems to be ok." + fi +} + install() { if [[ ''$INSTALLER_MODE == 'none' ]]; then echo "Skipping installation phase" @@ -495,12 +574,20 @@ install() { fi } - CONFIGURE=0 INSTALL=0 RECREATE=0 TRACING=1 ALWAYSYES=0 +SUDO_REQUIRED=0 + +# trap ctrl-c and call ctrl_c() +trap ctrl_c INT + +function ctrl_c() { + echo " Canceling installation process." + exit +} while getopts ":cirhy" opt; do case $opt in @@ -539,6 +626,8 @@ if [[ -f installer/config.sh ]]; then . installer/config.sh fi +sanity_checks + if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then step " clean cyphernodeconf image" try docker image rm cyphernodeconf > /dev/null 2>&1 From 474750c332fcc36fc08e522ea9db03a1e150a396 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 15:36:43 +0100 Subject: [PATCH 166/268] didididi :D tiny brain... tiny brain! --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 062ef2af7..38d85cb1b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -174,7 +174,7 @@ configure() { if [[ $arch =~ ^arm ]]; then - clear && echo "Thinking. This may take a while, since I'm a Raspberry PI and my brain is so small. :D" + clear && echo "Thinking. This may take a while, since I'm a Raspberry PI and my brain is so tiny. :(" else clear && echo "Thinking..." fi From c1459a51d3be1d02ec89607d87b3bec27baf77f0 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:07:17 +0100 Subject: [PATCH 167/268] added nano --- install/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/Dockerfile b/install/Dockerfile index 182f6d6cd..120ac66d8 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,6 +1,6 @@ FROM node:alpine -RUN apk add --update bash su-exec p7zip openssl && rm -rf /var/cache/apk/* +RUN apk add --update bash su-exec p7zip openssl nano && rm -rf /var/cache/apk/* RUN mkdir -p /app RUN mkdir /.config RUN chmod a+rwx /.config From aedb6b4790fe7715d63eeaf81529d8001a1c2fef Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:07:43 +0100 Subject: [PATCH 168/268] some rearranging --- install/generator-cyphernode/generators/app/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index c7c147259..5e5910fd7 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -342,13 +342,13 @@ module.exports = class extends Generator { gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, + gatekeeper_sslcert: '', + gatekeeper_sslkey: '', proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_datapath: '', lightning_nodename: '', lightning_nodecolor: '', - gatekeeper_sslcert: '', - gatekeeper_sslkey: '', installer_cleanup: false }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; From a08d3c29c9d8b1cc421d871ded31f7565d60221a Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:08:00 +0100 Subject: [PATCH 169/268] added help text --- .../generators/app/prompters/999_installer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index a844e2a26..ed2d74c68 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -28,7 +28,7 @@ module.exports = { type: 'list', name: 'installer_mode', default: utils._getDefault( 'installer_mode' ), - message: prefix()+chalk.red('Where do you want to install cyphernode?'), + message: prefix()+chalk.red('Where do you want to install cyphernode?')+utils._getHelp('installer_mode'), choices: [{ name: "Docker", value: "docker" From a458c72aa65e9325ccc7148c1387ea1dd2524bd0 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:08:19 +0100 Subject: [PATCH 170/268] fixed wrong help text key --- .../generators/app/prompters/000_cyphernode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index 622888099..6aef4d337 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -40,7 +40,7 @@ module.exports = { type: 'confirm', name: 'run_as_different_user', default: utils._getDefault( 'run_as_different_user' ), - message: prefix()+'Run as different user?'+utils._getHelp('gatekeeper_edit_ipwhitelist') + message: prefix()+'Run as different user?'+utils._getHelp('run_as_different_user') }, { when: function( props ) { From 8ca8ec90d68d797b90a2c5a7835d11a3f9d3b004 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:08:51 +0100 Subject: [PATCH 171/268] added missing help texts and markers to see where they will be shown --- .../generators/app/help.json | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index a2ece9fb3..9d2453455 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -1,36 +1,38 @@ { - "features": "", - "net": "", - "username": "", - "xpub": "", - "derivation_path": "", - "gatekeeper_clientkeyspassword": "", - "gatekeeper_recreatekeys": "", - "gatekeeper_edit_ipwhitelist": "", - "gatekeeper_ipwhitelist": "", - "gatekeeper_edit_apiproperties": "", - "gatekeeper_apiproperties": "", - "bitcoin_mode": "", - "bitcoin_node_ip": "", - "bitcoin_rpcuser": "", - "bitcoin_rpcpassword": "", - "bitcoin_prune": "", - "bitcoin_uacomment": "", - "lightning_implementation": "", - "lightning_external_ip": "", - "lightning_nodename": "", - "lightning_nodecolor": "", - "electrum_implementation": "", - "proxy_datapath": "", - "bitcoin_datapath": "", - "lightning_datapath": "", - "bitcoin_expose": "", - "docker_mode": "", - "__default__": "Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam" + "features": "** features **", + "net": "** net **", + "run_as_different_user": "** run_as_different_user **", + "username": "** username **", + "xpub": "** xpub **", + "derivation_path": "** derivation_path **", + "proxy_datapath": "** proxy_datapath **", + "gatekeeper_clientkeyspassword": "** gatekeeper_clientkeyspassword **", + "gatekeeper_clientkeyspassword_c": "** gatekeeper_clientkeyspassword_c **", + "gatekeeper_recreatekeys": "** gatekeeper_recreatekeys **", + "gatekeeper_recreatecert": "** gatekeeper_recreatecert **", + "gatekeeper_datapath": "** gatekeeper_datapath **", + "gatekeeper_edit_apiproperties": "** gatekeeper_edit_apiproperties **", + "gatekeeper_apiproperties": "** gatekeeper_apiproperties **", + "gatekeeper_sslcert": "** gatekeeper_sslcert **", + "gatekeeper_sslkey": "** gatekeeper_sslkey **", + "bitcoin_mode": "** bitcoin_mode **", + "bitcoin_node_ip": "** bitcoin_node_ip **", + "bitcoin_rpcuser": "** bitcoin_rpcuser **", + "bitcoin_rpcpassword": "** bitcoin_rpcpassword **", + "bitcoin_prune": "** bitcoin_prune **", + "bitcoin_prunesize": "** bitcoin_prunesize **", + "bitcoin_uacomment": "** bitcoin_uacomment **", + "bitcoin_datapath": "** bitcoin_datapath **", + "bitcoin_expose": "** bitcoin_expose **", + "lightning_implementation": "** lightning_implementation **", + "lightning_external_ip": "** lightning_external_ip **", + "lightning_nodename": "** lightning_nodename **", + "lightning_nodecolor": "** lightning_nodecolor **", + "lightning_datapath": "** lightning_datapath **", + "lightning_expose": "** lightning_expose **", + "installer_mode": "** installer_mode **", + "installer_cleanup": "** installer_cleanup **", + "docker_mode": "** docker_mode **", + "__default__": "Key missing!
There is no help text for this entry. :-(" } - - - - - \ No newline at end of file From b2e2dfed425a06eb0d11c2893cbfd72dad52d536 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 16:52:33 +0100 Subject: [PATCH 172/268] bitcoin.conf comparison should check if file exists! >:-[ --- dist/setup.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 38d85cb1b..4cdd41bb9 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -300,6 +300,12 @@ compare_bitcoinconf() { local new_bitcoinconf=$1 local old_bitcoinconf=$2 + local status + + if [[ ! -f $old_bitcoinconf || ! -f $new_bitcoinconf ]]; then + return 1 + fi + local old_config=$(process_bitcoinconf $old_bitcoinconf ) local new_config=$(process_bitcoinconf $new_bitcoinconf ) @@ -313,7 +319,6 @@ compare_bitcoinconf() { local new_testnet=$((($new_config>>2)&1)) local new_regtest=$((($new_config>>3)&1)) - local status if [[ $new_prune == 1 && $old_prune == 0 ]]; then # warn about data loss From 3825cecb40860913dbf15908527b74554d725860 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 17:38:13 +0100 Subject: [PATCH 173/268] simplificationa and bug fix --- dist/setup.sh | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 4cdd41bb9..041f605da 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -505,17 +505,6 @@ check_directory_owner() { echo $status } -check_is_sudoer() { - echo " check Cyphernode installer has determined that it needs sudo to continue." - echo " Let's verify that you have sudo rights..." - sudo echo " Yes! You have what it takes to run cyphernode." - - if [[ $? == 1 ]]; then - echo " AARGH! Mein Leben..." - return 1 - fi -} - check_bitcoind() { echo 0 } @@ -543,8 +532,12 @@ sanity_checks() { fi if [[ $sudo == 1 ]]; then - check_is_sudoer - if [[ $?==1 ]]; then + echo " check Cyphernode installer has determined that it needs sudo to continue." + echo " Let's verify that you have sudo rights..." + sudo echo " Yes! You have what it takes to run cyphernode." + + if [[ $? == 1 ]]; then + echo " AARGH! Mein Leben..." echo " To fix this, either ask your administrator to add you to the sudo group" if [[ $sudo_reason == 'user' ]]; then echo " or do not use the 'run as different user' option." @@ -629,6 +622,7 @@ fi if [[ -f installer/config.sh ]]; then . installer/config.sh + RUN_AS_USER="blah" fi sanity_checks From f5bcfe508950c7abb68f3e4fef7a37db880540cc Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 17:51:45 +0100 Subject: [PATCH 174/268] modify permissions now uses sudo when required --- dist/setup.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 041f605da..9b67f32dd 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -121,7 +121,15 @@ modify_permissions() { do if [[ -e $d ]]; then step " modify permissions: $d" - try chmod -R og-rwx $d + if [[ $SUDO_REQUIRED ]]; then + if [[ $(id -u) == 0 ]]; then + try chmod -R og-rwx $d + else + try sudo chmod -R og-rwx $d + fi + else + try chmod -R og-rwx $d + fi next fi done From 582a0098b598f33ba692d52d106a1b74e77a3ac5 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 17:53:02 +0100 Subject: [PATCH 175/268] damnit! --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 9b67f32dd..2ed19917f 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -121,7 +121,7 @@ modify_permissions() { do if [[ -e $d ]]; then step " modify permissions: $d" - if [[ $SUDO_REQUIRED ]]; then + if [[ $SUDO_REQUIRED == 1 ]]; then if [[ $(id -u) == 0 ]]; then try chmod -R og-rwx $d else From bbc182664c5ae0b75e64af70585323bcd83827db Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 18:04:41 +0100 Subject: [PATCH 176/268] fixed typo --- install/generator-cyphernode/generators/app/help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 9d2453455..19d36d484 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -20,7 +20,7 @@ "bitcoin_rpcuser": "** bitcoin_rpcuser **", "bitcoin_rpcpassword": "** bitcoin_rpcpassword **", "bitcoin_prune": "** bitcoin_prune **", - "bitcoin_prunesize": "** bitcoin_prunesize **", + "bitcoin_prune_size": "** bitcoin_prune_size **", "bitcoin_uacomment": "** bitcoin_uacomment **", "bitcoin_datapath": "** bitcoin_datapath **", "bitcoin_expose": "** bitcoin_expose **", From ee20c4dcfe9da6abd350c7a9bb245bcf522b712c Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 18:34:10 +0100 Subject: [PATCH 177/268] fixed some sudo stuff --- dist/setup.sh | 102 ++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 2ed19917f..530f9a3d6 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -114,6 +114,13 @@ echo ' ## /utils ---- +sudo_if_required() { + if [[ $SUDO_REQUIRED == 1 && ! $(id -u) == 0 ]]; then + try sudo $@ + else + try $@ + fi +} modify_permissions() { local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml $BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") @@ -121,37 +128,23 @@ modify_permissions() { do if [[ -e $d ]]; then step " modify permissions: $d" - if [[ $SUDO_REQUIRED == 1 ]]; then - if [[ $(id -u) == 0 ]]; then - try chmod -R og-rwx $d - else - try sudo chmod -R og-rwx $d - fi - else - try chmod -R og-rwx $d - fi + sudo_if_required chmod -R og-rwx $d next fi done } modify_owner() { - if [[ ! $RUN_AS_USER == $USER ]]; then - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") - local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) - for d in "${directories[@]}" - do - if [[ -e $d ]]; then - step " modify owner \"$RUN_AS_USER\": $d " - if [[ $(id -u) == 0 ]]; then - try chown -R $user $d - else - try sudo chown -R $user $d - fi - next - fi - done - fi + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) + for d in "${directories[@]}" + do + if [[ -e $d ]]; then + step " modify owner \"$RUN_AS_USER\": $d " + sudo_if_required chown -R $user $d + next + fi + done } configure() { @@ -241,27 +234,15 @@ copy_file() { create_user() { #check if user exists if [[ ! $RUN_AS_USER == $USER ]]; then - local OS=$(uname -s) - - if [[ $OS == 'Darwin' ]]; then - echo " Automatic user creation not supported on OSX." - echo " Please create the user \"$RUN_AS_USER\" by hand." - else - if [[ ! $RUN_AS_USER ]]; then - echo " No runtime user. Aborting" - exit 1 - fi - - id -u $RUN_AS_USER > /dev/null 2>&1 - if [[ $? == 1 ]]; then - step " create user $RUN_AS_USER " - if [[ $(id -u) == 0 ]]; then - try useradd $RUN_AS_USER - else - try sudo useradd $RUN_AS_USER - fi - next + id -u $RUN_AS_USER > /dev/null 2>&1 + if [[ $? == 1 ]]; then + step " create user $RUN_AS_USER " + if [[ $(id -u) == 0 ]]; then + try useradd $RUN_AS_USER + else + try sudo useradd $RUN_AS_USER fi + next fi fi } @@ -271,10 +252,10 @@ process_bitcoinconf() { local bitcoinconf=$1 # grep for prune entry and delete all whitespaces - local pruneEntry=$(grep -e ^prune $bitcoinconf | tr -d '[:space:]') - local txindexEntry=$(grep -e ^txindex $bitcoinconf | tr -d '[:space:]') - local testnetEntry=$(grep -e ^testnet $bitcoinconf | tr -d '[:space:]') - local regtestEntry=$(grep -e ^regtest $bitcoinconf | tr -d '[:space:]') + local pruneEntry=$(sudo_if_required grep -e ^prune $bitcoinconf | tr -d '[:space:]') + local txindexEntry=$(sudo_if_required grep -e ^txindex $bitcoinconf | tr -d '[:space:]') + local testnetEntry=$(sudo_if_required grep -e ^testnet $bitcoinconf | tr -d '[:space:]') + local regtestEntry=$(sudo_if_required grep -e ^regtest $bitcoinconf | tr -d '[:space:]') local prune=0 local txindex=0 @@ -368,11 +349,11 @@ install_docker() { if [ -d $GATEKEEPER_DATAPATH ]; then if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then - mkdir $GATEKEEPER_DATAPATH/certs + sudo_if_required mkdir $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 fi if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then - mkdir $GATEKEEPER_DATAPATH/private + sudo_if_required mkdir $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 fi copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED @@ -489,8 +470,6 @@ install_docker() { next fi - create_user - modify_owner cowsay } @@ -523,6 +502,14 @@ sanity_checks() { if [[ ''$RUN_AS_USER == '' ]]; then RUN_AS_USER=$USER + else + local OS=$(uname -s) + id -u $RUN_AS_USER > /dev/null 2>&1 + if [[ $OS == 'Darwin' && $? == 1 ]]; then + echo " Automatic user creation not supported on OSX." + echo " Please create the user \"$RUN_AS_USER\" by hand." + exit + fi fi local sudo=0 @@ -630,20 +617,19 @@ fi if [[ -f installer/config.sh ]]; then . installer/config.sh - RUN_AS_USER="blah" fi -sanity_checks - if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then step " clean cyphernodeconf image" try docker image rm cyphernodeconf > /dev/null 2>&1 next fi -modify_permissions - if [[ $INSTALL == 1 ]]; then + sanity_checks + create_user + modify_owner + modify_permissions install fi From 8302b8ca828a73c0313def4e6c0ad1c40d316c78 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 18:41:20 +0100 Subject: [PATCH 178/268] cleanup --- dist/setup.sh | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 530f9a3d6..7623a5142 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -13,24 +13,6 @@ # docker-compose -f docker-compose.yaml up [-d] -## utils ----- -trace() -{ - if [ -n "${TRACING}" ]; then - echo -n "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" - fi -} - -log() -{ - echo -n "${1}" -} - -logline() -{ - echo "${1}" -} - # FROM: https://stackoverflow.com/questions/5195607/checking-bash-exit-status-of-several-commands-efficiently # Use step(), try(), and next() to perform a series of commands and print # [ OK ] or [FAILED] at the end. The step as a whole fails if any individual @@ -42,7 +24,7 @@ logline() # try mount -o remount,rw /boot # next step() { - log "$@" + echo -n "$@" STEP_OK=0 [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ @@ -217,7 +199,7 @@ copy_file() { fi doCopy=1 else - logline "identical $sourceFile == $targetFile" + echo "identical $sourceFile == $targetFile" fi else doCopy=1 From b62506dc4834672009d625fe6488d42d7d3831db Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 18:49:36 +0100 Subject: [PATCH 179/268] more instructions --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 7623a5142..be4e64bc9 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -489,7 +489,7 @@ sanity_checks() { id -u $RUN_AS_USER > /dev/null 2>&1 if [[ $OS == 'Darwin' && $? == 1 ]]; then echo " Automatic user creation not supported on OSX." - echo " Please create the user \"$RUN_AS_USER\" by hand." + echo " Please create the user \"$RUN_AS_USER\" by hand and run: ./setup.sh -i" exit fi fi From 1af608bf5f3595ad2cd346a4852bd00ed9385b36 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 4 Nov 2018 18:49:48 +0100 Subject: [PATCH 180/268] color change --- .../generators/app/prompters/010_gatekeeper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index ef2dd84b0..6f4b5aed3 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -7,7 +7,7 @@ const capitalise = function( txt ) { }; const prefix = function() { - return chalk.bold.red(capitalise(name)+': '); + return chalk.green(capitalise(name)+': '); }; const hasAuthKeys = function( props ) { From 08f87be1f65782d347de78a99b96e0f10ec5874c Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 5 Nov 2018 21:58:31 +0100 Subject: [PATCH 181/268] cert creation now takes CNs as argument for which the cert is valid --- .../generators/app/help.json | 1 + .../generators/app/index.js | 11 +++- .../generators/app/lib/cert.js | 64 ++++++++++++++++++- .../app/prompters/010_gatekeeper.js | 8 +-- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 19d36d484..225d3d52a 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -15,6 +15,7 @@ "gatekeeper_apiproperties": "** gatekeeper_apiproperties **", "gatekeeper_sslcert": "** gatekeeper_sslcert **", "gatekeeper_sslkey": "** gatekeeper_sslkey **", + "gatekeeper_cns": "** gatekeeper_cns **", "bitcoin_mode": "** bitcoin_mode **", "bitcoin_node_ip": "** bitcoin_node_ip **", "bitcoin_rpcuser": "** bitcoin_rpcuser **", diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5e5910fd7..7c572e9aa 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -193,6 +193,7 @@ module.exports = class extends Generator { // save gatekeeper key password to check if it changed this.gatekeeper_clientkeyspassword = this.props.gatekeeper_clientkeyspassword; + this.gatekeeper_cns = this.props.gatekeeper_cns; let r = await this.prompt([{ type: 'confirm', @@ -250,13 +251,16 @@ module.exports = class extends Generator { } } - if( this.props.gatekeeper_recreatecert || + const oldCNS = (this.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); + const newCNS = (this.props.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); + + if( oldCNS.sort().join('') !== newCNS.sort().join('') || !this.props.gatekeeper_sslcert || !this.props.gatekeeper_sslkey ) { const cert = new Cert(); console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); try { - const result = await cert.create(); + const result = await cert.create(newCNS); if( result.code === 0 ) { this.props.gatekeeper_sslkey = result.key.toString(); this.props.gatekeeper_sslcert = result.cert.toString(); @@ -268,7 +272,7 @@ module.exports = class extends Generator { } } - delete this.props.gatekeeper_recreatecert; + delete this.props.gatekeeper_recreatekeys; } @@ -344,6 +348,7 @@ module.exports = class extends Generator { gatekeeper_keys: { configEntries: [], clientInformation: [] }, gatekeeper_sslcert: '', gatekeeper_sslkey: '', + gatekeeper_cns: '', proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_datapath: '', diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index 7108dabdd..01fdde454 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -3,32 +3,89 @@ const spawn = require('child_process').spawn; const defaultArgs = ['req', '-x509', '-newkey', 'rsa:4096', '-nodes']; const path = require('path'); const tmp = require('tmp'); +const validator = require('validator'); + +const confTmpl = ` +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +prompt = no +[req_distinguished_name] +CN = %PRIMARY_CN% +[v3_ca] +subjectAltName = @alt_names +[alt_names] +%ALT_DOMAINS% +%ALT_IPS% +`; + +const domainTmpl = 'DNS.%#% = %DOMAIN%'; +const ipTmpl = 'IP.%#% = %IP%' module.exports = class Cert { constructor( options ) { options = options || {}; - this.args = options.args || { subj: '/CN=localhost', days: 3650 }; + this.args = options.args || { days: 3650 }; } - async create() { + buildConfig( cns ) { + + let ips = []; + let domains = []; + + for( let cn of cns ) { + if( validator.isIP(cn) ) { + ips.push( cn ); + } else { + domains.push( cn ); + } + } + + let conf = confTmpl; + + if( !domains.length ) { + domains.push('localhost'); + } + + conf = conf.replace( '%PRIMARY_CN%', domains[0] ) + + let domainCount = 0; + domains = domains.map( d => domainTmpl.replace( '%#%', ++domainCount ).replace('%DOMAIN%', d) ); + conf = conf.replace( '%ALT_DOMAINS%', domains.join('\n') || '' ) + + let ipCount = 0; + ips = ips.map( ip => ipTmpl.replace( '%#%', ++ipCount ).replace('%IP%', ip) ); + conf = conf.replace( '%ALT_IPS%', ips.join('\n') || '' ) + + return conf; + } + + async create( cns ) { + cns = cns || []; let args = defaultArgs.slice(); const certFileTmp = tmp.fileSync(); const keyFileTmp = tmp.fileSync(); + const confFileTmp = tmp.fileSync(); args.push( '-out' ); args.push( certFileTmp.name ); args.push( '-keyout' ); args.push( keyFileTmp.name ); + args.push( '-config' ); + args.push( confFileTmp.name ); for( let k in this.args ) { args.push( '-'+k); args.push( this.args[k] ); } - const openssl = spawn('openssl', args, { stdio: ['ignore','ignore','ignore'] } ); + const conf = this.buildConfig( cns ); + fs.writeFileSync( confFileTmp.name, conf ); + + const openssl = spawn('openssl', args, { stdio: ['ignore', 'ignore', 'ignore'] } ); let code = await new Promise( function(resolve, reject) { openssl.on('exit', (code) => { @@ -41,6 +98,7 @@ module.exports = class Cert { certFileTmp.removeCallback(); keyFileTmp.removeCallback(); + confFileTmp.removeCallback(); return { code: code, diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index 6f4b5aed3..b0431183e 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -66,10 +66,10 @@ module.exports = { }, { when: function() { return hasCert( utils.props ); }, - type: 'confirm', - name: 'gatekeeper_recreatecert', - default: false, - message: prefix()+'Recreate gatekeeper ssl cert?'+utils._getHelp('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', From 32739f33108e619b32c73b5c7212c0adb82d282b Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 5 Nov 2018 22:23:29 +0100 Subject: [PATCH 182/268] removed ots client from build --- build.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sh b/build.sh index cf3068ead..754d4da03 100755 --- a/build.sh +++ b/build.sh @@ -49,7 +49,6 @@ build_docker_images() { trace "Creating SatoshiPortal images" build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile - build_docker_image install/SatoshiPortal/dockers/$archpath/ots/otsclient cyphernode/otsclient trace "Creating cyphernode images" build_docker_image api_auth_docker/ cyphernode/gatekeeper From bef6df09febff47f4b61e2342eb1878b3aaf35c8 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 5 Nov 2018 22:35:23 +0100 Subject: [PATCH 183/268] fixed alpine version to 3.8 --- api_auth_docker/Dockerfile | 2 +- cron_docker/Dockerfile | 2 +- proxy_docker/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 33f0f877f..ef25e78db 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:alpine +FROM nginx:alpine # should be 3.8 RUN apk add --update --no-cache \ bash \ diff --git a/cron_docker/Dockerfile b/cron_docker/Dockerfile index 1a3b75553..44eb44221 100644 --- a/cron_docker/Dockerfile +++ b/cron_docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3.8 RUN apk add --update --no-cache \ curl diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index 9714944ec..faf086b5a 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3.8 ENV HOME /proxy From 18b942b47ca48dec316de2170ee857c25d3d63c5 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:50:24 +0100 Subject: [PATCH 184/268] using Dockerfile-alpine when building clighting on arm --- build.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 754d4da03..ae0849ad6 100755 --- a/build.sh +++ b/build.sh @@ -36,11 +36,13 @@ build_docker_images() { git submodule update --recursive --remote local archpath=$(uname -m) + local clightning_dockerfile=Dockerfile # compat mode for SatoshiPortal repo # TODO: add more mappings? if [[ $archpath == 'armv7l' ]]; then archpath="rpi" + clightning_dockerfile="Dockerfile-alpine" fi trace "Creating cyphernodeconf image" @@ -48,7 +50,7 @@ build_docker_images() { trace "Creating SatoshiPortal images" build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin - build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $dockerfile + build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $clightning_dockerfile trace "Creating cyphernode images" build_docker_image api_auth_docker/ cyphernode/gatekeeper From 40425a963bd1598573565c05eee81af1c5be33d9 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:53:42 +0100 Subject: [PATCH 185/268] added otsclient to build --- build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sh b/build.sh index ae0849ad6..24b65d2e0 100755 --- a/build.sh +++ b/build.sh @@ -57,6 +57,7 @@ build_docker_images() { build_docker_image proxy_docker/ cyphernode/proxy build_docker_image cron_docker/ cyphernode/proxycron build_docker_image pycoin_docker/ cyphernode/pycoin + build_docker_image otsclient_docker/ cyphernode/otsclient } From 977c308883d797ea75ee0076144038fc5104dd0b Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:54:03 +0100 Subject: [PATCH 186/268] comment produced a build error --- api_auth_docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index ef25e78db..33f0f877f 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:alpine # should be 3.8 +FROM nginx:alpine RUN apk add --update --no-cache \ bash \ From 78103de4e6f88d42878494796ce6d1e855354688 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:56:04 +0100 Subject: [PATCH 187/268] added opentimestamps to setup script --- dist/setup.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index be4e64bc9..b6a2a8829 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -105,7 +105,7 @@ sudo_if_required() { } modify_permissions() { - local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml $BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml $BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OPENTIMESTAMPS_DATAPATH") for d in "${directories[@]}" do if [[ -e $d ]]; then @@ -117,7 +117,7 @@ modify_permissions() { } modify_owner() { - local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OPENTIMESTAMPS_DATAPATH") local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) for d in "${directories[@]}" do @@ -410,6 +410,14 @@ install_docker() { fi fi + if [[ $FEATURE_OPENTIMESTAMPS == true ]]; then + if [ ! -d $OPENTIMESTAMPS_DATAPATH ]; then + step " create $OPENTIMESTAMPS_DATAPATH" + sudo_if_required mkdir -p $OPENTIMESTAMPS_DATAPATH + next + fi + fi + local net_entry=$(docker network ls | grep cyphernodenet); if [[ $net_entry =~ 'cyphernodenet' ]]; then From f58841e338a092fd83d873b590d5e7ca365b3b22 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:56:25 +0100 Subject: [PATCH 188/268] checking some more if we need sudo --- dist/setup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index b6a2a8829..ff83d79a4 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -325,7 +325,7 @@ install_docker() { if [ ! -d $GATEKEEPER_DATAPATH ]; then step " create $GATEKEEPER_DATAPATH" - try mkdir -p $GATEKEEPER_DATAPATH + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH next fi @@ -347,14 +347,14 @@ install_docker() { if [ ! -d $PROXY_DATAPATH ]; then step " create $PROXY_DATAPATH" - try mkdir -p $PROXY_DATAPATH + sudo_if_required mkdir -p $PROXY_DATAPATH next fi if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then step " create $BITCOIN_DATAPATH" - try mkdir -p $BITCOIN_DATAPATH + sudo_if_required mkdir -p $BITCOIN_DATAPATH next fi if [ -d $BITCOIN_DATAPATH ]; then @@ -400,7 +400,7 @@ install_docker() { fi if [ ! -d $LIGHTNING_DATAPATH ]; then step " create $LIGHTNING_DATAPATH" - try mkdir -p $LIGHTNING_DATAPATH + sudo_if_required mkdir -p $LIGHTNING_DATAPATH next fi if [ -d $LIGHTNING_DATAPATH ]; then From 3d9df19602e6450a9e9ca7f94661904f181d562b Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:56:51 +0100 Subject: [PATCH 189/268] FROM version lock to same version as opentimestamps image --- install/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/Dockerfile b/install/Dockerfile index 120ac66d8..7d8e6d3e9 100644 --- a/install/Dockerfile +++ b/install/Dockerfile @@ -1,4 +1,4 @@ -FROM node:alpine +FROM node:11.1-alpine RUN apk add --update bash su-exec p7zip openssl nano && rm -rf /var/cache/apk/* RUN mkdir -p /app From 7f45d7940ed1cdfae474cbe206c82ff5ac7238c5 Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 13 Nov 2018 23:57:55 +0100 Subject: [PATCH 190/268] added opentimestamps feature to configtool --- .../generators/app/features.json | 4 ++++ .../generators/app/index.js | 1 + .../generators/app/prompters/999_installer.js | 9 +++++++++ .../app/templates/installer/config.sh | 2 ++ .../installer/docker/docker-compose.yaml | 18 ++++++++++++++++++ 5 files changed, 34 insertions(+) diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index e0145ec9e..362584e66 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -2,5 +2,9 @@ { "name": "Lightning node", "value": "lightning" + }, + { + "name": "Opentimestamps client", + "value": "opentimestamps" } ] \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 7c572e9aa..9a1bd2af4 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -354,6 +354,7 @@ module.exports = class extends Generator { lightning_datapath: '', lightning_nodename: '', lightning_nodecolor: '', + opentimestamps_datapath: '', installer_cleanup: false }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index ed2d74c68..eb520402b 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -76,6 +76,15 @@ module.exports = { validate: utils._pathValidator, message: prefix()+'Where is your lightning node data?'+utils._getHelp('lightning_datapath'), }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('opentimestamps') !== -1 }, + type: 'input', + name: 'opentimestamps_datapath', + default: utils._getDefault( 'opentimestamps_datapath' ), + filter: utils._trimFilter, + validate: utils._pathValidator, + message: prefix()+'Where is your opentimestamps data?'+utils._getHelp('opentimestamps_datapath'), + }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, type: 'confirm', diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 4a3c4d710..2178256a9 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,9 +1,11 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> +FEATURE_OPENTIMESTAMPS=<%= (features.indexOf('opentimestamps') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> +OPENTIMESTAMPS_DATAPATH=<%= opentimestamps_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> 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 4d702d21d..24d4ce8d3 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 @@ -48,6 +48,7 @@ services: volumes: - "<%= proxy_datapath %>:/proxy/db" - "<%= lightning_datapath %>:/proxy/.lightning" + - "<%= opentimestamps_datapath %>:/otsfiles" # deploy: # placement: # constraints: [node.hostname==dev] @@ -100,6 +101,23 @@ services: - cyphernodenet restart: always <% } %> +<% if ( features.indexOf('opentimestamps') !== -1 ) { %> + opentimestamps: + environment: + - "TRACING=1" + - "OTSCLIENT_LISTENING_PORT=6666" + image: cyphernode/opentimestamps +# deploy: +# placement: +# constraints: [node.hostname==dev] + volumes: + - "<%= opentimestamps_datapath%>:/otsfiles" + command: $USER /script/startotsclient.sh + networks: + - cyphernodenet + restart: always +<% } %> + <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind From 52dbacd085937272d502e37bd60860016e06b52e Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 14 Nov 2018 17:07:08 +0100 Subject: [PATCH 191/268] added opentimestamp env vars to proxy --- .../app/templates/installer/docker/docker-compose.yaml | 2 ++ 1 file changed, 2 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 24d4ce8d3..e3ca183b5 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 @@ -40,6 +40,8 @@ services: - "DERIVATION_PUB32=<%= xpub %>" - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" + - "OTSCLIENT_CONTAINER=opentimestamps:6666" + - "OTS_FILES=/otsfiles" image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/proxy <% if ( devmode ) { %> ports: From a03c0265e8dce74b53646fd2ceb38015648409ed Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 14 Nov 2018 22:14:10 +0100 Subject: [PATCH 192/268] fixed gatekeeper cert recreation --- .../generators/app/index.js | 10 +- .../app/prompters/010_gatekeeper.js | 111 ++++++++++-------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 9a1bd2af4..6538c2cfd 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -193,7 +193,6 @@ module.exports = class extends Generator { // save gatekeeper key password to check if it changed this.gatekeeper_clientkeyspassword = this.props.gatekeeper_clientkeyspassword; - this.gatekeeper_cns = this.props.gatekeeper_cns; let r = await this.prompt([{ type: 'confirm', @@ -251,16 +250,15 @@ module.exports = class extends Generator { } } - const oldCNS = (this.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); - const newCNS = (this.props.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); - - if( oldCNS.sort().join('') !== newCNS.sort().join('') || + if( this.props.gatekeeper_recreatecert || !this.props.gatekeeper_sslcert || !this.props.gatekeeper_sslkey ) { + delete this.props.gatekeeper_recreatecert; const cert = new Cert(); console.log(chalk.bold.green( '☕ Generating gatekeeper cert. This may take a while ☕' )); try { - const result = await cert.create(newCNS); + const cns = (this.props.gatekeeper_cns||'').split(',').map(e=>e.trim().toLowerCase()).filter(e=>!!e); + const result = await cert.create(cns); if( result.code === 0 ) { this.props.gatekeeper_sslkey = result.key.toString(); this.props.gatekeeper_sslcert = result.cert.toString(); diff --git a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index b0431183e..c0538c45e 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -11,22 +11,22 @@ const prefix = function() { }; const hasAuthKeys = function( props ) { - return props && - props.gatekeeper_keys && + return props && + props.gatekeeper_keys && props.gatekeeper_keys.configEntries && props.gatekeeper_keys.configEntries.length > 0; } const hasCert = function( props ) { - return props && - props.gatekeeper_sslkey && + return props && + props.gatekeeper_sslkey && props.gatekeeper_sslcert } let password = ''; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -39,55 +39,62 @@ 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()+'Config 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' ); + { + 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; } - return true; - } - }, - { - 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: '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' ) - }]; + { + 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' ) + }]; }, templates: function( props ) { return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem' ]; From 4ba6d4b4a5a13e05bbd7c9124769ddc22bbcb4da Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 14 Nov 2018 22:36:28 +0100 Subject: [PATCH 193/268] setup.sh now puts dockerd in swarm mode if needed --- dist/setup.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index ff83d79a4..9db9eea8c 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -418,6 +418,15 @@ install_docker() { fi fi + docker swarm join-token worker > /dev/null 2>&1 + local noSwarm=$?; + + if [[ $DOCKER_MODE == 'swarm' && $noSwarm == 1 ]]; then + step " init docker swarm" + try docker swarm init > /dev/null 2>&1 + next + fi + local net_entry=$(docker network ls | grep cyphernodenet); if [[ $net_entry =~ 'cyphernodenet' ]]; then From 7ecc6c3edc65d9a957be04a24f034806134cdfb6 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 14 Nov 2018 23:05:29 +0100 Subject: [PATCH 194/268] exitStatus.sh lets the setup bash script know if the docker process exited cleanly --- dist/setup.sh | 8 ++++++++ install/generator-cyphernode/generators/app/index.js | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/dist/setup.sh b/dist/setup.sh index 9db9eea8c..722cb69e1 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -167,6 +167,14 @@ configure() { -e DEFAULT_USER=$USER \ --log-driver=none$pw_env \ --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate + if [[ -f exitStatus.sh ]]; then + . ./exitStatus.sh + rm ./exitStatus.sh + fi + + if [[ ! $EXIT_STATUS == 0 ]]; then + exit 1 + fi } copy_file() { diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 6538c2cfd..b45b53c89 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -104,6 +104,10 @@ module.exports = class extends Generator { this.featureChoices = featureChoices; + if( fs.existsSync(path.join('/data', 'exitStatus.sh')) ) { + fs.unlinkSync(path.join('/data', 'exitStatus.sh')); + } + } async _initConfig() { @@ -311,6 +315,9 @@ module.exports = class extends Generator { } } + fs.writeFileSync(path.join('/data', 'exitStatus.sh'), 'EXIT_STATUS=0'); + + } install() { From 7b2a54f50a46f78dc94e4e6a7d33e3e3bd14d506 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 14 Nov 2018 23:05:44 +0100 Subject: [PATCH 195/268] fix: display fuckup --- 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 b45b53c89..5ad556579 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -152,7 +152,7 @@ module.exports = class extends Generator { } else { let r = {}; - process.stdout.write(reset); + process.stdout.write(clear+reset); while( !r.password0 || !r.password1 || r.password0 !== r.password1 ) { if( r.password0 && r.password1 && r.password0 !== r.password1 ) { From 688eb9dcb6bac43734c0bfd1c2119780470ea63e Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 3 Dec 2018 18:52:00 +0100 Subject: [PATCH 196/268] bump :) --- dist/setup.sh | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 722cb69e1..b5aab68d7 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -79,6 +79,11 @@ next() { return $STEP_OK } +#function finish { +# +#} +#trap finish EXIT + cowsay() { echo '                      _____________________________________  @@ -99,7 +104,7 @@ echo ' sudo_if_required() { if [[ $SUDO_REQUIRED == 1 && ! $(id -u) == 0 ]]; then try sudo $@ - else + else try $@ fi } @@ -138,7 +143,7 @@ configure() { recreate="recreate" fi - + local arch=$(uname -m) local pw_env='' @@ -162,10 +167,18 @@ configure() { clear && echo "Thinking..." fi + # before starting a new cyphernodeconf, kill all the others + local otherCyphernodeconf=$(docker ps | grep "cyphernodeconf" | awk '{ print $1 }'); + + if [[ ! ''$otherCyphernodeconf == '' ]]; then + docker rm -f $otherCyphernodeconf > /dev/null 2>&1 + fi + # configure features of cyphernode docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ --log-driver=none$pw_env \ + --network none \ --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate if [[ -f exitStatus.sh ]]; then . ./exitStatus.sh @@ -195,7 +208,7 @@ copy_file() { if [[ ! -f $sourceFile ]]; then return 1; fi - + if [[ -f $targetFile ]]; then ${sudo}cmp --silent $sourceFile $targetFile if [[ $? == 1 ]]; then @@ -206,7 +219,7 @@ copy_file() { next fi doCopy=1 - else + else echo "identical $sourceFile == $targetFile" fi else @@ -224,7 +237,7 @@ copy_file() { create_user() { #check if user exists if [[ ! $RUN_AS_USER == $USER ]]; then - id -u $RUN_AS_USER > /dev/null 2>&1 + id -u $RUN_AS_USER > /dev/null 2>&1 if [[ $? == 1 ]]; then step " create user $RUN_AS_USER " if [[ $(id -u) == 0 ]]; then @@ -352,7 +365,7 @@ install_docker() { copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED fi - + if [ ! -d $PROXY_DATAPATH ]; then step " create $PROXY_DATAPATH" sudo_if_required mkdir -p $PROXY_DATAPATH @@ -367,7 +380,7 @@ install_docker() { fi if [ -d $BITCOIN_DATAPATH ]; then - local cmpStatus=$(compare_bitcoinconf $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) + local cmpStatus=$(compare_bitcoinconf $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) if [[ $cmpStatus == 'dataloss' ]]; then if [[ $ALWAYSYES == 1 ]]; then @@ -511,11 +524,11 @@ sanity_checks() { RUN_AS_USER=$USER else local OS=$(uname -s) - id -u $RUN_AS_USER > /dev/null 2>&1 + id -u $RUN_AS_USER > /dev/null 2>&1 if [[ $OS == 'Darwin' && $? == 1 ]]; then echo " Automatic user creation not supported on OSX." echo " Please create the user \"$RUN_AS_USER\" by hand and run: ./setup.sh -i" - exit + exit fi fi @@ -526,12 +539,12 @@ sanity_checks() { sudo=1 sudo_reason='user' fi - + if [[ $sudo == 0 ]]; then # we still don't need sudo. Let's check access to directories sudo=$(check_directory_owner) sudo_reason='directories' - fi + fi if [[ $sudo == 1 ]]; then echo " check Cyphernode installer has determined that it needs sudo to continue." @@ -558,7 +571,7 @@ sanity_checks() { fi exit - else + else SUDO_REQUIRED=1 fi else From 1a3e8b7c8da1f361991ea8c56cf6f4f39fc6c654 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 3 Dec 2018 19:05:43 +0100 Subject: [PATCH 197/268] new default API properties in index.js --- api_auth_docker/api.properties | 32 ------------------- .../generators/app/index.js | 29 +++++++++++------ 2 files changed, 20 insertions(+), 41 deletions(-) delete mode 100644 api_auth_docker/api.properties diff --git a/api_auth_docker/api.properties b/api_auth_docker/api.properties deleted file mode 100644 index bde99947a..000000000 --- a/api_auth_docker/api.properties +++ /dev/null @@ -1,32 +0,0 @@ - -# Watcher can: -action_watch=watcher -action_unwatch=watcher -action_getactivewatches=watcher -action_getbestblockhash=watcher -action_getbestblockinfo=watcher -action_getblockinfo=watcher -action_gettransaction=watcher -action_ln_getinfo=watcher -action_ln_create_invoice=watcher - -# Spender can do what the watcher can do plus: -action_getbalance=spender -action_getnewaddress=spender -action_spend=spender -action_addtobatch=spender -action_batchspend=spender -action_deriveindex=spender -action_derivepubpath=spender -action_ln_pay=spender -action_ln_newaddr=spender -action_ots_stamp=spender -action_ots_getfile=spender - -# Admin can do what the spender can do plus: - - -# Should be called from inside the Swarm: -action_conf=internal -action_executecallbacks=internal -action_ots_backoffice=internal diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5ad556579..4e6452127 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -12,12 +12,13 @@ const Cert = require('./lib/cert.js'); const featureChoices = require('./features.json'); const uaCommentRegexp = /^[a-zA-Z0-9 \.,:_\-\?\/@]+$/; // TODO: look for spec of unsafe chars -const userRegexp = /^[a-zA-Z0-9\._\-]+$/; +const userRegexp = /^[a-zA-Z0-9\._\-]+$/; const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; const defaultAPIProperties = ` +# Watcher can: action_watch=watcher action_unwatch=watcher action_getactivewatches=watcher @@ -27,6 +28,8 @@ action_getblockinfo=watcher action_gettransaction=watcher action_ln_getinfo=watcher action_ln_create_invoice=watcher + +# Spender can do what the watcher can do plus: action_getbalance=spender action_getnewaddress=spender action_spend=spender @@ -36,8 +39,16 @@ action_deriveindex=spender action_derivepubpath=spender action_ln_pay=spender action_ln_newaddr=spender +action_ots_stamp=spender +action_ots_getfile=spender + +# Admin can do what the spender can do plus: + + +# Should be called from inside the Swarm: action_conf=internal action_executecallbacks=internal +action_ots_backoffice=internal `; const prefix = function() { @@ -137,7 +148,7 @@ module.exports = class extends Generator { console.log(chalk.bold.red('Password is wrong. Have a nice day.')); process.exit(1); } - + if( !r.value ) { console.log(chalk.bold.red('config archive is corrupt.')); process.exit(1); @@ -194,7 +205,7 @@ module.exports = class extends Generator { // no prompts return; } - + // save gatekeeper key password to check if it changed this.gatekeeper_clientkeyspassword = this.props.gatekeeper_clientkeyspassword; @@ -223,7 +234,7 @@ module.exports = class extends Generator { async configuring() { - if( this.props.gatekeeper_recreatekeys || + if( this.props.gatekeeper_recreatekeys || this.props.gatekeeper_keys.configEntries.length===0 ) { const apikey = new ApiKey(); @@ -255,7 +266,7 @@ module.exports = class extends Generator { } if( this.props.gatekeeper_recreatecert || - !this.props.gatekeeper_sslcert || + !this.props.gatekeeper_sslcert || !this.props.gatekeeper_sslkey ) { delete this.props.gatekeeper_recreatecert; const cert = new Cert(); @@ -296,7 +307,7 @@ module.exports = class extends Generator { this.destinationPath(p), this.props ); - } + } } if( this.props.gatekeeper_keys && this.props.gatekeeper_keys.clientInformation ) { @@ -374,8 +385,8 @@ module.exports = class extends Generator { } _optional(input,validator) { - if( input === undefined || - input === null || + if( input === undefined || + input === null || input === '' ) { return true; } @@ -384,7 +395,7 @@ module.exports = class extends Generator { _ipOrFQDNValidator( host ) { host = (host+"").trim(); - if( !(validator.isIP(host) || + if( !(validator.isIP(host) || validator.isFQDN(host)) ) { throw new Error( 'No IP address or fully qualified domain name' ) } From 80aac9c200e74d2f7793e565f09bdd6a7a3a5276 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 3 Dec 2018 17:20:09 -0500 Subject: [PATCH 198/268] Changed opentimestamps for otsclient --- dist/setup.sh | 13 ++++----- docker-compose.yml | 2 +- .../generators/app/features.json | 4 +-- .../generators/app/index.js | 2 +- .../generators/app/prompters/999_installer.js | 14 ++++----- .../app/templates/installer/config.sh | 4 +-- .../installer/docker/docker-compose.yaml | 29 +++++++++---------- 7 files changed, 33 insertions(+), 35 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index b5aab68d7..168ed2aab 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" "$OPENTIMESTAMPS_DATAPATH") + local directories=("installer" "gatekeeper" "lightning" "bitcoin" "docker-compose.yaml $BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_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" "$OPENTIMESTAMPS_DATAPATH") + local directories=("$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") local user=$(id -u $RUN_AS_USER):$(id -g $RUN_AS_USER) for d in "${directories[@]}" do @@ -431,10 +431,10 @@ install_docker() { fi fi - if [[ $FEATURE_OPENTIMESTAMPS == true ]]; then - if [ ! -d $OPENTIMESTAMPS_DATAPATH ]; then - step " create $OPENTIMESTAMPS_DATAPATH" - sudo_if_required mkdir -p $OPENTIMESTAMPS_DATAPATH + if [[ $FEATURE_OTSCLIENT == true ]]; then + if [ ! -d $OTSCLIENT_DATAPATH ]; then + step " create $OTSCLIENT_DATAPATH" + sudo_if_required mkdir -p $OTSCLIENT_DATAPATH next fi fi @@ -652,4 +652,3 @@ if [[ $INSTALL == 1 ]]; then modify_permissions install fi - diff --git a/docker-compose.yml b/docker-compose.yml index 1da585c87..a86207a4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: # otsclient JS env_file: - otsclient_docker/env.properties - image: cyphernode/ots:cyphernode-0.05 + image: cyphernode/otsclient:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] diff --git a/install/generator-cyphernode/generators/app/features.json b/install/generator-cyphernode/generators/app/features.json index 362584e66..84a1e9a01 100644 --- a/install/generator-cyphernode/generators/app/features.json +++ b/install/generator-cyphernode/generators/app/features.json @@ -5,6 +5,6 @@ }, { "name": "Opentimestamps client", - "value": "opentimestamps" + "value": "otsclient" } -] \ No newline at end of file +] diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 4e6452127..74d0f3cde 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -370,7 +370,7 @@ module.exports = class extends Generator { lightning_datapath: '', lightning_nodename: '', lightning_nodecolor: '', - opentimestamps_datapath: '', + otsclient_datapath: '', installer_cleanup: false }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index eb520402b..bacd2b5d9 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -20,7 +20,7 @@ const installerLunanode = function(props) { }; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -77,13 +77,13 @@ module.exports = { message: prefix()+'Where is your lightning node data?'+utils._getHelp('lightning_datapath'), }, { - when: function(props) { return installerDocker(props) && props.features.indexOf('opentimestamps') !== -1 }, + when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 }, type: 'input', - name: 'opentimestamps_datapath', - default: utils._getDefault( 'opentimestamps_datapath' ), + name: 'otsclient_datapath', + default: utils._getDefault( 'otsclient_datapath' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your opentimestamps data?'+utils._getHelp('opentimestamps_datapath'), + message: prefix()+'Where is your otsclient data?'+utils._getHelp('otsclient_datapath'), }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, @@ -98,7 +98,7 @@ module.exports = { name: 'lightning_expose', default: utils._getDefault( 'lightning_expose' ), message: prefix()+'Expose lightning node outside of the docker network?'+utils._getHelp('lightning_expose'), - }, + }, { when: installerDocker, type: 'list', @@ -127,4 +127,4 @@ module.exports = { } return ['config.sh','start.sh', 'stop.sh']; } -}; \ No newline at end of file +}; diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 2178256a9..399a3951b 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -1,11 +1,11 @@ INSTALLER_MODE=<%= installer_mode %> BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> -FEATURE_OPENTIMESTAMPS=<%= (features.indexOf('opentimestamps') != -1)?'true':'false' %> +FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> LIGHTNING_DATAPATH=<%= lightning_datapath %> -OPENTIMESTAMPS_DATAPATH=<%= opentimestamps_datapath %> +OTSCLIENT_DATAPATH=<%= otsclient_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> 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 e3ca183b5..df6692f58 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 @@ -5,7 +5,7 @@ services: # HTTP authentication API gate environment: - "TRACING=1" - image: cyphernode/gatekeeper + image: cyphernode/gatekeeper:cyphernode-0.05 ports: - "443:443" volumes: @@ -36,21 +36,20 @@ services: - "DB_PATH=/proxy/db" - "DB_FILE=/proxy/db/proxydb" - "PYCOIN_CONTAINER=pycoin:7777" - - "OTS_CONTAINER=otsclient:6666" - "DERIVATION_PUB32=<%= xpub %>" - "DERIVATION_PATH=<%= derivation_path %>" - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - - "OTSCLIENT_CONTAINER=opentimestamps:6666" - - "OTS_FILES=/otsfiles" - image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/proxy + - "OTSCLIENT_CONTAINER=otsclient:6666" + - "OTS_FILES=/proxy/otsfiles" + image: cyphernode/proxy:cyphernode-0.05 <% if ( devmode ) { %> ports: - "8888:8888" <% } %> volumes: - "<%= proxy_datapath %>:/proxy/db" - - "<%= lightning_datapath %>:/proxy/.lightning" - - "<%= opentimestamps_datapath %>:/otsfiles" + - "<%= lightning_datapath %>:/.lightning" + - "<%= otsclient_datapath %>:/proxy/otsfiles" # deploy: # placement: # constraints: [node.hostname==dev] @@ -60,7 +59,7 @@ services: proxycron: environment: - "PROXY_URL=proxy:8888/executecallbacks" - image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/proxycron + image: cyphernode/proxycron:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] @@ -70,7 +69,7 @@ services: pycoin: # Pycoin command: $USER ./startpycoin.sh - image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/pycoin + image: cyphernode/pycoin:cyphernode-0.05 environment: - "TRACING=1" - "PYCOIN_LISTENING_PORT=7777" @@ -87,7 +86,7 @@ services: <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: command: $USER lightningd - image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/clightning + image: cyphernode/clightning:v0.6.2 <% if( lightning_expose ) { %> ports: @@ -103,17 +102,17 @@ services: - cyphernodenet restart: always <% } %> -<% if ( features.indexOf('opentimestamps') !== -1 ) { %> - opentimestamps: +<% if ( features.indexOf('otsclient') !== -1 ) { %> + otsclient: environment: - "TRACING=1" - "OTSCLIENT_LISTENING_PORT=6666" - image: cyphernode/opentimestamps + image: cyphernode/otsclient:cyphernode-0.05 # deploy: # placement: # constraints: [node.hostname==dev] volumes: - - "<%= opentimestamps_datapath%>:/otsfiles" + - "<%= otsclient_datapath%>:/otsfiles" command: $USER /script/startotsclient.sh networks: - cyphernodenet @@ -123,7 +122,7 @@ services: <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind - image: <%= devregistry?'registry.skp.rocks:5000/$ARCH/':'' %>cyphernode/bitcoin + image: cyphernode/bitcoin:0.17.0 <% if( bitcoin_expose ) { %> ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" From 6ecd48a05828fd62c1ce50fdaaecb5e8fe1cf541 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 3 Dec 2018 17:28:15 -0500 Subject: [PATCH 199/268] c-lightning needs glibc and bin is taken from cyphernode clightning image --- proxy_docker/Dockerfile | 19 +++++++++++++++++-- proxy_docker/app/bin/README.md | 5 ----- proxy_docker/app/bin/lightning-cli_arm | Bin 366680 -> 0 bytes proxy_docker/app/bin/lightning-cli_x86 | Bin 347736 -> 0 bytes 4 files changed, 17 insertions(+), 7 deletions(-) delete mode 100644 proxy_docker/app/bin/README.md delete mode 100644 proxy_docker/app/bin/lightning-cli_arm delete mode 100644 proxy_docker/app/bin/lightning-cli_x86 diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile index faf086b5a..a4361c6fa 100644 --- a/proxy_docker/Dockerfile +++ b/proxy_docker/Dockerfile @@ -1,5 +1,21 @@ 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 \ @@ -30,7 +46,7 @@ COPY app/script/getactivewatches.sh ${HOME}/getactivewatches.sh COPY app/script/manage_missed_conf.sh ${HOME}/manage_missed_conf.sh COPY app/script/tests.sh ${HOME}/tests.sh COPY app/script/tests-cb.sh ${HOME}/tests-cb.sh -COPY app/bin/lightning-cli_x86 ${HOME}/lightning-cli +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME}/lightning-cli WORKDIR ${HOME} @@ -41,4 +57,3 @@ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ VOLUME ["${HOME}/db", "${HOME}/.lightning"] ENTRYPOINT ["su-exec"] - diff --git a/proxy_docker/app/bin/README.md b/proxy_docker/app/bin/README.md deleted file mode 100644 index 5b6177a13..000000000 --- a/proxy_docker/app/bin/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Nota bene - -lightning-cli binary has been pre-compiled in an Alpine docker container. - -Use lightning-cli_arm for armhf architecture, lightning-cli_x86 for x86_64. diff --git a/proxy_docker/app/bin/lightning-cli_arm b/proxy_docker/app/bin/lightning-cli_arm deleted file mode 100644 index 8f2e064ac6d7ff78f8d84af16bdf37abb363707c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 366680 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 QUwF^ck`;kSVz~Xk0Sr~7L;wH) diff --git a/proxy_docker/app/bin/lightning-cli_x86 b/proxy_docker/app/bin/lightning-cli_x86 deleted file mode 100644 index bf3e61695d2c8914ca2d6081a9036ea3b7f15ffa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 347736 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 Date: Mon, 3 Dec 2018 18:08:09 -0500 Subject: [PATCH 200/268] wallet prefix in bitcoin.conf and setup permissions glitch fixed --- dist/setup.sh | 2 +- doc/INSTALL.md | 18 +++++++++--------- .../app/templates/bitcoin/bitcoin.conf | 14 ++++++++++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 168ed2aab..02bd48e96 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" "$BITCOIN_DATAPATH" "$LIGHTNING_DATAPATH" "$PROXY_DATAPATH" "$GATEKEEPER_DATAPATH" "$OTSCLIENT_DATAPATH") for d in "${directories[@]}" do if [[ -e $d ]]; then diff --git a/doc/INSTALL.md b/doc/INSTALL.md index d78cd6a71..eeab4b987 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -128,13 +128,13 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech ## Test deployment from any host of the swarm ```shell -echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.2}' cyphernode:8888/spend -echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -echo "GET /ln_newaddr" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - -curl -v -H "Content-Type: application/json" -d '{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}' cyphernode:8888/ln_create_invoice -curl -v -H "Content-Type: application/json" -d '{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","msatoshi":10000,"description":"Bitcoin Outlet order #7082"}' cyphernode:8888/ln_pay +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 /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.2}' proxy:8888/spend +echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /ln_newaddr" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +curl -v -H "Content-Type: application/json" -d '{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}' proxy:8888/ln_create_invoice +curl -v -H "Content-Type: application/json" -d '{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","msatoshi":10000,"description":"Bitcoin Outlet order #7082"}' proxy:8888/ln_pay ``` diff --git a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf index 7fc76512b..c02e2fcdb 100644 --- a/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf +++ b/install/generator-cyphernode/generators/app/templates/bitcoin/bitcoin.conf @@ -26,12 +26,18 @@ rpcpassword=<%= bitcoin_rpcpassword %> rpcallowip=0.0.0.0/0 server=1 -wallet=watching01.dat -wallet=spending01.dat -wallet=ln01.dat +<% if (net === 'testnet') { %> +test.wallet=watching01.dat +test.wallet=spending01.dat +test.wallet=ln01.dat +<% } else { %> +main.wallet=watching01.dat +main.wallet=spending01.dat +main.wallet=ln01.dat +<% } %> walletnotify=curl proxy:8888/conf/%s <% if ( bitcoin_uacomment != null && bitcoin_uacomment != '' ) { %> uacomment=<%= bitcoin_uacomment %> -<% } %> \ No newline at end of file +<% } %> From 934cb26d273b47925c14c6de21930cf0b405a421 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 4 Dec 2018 13:03:58 -0500 Subject: [PATCH 201/268] Bitcoin version tag is v0.17.0 --- .../app/templates/installer/docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 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 df6692f58..d5c0df850 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 @@ -122,7 +122,7 @@ services: <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind - image: cyphernode/bitcoin:0.17.0 + image: cyphernode/bitcoin:v0.17.0 <% if( bitcoin_expose ) { %> ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" From 6f304199aea44fc3b0d8c42180c7e95ece887573 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 4 Dec 2018 13:55:52 -0500 Subject: [PATCH 202/268] amd64 and arm32v6 for the proxy because of glibc needed by lightning-cli --- proxy_docker/{Dockerfile => Dockerfile.amd64} | 0 proxy_docker/Dockerfile.arm32v6 | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+) rename proxy_docker/{Dockerfile => Dockerfile.amd64} (100%) create mode 100644 proxy_docker/Dockerfile.arm32v6 diff --git a/proxy_docker/Dockerfile b/proxy_docker/Dockerfile.amd64 similarity index 100% rename from proxy_docker/Dockerfile rename to proxy_docker/Dockerfile.amd64 diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 new file mode 100644 index 000000000..31ba7d762 --- /dev/null +++ b/proxy_docker/Dockerfile.arm32v6 @@ -0,0 +1,55 @@ +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 + +COPY app/script/callbacks_job.sh ${HOME}/callbacks_job.sh +COPY app/script/blockchainrpc.sh ${HOME}/blockchainrpc.sh +COPY app/script/call_lightningd.sh ${HOME}/call_lightningd.sh +COPY app/script/bitcoin.sh ${HOME}/bitcoin.sh +COPY app/script/ots.sh ${HOME}/ots.sh +COPY app/script/requesthandler.sh ${HOME}/requesthandler.sh +COPY app/script/watchrequest.sh ${HOME}/watchrequest.sh +COPY app/script/walletoperations.sh ${HOME}/walletoperations.sh +COPY app/script/confirmation.sh ${HOME}/confirmation.sh +COPY app/script/startproxy.sh ${HOME}/startproxy.sh +COPY app/script/trace.sh ${HOME}/trace.sh +COPY app/script/sendtobitcoinnode.sh ${HOME}/sendtobitcoinnode.sh +COPY app/script/responsetoclient.sh ${HOME}/responsetoclient.sh +COPY app/script/importaddress.sh ${HOME}/importaddress.sh +COPY app/script/sql.sh ${HOME}/sql.sh +COPY app/data/watching.sql ${HOME}/watching.sql +COPY app/script/computefees.sh ${HOME}/computefees.sh +COPY app/script/unwatchrequest.sh ${HOME}/unwatchrequest.sh +COPY app/script/getactivewatches.sh ${HOME}/getactivewatches.sh +COPY app/script/manage_missed_conf.sh ${HOME}/manage_missed_conf.sh +COPY app/script/tests.sh ${HOME}/tests.sh +COPY app/script/tests-cb.sh ${HOME}/tests-cb.sh +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME}/lightning-cli + +WORKDIR ${HOME} + +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ + && chmod o+w . \ + && mkdir db + +VOLUME ["${HOME}/db", "${HOME}/.lightning"] + +ENTRYPOINT ["su-exec"] From 24dce422ab4211751f100e8b10c5bc02734f9e85 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 4 Dec 2018 15:02:53 -0500 Subject: [PATCH 203/268] Using cyphernodeconf image from dockerhub --- dist/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 02bd48e96..3b67a3f42 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -179,7 +179,7 @@ configure() { -e DEFAULT_USER=$USER \ --log-driver=none$pw_env \ --network none \ - --rm$interactive cyphernodeconf:latest $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate + --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate if [[ -f exitStatus.sh ]]; then . ./exitStatus.sh rm ./exitStatus.sh From bf16774719ef4eee10f1ba3844a2d745ee0ee507 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 4 Dec 2018 18:23:05 -0500 Subject: [PATCH 204/268] We want en encrypted overlay network and no childs in service processes --- dist/setup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 3b67a3f42..047e4815c 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -444,7 +444,7 @@ install_docker() { if [[ $DOCKER_MODE == 'swarm' && $noSwarm == 1 ]]; then step " init docker swarm" - try docker swarm init > /dev/null 2>&1 + try docker swarm init --task-history-limit 1 > /dev/null 2>&1 next fi @@ -454,7 +454,7 @@ install_docker() { if [[ $net_entry =~ 'local' && $DOCKER_MODE == 'swarm' ]]; then step " recreate cyphernode network" try docker network rm cyphernodenet > /dev/null 2>&1 - try docker network create -d overlay cyphernodenet > /dev/null 2>&1 + try docker network create -d overlay --attachable --opt encrypted cyphernodenet > /dev/null 2>&1 next elif [[ $net_entry =~ 'swarm' && $DOCKER_MODE == 'compose' ]]; then step " recreate cyphernode network" @@ -465,7 +465,7 @@ install_docker() { else if [[ $DOCKER_MODE == 'swarm' ]]; then step " create cyphernode network" - try docker network create -d overlay cyphernodenet > /dev/null 2>&1 + try docker network create -d overlay --attachable --opt encrypted cyphernodenet > /dev/null 2>&1 next elif [[ $DOCKER_MODE == 'compose' ]]; then step " create cyphernode network" From fac34f3dec12b9dedcfabeee299685a80b1e7983 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 5 Dec 2018 12:04:58 -0500 Subject: [PATCH 205/268] Installation order changed for permissions/ownership of dirs --- dist/setup.sh | 6 +++--- doc/INSTALL-MANUAL-STEPS.md | 6 +++--- doc/INSTALL.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 047e4815c..eaaacb4b2 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -489,8 +489,6 @@ install_docker() { try chmod +x stop.sh next fi - - cowsay } check_directory_owner() { @@ -648,7 +646,9 @@ fi if [[ $INSTALL == 1 ]]; then sanity_checks create_user + install modify_owner modify_permissions - install fi + +cowsay diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 5b228a490..5220858d2 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -140,9 +140,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 -H "Authorization: Bearer $token" -k https://localhost/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 -H "Authorization: Bearer $token" -k https://localhost/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://localhost/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/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 ``` If you need the authorization header to copy/paste in another tool: diff --git a/doc/INSTALL.md b/doc/INSTALL.md index eeab4b987..eb982333c 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -114,9 +114,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 -H "Authorization: Bearer $token" -k https://localhost/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 -H "Authorization: Bearer $token" -k https://localhost/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://localhost/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/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 ``` If you need the authorization header to copy/paste in another tool: From fe64e45a561fc598e7133cc5c9054693bc78d4d0 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 18:26:04 +0100 Subject: [PATCH 206/268] added autostart command line switch -s --- dist/setup.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index eaaacb4b2..9550e665d 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -591,6 +591,7 @@ RECREATE=0 TRACING=1 ALWAYSYES=0 SUDO_REQUIRED=0 +AUTOSTART=0 # trap ctrl-c and call ctrl_c() trap ctrl_c INT @@ -600,7 +601,7 @@ function ctrl_c() { exit } -while getopts ":cirhy" opt; do +while getopts ":cirhys" opt; do case $opt in r) RECREATE=1 @@ -614,8 +615,15 @@ while getopts ":cirhy" opt; do y) ALWAYSYES=1 ;; + s) + AUTOSTART=1 + ;; h) - echo "Use -c to configure and -i to install or -r to recreate from config.json." >&2 + echo "-c configure" >&2 + echo "-r recreate" >&2 + echo "-i install" >&2 + echo "-y assume yes to all questions" >&2 + echo "-s autostart" >&2 exit ;; \?) @@ -651,4 +659,9 @@ if [[ $INSTALL == 1 ]]; then modify_permissions fi -cowsay +if [[ $AUTOSTART == 1 ]]; then + exec ./start.sh +else + cowsay +fi + From 904828036407224dc0a03501f7ae36633b2343c0 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 19:35:14 +0100 Subject: [PATCH 207/268] building with current tags so it fits the dockercompose file --- build.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/build.sh b/build.sh index 24b65d2e0..122f71d68 100755 --- a/build.sh +++ b/build.sh @@ -18,16 +18,16 @@ trace_rc() build_docker_image() { - + local dockerfile="Dockerfile" if [[ ""$3 != "" ]]; then dockerfile=$3 fi - trace "building docker image: $2:latest" + 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:latest + docker build $1 -f $1/$dockerfile -t $2 } @@ -46,19 +46,19 @@ build_docker_images() { fi trace "Creating cyphernodeconf image" - build_docker_image install/ cyphernodeconf + build_docker_image install/ cyphernode/cyphernodeconf:cyphernode-0.05 trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin - build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning $clightning_dockerfile - + build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin:cyphernode-0.05 + build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning:cyphernode-0.05 $clightning_dockerfile + trace "Creating cyphernode images" - build_docker_image api_auth_docker/ cyphernode/gatekeeper - build_docker_image proxy_docker/ cyphernode/proxy - build_docker_image cron_docker/ cyphernode/proxycron - build_docker_image pycoin_docker/ cyphernode/pycoin - build_docker_image otsclient_docker/ cyphernode/otsclient - + build_docker_image api_auth_docker/ cyphernode/gatekeeper:cyphernode-0.05 + build_docker_image proxy_docker/ cyphernode/proxy:cyphernode-0.05 + build_docker_image cron_docker/ cyphernode/proxycron:cyphernode-0.05 + build_docker_image pycoin_docker/ cyphernode/pycoin:cyphernode-0.05 + build_docker_image otsclient_docker/ cyphernode/otsclient:cyphernode-0.05 + } build_docker_images From 17c764ce72a6955e3aa2b2d21609cfabf602f4de Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 19:35:51 +0100 Subject: [PATCH 208/268] I hope this works ;-) --- api_auth_docker/entrypoint.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh index 6c4c968ab..158b281cd 100644 --- a/api_auth_docker/entrypoint.sh +++ b/api_auth_docker/entrypoint.sh @@ -11,9 +11,6 @@ if [[ $1 ]]; then fi -# create files with -rw-rw---- -# this will allow /var/run/fcgiwrap.socket to be accessed rw for group -su -c "umask 0006" $user - spawn-fcgi -M 0660 -s /var/run/fcgiwrap.socket -u $user -g nginx -U $user -- `which fcgiwrap` +chmod g+rw /var/run/fcgiwrap.socket nginx -g "daemon off;" From 4b72317260c73b844ed0784cf739571b4156a78e Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 19:37:08 +0100 Subject: [PATCH 209/268] localhost and 127.0.0.1 are always added to the gatekeeper cert, the current hostname will be set as default hostname, which can be changed during configuraion --- dist/setup.sh | 1 + install/generator-cyphernode/generators/app/index.js | 2 +- install/generator-cyphernode/generators/app/lib/cert.js | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 9550e665d..945dd1de0 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -177,6 +177,7 @@ configure() { # configure features of cyphernode docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ + -e DEFAULT_CERT_HOSTNAME=$(hostname) \ --log-driver=none$pw_env \ --network none \ --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 74d0f3cde..55076664f 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -364,7 +364,7 @@ module.exports = class extends Generator { gatekeeper_keys: { configEntries: [], clientInformation: [] }, gatekeeper_sslcert: '', gatekeeper_sslkey: '', - gatekeeper_cns: '', + gatekeeper_cns: process.env['DEFAULT_CERT_HOSTNAME'] || '', proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_datapath: '', diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index 01fdde454..8f2986f99 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -64,6 +64,8 @@ module.exports = class Cert { async create( cns ) { cns = cns || []; + cns = cns.concat(['127.0.0.1','localhost']); + let args = defaultArgs.slice(); const certFileTmp = tmp.fileSync(); @@ -84,9 +86,9 @@ module.exports = class Cert { const conf = this.buildConfig( cns ); fs.writeFileSync( confFileTmp.name, conf ); - + const openssl = spawn('openssl', args, { stdio: ['ignore', 'ignore', 'ignore'] } ); - + let code = await new Promise( function(resolve, reject) { openssl.on('exit', (code) => { resolve(code); From c984e8ff6acac29bf9833edbd693068692d8f629 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 19:45:49 +0100 Subject: [PATCH 210/268] only write stuff to config files, when its actually there --- .../generators/app/templates/installer/config.sh | 12 +++++++++--- .../templates/installer/docker/docker-compose.yaml | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 399a3951b..35ed8eeca 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -3,11 +3,17 @@ BITCOIN_INTERNAL=<%= (bitcoin_mode==="internal"?'true':'false') %> FEATURE_LIGHTNING=<%= (features.indexOf('lightning') != -1)?'true':'false' %> FEATURE_OTSCLIENT=<%= (features.indexOf('otsclient') != -1)?'true':'false' %> LIGHTNING_IMPLEMENTATION=<%= lightning_implementation %> -BITCOIN_DATAPATH=<%= bitcoin_datapath %> -LIGHTNING_DATAPATH=<%= lightning_datapath %> -OTSCLIENT_DATAPATH=<%= otsclient_datapath %> PROXY_DATAPATH=<%= proxy_datapath %> GATEKEEPER_DATAPATH=<%= gatekeeper_datapath %> DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> +<% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> +OTSCLIENT_DATAPATH=<%= otsclient_datapath %> +<% } %> +<% if ( features.indexOf('otsclient') !== -1 ) { %> +LIGHTNING_DATAPATH=<%= lightning_datapath %> +<% } %> +<% if ( bitcoin_mode==="internal" ) { %> +BITCOIN_DATAPATH=<%= bitcoin_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 d5c0df850..b28cb255c 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 @@ -48,8 +48,13 @@ services: <% } %> volumes: - "<%= proxy_datapath %>:/proxy/db" + <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> - "<%= lightning_datapath %>:/.lightning" + <% } %> + <% if ( features.indexOf('otsclient') !== -1 ) { %> - "<%= otsclient_datapath %>:/proxy/otsfiles" + <% } %> + # deploy: # placement: # constraints: [node.hostname==dev] From 5cb9f4e380164d742c86036391d016cb6d62b3f8 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 6 Dec 2018 14:07:32 -0500 Subject: [PATCH 211/268] Added help texts, api/keys sample files and minor stuff. --- api_auth_docker/README.md | 2 +- api_auth_docker/api-sample.properties | 33 +++++++++ api_auth_docker/keys-sample.properties | 9 +++ .../generators/app/help.json | 68 +++++++++---------- .../generators/app/index.js | 8 +-- 5 files changed, 81 insertions(+), 39 deletions(-) create mode 100644 api_auth_docker/api-sample.properties create mode 100644 api_auth_docker/keys-sample.properties diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index a5d999080..9de7336d3 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -43,7 +43,7 @@ dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32 Put the id, key and groups in keys.properties and give the id and key to the client. The key is a secret. keys.properties looks like this: ```property -#kappiid="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact +# kapi_id="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties new file mode 100644 index 000000000..f78e34bdc --- /dev/null +++ b/api_auth_docker/api-sample.properties @@ -0,0 +1,33 @@ +# The file api.properties generated by the installer should look like this. + +# Watcher can: +action_watch=watcher +action_unwatch=watcher +action_getactivewatches=watcher +action_getbestblockhash=watcher +action_getbestblockinfo=watcher +action_getblockinfo=watcher +action_gettransaction=watcher +action_ln_getinfo=watcher +action_ln_create_invoice=watcher + +# Spender can do what the watcher can do, plus: +action_getbalance=spender +action_getnewaddress=spender +action_spend=spender +action_addtobatch=spender +action_batchspend=spender +action_deriveindex=spender +action_derivepubpath=spender +action_ln_pay=spender +action_ln_newaddr=spender +action_ots_stamp=spender +action_ots_getfile=spender + +# Admin can do what the spender can do, plus: + + +# Should be called from inside the Docker network only: +action_conf=internal +action_executecallbacks=internal +action_ots_backoffice=internal diff --git a/api_auth_docker/keys-sample.properties b/api_auth_docker/keys-sample.properties new file mode 100644 index 000000000..637f0a3f3 --- /dev/null +++ b/api_auth_docker/keys-sample.properties @@ -0,0 +1,9 @@ +# The file keys.properties generated by the installer should look like this. + +# kapi_id="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact +kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 225d3d52a..b5fcc6bb6 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -1,39 +1,39 @@ { - "features": "** features **", - "net": "** net **", - "run_as_different_user": "** run_as_different_user **", - "username": "** username **", - "xpub": "** xpub **", - "derivation_path": "** derivation_path **", - "proxy_datapath": "** proxy_datapath **", - "gatekeeper_clientkeyspassword": "** gatekeeper_clientkeyspassword **", - "gatekeeper_clientkeyspassword_c": "** gatekeeper_clientkeyspassword_c **", - "gatekeeper_recreatekeys": "** gatekeeper_recreatekeys **", - "gatekeeper_recreatecert": "** gatekeeper_recreatecert **", - "gatekeeper_datapath": "** gatekeeper_datapath **", - "gatekeeper_edit_apiproperties": "** gatekeeper_edit_apiproperties **", - "gatekeeper_apiproperties": "** gatekeeper_apiproperties **", + "features": "What optional features do you want me to activate? Select multiple using the space bar.", + "net": "Running on what Bitcoin network?", + "run_as_different_user": "We recommend running Cyphernode as a different user. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", + "username": "Run Cyphernode as what user? We recommend user 'cyphernode' (without the quotes). If the user does not exist, we will create it for you.", + "xpub": "Optional. Cyphernode can derive addresses from this default xPub key. With that functionality, you don't have to provide your xPub every time you call the derive endpoints.", + "derivation_path": "Optional. Cyphernode can derive addresses from this default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derive endpoints.", + "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", + "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", + "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", + "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", + "gatekeeper_edit_apiproperties": "Not recommended. It is possible to manually edit the API endpoints/groups authorization.", + "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", "gatekeeper_sslkey": "** gatekeeper_sslkey **", - "gatekeeper_cns": "** gatekeeper_cns **", - "bitcoin_mode": "** bitcoin_mode **", - "bitcoin_node_ip": "** bitcoin_node_ip **", - "bitcoin_rpcuser": "** bitcoin_rpcuser **", - "bitcoin_rpcpassword": "** bitcoin_rpcpassword **", - "bitcoin_prune": "** bitcoin_prune **", - "bitcoin_prune_size": "** bitcoin_prune_size **", - "bitcoin_uacomment": "** bitcoin_uacomment **", - "bitcoin_datapath": "** bitcoin_datapath **", - "bitcoin_expose": "** bitcoin_expose **", - "lightning_implementation": "** lightning_implementation **", - "lightning_external_ip": "** lightning_external_ip **", - "lightning_nodename": "** lightning_nodename **", - "lightning_nodecolor": "** lightning_nodecolor **", - "lightning_datapath": "** lightning_datapath **", - "lightning_expose": "** lightning_expose **", - "installer_mode": "** installer_mode **", - "installer_cleanup": "** installer_cleanup **", - "docker_mode": "** docker_mode **", +"gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. 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 it.", + "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.", + "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", + "bitcoin_prune_size": "Minimum value is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", + "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", + "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", + "bitcoin_expose": "By default, Bitcoin node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", + "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", + "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", + "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", + "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be FF0000.", + "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", + "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", + "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", + "installer_mode": "As of today, two installation modes are supported: local docker (self-hosted) or on a lunanode VM (hosted at lunanode.com).", + "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", + "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc. If you don't know and the last Bitcoin block mined has an odd height number, choose Swarm... or not.", "__default__": "Key missing!
There is no help text for this entry. :-(" } - \ No newline at end of file diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 55076664f..6cd709aa5 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -29,7 +29,7 @@ action_gettransaction=watcher action_ln_getinfo=watcher action_ln_create_invoice=watcher -# Spender can do what the watcher can do plus: +# Spender can do what the watcher can do, plus: action_getbalance=spender action_getnewaddress=spender action_spend=spender @@ -42,10 +42,10 @@ action_ln_newaddr=spender action_ots_stamp=spender action_ots_getfile=spender -# Admin can do what the spender can do plus: +# Admin can do what the spender can do, plus: -# Should be called from inside the Swarm: +# Should be called from inside the Docker network only: action_conf=internal action_executecallbacks=internal action_ots_backoffice=internal @@ -346,7 +346,7 @@ module.exports = class extends Generator { installer_mode: 'docker', devmode: false, devregistry: false, - run_as_different_user: false, + run_as_different_user: true, username: 'cyphernode', docker_mode: 'compose', bitcoin_rpcuser: 'bitcoin', From 5f64a1b936a45f5922a231771725b7975946ec2a Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 20:06:48 +0100 Subject: [PATCH 212/268] added version tag to config files for later migration --- install/generator-cyphernode/generators/app/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 6cd709aa5..9a9654e06 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -16,6 +16,8 @@ const userRegexp = /^[a-zA-Z0-9\._\-]+$/; const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; +const configFileVersion='0.0.1'; + const defaultAPIProperties = ` # Watcher can: @@ -156,6 +158,7 @@ module.exports = class extends Generator { try { this.props = JSON.parse(r.value); + this.props.__version = this.props.__version || configFileVersion; } catch( err ) { console.log(chalk.bold.red('config archive is corrupt.')); process.exit(1); @@ -185,8 +188,14 @@ module.exports = class extends Generator { } this.configurationPassword = r.password0; - this.props = {}; + this.props = { + __version: configFileVersion + }; + + } + if( this.props.__version !== configFileVersion ) { + // migrate here } this._assignConfigDefaults(); From 6927812f9240e696f89d2b067195fdae914f6a4f Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 6 Dec 2018 21:56:36 +0100 Subject: [PATCH 213/268] specify user to run cyphernodeconf under by ENV variable --- dist/setup.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 945dd1de0..549fd6aee 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -174,13 +174,19 @@ configure() { docker rm -f $otherCyphernodeconf > /dev/null 2>&1 fi + local user=$(id -u):$(id -g) + + if [[ ! ''$CONFIGURE_AS_USER == '' ]]; then + user=$CONFIGURE_AS_USER + fi + # configure features of cyphernode docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ -e DEFAULT_CERT_HOSTNAME=$(hostname) \ --log-driver=none$pw_env \ --network none \ - --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $(id -u):$(id -g) yo --no-insight cyphernode$gen_options $recreate + --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $user yo --no-insight cyphernode$gen_options $recreate if [[ -f exitStatus.sh ]]; then . ./exitStatus.sh rm ./exitStatus.sh From 7e6638c940f031cae59f5b4c3d32e164e42a2a9a Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 8 Dec 2018 20:15:29 +0100 Subject: [PATCH 214/268] copying essential files for later use in proxy and gatekeeper --- dist/setup.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 549fd6aee..62a78440a 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -368,7 +368,8 @@ install_docker() { copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED - copy_file $sourceDataPath/gatekeeper/ip-whitelist.conf $GATEKEEPER_DATAPATH/ip-whitelist.conf 1 $SUDO_REQUIRED + copy_file $sourceDataPath/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED + copy_file $sourceDataPath/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED fi @@ -379,6 +380,8 @@ install_docker() { next fi + copy_file $sourceDataPath/installer/config.sh $PROXY_DATAPATH/config.sh 1 $SUDO_REQUIRED + if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then step " create $BITCOIN_DATAPATH" From 91b3f5323dad2e5b68c3ca10a95cef323e3c4b24 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 8 Dec 2018 20:33:37 +0100 Subject: [PATCH 215/268] disabling run as user option for OSX --- dist/setup.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 62a78440a..5ac58096a 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -528,16 +528,16 @@ sanity_checks() { echo " check requirements." + local OS=$(uname -s) + + if [[ $OS == 'Darwin' ]]; then + echo " Run as user option is not supported on OSX." + echo " Please run start.sh later as the user you are running this setup utility under." + RUN_AS_USER=$USER + fi + if [[ ''$RUN_AS_USER == '' ]]; then RUN_AS_USER=$USER - else - local OS=$(uname -s) - id -u $RUN_AS_USER > /dev/null 2>&1 - if [[ $OS == 'Darwin' && $? == 1 ]]; then - echo " Automatic user creation not supported on OSX." - echo " Please create the user \"$RUN_AS_USER\" by hand and run: ./setup.sh -i" - exit - fi fi local sudo=0 From 714e72e76ba725d65840ff6e18c224acf59d6035 Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 8 Dec 2018 20:39:44 +0100 Subject: [PATCH 216/268] only warn user if run as user option is set :-/ --- dist/setup.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 5ac58096a..449b30473 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -530,14 +530,12 @@ sanity_checks() { local OS=$(uname -s) - if [[ $OS == 'Darwin' ]]; then - echo " Run as user option is not supported on OSX." - echo " Please run start.sh later as the user you are running this setup utility under." - RUN_AS_USER=$USER - fi - if [[ ''$RUN_AS_USER == '' ]]; then RUN_AS_USER=$USER + elif [[ $OS == 'Darwin' ]]; then + echo " Run as user option is not supported on OSX." + echo " Please run start.sh later as the user you are running this setup utility under." + RUN_AS_USER=$USER fi local sudo=0 From 4546cb1d5491025b16ceeea54ac06d6f586d176e Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 8 Dec 2018 20:50:23 +0100 Subject: [PATCH 217/268] added checks for docker commands --- dist/setup.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dist/setup.sh b/dist/setup.sh index 449b30473..d17f4e1a0 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -528,6 +528,16 @@ sanity_checks() { echo " check requirements." + 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 ! [ -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 + local OS=$(uname -s) if [[ ''$RUN_AS_USER == '' ]]; then @@ -581,7 +591,7 @@ sanity_checks() { SUDO_REQUIRED=1 fi else - echo " check everything seems to be ok." + echo " nice! everything seems to be ok." fi } From 50235c0d64144144173d506e34e787b214953e2d Mon Sep 17 00:00:00 2001 From: jash Date: Sat, 8 Dec 2018 21:12:49 +0100 Subject: [PATCH 218/268] made xpub key optional --- .../generators/app/prompters/000_cyphernode.js | 18 +++++++++++++++--- .../installer/docker/docker-compose.yaml | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index 6aef4d337..ce495d885 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -11,7 +11,7 @@ const prefix = function() { }; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -43,7 +43,7 @@ module.exports = { message: prefix()+'Run as different user?'+utils._getHelp('run_as_different_user') }, { - when: function( props ) { + when: function( props ) { return props.run_as_different_user; }, type: 'input', @@ -54,14 +54,26 @@ module.exports = { validate: utils._usernameValidator }, { + type: 'confirm', + name: 'use_xpub', + default: utils._getDefault( 'want_xpub' )||false, + message: prefix()+'Use an xpub key to watch or generate adresses?'+utils._getHelp('use_xpub'), + }, + { + when: function( props ) { + return props.use_xpub; + }, type: 'input', name: 'xpub', default: utils._getDefault( 'xpub' ), - message: prefix()+'What is your xpub to watch?'+utils._getHelp('xpub'), + message: prefix()+'What is your xpub key?'+utils._getHelp('xpub'), filter: utils._trimFilter, validate: utils._xkeyValidator }, { + when: function( props ) { + return props.use_xpub; + }, type: 'input', name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), 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 b28cb255c..2278e8a19 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 @@ -36,8 +36,10 @@ services: - "DB_PATH=/proxy/db" - "DB_FILE=/proxy/db/proxydb" - "PYCOIN_CONTAINER=pycoin:7777" +<% if ( use_xpub && xpub ) { %> - "DERIVATION_PUB32=<%= xpub %>" - "DERIVATION_PATH=<%= derivation_path %>" +<% } %> - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - "OTSCLIENT_CONTAINER=otsclient:6666" - "OTS_FILES=/proxy/otsfiles" From 8b1fa000c267567538c03622c8d6c15905b536f9 Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 8 Dec 2018 21:23:07 -0500 Subject: [PATCH 219/268] lightning dir was created at the wrong place because of the volume --- proxy_docker/Dockerfile.amd64 | 2 +- proxy_docker/Dockerfile.arm32v6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index a4361c6fa..0cbabd780 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -54,6 +54,6 @@ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ && chmod o+w . \ && mkdir db -VOLUME ["${HOME}/db", "${HOME}/.lightning"] +VOLUME ["${HOME}/db", "/.lightning"] ENTRYPOINT ["su-exec"] diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index 31ba7d762..0a4f66c74 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -50,6 +50,6 @@ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ && chmod o+w . \ && mkdir db -VOLUME ["${HOME}/db", "${HOME}/.lightning"] +VOLUME ["${HOME}/db", "/.lightning"] ENTRYPOINT ["su-exec"] From eddf47223dd70cc54a0ff08e57340b93d588fb58 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 9 Dec 2018 11:53:55 +0100 Subject: [PATCH 220/268] more verbosity :) --- dist/setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/setup.sh b/dist/setup.sh index d17f4e1a0..6f27b2782 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -178,6 +178,7 @@ configure() { if [[ ! ''$CONFIGURE_AS_USER == '' ]]; then user=$CONFIGURE_AS_USER + step "configure as user \"$CONFIGURE_AS_USER\"" fi # configure features of cyphernode From 5136324728f2a755c770c33926d55a05a125b01e Mon Sep 17 00:00:00 2001 From: jash Date: Tue, 11 Dec 2018 23:34:27 +0100 Subject: [PATCH 221/268] fix datapath confusion --- .../generators/app/templates/installer/config.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 35ed8eeca..69a1129f7 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -9,10 +9,10 @@ DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> -OTSCLIENT_DATAPATH=<%= otsclient_datapath %> +OTSCLIENT_DATAPATH=<%= lightning_datapath %> <% } %> <% if ( features.indexOf('otsclient') !== -1 ) { %> -LIGHTNING_DATAPATH=<%= lightning_datapath %> +LIGHTNING_DATAPATH=<%= otsclient_datapath %> <% } %> <% if ( bitcoin_mode==="internal" ) { %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> From ebacdec65a637c81e746296873b174d626ce3113 Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 12 Dec 2018 13:14:04 +0100 Subject: [PATCH 222/268] fixed datapath confusion confusion --- .../generators/app/templates/installer/config.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/config.sh b/install/generator-cyphernode/generators/app/templates/installer/config.sh index 69a1129f7..ddd9ca3cc 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/config.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/config.sh @@ -9,10 +9,10 @@ DOCKER_MODE=<%= docker_mode %> RUN_AS_USER=<%= run_as_different_user?username:'' %> CLEANUP=<%= installer_cleanup?'true':'false' %> <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> -OTSCLIENT_DATAPATH=<%= lightning_datapath %> +LIGHTNING_DATAPATH=<%= lightning_datapath %> <% } %> <% if ( features.indexOf('otsclient') !== -1 ) { %> -LIGHTNING_DATAPATH=<%= otsclient_datapath %> +OTSCLIENT_DATAPATH=<%= otsclient_datapath %> <% } %> <% if ( bitcoin_mode==="internal" ) { %> BITCOIN_DATAPATH=<%= bitcoin_datapath %> From 0284f177a070c5ef92046c813c8e9fbb2e2af573 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 11 Dec 2018 16:51:17 -0500 Subject: [PATCH 223/268] Added installation and features tests --- dist/setup.sh | 15 +- .../generators/app/help.json | 7 +- .../generators/app/lib/cert.js | 2 +- .../generators/app/prompters/999_installer.js | 4 +- .../app/templates/installer/start.sh | 9 +- .../app/templates/installer/testfeatures.sh | 295 ++++++++++++++++++ 6 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh diff --git a/dist/setup.sh b/dist/setup.sh index 6f27b2782..86f0ea360 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -486,6 +486,8 @@ install_docker() { fi copy_file $sourceDataPath/installer/docker/docker-compose.yaml docker-compose.yaml + copy_file $sourceDataPath/installer/testinstall.sh testinstall.sh 0 + copy_file $sourceDataPath/installer/testfeatures.sh testfeatures.sh 0 copy_file $sourceDataPath/installer/start.sh start.sh 0 copy_file $sourceDataPath/installer/stop.sh stop.sh 0 @@ -500,6 +502,18 @@ install_docker() { try chmod +x stop.sh next fi + + if [[ ! -x testinstall.sh ]]; then + step " make testinstall.sh executable" + try chmod +x testinstall.sh + next + fi + + if [[ ! -x testfeatures.sh ]]; then + step " make testfeatures.sh executable" + try chmod +x testfeatures.sh + next + fi } check_directory_owner() { @@ -683,4 +697,3 @@ if [[ $AUTOSTART == 1 ]]; then else cowsay fi - diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index b5fcc6bb6..2ac473f9b 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -3,8 +3,9 @@ "net": "Running on what Bitcoin network?", "run_as_different_user": "We recommend running Cyphernode as a different user. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", "username": "Run Cyphernode as what user? We recommend user 'cyphernode' (without the quotes). If the user does not exist, we will create it for you.", - "xpub": "Optional. Cyphernode can derive addresses from this default xPub key. With that functionality, you don't have to provide your xPub every time you call the derive endpoints.", - "derivation_path": "Optional. Cyphernode can derive addresses from this default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derive endpoints.", + "use_xpub": "Cyphernode can take care of deriving your addresses from your xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path at each call.", + "xpub": "Cyphernode can derive addresses from this default xPub key. With that functionality, you don't have to provide your xPub every time you call the derive endpoints.", + "derivation_path": "Cyphernode can derive addresses from this default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derive endpoints.", "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", @@ -15,7 +16,7 @@ "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", "gatekeeper_sslkey": "** gatekeeper_sslkey **", -"gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. Make sure the provided domain names are in your DNS or client's hosts file and is reachable.", + "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. 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 it.", "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/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index 8f2986f99..e7444fc90 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -64,7 +64,7 @@ module.exports = class Cert { async create( cns ) { cns = cns || []; - cns = cns.concat(['127.0.0.1','localhost']); + cns = cns.concat(['127.0.0.1','localhost','gatekeeper']); let args = defaultArgs.slice(); diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index bacd2b5d9..6c400b679 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -123,8 +123,8 @@ module.exports = { }, templates: function( props ) { if( props.installer_mode === 'docker' ) { - return ['config.sh','start.sh', 'stop.sh', path.join('docker', 'docker-compose.yaml')]; + return ['config.sh','start.sh', 'stop.sh', 'testinstall.sh', 'testfeatures.sh', path.join('docker', 'docker-compose.yaml')]; } - return ['config.sh','start.sh', 'stop.sh']; + return ['config.sh','start.sh', 'stop.sh', 'testinstall.sh', 'testfeatures.sh']; } }; diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 189451e60..04092d2fb 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -8,4 +8,11 @@ export ARCH=$(uname -m) docker stack deploy -c docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> docker-compose -f docker-compose.yaml up -d --remove-orphans -<% } %> \ No newline at end of file +<% } %> + +# Will test if Cyphernode is fully up and running... +docker run --rm -it -v `pwd`/testfeatures.sh:/testfeatures.sh \ +-v `pwd`/gatekeeper/keys.properties:/keys.properties \ +-v `pwd`/gatekeeper/cert.pem:/cert.pem \ +-v <%= proxy_datapath %>:/proxy \ +--network cyphernodenet alpine:3.8 /testfeatures.sh diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh new file mode 100644 index 000000000..b3e94d6e1 --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -0,0 +1,295 @@ +#!/bin/sh + +apk add --update --no-cache openssl curl + +. keys.properties + +checkgatekeeper() { + echo ; echo "Testing Gatekeeper..." > /dev/console + + local rc + local id="001" + local k + eval k='$ukey_'$id + + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + + # Let's test expiration: 1 second in payload, request 2 seconds later + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Sleeping 2 seconds... " > /dev/console + sleep 2 + + echo " Testing expired request... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getblockinfo) + [ "${rc}" -ne "403" ] && return 10 + + # Let's test authentication (signature) + + p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + token="$h64.$p64.a$s" + + echo " Testing bad signature... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getblockinfo) + [ "${rc}" -ne "403" ] && return 30 + + # Let's test authorization (action access for groups) + + token="$h64.$p64.$s" + + echo " Testing watcher trying to do a spender action... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbalance) + [ "${rc}" -ne "403" ] && return 40 + + id="002" + eval k='$ukey_'$id + p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + token="$h64.$p64.$s" + + echo " Testing spender trying to do an internal action call... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/conf) + [ "${rc}" -ne "403" ] && return 50 + + + id="003" + eval k='$ukey_'$id + p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + token="$h64.$p64.$s" + + echo " Testing admin trying to do an internal action call... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/conf) + [ "${rc}" -ne "403" ] && return 60 + + echo "***** Gatekeeper rocks!" > /dev/console + + return 0 +} + +checkpycoin() { + echo ; echo "Testing Pycoin..." > /dev/console + local rc + local id="002" + local k + eval k='$ukey_'$id + + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Testing pycoin... " > /dev/console + rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) + [ "${rc}" -ne "200" ] && return 100 + + echo "***** Pycoin rocks!" > /dev/console + + return 0 +} + +checkots() { + echo ; echo "Testing OTSclient..." > /dev/console + local rc + local id="002" + local k + eval k='$ukey_'$id + + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Testing otsclient... " > /dev/console + rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ots_stamp) + echo "${rc}" | grep "Invalid hash 123 for sha256" > /dev/null + [ "$?" -ne "0" ] && return 200 + + echo "***** OTSclient rocks!" > /dev/console + + return 0 +} + +checkbitcoinnode() { + echo ; echo "Testing Bitcoin..." > /dev/console + local rc + local id="002" + local k + eval k='$ukey_'$id + + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Testing bitcoin node... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) + [ "${rc}" -ne "200" ] && return 300 + + echo "***** Bitcoin node rocks!" > /dev/console + + return 0 +} + +checklnnode() { + echo ; echo "Testing Lightning..." > /dev/console + local rc + local id="002" + local k + eval k='$ukey_'$id + + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Testing LN node... " > /dev/console + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) + [ "${rc}" -ne "200" ] && return 400 + + echo "***** LN node rocks!" > /dev/console + + return 0 +} + +checkservice() { + echo ; echo "Testing if Cyphernode is up and running... I will keep trying during up to 5 minutes to give time to Docker to deploy everything..." > /dev/console + + local outcome + local returncode=0 + local endtime=$(($(date +%s) + 300)) + local result + + while : + do + outcome=0 + for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + echo " Verifying ${container}..." > /dev/console + (ping -c 10 ${container} | 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 + eval wait '$'${container} ; returncode=$? ; outcome=$((${outcome} + ${returncode})) + eval c_${container}=${returncode} + done + + # If '0% packet loss' everywhere or 5 minutes passed, we get out of this loop + ([ "${outcome}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break + + sleep 5 + done + + # "containers": { + # "gatekeeper":true, + # "proxy":true, + # "proxycron":true, + # "pycoin":true, + # "otsclient":true, + # "bitcoin":true, + # "lightning":true + # } + for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do + echo " Analyzing ${container} results..." > /dev/console + [ -n "${result}" ] && result="${result}," + result="${result}\"${container}\":" + eval "returncode=\$c_${container}" + if [ "${returncode}" -eq "0" ]; then + result="${result}true" + else + result="${result}false" + fi + done + + result="\"containers\":{${result}}" + + echo $result + + return ${outcome} +} + +# /proxy/installation.json will contain something like that: +#{ +# "containers": { +# "gatekeeper":true, +# "proxy":true, +# "proxycron":true, +# "pycoin":true, +# "otsclient":true, +# "bitcoin":true, +# "lightning":true +# }, +# "features": { +# "gatekeeper":true, +# "pycoin":true, +# "otsclient":true, +# "bitcoin":true, +# "lightning":true +# } +#} + +# Let's first see if everything is up. + +result=$(checkservice) +returncode=$? +if [ "${returncode}" -ne "0" ]; then + echo "xxxxx Cyphernode could not fully start properly within 5 minutes." > /dev/console +else + echo "***** Cyphernode seems to be correctly deployed. Let's run more thourough tests..." > /dev/console +fi + +# Let's now check each feature fonctionality... +# "features": { +# "gatekeeper":true, +# "pycoin":true, +# "otsclient":true, +# "bitcoin":true, +# "lightning":true +# } + +result="${result},\"features\":{\"gatekeeper\":" +checkgatekeeper +returncode=$? +[ "${returncode}" -eq "0" ] && result="${result}true" +[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Gatekeeper error!" > /dev/console + +result="${result},\"pycoin\":" +checkpycoin +returncode=$? +[ "${returncode}" -eq "0" ] && result="${result}true" +[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Pycoin error!" > /dev/console + +<% if (features.indexOf('otsclient') != -1) { %> +result="${result},\"otsclient\":" +checkots +returncode=$? +[ "${returncode}" -eq "0" ] && result="${result}true" +[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx OTSclient error!" > /dev/console +<% } %> + +result="${result},\"bitcoin\":" +checkbitcoinnode +returncode=$? +[ "${returncode}" -eq "0" ] && result="${result}true" +[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Bitcoin error!" > /dev/console + +<% if (features.indexOf('lightning') != -1) { %> +result="${result},\"lightning\":" +checklnnode +returncode=$? +[ "${returncode}" -eq "0" ] && result="${result}true" +[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Lightning error!" > /dev/console +<% } %> + +result="{${result}}}" + +echo "${result}" > /proxy/installation.json + +echo ; echo "Tests finished." > /dev/console From fb3b421f20c9baca867d5bcb106886677c8d318f Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 11 Dec 2018 16:56:11 -0500 Subject: [PATCH 224/268] Cleaned obsolete files --- dist/setup.sh | 17 +++++------------ .../generators/app/prompters/999_installer.js | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 86f0ea360..9520427db 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -486,7 +486,6 @@ install_docker() { fi copy_file $sourceDataPath/installer/docker/docker-compose.yaml docker-compose.yaml - copy_file $sourceDataPath/installer/testinstall.sh testinstall.sh 0 copy_file $sourceDataPath/installer/testfeatures.sh testfeatures.sh 0 copy_file $sourceDataPath/installer/start.sh start.sh 0 copy_file $sourceDataPath/installer/stop.sh stop.sh 0 @@ -503,17 +502,11 @@ install_docker() { next fi - if [[ ! -x testinstall.sh ]]; then - step " make testinstall.sh executable" - try chmod +x testinstall.sh - next - fi - - if [[ ! -x testfeatures.sh ]]; then - step " make testfeatures.sh executable" - try chmod +x testfeatures.sh - next - fi + if [[ ! -x testfeatures.sh ]]; then + step " make testfeatures.sh executable" + try chmod +x testfeatures.sh + next + fi } check_directory_owner() { diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 6c400b679..d2231ba68 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -123,8 +123,8 @@ module.exports = { }, templates: function( props ) { if( props.installer_mode === 'docker' ) { - return ['config.sh','start.sh', 'stop.sh', 'testinstall.sh', 'testfeatures.sh', path.join('docker', 'docker-compose.yaml')]; + return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh', path.join('docker', 'docker-compose.yaml')]; } - return ['config.sh','start.sh', 'stop.sh', 'testinstall.sh', 'testfeatures.sh']; + return ['config.sh','start.sh', 'stop.sh', 'testfeatures.sh']; } }; From 819c5f99541df3aca11e922d2e4eb9694c29682e Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 11 Dec 2018 18:02:52 -0500 Subject: [PATCH 225/268] Feature testing tries during 2 minutes --- .../app/templates/installer/testfeatures.sh | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index b3e94d6e1..fc12e6f16 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -5,7 +5,7 @@ apk add --update --no-cache openssl curl . keys.properties checkgatekeeper() { - echo ; echo "Testing Gatekeeper..." > /dev/console + echo -e "\r\nTesting Gatekeeper..." > /dev/console local rc local id="001" @@ -72,7 +72,7 @@ checkgatekeeper() { } checkpycoin() { - echo ; echo "Testing Pycoin..." > /dev/console + echo -e "\r\nTesting Pycoin..." > /dev/console local rc local id="002" local k @@ -94,7 +94,7 @@ checkpycoin() { } checkots() { - echo ; echo "Testing OTSclient..." > /dev/console + echo -e "\r\nTesting OTSclient..." > /dev/console local rc local id="002" local k @@ -117,7 +117,7 @@ checkots() { } checkbitcoinnode() { - echo ; echo "Testing Bitcoin..." > /dev/console + echo -e "\r\nTesting Bitcoin..." > /dev/console local rc local id="002" local k @@ -139,7 +139,7 @@ checkbitcoinnode() { } checklnnode() { - echo ; echo "Testing Lightning..." > /dev/console + echo -e "\r\nTesting Lightning..." > /dev/console local rc local id="002" local k @@ -161,7 +161,7 @@ checklnnode() { } checkservice() { - echo ; echo "Testing if Cyphernode is up and running... I will keep trying during up to 5 minutes to give time to Docker to deploy everything..." > /dev/console + echo -e "\r\nTesting if Cyphernode is up and running... I will keep trying during up to 5 minutes to give time to Docker to deploy everything..." > /dev/console local outcome local returncode=0 @@ -197,7 +197,7 @@ checkservice() { # "lightning":true # } for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do - echo " Analyzing ${container} results..." > /dev/console + echo " Building ${container} results..." > /dev/console [ -n "${result}" ] && result="${result}," result="${result}\"${container}\":" eval "returncode=\$c_${container}" @@ -215,6 +215,35 @@ checkservice() { return ${outcome} } +timeout_feature() { + local testwhat=${1} + local returncode + local endtime=$(($(date +%s) + 120)) + + 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 + + echo "xxxxx Maybe it's too early, I'll retry in 5 seconds (for max 2 minutes total)." > /dev/console + + sleep 5 + done + + return ${returncode} +} + +feature_status() { + local returncode=${1} + local errormsg=${2} + + [ "${returncode}" -eq "0" ] && echo "true" + [ "${returncode}" -ne "0" ] && echo "false" && echo ${errormsg} > /dev/console +} + # /proxy/installation.json will contain something like that: #{ # "containers": { @@ -255,37 +284,32 @@ fi # } result="${result},\"features\":{\"gatekeeper\":" -checkgatekeeper +timeout_feature checkgatekeeper returncode=$? -[ "${returncode}" -eq "0" ] && result="${result}true" -[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Gatekeeper error!" > /dev/console +result="${result}$(feature_status ${returncode} 'xxxxx Gatekeeper error!')" result="${result},\"pycoin\":" -checkpycoin +timeout_feature checkpycoin returncode=$? -[ "${returncode}" -eq "0" ] && result="${result}true" -[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Pycoin error!" > /dev/console +result="${result}$(feature_status ${returncode} 'xxxxx Pycoin error!')" <% if (features.indexOf('otsclient') != -1) { %> result="${result},\"otsclient\":" -checkots +timeout_feature checkots returncode=$? -[ "${returncode}" -eq "0" ] && result="${result}true" -[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx OTSclient error!" > /dev/console +result="${result}$(feature_status ${returncode} 'xxxxx OTSclient error!')" <% } %> result="${result},\"bitcoin\":" -checkbitcoinnode +timeout_feature checkbitcoinnode returncode=$? -[ "${returncode}" -eq "0" ] && result="${result}true" -[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Bitcoin error!" > /dev/console +result="${result}$(feature_status ${returncode} 'xxxxx Bitcoin error!')" <% if (features.indexOf('lightning') != -1) { %> result="${result},\"lightning\":" -checklnnode +timeout_feature checklnnode returncode=$? -[ "${returncode}" -eq "0" ] && result="${result}true" -[ "${returncode}" -ne "0" ] && result="${result}false" && echo "xxxxx Lightning error!" > /dev/console +result="${result}$(feature_status ${returncode} 'xxxxx Lightning error!')" <% } %> result="{${result}}}" From 1d5a0bdae07273ab28a607e5c51f6139b3551ed4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 12 Dec 2018 11:49:49 -0500 Subject: [PATCH 226/268] Colors and visual stuff in setup --- .../generators/app/help.json | 70 +++++++++---------- .../generators/app/index.js | 2 +- .../generators/app/lib/html2ansi.js | 5 +- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 2ac473f9b..327213da4 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -1,40 +1,40 @@ { - "features": "What optional features do you want me to activate? Select multiple using the space bar.", - "net": "Running on what Bitcoin network?", - "run_as_different_user": "We recommend running Cyphernode as a different user. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", - "username": "Run Cyphernode as what user? We recommend user 'cyphernode' (without the quotes). If the user does not exist, we will create it for you.", - "use_xpub": "Cyphernode can take care of deriving your addresses from your xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path at each call.", - "xpub": "Cyphernode can derive addresses from this default xPub key. With that functionality, you don't have to provide your xPub every time you call the derive endpoints.", - "derivation_path": "Cyphernode can derive addresses from this default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derive endpoints.", - "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", - "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", - "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", - "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", - "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", - "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", - "gatekeeper_edit_apiproperties": "Not recommended. It is possible to manually edit the API endpoints/groups authorization.", - "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 Swarm, like the backoffice tasks used by the Cron container. The access groups for each API id/key are found in the keys.properties file.", + "features": "What optional features do you want me to activate? Select multiple choices using the space bar.", + "net": "You want Cyphernode to run on what Bitcoin network?", + "run_as_different_user": "We recommend running Cyphernode as a different user when possible. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", + "username": "Run Cyphernode as what user? We recommend user cyphernode. If the user does not exist, I will create it for you.", + "use_xpub": "Cyphernode can derive Bitcoin addresses from an xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path on each call.", + "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", + "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", + "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", + "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", + "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", + "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", + "gatekeeper_edit_apiproperties": "Not recommended. If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization.", + "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", "gatekeeper_sslkey": "** gatekeeper_sslkey **", - "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. 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 it.", - "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.", - "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", - "bitcoin_prune_size": "Minimum value is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", - "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", - "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", - "bitcoin_expose": "By default, Bitcoin node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", - "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", - "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", - "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", - "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be FF0000.", - "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", - "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", - "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", - "installer_mode": "As of today, two installation modes are supported: local docker (self-hosted) or on a lunanode VM (hosted at lunanode.com).", - "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", - "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc. If you don't know and the last Bitcoin block mined has an odd height number, choose Swarm... or not.", + "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. 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 it.", + "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.", + "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", + "bitcoin_prune_size": "Minimum size is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", + "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", + "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", + "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", + "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", + "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", + "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", + "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be FF0000.", + "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", + "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", + "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", + "installer_mode": "As of today, only one installation mode is supported: local docker (self-hosted). In the next release, we will support trivially installing Cyphernode on a Lunanode VM (hosted at lunanode.com).", + "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", + "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc. If you don't know and the last Bitcoin block mined has an odd height number, choose Swarm... or not.", "__default__": "Key missing!
There is no help text for this entry. :-(" } diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 9a9654e06..01b765735 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -475,7 +475,7 @@ module.exports = class extends Generator { return ''; } - return "\n\n"+wrap( html2ansi(helpText),82 )+"\n\n"; + return "\n\n"+chalk.reset.cyan(wrap( html2ansi(helpText),82 ))+"\n\n"; } }; diff --git a/install/generator-cyphernode/generators/app/lib/html2ansi.js b/install/generator-cyphernode/generators/app/lib/html2ansi.js index 44f24eef0..d63d81d97 100644 --- a/install/generator-cyphernode/generators/app/lib/html2ansi.js +++ b/install/generator-cyphernode/generators/app/lib/html2ansi.js @@ -11,7 +11,7 @@ const convert = function(data){ let v = data.childNodes && data.childNodes.length? data.childNodes.map(d=> convert(d)).join(''): data.value?data.value:''; - + switch(data.tagName){ case 'br': v += '\n' @@ -28,6 +28,9 @@ const convert = function(data){ if( attr.name === 'italic' && attr.value === 'true' ) { v = chalk.italic(v); } + if( attr.name === 'underline' && attr.value === 'true' ) { + v = chalk.underline(v); + } if( attr.name === 'strikethrough' && attr.value === 'true' ) { v = chalk.strikethrough(v); } From 2501a312dbbfdb0b2af830fb8cbbe55ca249d234 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 12 Dec 2018 14:00:10 -0500 Subject: [PATCH 227/268] Installation status json with arrays --- .../app/templates/installer/testfeatures.sh | 103 +++++++++--------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index fc12e6f16..4cdc245e2 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -84,7 +84,6 @@ checkpycoin() { local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" - echo " Testing pycoin... " > /dev/console rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) [ "${rc}" -ne "200" ] && return 100 @@ -106,7 +105,6 @@ checkots() { local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" - echo " Testing otsclient... " > /dev/console rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ots_stamp) echo "${rc}" | grep "Invalid hash 123 for sha256" > /dev/null [ "$?" -ne "0" ] && return 200 @@ -129,7 +127,6 @@ checkbitcoinnode() { local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" - echo " Testing bitcoin node... " > /dev/console rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) [ "${rc}" -ne "200" ] && return 300 @@ -151,7 +148,6 @@ checklnnode() { local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" - echo " Testing LN node... " > /dev/console rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) [ "${rc}" -ne "200" ] && return 400 @@ -184,31 +180,32 @@ checkservice() { # If '0% packet loss' everywhere or 5 minutes passed, we get out of this loop ([ "${outcome}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break + echo " Cyphernode still not ready, will retry in 5 seconds for max 5 minutes..." > /dev/console + sleep 5 done - # "containers": { - # "gatekeeper":true, - # "proxy":true, - # "proxycron":true, - # "pycoin":true, - # "otsclient":true, - # "bitcoin":true, - # "lightning":true - # } + # "containers": [ + # { "name": "gatekeeper", "active":true }, + # { "name": "proxy", "active":true }, + # { "name": "proxycron", "active":true }, + # { "name": "pycoin", "active":true }, + # { "name": "otsclient", "active":true }, + # { "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 - echo " Building ${container} results..." > /dev/console [ -n "${result}" ] && result="${result}," - result="${result}\"${container}\":" + result="${result}{\"name\":\"${container}\",\"active\":" eval "returncode=\$c_${container}" if [ "${returncode}" -eq "0" ]; then - result="${result}true" + result="${result}true}" else - result="${result}false" + result="${result}false}" fi done - result="\"containers\":{${result}}" + result="\"containers\":[${result}]" echo $result @@ -246,22 +243,22 @@ feature_status() { # /proxy/installation.json will contain something like that: #{ -# "containers": { -# "gatekeeper":true, -# "proxy":true, -# "proxycron":true, -# "pycoin":true, -# "otsclient":true, -# "bitcoin":true, -# "lightning":true -# }, -# "features": { -# "gatekeeper":true, -# "pycoin":true, -# "otsclient":true, -# "bitcoin":true, -# "lightning":true -# } +# "containers": [ +# { "name": "gatekeeper", "active":true }, +# { "name": "proxy", "active":true }, +# { "name": "proxycron", "active":true }, +# { "name": "pycoin", "active":true }, +# { "name": "otsclient", "active":true }, +# { "name": "bitcoin", "active":true }, +# { "name": "lightning", "active":true } +# ], +# "features": [ +# { "name": "gatekeeper", "working":true }, +# { "name": "pycoin", "working":true }, +# { "name": "otsclient", "working":true }, +# { "name": "bitcoin", "working":true }, +# { "name": "lightning", "working":true } +# ] #} # Let's first see if everything is up. @@ -275,44 +272,44 @@ else fi # Let's now check each feature fonctionality... -# "features": { -# "gatekeeper":true, -# "pycoin":true, -# "otsclient":true, -# "bitcoin":true, -# "lightning":true -# } - -result="${result},\"features\":{\"gatekeeper\":" +# "features": [ +# { "name": "gatekeeper", "working":true }, +# { "name": "pycoin", "working":true }, +# { "name": "otsclient", "working":true }, +# { "name": "bitcoin", "working":true }, +# { "name": "lightning", "working":true } +# ] + +result="${result},\"features\":[{\"name\":\"gatekeeper\",\"working\":" timeout_feature checkgatekeeper returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Gatekeeper error!')" +result="${result}$(feature_status ${returncode} 'xxxxx Gatekeeper error!')}" -result="${result},\"pycoin\":" +result="${result},{\"name\":\"pycoin\",\"working\":" timeout_feature checkpycoin returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Pycoin error!')" +result="${result}$(feature_status ${returncode} 'xxxxx Pycoin error!')}" <% if (features.indexOf('otsclient') != -1) { %> -result="${result},\"otsclient\":" +result="${result},{\"name\":\"otsclient\",\"working\":" timeout_feature checkots returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx OTSclient error!')" +result="${result}$(feature_status ${returncode} 'xxxxx OTSclient error!')}" <% } %> -result="${result},\"bitcoin\":" +result="${result},{\"name\":\"bitcoin\",\"working\":" timeout_feature checkbitcoinnode returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Bitcoin error!')" +result="${result}$(feature_status ${returncode} 'xxxxx Bitcoin error!')}" <% if (features.indexOf('lightning') != -1) { %> -result="${result},\"lightning\":" +result="${result},{\"name\":\"lightning\",\"working\":" timeout_feature checklnnode returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Lightning error!')" +result="${result}$(feature_status ${returncode} 'xxxxx Lightning error!')}" <% } %> -result="{${result}}}" +result="{${result}]}" echo "${result}" > /proxy/installation.json From ed9d4ba7f94f3c28d6c845c823ef454b712cb3c2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 13 Dec 2018 11:32:09 -0500 Subject: [PATCH 228/268] Docs and ability to run when not directly in dist folder... --- dist/setup.sh | 67 ++++----- dist/sr.sh | 2 +- doc/INSTALL-MANUAL-STEPS.md | 2 + doc/INSTALL-MANUALLY.md | 142 ++++++++++++++++++ doc/INSTALL.md | 121 ++------------- doc/README.md | 102 +++++++++++++ docker-compose-sample.yml | 120 +++++++++++++++ docker-compose.yml | 108 ------------- .../app/templates/installer/start.sh | 11 +- .../app/templates/installer/stop.sh | 6 +- .../app/templates/installer/testfeatures.sh | 52 +++---- 11 files changed, 450 insertions(+), 283 deletions(-) create mode 100644 doc/INSTALL-MANUALLY.md create mode 100644 doc/README.md create mode 100644 docker-compose-sample.yml delete mode 100644 docker-compose.yml diff --git a/dist/setup.sh b/dist/setup.sh index 9520427db..7bcd18092 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -135,7 +135,6 @@ modify_owner() { } configure() { - local current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ## build setup docker image local recreate="" @@ -188,9 +187,9 @@ configure() { --log-driver=none$pw_env \ --network none \ --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $user yo --no-insight cyphernode$gen_options $recreate - if [[ -f exitStatus.sh ]]; then - . ./exitStatus.sh - rm ./exitStatus.sh + if [[ -f $current_path/exitStatus.sh ]]; then + . $current_path/exitStatus.sh + rm $current_path/exitStatus.sh fi if [[ ! $EXIT_STATUS == 0 ]]; then @@ -350,8 +349,6 @@ install_docker() { archpath="rpi" fi - local sourceDataPath=. - if [ ! -d $GATEKEEPER_DATAPATH ]; then step " create $GATEKEEPER_DATAPATH" sudo_if_required mkdir -p $GATEKEEPER_DATAPATH @@ -367,12 +364,12 @@ install_docker() { sudo_if_required mkdir $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 fi - copy_file $sourceDataPath/gatekeeper/api.properties $GATEKEEPER_DATAPATH/api.properties 1 $SUDO_REQUIRED - copy_file $sourceDataPath/gatekeeper/keys.properties $GATEKEEPER_DATAPATH/keys.properties 1 $SUDO_REQUIRED - copy_file $sourceDataPath/config.7z $GATEKEEPER_DATAPATH/config.7z 1 $SUDO_REQUIRED - copy_file $sourceDataPath/client.7z $GATEKEEPER_DATAPATH/client.7z 1 $SUDO_REQUIRED - copy_file $sourceDataPath/gatekeeper/cert.pem $GATEKEEPER_DATAPATH/certs/cert.pem 1 $SUDO_REQUIRED - copy_file $sourceDataPath/gatekeeper/key.pem $GATEKEEPER_DATAPATH/private/key.pem 1 $SUDO_REQUIRED + 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 fi if [ ! -d $PROXY_DATAPATH ]; then @@ -381,7 +378,7 @@ install_docker() { next fi - copy_file $sourceDataPath/installer/config.sh $PROXY_DATAPATH/config.sh 1 $SUDO_REQUIRED + copy_file $current_path/installer/config.sh $PROXY_DATAPATH/config.sh 1 $SUDO_REQUIRED if [[ $BITCOIN_INTERNAL == true ]]; then if [ ! -d $BITCOIN_DATAPATH ]; then @@ -391,18 +388,18 @@ install_docker() { fi if [ -d $BITCOIN_DATAPATH ]; then - local cmpStatus=$(compare_bitcoinconf $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) + local cmpStatus=$(compare_bitcoinconf $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf) if [[ $cmpStatus == 'dataloss' ]]; then if [[ $ALWAYSYES == 1 ]]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED else while true; do echo " Really copy bitcoin.conf with pruning option?" read -p " This will discard some blockchain data. (yn) " yn case $yn in - [Yy]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED; break;; - [Nn]* ) copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + [Yy]* ) copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED; break;; + [Nn]* ) copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually."; break;; @@ -411,7 +408,7 @@ install_docker() { done fi elif [[ $cmpStatus == 'incompatible' ]]; then - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED + copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf.cyphernode 0 $SUDO_REQUIRED echo " Blockchain data is not compatible, due to misconfigured nets." echo " Your cyphernode installation is most likely broken." echo " Please check bitcoin.conf.cyphernode on how to repair it manually." @@ -419,7 +416,7 @@ install_docker() { if [[ $cmpStatus == 'reindex' ]]; then echo " Warning Reindexing will take some time." fi - copy_file $sourceDataPath/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + copy_file $current_path/bitcoin/bitcoin.conf $BITCOIN_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED fi fi fi @@ -436,8 +433,8 @@ install_docker() { next fi if [ -d $LIGHTNING_DATAPATH ]; then - copy_file $sourceDataPath/lightning/c-lightning/config $LIGHTNING_DATAPATH/config 1 $SUDO_REQUIRED - copy_file $sourceDataPath/lightning/c-lightning/bitcoin.conf $LIGHTNING_DATAPATH/bitcoin.conf 1 $SUDO_REQUIRED + 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 fi @@ -485,26 +482,26 @@ install_docker() { fi fi - copy_file $sourceDataPath/installer/docker/docker-compose.yaml docker-compose.yaml - copy_file $sourceDataPath/installer/testfeatures.sh testfeatures.sh 0 - copy_file $sourceDataPath/installer/start.sh start.sh 0 - copy_file $sourceDataPath/installer/stop.sh stop.sh 0 + copy_file $current_path/installer/docker/docker-compose.yaml $current_path/docker-compose.yaml + copy_file $current_path/installer/testfeatures.sh $current_path/testfeatures.sh 0 + copy_file $current_path/installer/start.sh $current_path/start.sh 0 + copy_file $current_path/installer/stop.sh $current_path/stop.sh 0 - if [[ ! -x start.sh ]]; then + if [[ ! -x $current_path/start.sh ]]; then step " make start.sh executable" - try chmod +x start.sh + try chmod +x $current_path/start.sh next fi - if [[ ! -x stop.sh ]]; then + if [[ ! -x $current_path/stop.sh ]]; then step " make stop.sh executable" - try chmod +x stop.sh + try chmod +x $current_path/stop.sh next fi - if [[ ! -x testfeatures.sh ]]; then + if [[ ! -x $current_path/testfeatures.sh ]]; then step " make testfeatures.sh executable" - try chmod +x testfeatures.sh + try chmod +x $current_path/testfeatures.sh next fi } @@ -627,6 +624,8 @@ function ctrl_c() { exit } +export current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + while getopts ":cirhys" opt; do case $opt in r) @@ -667,8 +666,8 @@ if [[ $CONFIGURE == 1 ]]; then configure $RECREATE fi -if [[ -f installer/config.sh ]]; then - . installer/config.sh +if [[ -f $current_path/installer/config.sh ]]; then + . $current_path/installer/config.sh fi if [[ $CLEANUP == 'true' && $(docker image ls | grep cyphernodeconf) =~ cyphernodeconf ]]; then @@ -686,7 +685,7 @@ if [[ $INSTALL == 1 ]]; then fi if [[ $AUTOSTART == 1 ]]; then - exec ./start.sh + exec $current_path/start.sh else cowsay fi diff --git a/dist/sr.sh b/dist/sr.sh index e256288b2..e09d67870 100644 --- a/dist/sr.sh +++ b/dist/sr.sh @@ -1 +1 @@ -curl -fsSL https://raw.githubusercontent.com/schulterklopfer/cyphernode/features/install/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh \ No newline at end of file +curl -fsSL https://raw.githubusercontent.com/SatoshiPortal/cyphernode/master/dist/setup.sh -o setup_cyphernode.sh && chmod +x setup_cyphernode.sh && ./setup_cyphernode.sh diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 5220858d2..307f43832 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -1,3 +1,5 @@ +# 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. ## Update server and install git diff --git a/doc/INSTALL-MANUALLY.md b/doc/INSTALL-MANUALLY.md new file mode 100644 index 000000000..b50e090f2 --- /dev/null +++ b/doc/INSTALL-MANUALLY.md @@ -0,0 +1,142 @@ +# This README file can be used if you want to install manually. This is the old documentation before there was the installer. + +# Cyphernode + +Indirection layer between client and Bitcoin-related services. + +Here's the plan: + +- The containers are not publicly exposing ports. +- Everything is accessible exclusively within the encrypted overlay network. +- If your system is distributed: + - ...should be doubly encrypted by an OpenVPN tunnel + - ...the hosts should be secured and the VPN tunnel should have limited scope by iptables rules on each host. +- We can have different Bitcoin Nodes for watching and spending, giving the flexibility to have different security models one each. +- Only the Proxy has Bitcoin Node RPC credentials. +- The Proxy is exclusively accessible by the Overlay network's containers. +- To manually manage the Proxy (and have access to it), one has to gain access to the Docker host servers as a docker user. +- **Coming soon**: added security to use the spending features of the Proxy with Trezor and Coldcard. + +## See [Step-by-step detailed instructions](INSTALL-MANUAL-STEPS.md) for real-world copy-paste standard install instructions + +## Setting up + +Default setup assumes your Bitcoin Node is already running somewhere. The reason is that it takes a lot of disk space and often already exists in your infrastructure, why not reusing it. After all, full blockchain sync takes a while. + +You could also just uncomment it in the docker-compose file. If you run it in pruned mode, say so in config.properties. The computefees feature won't work in pruned mode. + +### Set the swarm +(10.8.0.2 is the host's VPN IP address) + +```shell +debian@dev:~/dev/Cyphernode$ docker swarm init --task-history-limit 1 --advertise-addr 10.8.0.2 +Swarm initialized: current node (hufy324d291dyakizsuvjd0uw) is now a manager. + +To add a worker to this swarm, run the following command: + + docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377 + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. +``` + +### Create the Overlay Network and make sure your app joins it! +(if your app is not a Docker container, you will have to expose Cyphernode's port and secure it! In that case, use a reverse proxy with TLS) + +```shell +debian@dev:~/dev/Cyphernode$ docker network create --driver=overlay --attachable --opt encrypted cyphernodenet +debian@dev:~/dev/Cyphernode$ docker network connect cyphernodenet yourappcontainer +``` + +### Configuration + +```shell +debian@dev:~/dev/Cyphernode$ vi proxy_docker/env.properties +debian@dev:~/dev/Cyphernode$ vi cron_docker/env.properties +debian@dev:~/dev/Cyphernode$ vi pycoin_docker/env.properties +debian@dev:~/dev/Cyphernode$ vi api_auth_docker/env.properties +``` + +### Build cron image + +[See how to build proxycron image](../cron_docker) + +### Build btcproxy image + +[See how to build btcproxy image](../proxy_docker) + +### Build pycoin image + +[See how to build pycoin image](../pycoin_docker) + +### Build btcnode image + +[See how to build btcnode image](https://github.com/SatoshiPortal/dockers/tree/master/x86_64/bitcoin-core) + +### Build clightning image + +[See how to build clightning image](https://github.com/SatoshiPortal/dockers/tree/master/x86_64/LN/c-lightning) + +### Build the authenticated HTTP API image + +[See how to build authapi image](../api_auth_docker) + +### Deploy + +**Edit docker-compose.yml to specify special deployment constraints or if you want to run the Bitcoin node on the same machine: uncomment corresponding lines.** + +```shell +debian@dev:~/dev/Cyphernode$ USER=`id -u cyphernode`:`id -g cyphernode` docker stack deploy --compose-file docker-compose.yml cyphernodestack +Creating service cyphernodestack_authapi +Creating service cyphernodestack_cyphernode +Creating service cyphernodestack_proxycronnode +Creating service cyphernodestack_pycoinnode +Creating service cyphernodestack_clightningnode +``` + +## Off-site Bitcoin Node + +This section is useful if you already have a Bitcoin Core node running and you want to use it in Cyphernode. In that case, please comment out the btcnode section from docker-compose.yml. + +### Join swarm created on Cyphernode server + +```shell +pi@SP-BTC01:~ $ docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377 +``` + +### Build node container image + +[See how to build Bitcoin Node image](https://github.com/SatoshiPortal/dockers/tree/master/rpi/bitcoin-core) + +### Connect already-running node + +```shell +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 +``` + +If you need the authorization header to copy/paste in another tool: + +```shell +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" +``` + +## Test deployment from any host of the swarm + +```shell +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 /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.2}' proxy:8888/spend +echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /ln_newaddr" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +curl -v -H "Content-Type: application/json" -d '{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}' proxy:8888/ln_create_invoice +curl -v -H "Content-Type: application/json" -d '{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","msatoshi":10000,"description":"Bitcoin Outlet order #7082"}' proxy:8888/ln_pay +``` diff --git a/doc/INSTALL.md b/doc/INSTALL.md index eb982333c..fa5578467 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -1,117 +1,34 @@ # Cyphernode -Indirection layer between client and Bitcoin-related services. +## Setting Up -Here's the plan: +### Installer -- The containers are not publicly exposing ports. -- Everything is accessible exclusively within the encrypted overlay network. -- If your system is distributed: - - ...should be doubly encrypted by an OpenVPN tunnel - - ...the hosts should be secured and the VPN tunnel should have limited scope by iptables rules on each host. -- We can have different Bitcoin Nodes for watching and spending, giving the flexibility to have different security models one each. -- Only the Proxy has Bitcoin Node RPC credentials. -- The Proxy is exclusively accessible by the Overlay network's containers. -- To manually manage the Proxy (and have access to it), one has to gain access to the Docker host servers as a docker user. -- **Coming soon**: added security to use the spending features of the Proxy with Trezor and Coldcard. +We are providing an installer to help you setup Cyphernode. All the Docker images used by Cyphernode have been prebuilt for x86 and ARM (RPi) architectures and are hosted on the Docker hub public registry, Cyphernode repository (https://hub.docker.com/u/cyphernode/). -## See [Step-by-step detailed instructions](INSTALL-MANUAL-STEPS.md) for real-world copy-paste standard install instructions - -## Setting up - -Default setup assumes your Bitcoin Node is already running somewhere. The reason is that it takes a lot of disk space and often already exists in your infrastructure, why not reusing it. After all, full blockchain sync takes a while. - -You could also just uncomment it in the docker-compose file. If you run it in pruned mode, say so in config.properties. The computefees feature won't work in pruned mode. - -### Set the swarm -(10.8.0.2 is the host's VPN IP address) - -```shell -debian@dev:~/dev/Cyphernode$ docker swarm init --task-history-limit 1 --advertise-addr 10.8.0.2 -Swarm initialized: current node (hufy324d291dyakizsuvjd0uw) is now a manager. - -To add a worker to this swarm, run the following command: - - docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377 - -To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. -``` - -### Create the Overlay Network and make sure your app joins it! -(if your app is not a Docker container, you will have to expose Cyphernode's port and secure it! In that case, use a reverse proxy with TLS) +You can clone the git repository and install: ```shell -debian@dev:~/dev/Cyphernode$ docker network create --driver=overlay --attachable --opt encrypted cyphernodenet -debian@dev:~/dev/Cyphernode$ docker network connect cyphernodenet yourappcontainer +git clone https://github.com/SatoshiPortal/cyphernode.git +cd cyphernode/dist +./setup.sh ``` -### Configuration +Or you can simply run this magic command to start setup and installation: ```shell -debian@dev:~/dev/Cyphernode$ vi proxy_docker/env.properties -debian@dev:~/dev/Cyphernode$ vi cron_docker/env.properties -debian@dev:~/dev/Cyphernode$ vi pycoin_docker/env.properties -debian@dev:~/dev/Cyphernode$ vi api_auth_docker/env.properties +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 cron image - -[See how to build proxycron image](../cron_docker) - -### Build btcproxy image - -[See how to build btcproxy image](../proxy_docker) - -### Build pycoin image - -[See how to build pycoin image](../pycoin_docker) - -### Build btcnode image - -[See how to build btcnode image](https://github.com/SatoshiPortal/dockers/tree/master/x86_64/bitcoin-core) - -### Build clightning image - -[See how to build clightning image](https://github.com/SatoshiPortal/dockers/tree/master/x86_64/LN/c-lightning) - -### Build the authenticated HTTP API image +## Manually test your installation through the Gatekeeper -[See how to build authapi image](../api_auth_docker) - -### Deploy - -**Edit docker-compose.yml to specify special deployment constraints or if you want to run the Bitcoin node on the same machine: uncomment corresponding lines.** - -```shell -debian@dev:~/dev/Cyphernode$ USER=`id -u cyphernode`:`id -g cyphernode` docker stack deploy --compose-file docker-compose.yml cyphernodestack -Creating service cyphernodestack_authapi -Creating service cyphernodestack_cyphernode -Creating service cyphernodestack_proxycronnode -Creating service cyphernodestack_pycoinnode -Creating service cyphernodestack_clightningnode -``` - -## Off-site Bitcoin Node - -This section is useful if you already have a Bitcoin Core node running and you want to use it in Cyphernode. In that case, please comment out the btcnode section from docker-compose.yml. - -### Join swarm created on Cyphernode server - -```shell -pi@SP-BTC01:~ $ docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377 -``` - -### Build node container image - -[See how to build Bitcoin Node image](https://github.com/SatoshiPortal/dockers/tree/master/rpi/bitcoin-core) - -### Connect already-running node +If you need the authorization header to copy/paste in another tool, put your API ID (id=) and API key (k=) in the following command: ```shell -pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" ``` -## Test deployment from outside of the Swarm +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 @@ -119,22 +36,12 @@ id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(ech 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 ``` -If you need the authorization header to copy/paste in another tool: - -```shell -id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" -``` - -## Test deployment from any host of the swarm +## Manually test your installation directly on the Proxy: ```shell 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 /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - -curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.2}' proxy:8888/spend echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - -echo "GET /ln_newaddr" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - -curl -v -H "Content-Type: application/json" -d '{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}' proxy:8888/ln_create_invoice -curl -v -H "Content-Type: application/json" -d '{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","msatoshi":10000,"description":"Bitcoin Outlet order #7082"}' proxy:8888/ln_pay ``` diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 000000000..a97c123c1 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,102 @@ +# Cyphernode + +Indirection layer (API) between your applications and Bitcoin-related services. + +Your application <-----> Cyphernode + +Cyphernode is: + +Gatekeeper (TLS, JWT) <-----> Proxy (Cyphernode Core) <-----> Feature Containers + +- By default, the only exposed (published) port is 443 (HTTPS) on the Gatekeeper. +- By default, everything else is accessible exclusively within the encrypted overlay network. +- If your system is distributed (customized Cyphernode setup), the overlay network... + - ...should be doubly encrypted with a VPN or SSH tunnel + - ...the hosts should be secured and the VPN/SSH tunnel should have limited scope by iptables rules on each host. +- We can have different Bitcoin Nodes for watching and spending, giving the flexibility to have different security models one each. +- Only the Proxy has Bitcoin Node RPC credentials. +- To manually manage the Proxy (and have access to it), one has to gain access to the Docker host servers as a docker user. + +## Setting Up + +### Installer + +We are providing an installer to help you setup Cyphernode. + +#### See [Instructions for installation](INSTALL.md) for automatic install instructions + +All the Docker images used by Cyphernode have been prebuilt for x86 and ARM (RPi) architectures and are hosted on the Docker hub public registry, Cyphernode repository (https://hub.docker.com/u/cyphernode/). + +### Build from sources + +However, it is possible for you to build from sources. In that case, please refer to the files INSTALL-MANUALLY.md and INSTALL-MANUAL-STEPS.md. + +#### See [Instructions for manual installation](INSTALL-MANUALLY.md) for manual build and install instructions +#### See [Step-by-step detailed instructions](INSTALL-MANUAL-STEPS.md) for real-world copy-paste standard install instructions + +# For Your Information + +Current components in Cyphernode: + +- Gatekeeper: front door where all requests hit Cyphernode. Takes care of: TLS, authentication and authorization. +- Proxy: request handler. Well dispatch authenticated and authorized requests to the right component. Use a SQLite3 database for its tasks. +- Proxy Cron: scheduler. Can call the proxy on regular interval for asynchronous tasks like payment notifications on watches, callbacks when OTS files are ready, etc. +- Pycoin: Bitcoin keys and addresses tool. Used by Cyphernode to derive addresses from an xPub and a derivation path. +- Bitcoin: Bitcoin Core node. Cyphernode uses a watching wallet for watchers (no funds) and a spending wallet for spending. Mandatory component, but optionally part of Cyphernode installation, as we can use an already running Bitcoin Core node. +- Lightning: optional. C-Lightning node. The LN node will use the Bitcoin node for its tasks. +- OTSclient: optional. Used to stamp hashes on the Bitcoin blockchain. + +Future components: + +- Trezor-connect: use a Trezor to authenticate. Will be used to log into control panel (see next point) and other. +- Control Panel: web control panel, with different functionalities depending on user's group: admin, spender, watcher. +- Grafana: displays stats graphics on Cyphernode use and load. +- PGP: signs anything with your PGP key. +- PSBT: sign transactions using a Coldcard. +- Electrum (Personal) Server: would be part of the installation for your convenience, but not really used by Cyphernode. + +## Bitcoin Core Node + +If you decide to have a prune Bitcoin Core node, the fee calculation on incoming transactions won't work. We can't compute the fees on someone else's transactions without having the whole indexed blockchain. + +## Lightning Network + +Currently, the LN functionalities of Cyphernode are very limited. Maybe even hard to use. You can: + +- Get information on your LN node: ln_getinfo +- Get a Bitcoin address where to send your funds to be used by your LN node: ln_newaddr +- Create an invoice, so people can send you payment; the burden of creating a channel/route to you is on the payer: ln_create_invoice +- Pay an invoice. You have to have the invoice and your LN node must already be connected to the network: ln_pay + +Basic and crucial functionalities that's missing (you have to manually use lightning-cli on your LN node): + +- Be notified when a LN payment is received +- Connect your node to the LN network +- Open/close channels + +## Manually test your installation through the Gatekeeper + +If you need the authorization header to copy/paste in another tool, put your API ID (id=) and API key (k=) in the following command: + +```shell +id="003";h64=$(echo -n "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo -n "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";echo "Bearer $token" +``` + +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 +``` + + +## Manually test your installation directly on the Proxy: + +```shell +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 /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc proxy:8888 - +``` diff --git a/docker-compose-sample.yml b/docker-compose-sample.yml new file mode 100644 index 000000000..8b4adad7f --- /dev/null +++ b/docker-compose-sample.yml @@ -0,0 +1,120 @@ +version: "3" + +services: + + gatekeeper: + # HTTP authentication API gate + environment: + - "TRACING=1" + image: cyphernode/gatekeeper:latest + ports: + - "443:443" + volumes: + - "~/cn-files/cn-gatekeeper/certs:/etc/ssl/certs" + - "~/cn-files/cn-gatekeeper/private:/etc/ssl/private" + - "~/cn-files/cn-gatekeeper/keys.properties:/etc/nginx/conf.d/keys.properties" + - "~/cn-files/cn-gatekeeper/api.properties:/etc/nginx/conf.d/api.properties" + command: $USER +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always + + proxy: + command: $USER ./startproxy.sh + # Bitcoin Mini Proxy + environment: + - "TRACING=1" + - "WATCHER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/watching01.dat" + - "WATCHER_BTC_NODE_RPC_USER=bitcoin:CHANGEME" + - "WATCHER_BTC_NODE_RPC_CFG=/tmp/watcher_btcnode_curlcfg.properties" + - "SPENDER_BTC_NODE_RPC_URL=bitcoin:18332/wallet/spending01.dat" + - "SPENDER_BTC_NODE_RPC_USER=bitcoin:CHANGEME" + - "SPENDER_BTC_NODE_RPC_CFG=/tmp/spender_btcnode_curlcfg.properties" + - "PROXY_LISTENING_PORT=8888" + - "DB_PATH=/proxy/db" + - "DB_FILE=/proxy/db/proxydb" + - "PYCOIN_CONTAINER=pycoin:7777" + - "WATCHER_BTC_NODE_PRUNED=false" + - "OTSCLIENT_CONTAINER=otsclient:6666" + - "OTS_FILES=/proxy/otsfiles" + image: cyphernode/proxy:latest + + volumes: + - "~/cn-files/cn-proxydb:/proxy/db" + - "~/cn-files/cn-lndata:/.lightning" + - "~/cn-files/cn-otsfiles:/proxy/otsfiles" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always + + proxycron: + environment: + - "PROXY_URL=proxy:8888/executecallbacks" + image: cyphernode/proxycron:latest +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always + + pycoin: + # Pycoin + command: $USER ./startpycoin.sh + image: cyphernode/pycoin:latest + environment: + - "TRACING=1" + - "PYCOIN_LISTENING_PORT=7777" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always + + lightning: + command: $USER lightningd + image: cyphernode/clightning:v0.6.2 + volumes: + - "~/cn-files/cn-lndata:/.lightning" + - "~/cn-files/cn-lndata/bitcoin.conf:/.bitcoin/bitcoin.conf" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + restart: always + + otsclient: + environment: + - "TRACING=1" + - "OTSCLIENT_LISTENING_PORT=6666" + image: cyphernode/otsclient:latest +# deploy: +# placement: +# constraints: [node.hostname==dev] + volumes: + - "~/cn-files/cn-otsfiles:/otsfiles" + command: $USER /script/startotsclient.sh + networks: + - cyphernodenet + restart: always + + bitcoin: + command: $USER bitcoind + image: cyphernode/bitcoin:v0.17.0 + volumes: + - "~/cn-files/cn-btcdata:/.bitcoin" + networks: + - cyphernodenet + restart: always + +networks: + cyphernodenet: + external: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a86207a4c..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,108 +0,0 @@ -version: "3" - -services: - gatekeeper: - # HTTP authentication API gate - env_file: - - api_auth_docker/env.properties - image: cyphernode/gatekeeper:cyphernode-0.05 - ports: -# - "80:80" - - "443:443" - volumes: - - "~/cyphernode-ssl/certs:/etc/ssl/certs" - - "~/cyphernode-ssl/private:/etc/ssl/private" -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - - proxy: - # Bitcoin Mini Proxy - env_file: - - proxy_docker/env.properties - image: cyphernode/proxy:cyphernode-0.05 - volumes: - # Variable substitutions don't work - # Match with DB_PATH in proxy_docker/env.properties - - "~/btcproxydb:/proxy/db" - # c-lightning looks for $HOME/.lightning/, and $HOME is set to / in the container - - "~/lndata:/.lightning" - # OTS files, shared with otsclient container - - "~/otsfiles:/otsfiles" -# deploy: -# placement: -# constraints: [node.hostname==dev] - command: $USER ./startproxy.sh - networks: - - cyphernodenet - - proxycron: - # Async jobs - env_file: - - cron_docker/env.properties - image: cyphernode/proxycron:cyphernode-0.05 -# deploy: -# placement: -# constraints: [node.hostname==dev] - networks: - - cyphernodenet - - pycoin: - # Pycoin - env_file: - - pycoin_docker/env.properties - image: cyphernode/pycoin:cyphernode-0.05 -# deploy: -# placement: -# constraints: [node.hostname==dev] - command: $USER ./startpycoin.sh - networks: - - cyphernodenet - - otsclient: - # otsclient JS - env_file: - - otsclient_docker/env.properties - image: cyphernode/otsclient:cyphernode-0.05 -# deploy: -# placement: -# constraints: [node.hostname==dev] - volumes: - - "~/otsfiles:/otsfiles" - command: $USER /script/startotsclient.sh - networks: - - cyphernodenet - - lightning: - # c-lightning lightning network node - image: cyphernode/clightning:dev - ports: - - "9735:9735" - volumes: - - "~/lndata:/.lightning" - - "~/lndata/bitcoin.conf:/.bitcoin/bitcoin.conf" -# deploy: -# placement: -# constraints: [node.hostname==dev] - command: $USER lightningd - networks: - - cyphernodenet - - bitcoin: - # Bitcoin node - image: cyphernode/bitcoin:0.17.0 -# ports: -# - "18333:18333" -# - "29000:29000" -# - "8333:8333" - volumes: - - "~/btcdata:/.bitcoin" - command: $USER bitcoind - networks: - - cyphernodenet - -networks: - cyphernodenet: - external: true diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 04092d2fb..ba3d39247 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -3,16 +3,17 @@ # run as user <%= username %> export USER=$(id -u <%= run_as_different_user?username:default_username %>):$(id -g <%= run_as_different_user?username:default_username %>) export ARCH=$(uname -m) +current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" <% if (docker_mode == 'swarm') { %> -docker stack deploy -c docker-compose.yaml cyphernode +docker stack deploy -c $current_path/docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> -docker-compose -f docker-compose.yaml up -d --remove-orphans +docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans <% } %> # Will test if Cyphernode is fully up and running... -docker run --rm -it -v `pwd`/testfeatures.sh:/testfeatures.sh \ --v `pwd`/gatekeeper/keys.properties:/keys.properties \ --v `pwd`/gatekeeper/cert.pem:/cert.pem \ +docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ +-v $current_path/gatekeeper/keys.properties:/keys.properties \ +-v $current_path/gatekeeper/cert.pem:/cert.pem \ -v <%= proxy_datapath %>:/proxy \ --network cyphernodenet alpine:3.8 /testfeatures.sh diff --git a/install/generator-cyphernode/generators/app/templates/installer/stop.sh b/install/generator-cyphernode/generators/app/templates/installer/stop.sh index d97a213cd..7fc127946 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/stop.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/stop.sh @@ -1,5 +1,7 @@ #!/bin/sh +current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" + <% if (docker_mode == 'swarm') { %> export USER=$(id -u):$(id -g) export ARCH=$(uname -m) @@ -7,5 +9,5 @@ docker stack rm cyphernode <% } else if(docker_mode == 'compose') { %> export USER=$(id -u):$(id -g) export ARCH=$(uname -m) -docker-compose -f docker-compose.yaml down -<% } %> \ No newline at end of file +docker-compose -f $current_path/docker-compose.yaml down +<% } %> diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 4cdc245e2..dd874c068 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -1,11 +1,11 @@ #!/bin/sh -apk add --update --no-cache openssl curl +apk add --update --no-cache openssl curl > /dev/null . keys.properties checkgatekeeper() { - echo -e "\r\nTesting Gatekeeper..." > /dev/console + echo -e "\r\n\e[1;36mTesting Gatekeeper...\e[0;32m" > /dev/console local rc local id="001" @@ -20,7 +20,7 @@ checkgatekeeper() { local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" - echo " Sleeping 2 seconds... " > /dev/console + echo -e " Sleeping 2 seconds... " > /dev/console sleep 2 echo " Testing expired request... " > /dev/console @@ -66,13 +66,13 @@ checkgatekeeper() { rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/conf) [ "${rc}" -ne "403" ] && return 60 - echo "***** Gatekeeper rocks!" > /dev/console + echo -e "\e[1;36mGatekeeper rocks!" > /dev/console return 0 } checkpycoin() { - echo -e "\r\nTesting Pycoin..." > /dev/console + echo -en "\r\n\e[1;36mTesting Pycoin... " > /dev/console local rc local id="002" local k @@ -87,13 +87,13 @@ checkpycoin() { rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) [ "${rc}" -ne "200" ] && return 100 - echo "***** Pycoin rocks!" > /dev/console + echo -e "\e[1;36mPycoin rocks!" > /dev/console return 0 } checkots() { - echo -e "\r\nTesting OTSclient..." > /dev/console + echo -en "\r\n\e[1;36mTesting OTSclient... " > /dev/console local rc local id="002" local k @@ -109,13 +109,13 @@ checkots() { echo "${rc}" | grep "Invalid hash 123 for sha256" > /dev/null [ "$?" -ne "0" ] && return 200 - echo "***** OTSclient rocks!" > /dev/console + echo -e "\e[1;36mOTSclient rocks!" > /dev/console return 0 } checkbitcoinnode() { - echo -e "\r\nTesting Bitcoin..." > /dev/console + echo -en "\r\n\e[1;36mTesting Bitcoin... " > /dev/console local rc local id="002" local k @@ -130,13 +130,13 @@ checkbitcoinnode() { rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) [ "${rc}" -ne "200" ] && return 300 - echo "***** Bitcoin node rocks!" > /dev/console + echo -e "\e[1;36mBitcoin node rocks!" > /dev/console return 0 } checklnnode() { - echo -e "\r\nTesting Lightning..." > /dev/console + echo -en "\r\n\e[1;36mTesting Lightning... " > /dev/console local rc local id="002" local k @@ -151,13 +151,13 @@ checklnnode() { rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) [ "${rc}" -ne "200" ] && return 400 - echo "***** LN node rocks!" > /dev/console + echo -e "\e[1;36mLN node rocks!" > /dev/console return 0 } checkservice() { - echo -e "\r\nTesting if Cyphernode is up and running... I will keep trying during up to 5 minutes to give time to Docker to deploy everything..." > /dev/console + echo -e "\r\n\e[1;36mTesting if Cyphernode is up and running... \e[0;36mI will keep trying during up to 5 minutes to give time to Docker to deploy everything...\e[0;32m" > /dev/console local outcome local returncode=0 @@ -168,8 +168,8 @@ checkservice() { do outcome=0 for container in gatekeeper proxy proxycron pycoin <%= (features.indexOf('otsclient') != -1)?'otsclient ':'' %>bitcoin <%= (features.indexOf('lightning') != -1)?'lightning ':'' %>; do - echo " Verifying ${container}..." > /dev/console - (ping -c 10 ${container} | grep "0% packet loss" > /dev/null) & + 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 @@ -180,7 +180,7 @@ checkservice() { # If '0% packet loss' everywhere or 5 minutes passed, we get out of this loop ([ "${outcome}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - echo " Cyphernode still not ready, will retry in 5 seconds for max 5 minutes..." > /dev/console + echo -e "\e[1;31mCyphernode still not ready, will retry every 5 seconds for 5 minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console sleep 5 done @@ -225,7 +225,7 @@ timeout_feature() { # If no error or 2 minutes passed, we get out of this loop ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - echo "xxxxx Maybe it's too early, I'll retry in 5 seconds (for max 2 minutes total)." > /dev/console + echo -e "\e[1;31mMaybe it's too early, I'll retry every 5 seconds for 2 minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console sleep 5 done @@ -238,7 +238,7 @@ feature_status() { local errormsg=${2} [ "${returncode}" -eq "0" ] && echo "true" - [ "${returncode}" -ne "0" ] && echo "false" && echo ${errormsg} > /dev/console + [ "${returncode}" -ne "0" ] && echo "false" && echo -e "\e[1;31m${errormsg}" > /dev/console } # /proxy/installation.json will contain something like that: @@ -266,9 +266,9 @@ feature_status() { result=$(checkservice) returncode=$? if [ "${returncode}" -ne "0" ]; then - echo "xxxxx Cyphernode could not fully start properly within 5 minutes." > /dev/console + echo -e "\e[1;31mCyphernode could not fully start properly within 5 minutes." > /dev/console else - echo "***** Cyphernode seems to be correctly deployed. Let's run more thourough tests..." > /dev/console + echo -e "\e[1;36mCyphernode seems to be correctly deployed. Let's run more thourough tests..." > /dev/console fi # Let's now check each feature fonctionality... @@ -283,34 +283,34 @@ fi result="${result},\"features\":[{\"name\":\"gatekeeper\",\"working\":" timeout_feature checkgatekeeper returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Gatekeeper error!')}" +result="${result}$(feature_status ${returncode} 'Gatekeeper error!')}" result="${result},{\"name\":\"pycoin\",\"working\":" timeout_feature checkpycoin returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Pycoin error!')}" +result="${result}$(feature_status ${returncode} 'Pycoin error!')}" <% if (features.indexOf('otsclient') != -1) { %> result="${result},{\"name\":\"otsclient\",\"working\":" timeout_feature checkots returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx OTSclient error!')}" +result="${result}$(feature_status ${returncode} 'OTSclient error!')}" <% } %> result="${result},{\"name\":\"bitcoin\",\"working\":" timeout_feature checkbitcoinnode returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Bitcoin error!')}" +result="${result}$(feature_status ${returncode} 'Bitcoin error!')}" <% if (features.indexOf('lightning') != -1) { %> result="${result},{\"name\":\"lightning\",\"working\":" timeout_feature checklnnode returncode=$? -result="${result}$(feature_status ${returncode} 'xxxxx Lightning error!')}" +result="${result}$(feature_status ${returncode} 'Lightning error!')}" <% } %> result="{${result}]}" echo "${result}" > /proxy/installation.json -echo ; echo "Tests finished." > /dev/console +echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console From 36dfc929850d6f4a6048a7b754035ab86a2634b4 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 13 Dec 2018 14:41:26 -0500 Subject: [PATCH 229/268] SQL migration --- doc/INSTALL-MANUALLY.md | 8 +++ doc/INSTALL.md | 8 +++ proxy_docker/Dockerfile.amd64 | 49 ++++++++++--------- proxy_docker/Dockerfile.arm32v6 | 49 ++++++++++--------- .../app/data/{watching.sql => cyphernode.sql} | 12 +++++ .../app/data/sqlmigrate20181213_0-0.1.sh | 10 ++++ .../app/data/sqlmigrate20181213_0-0.1.sql | 23 +++++++++ proxy_docker/app/script/startproxy.sh | 7 ++- 8 files changed, 117 insertions(+), 49 deletions(-) rename proxy_docker/app/data/{watching.sql => cyphernode.sql} (82%) create mode 100644 proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh create mode 100644 proxy_docker/app/data/sqlmigrate20181213_0-0.1.sql diff --git a/doc/INSTALL-MANUALLY.md b/doc/INSTALL-MANUALLY.md index b50e090f2..475bf5392 100644 --- a/doc/INSTALL-MANUALLY.md +++ b/doc/INSTALL-MANUALLY.md @@ -1,5 +1,13 @@ # This README file can be used if you want to install manually. This is the old documentation before there was the installer. +## Upgrading + +Your proxy's database won't be lost. Migration scripts are taking care of automatically migrating the database when starting the proxy. + +``` +proxy_docker/app/data/sqlmigrate* +``` + # Cyphernode Indirection layer between client and Bitcoin-related services. diff --git a/doc/INSTALL.md b/doc/INSTALL.md index fa5578467..75fc6bb3f 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -20,6 +20,14 @@ 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 ``` +## Upgrading + +Your proxy's database won't be lost. Migration scripts are taking care of automatically migrating the database when starting the proxy. + +``` +proxy_docker/app/data/sqlmigrate* +``` + ## Manually test your installation through the Gatekeeper If you need the authorization header to copy/paste in another tool, put your API ID (id=) and API key (k=) in the following command: diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 0cbabd780..74823b1d3 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -24,33 +24,34 @@ RUN apk add --update --no-cache \ curl \ su-exec -COPY app/script/callbacks_job.sh ${HOME}/callbacks_job.sh -COPY app/script/blockchainrpc.sh ${HOME}/blockchainrpc.sh -COPY app/script/call_lightningd.sh ${HOME}/call_lightningd.sh -COPY app/script/bitcoin.sh ${HOME}/bitcoin.sh -COPY app/script/ots.sh ${HOME}/ots.sh -COPY app/script/requesthandler.sh ${HOME}/requesthandler.sh -COPY app/script/watchrequest.sh ${HOME}/watchrequest.sh -COPY app/script/walletoperations.sh ${HOME}/walletoperations.sh -COPY app/script/confirmation.sh ${HOME}/confirmation.sh -COPY app/script/startproxy.sh ${HOME}/startproxy.sh -COPY app/script/trace.sh ${HOME}/trace.sh -COPY app/script/sendtobitcoinnode.sh ${HOME}/sendtobitcoinnode.sh -COPY app/script/responsetoclient.sh ${HOME}/responsetoclient.sh -COPY app/script/importaddress.sh ${HOME}/importaddress.sh -COPY app/script/sql.sh ${HOME}/sql.sh -COPY app/data/watching.sql ${HOME}/watching.sql -COPY app/script/computefees.sh ${HOME}/computefees.sh -COPY app/script/unwatchrequest.sh ${HOME}/unwatchrequest.sh -COPY app/script/getactivewatches.sh ${HOME}/getactivewatches.sh -COPY app/script/manage_missed_conf.sh ${HOME}/manage_missed_conf.sh -COPY app/script/tests.sh ${HOME}/tests.sh -COPY app/script/tests-cb.sh ${HOME}/tests-cb.sh -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME}/lightning-cli +COPY app/data/cyphernode.sql ${HOME} +COPY app/data/sqlmigrate* ${HOME} +COPY app/script/callbacks_job.sh ${HOME} +COPY app/script/blockchainrpc.sh ${HOME} +COPY app/script/call_lightningd.sh ${HOME} +COPY app/script/bitcoin.sh ${HOME} +COPY app/script/ots.sh ${HOME} +COPY app/script/requesthandler.sh ${HOME} +COPY app/script/watchrequest.sh ${HOME} +COPY app/script/walletoperations.sh ${HOME} +COPY app/script/confirmation.sh ${HOME} +COPY app/script/startproxy.sh ${HOME} +COPY app/script/trace.sh ${HOME} +COPY app/script/sendtobitcoinnode.sh ${HOME} +COPY app/script/responsetoclient.sh ${HOME} +COPY app/script/importaddress.sh ${HOME} +COPY app/script/sql.sh ${HOME} +COPY app/script/computefees.sh ${HOME} +COPY app/script/unwatchrequest.sh ${HOME} +COPY app/script/getactivewatches.sh ${HOME} +COPY app/script/manage_missed_conf.sh ${HOME} +COPY app/script/tests.sh ${HOME} +COPY app/script/tests-cb.sh ${HOME} +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME} WORKDIR ${HOME} -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ && mkdir db diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index 0a4f66c74..7fa641e13 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -20,33 +20,34 @@ RUN apk add --update --no-cache \ curl \ su-exec -COPY app/script/callbacks_job.sh ${HOME}/callbacks_job.sh -COPY app/script/blockchainrpc.sh ${HOME}/blockchainrpc.sh -COPY app/script/call_lightningd.sh ${HOME}/call_lightningd.sh -COPY app/script/bitcoin.sh ${HOME}/bitcoin.sh -COPY app/script/ots.sh ${HOME}/ots.sh -COPY app/script/requesthandler.sh ${HOME}/requesthandler.sh -COPY app/script/watchrequest.sh ${HOME}/watchrequest.sh -COPY app/script/walletoperations.sh ${HOME}/walletoperations.sh -COPY app/script/confirmation.sh ${HOME}/confirmation.sh -COPY app/script/startproxy.sh ${HOME}/startproxy.sh -COPY app/script/trace.sh ${HOME}/trace.sh -COPY app/script/sendtobitcoinnode.sh ${HOME}/sendtobitcoinnode.sh -COPY app/script/responsetoclient.sh ${HOME}/responsetoclient.sh -COPY app/script/importaddress.sh ${HOME}/importaddress.sh -COPY app/script/sql.sh ${HOME}/sql.sh -COPY app/data/watching.sql ${HOME}/watching.sql -COPY app/script/computefees.sh ${HOME}/computefees.sh -COPY app/script/unwatchrequest.sh ${HOME}/unwatchrequest.sh -COPY app/script/getactivewatches.sh ${HOME}/getactivewatches.sh -COPY app/script/manage_missed_conf.sh ${HOME}/manage_missed_conf.sh -COPY app/script/tests.sh ${HOME}/tests.sh -COPY app/script/tests-cb.sh ${HOME}/tests-cb.sh -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME}/lightning-cli +COPY app/data/cyphernode.sql ${HOME} +COPY app/data/sqlmigrate* ${HOME} +COPY app/script/callbacks_job.sh ${HOME} +COPY app/script/blockchainrpc.sh ${HOME} +COPY app/script/call_lightningd.sh ${HOME} +COPY app/script/bitcoin.sh ${HOME} +COPY app/script/ots.sh ${HOME} +COPY app/script/requesthandler.sh ${HOME} +COPY app/script/watchrequest.sh ${HOME} +COPY app/script/walletoperations.sh ${HOME} +COPY app/script/confirmation.sh ${HOME} +COPY app/script/startproxy.sh ${HOME} +COPY app/script/trace.sh ${HOME} +COPY app/script/sendtobitcoinnode.sh ${HOME} +COPY app/script/responsetoclient.sh ${HOME} +COPY app/script/importaddress.sh ${HOME} +COPY app/script/sql.sh ${HOME} +COPY app/script/computefees.sh ${HOME} +COPY app/script/unwatchrequest.sh ${HOME} +COPY app/script/getactivewatches.sh ${HOME} +COPY app/script/manage_missed_conf.sh ${HOME} +COPY app/script/tests.sh ${HOME} +COPY app/script/tests-cb.sh ${HOME} +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME} WORKDIR ${HOME} -RUN chmod +x startproxy.sh requesthandler.sh lightning-cli \ +RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ && mkdir db diff --git a/proxy_docker/app/data/watching.sql b/proxy_docker/app/data/cyphernode.sql similarity index 82% rename from proxy_docker/app/data/watching.sql rename to proxy_docker/app/data/cyphernode.sql index 0717ec4a2..48bb1adce 100644 --- a/proxy_docker/app/data/watching.sql +++ b/proxy_docker/app/data/cyphernode.sql @@ -63,3 +63,15 @@ CREATE TABLE stamp ( calledback INTEGER DEFAULT FALSE, inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP ); +CREATE INDEX idx_stamp_hash ON stamp (hash); +CREATE INDEX idx_stamp_calledback ON stamp (calledback); + +CREATE TABLE cyphernode_props ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + property TEXT, + value TEXT, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_cp_property ON cyphernode_props (property); + +INSERT INTO cyphernode_props (property, value) VALUES ("version", "0.1"); diff --git a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh new file mode 100644 index 000000000..0b69b1649 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +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..." + cat sqlmigrate20181213_0-0.1.sql | sqlite3 $DB_FILE +else + echo "Database v0 to v0.1 migration already done, skipping!" +fi diff --git a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sql b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sql new file mode 100644 index 000000000..4bc7f7bf8 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sql @@ -0,0 +1,23 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE stamp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE, + callbackUrl TEXT, + requested INTEGER DEFAULT FALSE, + upgraded INTEGER DEFAULT FALSE, + calledback INTEGER DEFAULT FALSE, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_stamp_hash ON stamp (hash); +CREATE INDEX idx_stamp_calledback ON stamp (calledback); + +CREATE TABLE cyphernode_props ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + property TEXT, + value TEXT, + inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_cp_property ON cyphernode_props (property); + +INSERT INTO cyphernode_props (property, value) VALUES ("version", "0.1"); diff --git a/proxy_docker/app/script/startproxy.sh b/proxy_docker/app/script/startproxy.sh index ab367cccb..eaec2e343 100644 --- a/proxy_docker/app/script/startproxy.sh +++ b/proxy_docker/app/script/startproxy.sh @@ -32,7 +32,12 @@ createCurlConfig() { if [ ! -e ${DB_FILE} ]; then echo "DB not found, creating..." - cat watching.sql | sqlite3 $DB_FILE + cat cyphernode.sql | sqlite3 $DB_FILE +else + echo "DB found, migrating..." + for script in sqlmigrate*.sh; do + sh $script + done fi chmod 0600 $DB_FILE From 57f2217abb10ac980d241879ea1681f753f86d26 Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 15 Dec 2018 00:52:24 -0500 Subject: [PATCH 230/268] Status page --- api_auth_docker/Dockerfile | 1 + api_auth_docker/Dockerfile-debian | 22 ------------ api_auth_docker/default-ssl.conf | 6 ++++ build.sh | 19 ++++++----- dist/setup.sh | 1 + .../generators/app/index.js | 2 ++ .../generators/app/lib/cert.js | 16 +++++++++ .../app/prompters/010_gatekeeper.js | 4 +-- .../app/templates/gatekeeper/htpasswd | 1 + .../installer/docker/docker-compose.yaml | 1 + .../app/templates/installer/start.sh | 4 +++ proxy_docker/Dockerfile.amd64 | 1 + proxy_docker/Dockerfile.arm32v6 | 1 + proxy_docker/app/script/ots.sh | 2 +- proxy_docker/app/script/requesthandler.sh | 7 ++++ proxy_docker/app/script/responsetoclient.sh | 34 +++++++++++++++++-- proxy_docker/app/script/statuspage.sh | 23 +++++++++++++ 17 files changed, 108 insertions(+), 37 deletions(-) delete mode 100644 api_auth_docker/Dockerfile-debian create mode 100644 install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd create mode 100644 proxy_docker/app/script/statuspage.sh diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 33f0f877f..0c54eda4d 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -12,6 +12,7 @@ RUN apk add --update --no-cache \ COPY auth.sh /etc/nginx/conf.d COPY default-ssl.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 COPY tests.sh /etc/nginx/conf.d diff --git a/api_auth_docker/Dockerfile-debian b/api_auth_docker/Dockerfile-debian deleted file mode 100644 index f9ec64379..000000000 --- a/api_auth_docker/Dockerfile-debian +++ /dev/null @@ -1,22 +0,0 @@ -FROM nginx:1.14 - -RUN apt-get update \ - && apt-get install -y \ - openssl \ - spawn-fcgi \ - fcgiwrap \ - jq \ - curl - -COPY auth.sh /etc/nginx/conf.d -COPY default-ssl.conf /etc/nginx/conf.d/default.conf -COPY entrypoint.sh entrypoint.sh -COPY keys.properties /etc/nginx/conf.d -COPY api.properties /etc/nginx/conf.d -COPY trace.sh /etc/nginx/conf.d -COPY tests.sh /etc/nginx/conf.d -COPY ip-whitelist.conf /etc/nginx/conf.d - -RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh - -ENTRYPOINT ["./entrypoint.sh"] diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index 4427e2434..726191f35 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -7,6 +7,12 @@ server { 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; + proxy_pass http://proxy:8888; + } + location / { auth_request /auth; proxy_pass http://proxy:8888; diff --git a/build.sh b/build.sh index 122f71d68..e7776e888 100755 --- a/build.sh +++ b/build.sh @@ -35,26 +35,28 @@ build_docker_images() { trace "Updating SatoshiPortal repos" git submodule update --recursive --remote - local archpath=$(uname -m) - local clightning_dockerfile=Dockerfile + local bitcoin_dockerfile=Dockerfile.amd64 + local clightning_dockerfile=Dockerfile.amd64 + local proxy_dockerfile=Dockerfile.amd64 # compat mode for SatoshiPortal repo # TODO: add more mappings? - if [[ $archpath == 'armv7l' ]]; then - archpath="rpi" - clightning_dockerfile="Dockerfile-alpine" + if [[ $(uname -m) == 'armv7l' ]]; then + bitcoin_dockerfile="Dockerfile.arm32v6" + clightning_dockerfile="Dockerfile.arm32v6" + proxy_dockerfile="Dockerfile.arm32v6" fi trace "Creating cyphernodeconf image" build_docker_image install/ cyphernode/cyphernodeconf:cyphernode-0.05 trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/$archpath/bitcoin-core cyphernode/bitcoin:cyphernode-0.05 - build_docker_image install/SatoshiPortal/dockers/$archpath/LN/c-lightning cyphernode/clightning:cyphernode-0.05 $clightning_dockerfile + build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:cyphernode-0.05 $bitcoin_dockerfile + build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:cyphernode-0.05 $clightning_dockerfile trace "Creating cyphernode images" build_docker_image api_auth_docker/ cyphernode/gatekeeper:cyphernode-0.05 - build_docker_image proxy_docker/ cyphernode/proxy:cyphernode-0.05 + build_docker_image proxy_docker/ cyphernode/proxy:cyphernode-0.05 $proxy_dockerfile build_docker_image cron_docker/ cyphernode/proxycron:cyphernode-0.05 build_docker_image pycoin_docker/ cyphernode/pycoin:cyphernode-0.05 build_docker_image otsclient_docker/ cyphernode/otsclient:cyphernode-0.05 @@ -62,4 +64,3 @@ build_docker_images() { } build_docker_images - diff --git a/dist/setup.sh b/dist/setup.sh index 7bcd18092..b14eb9595 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -370,6 +370,7 @@ 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/gatekeeper/htpasswd $GATEKEEPER_DATAPATH/htpasswd 1 $SUDO_REQUIRED fi if [ ! -d $PROXY_DATAPATH ]; then diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 01b765735..19ac67ba6 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -198,6 +198,8 @@ module.exports = class extends Generator { // migrate here } + this.props.gatekeeper_statuspw = await new Cert().passwd(this.configurationPassword); + this._assignConfigDefaults(); for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index e7444fc90..cd2adfbb2 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -113,4 +113,20 @@ module.exports = class Cert { 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/prompters/010_gatekeeper.js b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js index c0538c45e..9bf33217d 100644 --- a/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js +++ b/install/generator-cyphernode/generators/app/prompters/010_gatekeeper.js @@ -97,6 +97,6 @@ module.exports = { }]; }, templates: function( props ) { - return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem' ]; + return [ 'keys.properties', 'api.properties', 'cert.pem', 'key.pem', 'htpasswd' ]; } -}; \ No newline at end of file +}; diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd new file mode 100644 index 000000000..8857f927c --- /dev/null +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd @@ -0,0 +1 @@ +cyphernode:<%- gatekeeper_statuspw %> 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 2278e8a19..aa0d2082f 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 @@ -13,6 +13,7 @@ services: - "<%= gatekeeper_datapath %>/private:/etc/ssl/private" - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties" - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" + - "<%= gatekeeper_datapath %>/htpasswd:/etc/nginx/conf.d/status/htpasswd" command: $USER # deploy: diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index ba3d39247..b5fe2096b 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -17,3 +17,7 @@ docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v $current_path/gatekeeper/cert.pem:/cert.pem \ -v <%= proxy_datapath %>:/proxy \ --network cyphernodenet alpine:3.8 /testfeatures.sh + +echo "Point your favorite browser to one of the following URLs to access Cyphernode's status page:" +echo +echo diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 74823b1d3..2a6e7387b 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -26,6 +26,7 @@ RUN apk add --update --no-cache \ COPY app/data/cyphernode.sql ${HOME} COPY app/data/sqlmigrate* ${HOME} +COPY app/html/statuspage.sh ${HOME} COPY app/script/callbacks_job.sh ${HOME} COPY app/script/blockchainrpc.sh ${HOME} COPY app/script/call_lightningd.sh ${HOME} diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index 7fa641e13..7817014ea 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -22,6 +22,7 @@ RUN apk add --update --no-cache \ COPY app/data/cyphernode.sql ${HOME} COPY app/data/sqlmigrate* ${HOME} +COPY app/html/statuspage.sh ${HOME} COPY app/script/callbacks_job.sh ${HOME} COPY app/script/blockchainrpc.sh ${HOME} COPY app/script/call_lightningd.sh ${HOME} diff --git a/proxy_docker/app/script/ots.sh b/proxy_docker/app/script/ots.sh index a24ee6b68..d3a54af8f 100644 --- a/proxy_docker/app/script/ots.sh +++ b/proxy_docker/app/script/ots.sh @@ -221,7 +221,7 @@ serve_ots_getfile() local hash=${1} trace "[serve_ots_getfile] hash=${hash}" - file_response_to_client "/otsfiles/" "${hash}.ots" + binfile_response_to_client "/otsfiles/" "${hash}.ots" returncode=$? trace_rc ${returncode} diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 433ffbad2..81d7fe0f6 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -18,6 +18,7 @@ . ./bitcoin.sh . ./call_lightningd.sh . ./ots.sh +. ./statuspage.sh main() { @@ -243,6 +244,12 @@ main() serve_ots_getfile $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3) break ;; + status) + # curl (GET) http://192.168.111.152:8080/status + + status_page + break + ;; esac break fi diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh index 193d0c7e5..774fb6639 100644 --- a/proxy_docker/app/script/responsetoclient.sh +++ b/proxy_docker/app/script/responsetoclient.sh @@ -8,19 +8,47 @@ response_to_client() local response=${1} local returncode=${2} + local contenttype=${3} + + [ -z "${contenttype}" ] && contenttype="application/json" ([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n" [ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n" - echo -en "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}" + echo -en "Content-Type: ${contenttype}\r\nContent-Length: ${#response}\r\n\r\n${response}" # Small delay needed for the data to be processed correctly by peer sleep 0.2s } -file_response_to_client() +htmlfile_response_to_client() +{ + trace "Entering htmlfile_response_to_client()..." + + local path=${1} + local filename=${2} + local pathfile="${path}${filename}" + local returncode + + trace "[htmlfile_response_to_client] path=${path}" + trace "[htmlfile_response_to_client] filename=${filename}" + trace "[htmlfile_response_to_client] pathfile=${pathfile}" + local file_length=$(stat -c'%s' ${pathfile}) + trace "[htmlfile_response_to_client] file_length=${file_length}" + + [ -r "${pathfile}" ] \ + && echo -ne "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: ${file_length}\r\n\r\n" \ + && cat ${pathfile} + + [ ! -r "${pathfile}" ] && echo -ne "HTTP/1.1 404 Not Found\r\n" + + # Small delay needed for the data to be processed correctly by peer + sleep 0.5s +} + +binfile_response_to_client() { - trace "Entering file_response_to_client()..." + trace "Entering binfile_response_to_client()..." local path=${1} local filename=${2} diff --git a/proxy_docker/app/script/statuspage.sh b/proxy_docker/app/script/statuspage.sh new file mode 100644 index 000000000..a86956ce3 --- /dev/null +++ b/proxy_docker/app/script/statuspage.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +. ./trace.sh +. ./responsetoclient.sh + +status_page() { + cat < statuspage.html + + + + +Hello from Cyphernode!

+EOF + + cat db/installation.json >> statuspage.html + + cat <> statuspage.html + + +EOF + + htmlfile_response_to_client ./ statuspage.html +} From 7db08bf43fee59ecd03d0c8026b63d44a6b4b88e Mon Sep 17 00:00:00 2001 From: kexkey Date: Sat, 15 Dec 2018 15:01:45 -0500 Subject: [PATCH 231/268] Moved statuspage from proxy to gatekeeper and refactored correctly --- api_auth_docker/default-ssl.conf | 3 +- api_auth_docker/statuspage.html | 53 +++++++++++++++++++ .../installer/docker/docker-compose.yaml | 1 + .../app/templates/installer/start.sh | 2 +- .../app/templates/installer/testfeatures.sh | 2 +- proxy_docker/app/script/requesthandler.sh | 6 --- proxy_docker/app/script/responsetoclient.sh | 2 +- proxy_docker/app/script/statuspage.sh | 23 -------- 8 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 api_auth_docker/statuspage.html delete mode 100644 proxy_docker/app/script/statuspage.sh diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index 726191f35..65f40bc48 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -10,7 +10,8 @@ server { location /status { auth_basic "status"; auth_basic_user_file conf.d/status/htpasswd; - proxy_pass http://proxy:8888; + root /etc/nginx/conf.d; + index statuspage.html; } location / { diff --git a/api_auth_docker/statuspage.html b/api_auth_docker/statuspage.html new file mode 100644 index 000000000..f0030a9f5 --- /dev/null +++ b/api_auth_docker/statuspage.html @@ -0,0 +1,53 @@ + + + + + + + + + +

+


+
+
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 aa0d2082f..638e0f5bc 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
@@ -14,6 +14,7 @@ services:
       - "<%= gatekeeper_datapath %>/keys.properties:/etc/nginx/conf.d/keys.properties"
       - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties"
       - "<%= gatekeeper_datapath %>/htpasswd:/etc/nginx/conf.d/status/htpasswd"
+      - "<%= gatekeeper_datapath %>/installation.json:/etc/nginx/conf.d/status/installation.json"
     command: $USER
 
 #    deploy:
diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh
index b5fe2096b..c4ae464c7 100644
--- a/install/generator-cyphernode/generators/app/templates/installer/start.sh
+++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh
@@ -15,7 +15,7 @@ docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans
 docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \
 -v $current_path/gatekeeper/keys.properties:/keys.properties \
 -v $current_path/gatekeeper/cert.pem:/cert.pem \
--v <%= proxy_datapath %>:/proxy \
+-v <%= gatekeeper_datapath %>:/gatekeeper \
 --network cyphernodenet alpine:3.8 /testfeatures.sh
 
 echo "Point your favorite browser to one of the following URLs to access Cyphernode's status page:"
diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh
index dd874c068..2c17bc1bd 100644
--- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh
+++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh
@@ -311,6 +311,6 @@ result="${result}$(feature_status ${returncode} 'Lightning error!')}"
 
 result="{${result}]}"
 
-echo "${result}" > /proxy/installation.json
+echo "${result}" > /gatekeeper/installation.json
 
 echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console
diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh
index 81d7fe0f6..4be59ed66 100644
--- a/proxy_docker/app/script/requesthandler.sh
+++ b/proxy_docker/app/script/requesthandler.sh
@@ -244,12 +244,6 @@ main()
           serve_ots_getfile $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)
           break
           ;;
-        status)
-          # curl (GET) http://192.168.111.152:8080/status
-
-          status_page
-          break
-          ;;
       esac
       break
     fi
diff --git a/proxy_docker/app/script/responsetoclient.sh b/proxy_docker/app/script/responsetoclient.sh
index 774fb6639..ad4467d66 100644
--- a/proxy_docker/app/script/responsetoclient.sh
+++ b/proxy_docker/app/script/responsetoclient.sh
@@ -18,7 +18,7 @@ response_to_client()
   echo -en "Content-Type: ${contenttype}\r\nContent-Length: ${#response}\r\n\r\n${response}"
 
   # Small delay needed for the data to be processed correctly by peer
-  sleep 0.2s
+  sleep 0.5s
 }
 
 htmlfile_response_to_client()
diff --git a/proxy_docker/app/script/statuspage.sh b/proxy_docker/app/script/statuspage.sh
deleted file mode 100644
index a86956ce3..000000000
--- a/proxy_docker/app/script/statuspage.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh
-
-. ./trace.sh
-. ./responsetoclient.sh
-
-status_page() {
-  cat < statuspage.html
-
-
-
-
-Hello from Cyphernode!

-EOF - - cat db/installation.json >> statuspage.html - - cat <> statuspage.html - - -EOF - - htmlfile_response_to_client ./ statuspage.html -} From b6102b9768d5313f1e955214bc477fd3779fe60e Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 12:35:20 -0500 Subject: [PATCH 232/268] Won't test features if containers down... --- .../app/templates/installer/testfeatures.sh | 141 +++++++++++------- 1 file changed, 90 insertions(+), 51 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 2c17bc1bd..6b07f8a06 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -1,6 +1,6 @@ #!/bin/sh -apk add --update --no-cache openssl curl > /dev/null +apk add --update --no-cache openssl curl jq > /dev/null . keys.properties @@ -74,17 +74,18 @@ checkgatekeeper() { checkpycoin() { echo -en "\r\n\e[1;36mTesting Pycoin... " > /dev/console local rc - local id="002" - local k - eval k='$ukey_'$id +# local id="002" +# local k +# eval k='$ukey_'$id - local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) +# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) - local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) - local token="$h64.$p64.$s" +# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) +# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) +# local token="$h64.$p64.$s" - rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) + rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" http://proxy:8888/derivepubpath) +# rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) [ "${rc}" -ne "200" ] && return 100 echo -e "\e[1;36mPycoin rocks!" > /dev/console @@ -95,17 +96,18 @@ checkpycoin() { checkots() { echo -en "\r\n\e[1;36mTesting OTSclient... " > /dev/console local rc - local id="002" - local k - eval k='$ukey_'$id +# local id="002" +# local k +# eval k='$ukey_'$id - local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) +# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) - local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) - local token="$h64.$p64.$s" +# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) +# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) +# local token="$h64.$p64.$s" - rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ots_stamp) + rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' http://proxy:8888/ots_stamp) +# rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ots_stamp) echo "${rc}" | grep "Invalid hash 123 for sha256" > /dev/null [ "$?" -ne "0" ] && return 200 @@ -117,17 +119,18 @@ checkots() { checkbitcoinnode() { echo -en "\r\n\e[1;36mTesting Bitcoin... " > /dev/console local rc - local id="002" - local k - eval k='$ukey_'$id +# local id="002" +# local k +# eval k='$ukey_'$id - local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) +# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) - local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) - local token="$h64.$p64.$s" +# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) +# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) +# local token="$h64.$p64.$s" - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) + rc=$(curl -s -o /dev/null -w "%{http_code}" http://proxy:8888/getbestblockhash) +# rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) [ "${rc}" -ne "200" ] && return 300 echo -e "\e[1;36mBitcoin node rocks!" > /dev/console @@ -138,17 +141,18 @@ checkbitcoinnode() { checklnnode() { echo -en "\r\n\e[1;36mTesting Lightning... " > /dev/console local rc - local id="002" - local k - eval k='$ukey_'$id +# local id="002" +# local k +# eval k='$ukey_'$id - local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) +# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) - local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) - local token="$h64.$p64.$s" +# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) +# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) +# local token="$h64.$p64.$s" - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) + rc=$(curl -s -o /dev/null -w "%{http_code}" http://proxy:8888/ln_getinfo) +# rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) [ "${rc}" -ne "200" ] && return 400 echo -e "\e[1;36mLN node rocks!" > /dev/console @@ -159,9 +163,11 @@ checklnnode() { checkservice() { echo -e "\r\n\e[1;36mTesting if Cyphernode is up and running... \e[0;36mI will keep trying during up to 5 minutes to give time to Docker to deploy everything...\e[0;32m" > /dev/console + local interval=10 + local totaltime=120 local outcome local returncode=0 - local endtime=$(($(date +%s) + 300)) + local endtime=$(($(date +%s) + ${totaltime})) local result while : @@ -180,9 +186,9 @@ checkservice() { # If '0% packet loss' everywhere or 5 minutes passed, we get out of this loop ([ "${outcome}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - echo -e "\e[1;31mCyphernode still not ready, will retry every 5 seconds for 5 minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console + echo -e "\e[1;31mCyphernode still not ready, will retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console - sleep 5 + sleep ${interval} done # "containers": [ @@ -213,6 +219,8 @@ checkservice() { } timeout_feature() { + local interval=10 + local totaltime=60 local testwhat=${1} local returncode local endtime=$(($(date +%s) + 120)) @@ -225,9 +233,9 @@ timeout_feature() { # If no error or 2 minutes passed, we get out of this loop ([ "${returncode}" -eq "0" ] || [ $(date +%s) -gt ${endtime} ]) && break - echo -e "\e[1;31mMaybe it's too early, I'll retry every 5 seconds for 2 minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console + echo -e "\e[1;31mMaybe it's too early, I'll retry every ${interval} seconds for $((${totaltime} / 60)) minutes ($((${endtime} - $(date +%s))) seconds left)." > /dev/console - sleep 5 + sleep ${interval} done return ${returncode} @@ -263,10 +271,16 @@ feature_status() { # Let's first see if everything is up. -result=$(checkservice) +brokenproxy="false" +containers=$(checkservice) returncode=$? if [ "${returncode}" -ne "0" ]; then - echo -e "\e[1;31mCyphernode could not fully start properly within 5 minutes." > /dev/console + echo -e "\e[1;31mCyphernode could not fully start properly within delay." > /dev/console + status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"proxy\") | .active") + if [ "${status}" = "false" ]; then + echo -e "\e[1;31mThe Proxy, the main Cyphernode's component, is not responding. We will only test the gatekeeper if its container is up, but you'll see errors for the other components. Please check the logs." > /dev/console + brokenproxy="true" + fi else echo -e "\e[1;36mCyphernode seems to be correctly deployed. Let's run more thourough tests..." > /dev/console fi @@ -280,32 +294,57 @@ fi # { "name": "lightning", "working":true } # ] -result="${result},\"features\":[{\"name\":\"gatekeeper\",\"working\":" -timeout_feature checkgatekeeper -returncode=$? +result="${containers},\"features\":[{\"name\":\"gatekeeper\",\"working\":" +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"gatekeeper\") | .active") +if [ "${status}" = "true" ]; then + timeout_feature checkgatekeeper + returncode=$? +else + returncode=1 +fi result="${result}$(feature_status ${returncode} 'Gatekeeper error!')}" result="${result},{\"name\":\"pycoin\",\"working\":" -timeout_feature checkpycoin -returncode=$? +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"pycoin\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checkpycoin + returncode=$? +else + returncode=1 +fi result="${result}$(feature_status ${returncode} 'Pycoin error!')}" <% if (features.indexOf('otsclient') != -1) { %> result="${result},{\"name\":\"otsclient\",\"working\":" -timeout_feature checkots -returncode=$? +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"otsclient\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checkots + returncode=$? +else + returncode=1 +fi result="${result}$(feature_status ${returncode} 'OTSclient error!')}" <% } %> result="${result},{\"name\":\"bitcoin\",\"working\":" -timeout_feature checkbitcoinnode -returncode=$? +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"bitcoin\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checkbitcoinnode + returncode=$? +else + returncode=1 +fi result="${result}$(feature_status ${returncode} 'Bitcoin error!')}" <% if (features.indexOf('lightning') != -1) { %> result="${result},{\"name\":\"lightning\",\"working\":" -timeout_feature checklnnode -returncode=$? +status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"lightning\") | .active") +if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then + timeout_feature checklnnode + returncode=$? +else + returncode=1 +fi result="${result}$(feature_status ${returncode} 'Lightning error!')}" <% } %> From 4e41a1e39a2993ae932ca8d24f01df53c8179a78 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 12:45:47 -0500 Subject: [PATCH 233/268] Cleaned comments --- .../app/templates/installer/testfeatures.sh | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 6b07f8a06..f82ae1bd3 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -74,18 +74,8 @@ checkgatekeeper() { checkpycoin() { echo -en "\r\n\e[1;36mTesting Pycoin... " > /dev/console local rc -# local id="002" -# local k -# eval k='$ukey_'$id - -# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - -# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) -# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) -# local token="$h64.$p64.$s" rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" http://proxy:8888/derivepubpath) -# rc=$(curl -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/derivepubpath) [ "${rc}" -ne "200" ] && return 100 echo -e "\e[1;36mPycoin rocks!" > /dev/console @@ -96,18 +86,8 @@ checkpycoin() { checkots() { echo -en "\r\n\e[1;36mTesting OTSclient... " > /dev/console local rc -# local id="002" -# local k -# eval k='$ukey_'$id - -# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - -# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) -# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) -# local token="$h64.$p64.$s" rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' http://proxy:8888/ots_stamp) -# rc=$(curl -s -H "Content-Type: application/json" -d '{"hash":"123","callbackUrl":"http://callback"}' -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ots_stamp) echo "${rc}" | grep "Invalid hash 123 for sha256" > /dev/null [ "$?" -ne "0" ] && return 200 @@ -119,18 +99,8 @@ checkots() { checkbitcoinnode() { echo -en "\r\n\e[1;36mTesting Bitcoin... " > /dev/console local rc -# local id="002" -# local k -# eval k='$ukey_'$id - -# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - -# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) -# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) -# local token="$h64.$p64.$s" rc=$(curl -s -o /dev/null -w "%{http_code}" http://proxy:8888/getbestblockhash) -# rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbestblockhash) [ "${rc}" -ne "200" ] && return 300 echo -e "\e[1;36mBitcoin node rocks!" > /dev/console @@ -141,18 +111,8 @@ checkbitcoinnode() { checklnnode() { echo -en "\r\n\e[1;36mTesting Lightning... " > /dev/console local rc -# local id="002" -# local k -# eval k='$ukey_'$id - -# local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) - -# local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) -# local s=$(echo -n "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) -# local token="$h64.$p64.$s" rc=$(curl -s -o /dev/null -w "%{http_code}" http://proxy:8888/ln_getinfo) -# rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/ln_getinfo) [ "${rc}" -ne "200" ] && return 400 echo -e "\e[1;36mLN node rocks!" > /dev/console From 85e9748142880e77155c6b6de7dfd0ffe480f5ce Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 14:01:48 -0500 Subject: [PATCH 234/268] Multiple file copy in dockerfile needs slash as dir --- proxy_docker/Dockerfile.amd64 | 2 +- proxy_docker/Dockerfile.arm32v6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 2a6e7387b..1be299a79 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -25,7 +25,7 @@ RUN apk add --update --no-cache \ su-exec COPY app/data/cyphernode.sql ${HOME} -COPY app/data/sqlmigrate* ${HOME} +COPY app/data/sqlmigrate* ${HOME}/ COPY app/html/statuspage.sh ${HOME} COPY app/script/callbacks_job.sh ${HOME} COPY app/script/blockchainrpc.sh ${HOME} diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index 7817014ea..0fcd39177 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -21,7 +21,7 @@ RUN apk add --update --no-cache \ su-exec COPY app/data/cyphernode.sql ${HOME} -COPY app/data/sqlmigrate* ${HOME} +COPY app/data/sqlmigrate* ${HOME}/ COPY app/html/statuspage.sh ${HOME} COPY app/script/callbacks_job.sh ${HOME} COPY app/script/blockchainrpc.sh ${HOME} From f6c520690941faa333371844e2e1a7f6c2d247d7 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 14:43:01 -0500 Subject: [PATCH 235/268] Proxy's dockerfile was buggy with wildcard copy --- proxy_docker/Dockerfile.amd64 | 30 ++++-------------------------- proxy_docker/Dockerfile.arm32v6 | 30 ++++-------------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 1be299a79..4154dc242 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -24,34 +24,12 @@ RUN apk add --update --no-cache \ curl \ su-exec -COPY app/data/cyphernode.sql ${HOME} -COPY app/data/sqlmigrate* ${HOME}/ -COPY app/html/statuspage.sh ${HOME} -COPY app/script/callbacks_job.sh ${HOME} -COPY app/script/blockchainrpc.sh ${HOME} -COPY app/script/call_lightningd.sh ${HOME} -COPY app/script/bitcoin.sh ${HOME} -COPY app/script/ots.sh ${HOME} -COPY app/script/requesthandler.sh ${HOME} -COPY app/script/watchrequest.sh ${HOME} -COPY app/script/walletoperations.sh ${HOME} -COPY app/script/confirmation.sh ${HOME} -COPY app/script/startproxy.sh ${HOME} -COPY app/script/trace.sh ${HOME} -COPY app/script/sendtobitcoinnode.sh ${HOME} -COPY app/script/responsetoclient.sh ${HOME} -COPY app/script/importaddress.sh ${HOME} -COPY app/script/sql.sh ${HOME} -COPY app/script/computefees.sh ${HOME} -COPY app/script/unwatchrequest.sh ${HOME} -COPY app/script/getactivewatches.sh ${HOME} -COPY app/script/manage_missed_conf.sh ${HOME} -COPY app/script/tests.sh ${HOME} -COPY app/script/tests-cb.sh ${HOME} -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME} - WORKDIR ${HOME} +COPY app/data/* ./ +COPY app/script/* /. +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli . + RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ && mkdir db diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index 0fcd39177..dc91d9775 100644 --- a/proxy_docker/Dockerfile.arm32v6 +++ b/proxy_docker/Dockerfile.arm32v6 @@ -20,34 +20,12 @@ RUN apk add --update --no-cache \ curl \ su-exec -COPY app/data/cyphernode.sql ${HOME} -COPY app/data/sqlmigrate* ${HOME}/ -COPY app/html/statuspage.sh ${HOME} -COPY app/script/callbacks_job.sh ${HOME} -COPY app/script/blockchainrpc.sh ${HOME} -COPY app/script/call_lightningd.sh ${HOME} -COPY app/script/bitcoin.sh ${HOME} -COPY app/script/ots.sh ${HOME} -COPY app/script/requesthandler.sh ${HOME} -COPY app/script/watchrequest.sh ${HOME} -COPY app/script/walletoperations.sh ${HOME} -COPY app/script/confirmation.sh ${HOME} -COPY app/script/startproxy.sh ${HOME} -COPY app/script/trace.sh ${HOME} -COPY app/script/sendtobitcoinnode.sh ${HOME} -COPY app/script/responsetoclient.sh ${HOME} -COPY app/script/importaddress.sh ${HOME} -COPY app/script/sql.sh ${HOME} -COPY app/script/computefees.sh ${HOME} -COPY app/script/unwatchrequest.sh ${HOME} -COPY app/script/getactivewatches.sh ${HOME} -COPY app/script/manage_missed_conf.sh ${HOME} -COPY app/script/tests.sh ${HOME} -COPY app/script/tests-cb.sh ${HOME} -COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ${HOME} - WORKDIR ${HOME} +COPY app/data/* ./ +COPY app/script/* ./ +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli . + RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ && mkdir db From 8d199f8b2871cf10495749b977d8fcb5f8a2917a Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 14:58:36 -0500 Subject: [PATCH 236/268] grr --- proxy_docker/Dockerfile.amd64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 4154dc242..8f2ca4a17 100644 --- a/proxy_docker/Dockerfile.amd64 +++ b/proxy_docker/Dockerfile.amd64 @@ -27,7 +27,7 @@ RUN apk add --update --no-cache \ WORKDIR ${HOME} COPY app/data/* ./ -COPY app/script/* /. +COPY app/script/* ./ COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli . RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ From 28763aa6104899ffa63ce2820763692ede656c31 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 15:38:24 -0500 Subject: [PATCH 237/268] No statuspage in proxy and small fixes --- .../generators/app/templates/installer/testfeatures.sh | 6 +++--- proxy_docker/app/script/requesthandler.sh | 1 - 2 files changed, 3 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 f82ae1bd3..43173bb33 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -121,8 +121,6 @@ checklnnode() { } checkservice() { - echo -e "\r\n\e[1;36mTesting if Cyphernode is up and running... \e[0;36mI will keep trying during up to 5 minutes to give time to Docker to deploy everything...\e[0;32m" > /dev/console - local interval=10 local totaltime=120 local outcome @@ -130,6 +128,8 @@ checkservice() { local endtime=$(($(date +%s) + ${totaltime})) local result + echo -e "\r\n\e[1;36mTesting if Cyphernode is up and running... \e[0;36mI will keep trying during up to $((${totaltime} / 60)) minutes to give time to Docker to deploy everything...\e[0;32m" > /dev/console + while : do outcome=0 @@ -183,7 +183,7 @@ timeout_feature() { local totaltime=60 local testwhat=${1} local returncode - local endtime=$(($(date +%s) + 120)) + local endtime=$(($(date +%s) + ${totaltime})) while : do diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 4be59ed66..433ffbad2 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -18,7 +18,6 @@ . ./bitcoin.sh . ./call_lightningd.sh . ./ots.sh -. ./statuspage.sh main() { From f6a7b6c28a910d0859fde145ad21414adc8e8157 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 17 Dec 2018 20:13:14 -0500 Subject: [PATCH 238/268] Dockerfile copies were not right --- api_auth_docker/Dockerfile | 8 ++++---- dist/setup.sh | 2 +- proxy_docker/Dockerfile.amd64 | 2 +- proxy_docker/Dockerfile.arm32v6 | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 0c54eda4d..6a534f655 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -10,12 +10,12 @@ RUN apk add --update --no-cache \ jq \ su-exec -COPY auth.sh /etc/nginx/conf.d +COPY auth.sh /etc/nginx/conf.d/ COPY default-ssl.conf /etc/nginx/conf.d/default.conf -COPY statuspage.html /etc/nginx/conf.d/status +COPY statuspage.html /etc/nginx/conf.d/status/ COPY entrypoint.sh entrypoint.sh -COPY trace.sh /etc/nginx/conf.d -COPY tests.sh /etc/nginx/conf.d +COPY trace.sh /etc/nginx/conf.d/ +COPY tests.sh /etc/nginx/conf.d/ RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh diff --git a/dist/setup.sh b/dist/setup.sh index b14eb9595..8f430f8be 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -539,7 +539,7 @@ sanity_checks() { exit fi - if ! [ -x "$(command -v docker-compose)" ]; then + if [[ $DOCKER_MODE == 'compose' && ! -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 diff --git a/proxy_docker/Dockerfile.amd64 b/proxy_docker/Dockerfile.amd64 index 8f2ca4a17..ff124f1ee 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.6.2 /usr/bin/lightning-cli . +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ./ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ diff --git a/proxy_docker/Dockerfile.arm32v6 b/proxy_docker/Dockerfile.arm32v6 index dc91d9775..ed8d44e91 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.6.2 /usr/bin/lightning-cli . +COPY --from=cyphernode/clightning:v0.6.2 /usr/bin/lightning-cli ./ RUN chmod +x startproxy.sh requesthandler.sh lightning-cli sqlmigrate*.sh \ && chmod o+w . \ From a338d17fc778093a335b4c733ab8abe3f109c9fb Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 18 Dec 2018 15:04:19 -0500 Subject: [PATCH 239/268] URLs after setup, colors, status page, downloadable files --- api_auth_docker/statuspage.html | 33 +++++++++++-------- .../generators/app/help.json | 10 +++--- .../generators/app/index.js | 6 ++++ .../generators/app/lib/cert.js | 3 +- .../installer/docker/docker-compose.yaml | 2 ++ .../app/templates/installer/start.sh | 6 ++-- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/api_auth_docker/statuspage.html b/api_auth_docker/statuspage.html index f0030a9f5..dadca7bf4 100644 --- a/api_auth_docker/statuspage.html +++ b/api_auth_docker/statuspage.html @@ -4,18 +4,11 @@ - -

-


+  
+

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

+

+  
diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 327213da4..5e9b339ce 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -2,7 +2,7 @@ "features": "What optional features do you want me to activate? Select multiple choices using the space bar.", "net": "You want Cyphernode to run on what Bitcoin network?", "run_as_different_user": "We recommend running Cyphernode as a different user when possible. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", - "username": "Run Cyphernode as what user? We recommend user cyphernode. If the user does not exist, I will create it for you.", + "username": "Run Cyphernode as what user? We recommend user cyphernode. If the user does not exist, I will create it for you.", "use_xpub": "Cyphernode can derive Bitcoin addresses from an xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path on each call.", "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", @@ -16,20 +16,20 @@ "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", "gatekeeper_sslkey": "** gatekeeper_sslkey **", - "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide cyphernodehost,192.168.7.44 as a possible domains. '127.0.0.1,localhost' will be added automatically. Make sure the provided domain names are in your DNS or client's hosts file and is reachable.", + "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide 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 it.", "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.", "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", - "bitcoin_prune_size": "Minimum size is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", + "bitcoin_prune_size": "Minimum size is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", - "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", + "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", - "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be FF0000.", + "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 19ac67ba6..8a7abd9b5 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -288,6 +288,12 @@ module.exports = class extends Generator { if( result.code === 0 ) { this.props.gatekeeper_sslkey = result.key.toString(); this.props.gatekeeper_sslcert = result.cert.toString(); + + // Total array of cns, used to create Cyphernode's URLs + this.props.cns = [] + result.cns.forEach(e => { + this.props.cns.push(e) + }) } else { console.log(chalk.bold.red( 'error! Gatekeeper cert was not created' )); } diff --git a/install/generator-cyphernode/generators/app/lib/cert.js b/install/generator-cyphernode/generators/app/lib/cert.js index cd2adfbb2..1a61c9983 100644 --- a/install/generator-cyphernode/generators/app/lib/cert.js +++ b/install/generator-cyphernode/generators/app/lib/cert.js @@ -105,7 +105,8 @@ module.exports = class Cert { return { code: code, key: key, - cert: cert + cert: cert, + cns: cns } } 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 638e0f5bc..96476d50d 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 @@ -15,6 +15,8 @@ services: - "<%= gatekeeper_datapath %>/api.properties:/etc/nginx/conf.d/api.properties" - "<%= gatekeeper_datapath %>/htpasswd:/etc/nginx/conf.d/status/htpasswd" - "<%= gatekeeper_datapath %>/installation.json:/etc/nginx/conf.d/status/installation.json" + - "<%= gatekeeper_datapath %>/client.7z:/etc/nginx/conf.d/status/client.7z" + - "<%= gatekeeper_datapath %>/config.7z:/etc/nginx/conf.d/status/config.7z" command: $USER # deploy: diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index c4ae464c7..4d09bcd21 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -18,6 +18,6 @@ docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v <%= gatekeeper_datapath %>:/gatekeeper \ --network cyphernodenet alpine:3.8 /testfeatures.sh -echo "Point your favorite browser to one of the following URLs to access Cyphernode's status page:" -echo -echo +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" From 08a683266eaef5bc8c1e63e41b863f847f8670a0 Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 18 Dec 2018 17:12:20 -0500 Subject: [PATCH 240/268] use_xpub was not saved in config and fixed startup on OSX --- .../generators/app/prompters/000_cyphernode.js | 4 ++-- .../generators/app/templates/installer/start.sh | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index ce495d885..f0d9265c9 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -56,7 +56,7 @@ module.exports = { { type: 'confirm', name: 'use_xpub', - default: utils._getDefault( 'want_xpub' )||false, + default: utils._getDefault( 'use_xpub' )||false, message: prefix()+'Use an xpub key to watch or generate adresses?'+utils._getHelp('use_xpub'), }, { @@ -85,4 +85,4 @@ module.exports = { templates: function( props ) { return []; } -}; \ 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 4d09bcd21..602157742 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -1,7 +1,17 @@ #!/bin/sh -# run as user <%= username %> -export USER=$(id -u <%= run_as_different_user?username:default_username %>):$(id -g <%= run_as_different_user?username:default_username %>) +<% if (run_as_different_user) { %> +OS=$(uname -s) +if [ "$OS" = "Darwin" ]; then + printf "\r\n\033[0;91m'Run as another user' feature is not supported on OSX. User <%= default_username %> will be used to run Cyphernode.\033[0m\r\n\r\n" + export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) +else + export USER=$(id -u <%= username %>):$(id -g <%= username %>) +fi +<% } else { %> +export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) +<% } %> + export ARCH=$(uname -m) current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" From ff1f226f8e190d55202c4e5b23703f67a4fc99aa Mon Sep 17 00:00:00 2001 From: jash Date: Wed, 19 Dec 2018 15:40:04 +0100 Subject: [PATCH 241/268] made lightning node alias and color optional. added lightning node name generator for nice default aliases --- .../generators/app/index.js | 14 +- .../generators/app/lib/name.js | 2370 +++++++++++++++++ .../generators/app/prompters/200_lightning.js | 18 +- .../templates/lightning/c-lightning/config | 4 + 4 files changed, 2398 insertions(+), 8 deletions(-) create mode 100644 install/generator-cyphernode/generators/app/lib/name.js diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 8a7abd9b5..8bd80bb49 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -6,6 +6,7 @@ const fs = require('fs'); const validator = require('validator'); const path = require("path"); const coinstring = require('coinstring'); +const name = require('./lib/name.js'); const Archive = require('./lib/archive.js'); const ApiKey = require('./lib/apikey.js'); const Cert = require('./lib/cert.js'); @@ -16,8 +17,7 @@ const userRegexp = /^[a-zA-Z0-9\._\-]+$/; const reset = '\u001B8\u001B[u'; const clear = '\u001Bc'; -const configFileVersion='0.0.1'; - +const configFileVersion='0.1.0'; const defaultAPIProperties = ` # Watcher can: @@ -302,7 +302,6 @@ module.exports = class extends Generator { } } - delete this.props.gatekeeper_recreatekeys; } @@ -385,7 +384,7 @@ module.exports = class extends Generator { proxy_datapath: '', lightning_implementation: 'c-lightning', lightning_datapath: '', - lightning_nodename: '', + lightning_nodename: name.generate(), lightning_nodecolor: '', otsclient_datapath: '', installer_cleanup: false @@ -442,6 +441,13 @@ module.exports = class extends Generator { return true; } + _lightningNodeNameValidator(name) { + if( !name || name.length > 32 ) { + throw new Error('Please enter anything shorter than 32 characters'); + } + return true; + } + _notEmptyValidator( path ) { if( !path ) { throw new Error('Please enter something'); diff --git a/install/generator-cyphernode/generators/app/lib/name.js b/install/generator-cyphernode/generators/app/lib/name.js new file mode 100644 index 000000000..82602e303 --- /dev/null +++ b/install/generator-cyphernode/generators/app/lib/name.js @@ -0,0 +1,2370 @@ +const MAXLENGTH = 32; + +const ADJECTIVES = [ + /*a*/ ["Abiding", + "Able", + "Able-bodied", + "Abounding", + "Aboveboard", + "Absolute", + "Absolved", + "Abundant", + "Academic", + "Acceptable", + "Accepted", + "Accepting", + "Accessible", + "Acclaimed", + "Accommodating", + "Accomplished", + "Accordant", + "Accountable", + "Accredited", + "Accurate", + "Accustomed", + "Acknowledged", + "Acquainted", + "Active", + "Actual", + "Acuminous", + "Acute", + "Adamant", + "Adaptable", + "Adept", + "Adequate", + "Adjusted", + "Admirable", + "Admired", + "Admissible", + "Adonic", + "Adorable", + "Adored", + "Adroit", + "Advanced", + "Advantaged", + "Advantageous", + "Adventuresome", + "Adventurous", + "Advisable", + "Aesthetic", + "Aesthetical", + "Affable", + "Affecting", + "Affectionate", + "Affective", + "Affiliated", + "Affined", + "Affluent", + "Affluential", + "Ageless", + "Agile", + "Agreeable", + "Aholic", + "Alacritous", + "Alert", + "Alive", + "All-important", + "Allegiant", + "Allied", + "Alluring", + "Alright", + "Alternate", + "Altruistic", + "Amative", + "Amatory", + "Amazing", + "Ambidextrous", + "Ambitious", + "Amelioratory", + "Amenable", + "Amiable", + "Amicable", + "Amusing", + "Anamnestic", + "Angelic", + "Aplenty", + "Apollonian", + "Appealing", + "Appeasing", + "Appetent", + "Appetizing", + "Apposite", + "Appreciated", + "Appreciative", + "Apprehensible", + "Approachable", + "Appropriate", + "Approving", + "Apropos", + "Apt", + "Ardent", + "Aristocratic", + "Arousing", + "Arresting", + "Articulate", + "Artistic", + "Ascendant", + "Ascending", + "Aspirant", + "Aspiring", + "Assertive", + "Assiduous", + "Assistant", + "Assisting", + "Assistive", + "Associate", + "Associated", + "Associative", + "Assured", + "Assuring", + "Astir", + "Astonishing", + "Astounding", + "Astronomical", + "Astute", + "Athletic", + "Attainable", + "Attendant", + "Attentive", + "Attractive", + "Atypical", + "Au fait", + "August", + "Auspicious", + "Authentic", + "Authoritative", + "Authorized", + "Autonomous", + "Available", + "Avant-garde", + "Avid", + "Awaited", + "Awake", + "Aware", + "Awash", + "Awesome", + "Axiological"], + /*b*/ ["Balanced", + "Baronial", + "Beaming", + "Beatific", + "Beauteous", + "Beautified", + "Beautiful", + "Becoming", + "Beefy", + "Believable", + "Beloved", + "Benedictory", + "Benefic", + "Beneficent", + "Beneficial", + "Beneficiary", + "Benevolent", + "Benign", + "Benignant", + "Bent on", + "Best", + "Better", + "Big", + "Big-hearted", + "Big-time", + "Biggest", + "Bijou", + "Blameless", + "Blazing", + "Blessed", + "Blissful", + "Blithe", + "Blooming", + "Blue-ribbon", + "Bodacious", + "Boisterous", + "Bold", + "Bona fide", + "Bonny", + "Bonzer", + "Boss", + "Bound", + "Bounteous", + "Bountiful", + "Brainy", + "Brave", + "Brawny", + "Breezy", + "Brief", + "Bright", + "Brill", + "Brilliant", + "Brimming", + "Brisk", + "Broadminded", + "Brotherly", + "Bubbly", + "Budding", + "Buff", + "Bullish", + "Buoyant", + "Businesslike", + "Bustling", + "Busy", + "Buxom"], + /*c*/ ["Calm", + "Calmative", + "Calming", + "Can-do", + "Candescent", + "Canny", + "Canty", + "Capable", + "Capital", + "Captivating", + "Cared for", + "Carefree", + "Careful", + "Caring", + "Casual", + "Causative", + "Celebrated", + "Celeritous", + "Celestial", + "Centered", + "Central", + "Cerebral", + "Certain", + "Champion", + "Changeable", + "Changeless", + "Charismatic", + "Charitable", + "Charming", + "Cheerful", + "Cherished", + "Cherry", + "Chic", + "Childlike", + "Chipper", + "Chirpy", + "Chivalrous", + "Choice", + "Chosen", + "Chummy", + "Civic", + "Civil", + "Civilized", + "Clairvoyant", + "Classic", + "Classical", + "Classy", + "Clean", + "Clear", + "Clear-cut", + "Clearheaded", + "Clement", + "Clever", + "Close", + "Clubby", + "Coadjutant", + "Coequal", + "Cogent", + "Cognizant", + "Coherent", + "Collected", + "Colossal", + "Colourful", + "Coltish", + "Come-at-able", + "Comely", + "Comfortable", + "Comforting", + "Comic", + "Comical", + "Commanding", + "Commendable", + "Commendatory", + "Commending", + "Commiserative", + "Committed", + "Commodious", + "Commonsensical", + "Communicative", + "Commutual", + "Companionable", + "Compassionate", + "Compatible", + "Compelling", + "Competent", + "Complete", + "Completed", + "Complimentary", + "Composed", + "Comprehensive", + "Concentrated", + "Concise", + "Conclusive", + "Concordant", + "Concrete", + "Condolatory", + "Confederate", + "Conferrable", + "Confident", + "Congenial", + "Congruous", + "Connected", + "Conscientious", + "Conscious", + "Consensual", + "Consentaneous", + "Consentient", + "Consequential", + "Considerable", + "Considerate", + "Consistent", + "Consonant", + "Conspicuous", + "Constant", + "Constitutional", + "Constructive", + "Contemplative", + "Contemporary", + "Content", + "Contributive", + "Convenient", + "Conversant", + "Convictive", + "Convincing", + "Convivial", + "Cool", + "Cooperative", + "Coordinated", + "Copacetic", + "Copious", + "Cordial", + "Correct", + "Coruscant", + "Cosmic", + "Cosy", + "Courageous", + "Courteous", + "Courtly", + "Cozy", + "Crackerjack", + "Creamy", + "Creative", + "Credible", + "Creditable", + "Crisp", + "Crucial", + "Crystal (Clear)", + "Cuddly", + "Cultivated", + "Cultured", + "Cunning", + "Curious", + "Current", + "Curvaceous", + "Cushy", + "Cute", + "Cutting-edge"], + /*d*/ ["Dainty", + "Dandy", + "Dapper", + "Daring", + "Darling", + "Dashing", + "Dauntless", + "Dazzling", + "Dear", + "Debonair", + "Decent", + "Deciding", + "Decisive", + "Decorous", + "Dedicated", + "Deep", + "Defiant", + "Defiantly", + "Definite", + "Deft", + "Delectable", + "Deliberate", + "Delicate", + "Delicious", + "Delighted", + "Delightful", + "Deluxe", + "Demonstrative", + "Demulcent", + "Dependable", + "Deserving", + "Designer", + "Desirable", + "Desired", + "Desirous", + "Destined", + "Determined", + "Developed", + "Developing", + "Devoted", + "Devotional", + "Devout", + "Dexterous", + "Didactic", + "Different", + "Dignified", + "Diligent", + "Dinkum", + "Diplomatic", + "Direct", + "Disarming", + "Discerning", + "Disciplined", + "Discreet", + "Discrete", + "Discriminating", + "Dispassionate", + "Distinct", + "Distinctive", + "Distinguished", + "Distinguishing", + "Diverse", + "Diverting", + "Divine", + "Doable", + "Dominant", + "Doting", + "Doubtless", + "Doughty", + "Down-to-earth", + "Dreamy", + "Driven", + "Driving", + "Durable", + "Dutiful", + "Dynamic", + "Dynamite"], + /*e*/ ["Eager", + "Early", + "Earnest", + "Earthly", + "Earthy", + "Easy", + "Easygoing", + "Ebullient", + "Eclectic", + "Economic", + "Economical", + "Ecstatic", + "Ecumenical", + "Edified", + "Educated", + "Educational", + "Effective", + "Effectual", + "Effervescent", + "Efficient", + "Effortless", + "Elaborate", + "Elated", + "Elating", + "Elder", + "Electric", + "Electrifying", + "Eleemosynary", + "Elegant", + "Elemental", + "Eligible", + "Eloquent", + "Emerging", + "Eminent", + "Empathetic", + "Employable", + "Empowered", + "Enamored", + "Enchanting", + "Encouraged", + "Encouraging", + "Endearing", + "Enduring", + "Energetic", + "Energizing", + "Engaging", + "Enhanced", + "Enjoyable", + "Enlightened", + "Enlightening", + "Enlivened", + "Enlivening", + "Enormous", + "Enough", + "Enriching", + "Enterprising", + "Entertaining", + "Enthralling", + "Enthusiastic", + "Enticing", + "Entrancing", + "Entrepreneurial", + "Epicurean", + "Epideictic", + "Equable", + "Equal", + "Equiponderant", + "Equipped", + "Equitable", + "Equivalent", + "Erotic", + "Erudite", + "Especial", + "Essential", + "Established", + "Esteemed", + "Esthetic", + "Esthetical", + "Eternal", + "Ethical", + "Euphoric", + "Even-handed", + "Eventful", + "Evident", + "Evocative", + "Exact", + "Exalted", + "Exceeding", + "Excellent", + "Exceptional", + "Executive", + "Exhilarating", + "Exotic", + "Expansive", + "Expectant", + "Expeditious", + "Expeditive", + "Expensive", + "Experienced", + "Explorative", + "Expressive", + "Exquisite", + "Extraordinary", + "Exuberant", + "Exultant", + "Eye-catching"], + /*f*/ ["Fab", + "Fabulous", + "Facile", + "Factual", + "Facultative", + "Fain", + "Fair", + "Faithful", + "Famed", + "Familial", + "Familiar", + "Family", + "Famous", + "Fancy", + "Fantastic", + "Far-reaching", + "Far-sighted", + "Fascinating", + "Fashionable", + "Fast", + "Faultless", + "Favorable", + "Favored", + "Favorite", + "Fearless", + "Feasible", + "Fecund", + "Felicitous", + "Fertile", + "Fervent", + "Festal", + "Festive", + "Fetching", + "Fiery", + "Fine", + "Finer", + "Finest", + "Firm", + "First", + "First-class", + "First-rate", + "Fit", + "Fitting", + "Flamboyant", + "Flash", + "Flashy", + "Flavorful", + "Flawless", + "Fleet", + "Flexible", + "Flourishing", + "Fluent", + "Flying", + "Focused", + "Fond", + "For real", + "Forceful", + "Foremost", + "Foresighted", + "Forgiving", + "Formidable", + "Forthcoming", + "Forthright", + "Fortified", + "Fortuitous", + "Fortunate", + "Forward", + "Foundational", + "Four-star", + "Foxy", + "Fragrant", + "Frank", + "Fraternal", + "Free", + "Freely", + "Fresh", + "Friendly", + "Frisky", + "Frolicsome", + "Front-page", + "Fruitful", + "Fulfilled", + "Fulfilling", + "Full", + "Fun", + "Funny", + "Futuristic"], + /*g*/ ["Gainful", + "Gallant", + "Galore", + "Game", + "Gamesome", + "Generous", + "Genial", + "Genteel", + "Gentle", + "Genuine", + "Germane", + "Get-at-able", + "Gettable", + "Giddy", + "Gifted", + "Giving", + "Glad", + "Glamorous", + "Gleaming", + "Gleeful", + "Glorious", + "Glowing", + "Gnarly", + "Goal-oriented", + "Godly", + "Golden", + "Good", + "Good-humored", + "Good-looking", + "Good-natured", + "Goodhearted", + "Goodly", + "Gorgeous", + "Graced", + "Graceful", + "Gracile", + "Gracious", + "Gradely", + "Graithly", + "Grand", + "Grateful", + "Gratified", + "Gratifying", + "Great", + "Greatest", + "Greathearted", + "Gregarious", + "Groovy", + "Grounded", + "Growing", + "Grown", + "Guaranteed", + "Gubernatorial", + "Guided", + "Guiding", + "Guileless", + "Guilt-free", + "Guiltless", + "Gumptious", + "Gustatory", + "Gutsy", + "Gymnastic"], + /*h*/ ["Halcyon", + "Hale", + "Hallowed", + "Handsome", + "Handy", + "Happening", + "Happy", + "Happy-go-lucky", + "Hard-working", + "Hardy", + "Harmless", + "Harmonious", + "Head", + "Healing", + "Healthful", + "Healthy", + "Heart-to-heart", + "Heartfelt", + "Hearty", + "Heavenly", + "Heedful", + "Hegemonic", + "Helpful", + "Hep", + "Heralded", + "Heroic", + "Heteroclite", + "Heuristic", + "High", + "High-class", + "High-minded", + "High-power", + "High-powered", + "High-priority", + "High-reaching", + "High-spirited", + "Highest", + "Highly regarded", + "Highly valued", + "Hilarious", + "Hip", + "Holy", + "Homely", + "Honest", + "Honeyed", + "Honorary", + "Honorable", + "Honored", + "Hopeful", + "Hortative", + "Hospitable", + "Hot", + "Hotshot", + "Huggy", + "Humane", + "Humanitarian", + "Humble", + "Humorous", + "Hunky", + "Hygienic", + "Hypersonic", + "Hypnotic"], + /*i*/ ["Ideal", + "Idealistic", + "Idiosyncratic", + "Idolized", + "Illimitable", + "Illuminated", + "Illuminating", + "Illustrious", + "Imaginative", + "Imitable", + "Immaculate", + "Immeasurable", + "Immediate", + "Immense", + "Immortal", + "Immune", + "Impartial", + "Impassioned", + "Impeccable", + "Impeccant", + "Imperturbable", + "Impish", + "Important", + "Impressive", + "Improved", + "Improving", + "Improvisational", + "In", + "Incisive", + "Included", + "Inclusive", + "Incomparable", + "Incomplex", + "Incontestable", + "Incontrovertible", + "Incorrupt", + "Incredible", + "Inculpable", + "Indefatigable", + "Independent", + "Indestructible", + "Indispensable", + "Indisputable", + "Individual", + "Individualistic", + "Indivisible", + "Indomitable", + "Indubitable", + "Industrious", + "Inerrant", + "Inexhaustible", + "Infallible", + "Infant", + "Infinite", + "Influential", + "Informative", + "Informed", + "Ingenious", + "Inimitable", + "Initiate", + "Initiative", + "Innocent", + "Innovative", + "Innoxious", + "Inquisitive", + "Insightful", + "Inspired", + "Inspiring", + "Inspiriting", + "Instantaneous", + "Instinctive", + "Instructive", + "Instrumental", + "Integral", + "Integrated", + "Intellectual", + "Intelligent", + "Intense", + "Intent", + "Interactive", + "Interconnected", + "Interested", + "Interesting", + "Internal", + "Intertwined", + "Intimate", + "Intoxicating", + "Intrepid", + "Intriguing", + "Introducer", + "Inventive", + "Invigorated", + "Invigorating", + "Invincible", + "Inviolable", + "Inviting", + "Irrefragable", + "Irrefutable", + "Irreplaceable", + "Irrepressible", + "Irreproachable", + "Irresistible"], + /*j*/ ["Jaculable", + "Jam-packed", + "Jaunty", + "Jazzed", + "Jazzy", + "Jessant", + "Jestful", + "Jesting", + "Jewelled", + "Jiggish", + "Jigjog", + "Jimp", + "Jobbing", + "Jocose", + "Jocoserious", + "Jocular", + "Joculatory", + "Jocund", + "Joint", + "Jointed", + "Jolif", + "Jolly", + "Jovial", + "Joyful", + "Joyous", + "Joysome", + "Jubilant", + "Judicious", + "Juicy", + "Jump", + "Just", + "Justified"], + /*k*/ ["Keen", + "Kempt", + "Key", + "Kind", + "Kind-hearted", + "Kindly", + "Kindred", + "Kinetic", + "King-sized", + "Kingly", + "Kissable", + "Knightly", + "Knowable", + "Knowing", + "Knowledgeable", + "Kooky"], + /*l*/ ["Ladylike", + "Laid-back", + "Large", + "Lasting", + "Latitudinarian", + "Laudable", + "Laureate", + "Lavish", + "Law-abiding", + "Lawful", + "Leading", + "Leading-edge", + "Learned", + "Legal", + "Legendary", + "Legible", + "Legit", + "Legitimate", + "Leisured", + "Leisurely", + "Lenien", + "Leonine", + "Lepid", + "Lettered", + "Level-headed", + "Liberal", + "Liberated", + "Liberating", + "Light-hearted", + "Lightly", + "Likable", + "Like", + "Like-minded", + "Liked", + "Likely", + "Limber", + "Lionhearted", + "Literary", + "Literate", + "Lithe", + "Lithesome", + "Live", + "Lively", + "Logical", + "Long-established", + "Long-standing", + "Lordly", + "Lovable", + "Loved", + "Lovely", + "Loving", + "Loyal", + "Lucent", + "Lucid", + "Lucky", + "Lucrative", + "Luminous", + "Luscious", + "Lush", + "Lustrous", + "Lusty", + "Luxuriant", + "Luxurious"], + /*m*/ ["Made", + "Magical", + "Magnanimous", + "Magnetic", + "Magnificent", + "Maiden", + "Main", + "Majestic", + "Major", + "Malleable", + "Manageable", + "Managerial", + "Manifest", + "Manly", + "Mannerly", + "Many", + "Marked", + "Marvelous", + "Masculine", + "Master", + "Masterful", + "Masterly", + "Matchless", + "Maternal", + "Matter-of-fact", + "Mature", + "Maturing", + "Maximal", + "Meaningful", + "Mediate", + "Meditative", + "Meek", + "Mellow", + "Melodious", + "Memorable", + "Merciful", + "Meritable", + "Meritorious", + "Merry", + "Mesmerizing", + "Metaphysical", + "Meteoric", + "Methodical", + "Meticulous", + "Mettlesome", + "Mighty", + "Mindful", + "Minikin", + "Ministerial", + "Mint", + "Miraculous", + "Mirthful", + "Mitigative", + "Mitigatory", + "Model", + "Modern", + "Modernistic", + "Modest", + "Momentous", + "Moneyed", + "Moral", + "More", + "Most", + "Mother", + "Motivated", + "Motivating", + "Motivational", + "Motor", + "Moving", + "Much", + "Mucho", + "Multidimensional", + "Multidisciplined", + "Multifaceted", + "Munificent", + "Muscular", + "Musical", + "Must", + "Mutual"], + /*n*/ ["National", + "Nationwide", + "Native", + "Natty", + "Natural", + "Nearby", + "Neat", + "Necessary", + "Needed", + "Neighborly", + "Neoteric", + "Nestling", + "Never-failing", + "New", + "New-fashioned", + "Newborn", + "Nice", + "Nifty", + "Nimble", + "Nimble-witted", + "Nippy", + "Noble", + "Noetic", + "Nonchalant", + "Nonpareil", + "Normal", + "Notable", + "Noted", + "Noteworthy", + "Noticeable", + "Nourished", + "Nourishing", + "Novel", + "Now", + "Nubile", + "Number one", + "Nutrimental"], + /*o*/ ["Objective", + "Obliging", + "Observant", + "Obtainable", + "Oecumenical", + "Official", + "OK", + "Okay", + "Olympian", + "On", + "Once", + "One", + "Onward", + "Open", + "Open-handed", + "Open-hearted", + "Open-minded", + "Operative", + "Opportune", + "Optimal", + "Optimistic", + "Optimum", + "Opulent", + "Orderly", + "Organic", + "Organized", + "Oriented", + "Original", + "Ornamental", + "Out-of-sight", + "Out-of-this-world", + "Outgoing", + "Outstanding", + "Overflowing", + "Overjoyed", + "Overriding", + "Overt"], + /*p*/ ["Palatable", + "Pally", + "Palpable", + "Par excellence", + "Paradisiac", + "Paradisiacal", + "Paramount", + "Parental", + "Parnassian", + "Participant", + "Participative", + "Particular", + "Partisan", + "Passionate", + "Paternal", + "Patient", + "Peaceable", + "Peaceful", + "Peachy", + "Peerless", + "Penetrating", + "Peppy", + "Perceptive", + "Perfect", + "Perky", + "Permanent", + "Permissive", + "Perseverant", + "Persevering", + "Persistent", + "Personable", + "Perspective", + "Perspicacious", + "Perspicuous", + "Persuasive", + "Pert", + "Pertinent", + "Pet", + "Petite", + "Phenomenal", + "Philanthropic", + "Philoprogenitive", + "Philosophical", + "Picked", + "Picturesque", + "Pierian", + "Pilot", + "Pioneering", + "Pious", + "Piquant", + "Pithy", + "Pivotal", + "Placid", + "Plausible", + "Playful", + "Pleasant", + "Pleased", + "Pleasing", + "Pleasurable", + "Plenary", + "Plenteous", + "Plentiful", + "Plenty", + "Pliable", + "Plucky", + "Plummy", + "Plus", + "Plush", + "Poetic", + "Poignant", + "Poised", + "Polished", + "Polite", + "Popular", + "Posh", + "Positive", + "Possible", + "Potent", + "Potential", + "Powerful", + "Practicable", + "Practical", + "Practised", + "Pragmatic", + "Praiseworthy", + "Prayerful", + "Precious", + "Precise", + "Predominant", + "Preeminent", + "Preferable", + "Preferred", + "Premier", + "Premium", + "Prepared", + "Preponderant", + "Prepotent", + "Present", + "Prestigious", + "Pretty", + "Prevailing", + "Prevalent", + "Prevenient", + "Primal", + "Primary", + "Prime", + "Prime mover", + "Primed", + "Primo", + "Princely", + "Principal", + "Principled", + "Pristine", + "Privileged", + "Prize", + "Prizewinning", + "Prized", + "Pro", + "Proactive", + "Probable", + "Probative", + "Procurable", + "Prodigious", + "Productive", + "Professional", + "Proficient", + "Profitable", + "Profound", + "Profuse", + "Progressive", + "Prolific", + "Prominent", + "Promising", + "Prompt", + "Proper", + "Propertied", + "Prophetic", + "Propitious", + "Prospective", + "Prosperous", + "Protean", + "Protective", + "Proud", + "Provocative", + "Prudent", + "Psyched up", + "Public-spirited", + "Puissant", + "Pukka", + "Pulchritudinous", + "Pumped up", + "Punchy", + "Punctilious", + "Punctual", + "Pure", + "Purposeful"], + /*q*/ ["Quaint", + "Qualified", + "Qualitative", + "Quality", + "Quantifiable", + "Queenly", + "Quemeful", + "Quick", + "Quick-witted", + "Quiet", + "Quietsome", + "Quintessential", + "Quirky", + "Quiver", + "Quixotic", + "Quotable"], + /*r*/ ["Racy", + "Rad", + "Radiant", + "Rapid", + "Rapturous", + "Rational", + "Razor-sharp", + "Reachable", + "Ready", + "Real", + "Realistic", + "Realizable", + "Reasonable", + "Reassuring", + "Receptive", + "Recherche", + "Recipient", + "Reciprocal", + "Recognizable", + "Recognized", + "Recommendable", + "Recuperative", + "Red-carpet", + "Refined", + "Reflective", + "Refreshing", + "Refulgent", + "Regal", + "Regnant", + "Regular", + "Rejuvenescent", + "Relaxed", + "Relevant", + "Reliable", + "Relieved", + "Remarkable", + "Remissive", + "Renowned", + "Reputable", + "Resilient", + "Resolute", + "Resolved", + "Resounding", + "Resourceful", + "Respectable", + "Respectful", + "Resplendent", + "Responsible", + "Responsive", + "Restful", + "Restorative", + "Retentive", + "Revealing", + "Revered", + "Reverent", + "Revitalizing", + "Revolutionary", + "Rewardable", + "Rewarding", + "Rhapsodic", + "Rich", + "Right", + "Righteous", + "Rightful", + "Risible", + "Robust", + "Rollicking", + "Romantic", + "Rooted", + "Rosy", + "Round", + "Rounded", + "Rousing", + "Rugged", + "Ruling"], + /*s*/ ["Saccharine", + "Sacred", + "Sacrosanct", + "Safe", + "Sagacious", + "Sage", + "Saintly", + "Salient", + "Salubrious", + "Salutary", + "Salutiferous", + "Sanctified", + "Sanctimonious", + "Sanctioned", + "Sanguine", + "Sapid", + "Sapient", + "Sapoforic", + "Sassy", + "Satisfactory", + "Satisfied", + "Satisfying", + "Saucy", + "Saving", + "Savory", + "Savvy", + "Scenic", + "Scholarly", + "Scientific", + "Scintillating", + "Scrumptious", + "Scrupulous", + "Seamless", + "Seasonal", + "Seasoned", + "Second-to-none", + "Secure", + "Sedulous", + "Seemly", + "Select", + "Self-assertive", + "Self-assured", + "Self-confident", + "Self-disciplined", + "Self-made", + "Self-sacrificing", + "Self-starting", + "Self-sufficient", + "Selfless", + "Sensational", + "Sensible", + "Sensitive", + "Sensual", + "Sensuous", + "Sentimental", + "Sequacious", + "Serendipitous", + "Serene", + "Service", + "Set", + "Settled", + "Sexual", + "Sexy", + "Shapely", + "Sharp", + "Shatterproof", + "Sheen", + "Shining", + "Shiny", + "Shipshape", + "Showy", + "Shrewd", + "Sightly", + "Significant", + "Silken", + "Silky", + "Silver", + "Silver-toned", + "Silvery", + "Simple", + "Sincere", + "Sinewy", + "Singular", + "Sisterly", + "Sizable", + "Sizzling", + "Skillful", + "Skilled", + "Sleek", + "Slick", + "Slinky", + "Smacking", + "Smart", + "Smashing", + "Smiley", + "Smooth", + "Snap", + "Snappy", + "Snazzy", + "Snod", + "Snug", + "Soaring", + "Sociable", + "Social", + "Societal", + "Soft", + "Soft-hearted", + "Soigne", + "Solicitous", + "Solid", + "Sonsy", + "Sooth", + "Soothing", + "Sophisticated", + "Sought-after", + "Soulful", + "Sound", + "Souped-up", + "Sovereign", + "Spacious", + "Spangly", + "Spanking", + "Sparkling", + "Sparkly", + "Special", + "Spectacular", + "Specular", + "Speedy", + "Spellbinding", + "Spicy", + "Spiffy", + "Spirited", + "Spiritual", + "Splendid", + "Splendiferous", + "Spontaneous", + "Sport", + "Sporting", + "Sportive", + "Sporty", + "Spot", + "Spotless", + "Spot on", + "Sprightly", + "Spruce", + "Spry", + "Spunky", + "Square", + "Stable", + "Stacked", + "Stainless", + "Stalwart", + "Staminal", + "Standard", + "Standing", + "Stand-up", + "Star", + "Starry", + "State", + "Stately", + "Statuesque", + "Staunch", + "Steadfast", + "Steady", + "Steamy", + "Stellar", + "Sterling", + "Sthenic", + "Stick-to-itive", + "Stimulant", + "Stimulating", + "Stimulative", + "Stipendiary", + "Stirred", + "Stirring", + "Stocky", + "Stoical", + "Storied", + "Stout", + "Stouthearted", + "Straight-out", + "Straightforward", + "Strapping", + "Strategic", + "Street-smart", + "Streetwise", + "Strenuous", + "Striking", + "Strong", + "Studious", + "Stunning", + "Stupendous", + "Sturdy", + "Stylish", + "Suasive", + "Suave", + "Sublime", + "Substantial", + "Substant", + "Substantive", + "Subtle", + "Successful", + "Succinct", + "Succulent", + "Sufficient", + "Sugary", + "Suitable", + "Sultry", + "Summary", + "Summery", + "Sumptuous", + "Sun-kissed", + "Sunny", + "Super", + "Superabundant", + "Super-duper", + "Supereminent", + "Superethical", + "Superexcellent", + "Superb", + "Supercalifragilisticexpialidocious", + "Superfluous", + "Superior", + "Superlative", + "Supernal", + "Supersonic", + "Supple", + "Supportive", + "Supreme", + "Sure", + "Sure-fire", + "Sure-footed", + "Sure-handed", + "Surpassing", + "Sustained", + "Svelte", + "Swank", + "Swashbuckling", + "Sweet", + "Swell", + "Swift", + "Swish", + "Sybaritic", + "Sylvan", + "Symmetrical", + "Sympathetic", + "Symphonious", + "Synergistic", + "Systematic"], + /*t*/ ["Tactful", + "Tailor-made", + "Take-charge", + "Talented", + "Tangible", + "Tasteful", + "Tasty", + "Teachable", + "Teeming", + "Tempean", + "Temperate", + "Tenable", + "Tenacious", + "Tender", + "Tender-hearted", + "Terrific", + "Testimonial", + "Thankful", + "Thankworthy", + "Therapeutic", + "Thorough", + "Thoughtful", + "Thrilled", + "Thrilling", + "Thriving", + "Tidy", + "Tight", + "Time-honored", + "Time-saving", + "Timeless", + "Timely", + "Tiptop", + "Tireless", + "Titanic", + "Titillating", + "Today", + "Together", + "Tolerant", + "Top", + "Top drawer", + "Top-notch", + "Tops", + "Total", + "Totally-tubular", + "Touching", + "Tough", + "Trailblazing", + "Tranquil", + "Transcendent", + "Transcendental", + "Transient", + "Transnormal", + "Transparent", + "Transpicuous", + "Traveled", + "Tremendous", + "Tretis", + "Trim", + "Triumphant", + "True", + "True-blue", + "Trustful", + "Trusting", + "Trustworthy", + "Trusty", + "Truthful", + "Tubular", + "Tuneful", + "Turgent", + "Tympanic"], + /*u*/ ["Uber", + "Ultimate", + "Ultra", + "Ultraprecise", + "Unabashed", + "Unadulterated", + "Unaffected", + "Unafraid", + "Unalloyed", + "Unambiguous", + "Unanimous", + "Unarguable", + "Unassuming", + "Unattached", + "Unbeaten", + "Unbelieavable", + "Unbiased", + "Unbigoted", + "Unblemished", + "Unbroken", + "Uncommon", + "Uncomplicated", + "Unconditional", + "Uncontestable", + "Unconventional", + "Uncorrupted", + "Uncritical", + "Undamaged", + "Undauntable", + "Undaunted", + "Undefeated", + "Undefiled", + "Undeniable", + "Under control", + "Understandable", + "Understanding", + "Understood", + "Undesigning", + "Undiminished", + "Undisputed", + "Undivided", + "Undoubted", + "Unencumbered", + "Unequalled", + "Unequivocal", + "Unerring", + "Unfailing", + "Unfaltering", + "Unfaultable", + "Unfeigned", + "Unfettered", + "Unflagging", + "Unflappable", + "Ungrudging", + "Unhampered", + "Unharmed", + "Unhesitating", + "Unhurt", + "Unified", + "Unimpaired", + "Unimpeachable", + "Unimpeded", + "Unique", + "United", + "Universal", + "Unlimited", + "Unmistakable", + "Unmitigated", + "Unobjectionable", + "Unobstructed", + "Unobtrusive", + "Unopposed", + "UnUnprejudiced", + "Unpretentious", + "Unquestionable", + "Unrefuted", + "Unreserved", + "Unrivalled", + "Unruffled", + "Unselfish", + "Unshakable", + "Unshaken", + "Unspoiled", + "Unspoilt", + "Unstoppable", + "Unsullied", + "Unsurpassed", + "Untarnished", + "Untiring", + "Untouched", + "Untroubled", + "Unusual", + "Unwavering", + "Up", + "Up-front", + "Up-to-date", + "Upbeat", + "Upcoming", + "Uplifted", + "Uplifting", + "Uppermost", + "Upright", + "Upstanding", + "Upward", + "Upwardly", + "Urbane", + "Usable", + "Useful", + "User-friendly", + "Utmost"], + /*v*/ ["Valiant", + "Valid", + "Validatory", + "Valorous", + "Valuable", + "Valued", + "Vast", + "Vaulting", + "Vehement", + "Venerable", + "Venturesome", + "Venust", + "Veracious", + "Verdurous", + "Veridical", + "Verified", + "Versatile", + "Versed", + "Very", + "Vestal", + "Veteran", + "Viable", + "Vibrant", + "Vibratile", + "Victor", + "Victorious", + "Vigilant", + "Vigorous", + "Virile", + "Virtuous", + "Visionary", + "Vital", + "Vivacious", + "Vivid", + "Vocal", + "Volant", + "Volitional", + "Voluptuous", + "Vulnerary"], + /*w*/ ["Wanted", + "Warm", + "Warm-hearted", + "Warranted", + "Wealthy", + "Weighty", + "Welcome", + "Welcomed", + "Welcoming", + "Weleful", + "Welfaring", + "Well", + "Well-behaved", + "Well-built", + "Well-disposed", + "Well-established", + "Well-founded", + "Well-grounded", + "Well-informed", + "Well-intentioned", + "Well-known", + "Well-liked", + "Well-made", + "Well-meaning", + "Well-planned", + "Well-read", + "Well-received", + "Well-spoken", + "Well-suited", + "Well-timed", + "Welsome", + "Whimsical", + "Whiz-bang", + "Whole", + "Wholehearted", + "Wholesome", + "Whopping", + "Wide-awake", + "Widely used", + "Willed", + "Willing", + "Winged", + "Winning", + "Winsome", + "Wired", + "Wise", + "With it", + "Within reach", + "Without equal", + "Witty", + "Wizard", + "Wizardly", + "Won", + "Wonderful", + "Wondrous", + "Workable", + "World-class", + "Worldly", + "Worldly-wise", + "Worshipful", + "Worth", + "Worthwhile", + "Worthy"], + /*y*/ ["Yern", + "Young", + "Young-at-Heart", + "Youthful", + "Yummy"], + /*z*/ ["Zaftig", + "Zany", + "Zappy", + "Zazzy", + "Zealed", + "Zealful", + "Zealous", + "Zestful", + "Zesty", + "Zingy", + "Zippy", + "Zootrophic", + "Zooty"], +]; + +const ANIMALS = [ + [ + "Aardvark", + "Abyssinian", + "Affenpinscher", + "Akbash", + "Akita", + "Albatross", + "Alligator", + "Alpaca", + "Angelfish", + "Ant", + "Anteater", + "Antelope", + "Ape", + "Armadillo", + "Avocet", + "Axolotl", + ], + [ + "Baboon", + "Badger", + "Balinese", + "Bandicoot", + "Barb", + "Barnacle", + "Barracuda", + "Bat", + "Beagle", + "Bear", + "Beaver", + "Bee", + "Beetle", + "Binturong", + "Bird", + "Birman", + "Bison", + "Bloodhound", + "Boar", + "Bobcat", + "Bombay", + "Bongo", + "Bonobo", + "Booby", + "Budgerigar", + "Buffalo", + "Bulldog", + "Bullfrog", + "Burmese", + "Butterfly" + ], + [ + "Caiman", + "Camel", + "Capybara", + "Caracal", + "Caribou", + "Cassowary", + "Cat", + "Caterpillar", + "Catfish", + "Cattle", + "Centipede", + "Chameleon", + "Chamois", + "Cheetah", + "Chicken", + "Chihuahua", + "Chimpanzee", + "Chinchilla", + "Chinook", + "Chipmunk", + "Chough", + "Cichlid", + "Clam", + "Coati", + "Cobra", + "Cockroach", + "Cod", + "Collie", + "Coral", + "Cormorant", + "Cougar", + "Cow", + "Coyote", + "Crab", + "Crane", + "Crocodile", + "Crow", + "Curlew", + "Cuscus", + "Cuttlefish" + ], + [ + "Dachshund", + "Dalmatian", + "Deer", + "Dhole", + "Dingo", + "Dinosaur", + "Discus", + "Dodo", + "Dog", + "Dogfish", + "Dolphin", + "Donkey", + "Dormouse", + "Dotterel", + "Dove", + "Dragonfly", + "Drever", + "Duck", + "Dugong", + "Dunker", + "Dunlin" + ], + [ + "Eagle", + "Earwig", + "Echidna", + "Eel", + "Eland", + "Elephant", + "Elephant seal", + "Elk", + "Emu" + ], + [ + "Falcon", + "Ferret", + "Finch", + "Fish", + "Flamingo", + "Flounder", + "Fly", + "Fossa", + "Fox", + "Frigatebird", + "Frog" + ], + [ + "Galago", + "Gar", + "Gaur", + "Gazelle", + "Gecko", + "Gerbil", + "Gharial", + "Giant Panda", + "Gibbon", + "Giraffe", + "Gnat", + "Gnu", + "Goat", + "Goldfinch", + "Goldfish", + "Goose", + "Gopher", + "Gorilla", + "Goshawk", + "Grasshopper", + "Greyhound", + "Grouse", + "Guanaco", + "Guinea fowl", + "Guinea pig", + "Gull", + "Guppy" + ], + [ + "Hamster", + "Hare", + "Harrier", + "Havanese", + "Hawk", + "Hedgehog", + "Heron", + "Herring", + "Himalayan", + "Hippopotamus", + "Hornet", + "Horse", + "Human", + "Hummingbird", + "Hyena" + ], + [ + "Ibis", + "Iguana", + "Impala", + "Indri", + "Insect" + ], + [ + "Jackal", + "Jaguar", + "Javanese", + "Jay", + "Jay, Blue", + "Jellyfish" + ],[ + "Kakapo", + "Kangaroo", + "Kingfisher", + "Kiwi", + "Koala", + "Komodo dragon", + "Kouprey", + "Kudu" + ], + [ + "Labradoodle", + "Ladybird", + "Lapwing", + "Lark", + "Lemming", + "Lemur", + "Leopard", + "Liger", + "Lion", + "Lionfish", + "Lizard", + "Llama", + "Lobster", + "Locust", + "Loris", + "Louse", + "Lynx", + "Lyrebird" + ], + [ + "Macaw", + "Magpie", + "Mallard", + "Maltese", + "Manatee", + "Mandrill", + "Markhor", + "Marten", + "Mastiff", + "Mayfly", + "Meerkat", + "Millipede", + "Mink", + "Mole", + "Molly", + "Mongoose", + "Mongrel", + "Monkey", + "Moorhen", + "Moose", + "Mosquito", + "Moth", + "Mouse", + "Mule" + ], + [ + "Narwhal", + "Neanderthal", + "Newfoundland", + "Newt", + "Nightingale", + "Numbat" + ], + [ + "Ocelot", + "Octopus", + "Okapi", + "Olm", + "Opossum", + "Orang-utan", + "Oryx", + "Ostrich", + "Otter", + "Owl", + "Ox", + "Oyster" + ], + [ + "Pademelon", + "Panther", + "Parrot", + "Partridge", + "Peacock", + "Peafowl", + "Pekingese", + "Pelican", + "Penguin", + "Persian", + "Pheasant", + "Pig", + "Pigeon", + "Pika", + "Pike", + "Piranha", + "Platypus", + "Pointer", + "Pony", + "Poodle", + "Porcupine", + "Porpoise", + "Possum", + "Prairie Dog", + "Prawn", + "Puffin", + "Pug", + "Puma" + ], + [ + "Quail", + "Quelea", + "Quetzal", + "Quokka", + "Quoll" + ], + [ + "Rabbit", + "Raccoon", + "Ragdoll", + "Rail", + "Ram", + "Rat", + "Rattlesnake", + "Raven", + "Red deer", + "Red panda", + "Reindeer", + "Rhinoceros", + "Robin", + "Rook", + "Rottweiler", + "Ruff" + ], + [ + "Salamander", + "Salmon", + "Sand Dollar", + "Sandpiper", + "Saola", + "Sardine", + "Scorpion", + "Sea lion", + "Sea Urchin", + "Seahorse", + "Seal", + "Serval", + "Shark", + "Sheep", + "Shrew", + "Shrimp", + "Siamese", + "Siberian", + "Skunk", + "Sloth", + "Snail", + "Snake", + "Snowshoe", + "Somali", + "Sparrow", + "Spider", + "Sponge", + "Squid", + "Squirrel", + "Starfish", + "Starling", + "Stingray", + "Stinkbug", + "Stoat", + "Stork", + "Swallow", + "Swan" + ], + [ + "Tang", + "Tapir", + "Tarsier", + "Termite", + "Tetra", + "Tiffany", + "Tiger", + "Toad", + "Tortoise", + "Toucan", + "Tropicbird", + "Trout", + "Tuatara", + "Turkey", + "Turtle" + ], + [ + "Uakari", + "Uguisu", + "Umbrellabird" + ], + [ + "Vicuña", + "Viper", + "Vulture" + ], + [ + "Wallaby", + "Walrus", + "Warthog", + "Wasp", + "Water buffalo", + "Weasel", + "Whale", + "Whippet", + "Wildebeest", + "Wolf", + "Wolverine", + "Wombat", + "Woodcock", + "Woodlouse", + "Woodpecker", + "Worm", + "Wrasse", + "Wren" + ], + [ + "Yak" + ], + [ + "Zebra", + "Zebu", + "Zonkey", + "Zorse" + ] +]; + +module.exports.generate = function() { + const index = Math.round((Math.random()*(ADJECTIVES.length-1))); + let name; + do { + name = ( + '🚀 '+ADJECTIVES[index][Math.round(Math.random()*(ADJECTIVES[index].length-1))]+ + ' '+ + ANIMALS[index][Math.round(Math.random()*(ANIMALS[index].length-1))]+' 🚀' + ).replace(/\s+/g, ' '); + } while( name.length > MAXLENGTH ); + + return name +} + diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index 5d758a4e3..afc0ef337 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -21,7 +21,7 @@ const templates = { }; module.exports = { - name: function() { + name: function() { return name; }, prompts: function( utils ) { @@ -42,7 +42,7 @@ module.exports = { name: 'LND', value: 'lnd' } - + ] }, */ @@ -61,7 +61,12 @@ module.exports = { name: 'lightning_nodename', default: utils._getDefault( 'lightning_nodename' ), filter: utils._trimFilter, - validate: utils._notEmptyValidator, + validate: (input)=>{ + if( !input.trim() ) { + return true; + } + return utils._lightningNodeNameValidator(input); + }, message: prefix()+'What name has your lightning node?'+utils._getHelp('lightning_nodename'), }, { @@ -70,7 +75,12 @@ module.exports = { name: 'lightning_nodecolor', default: utils._getDefault( 'lightning_nodecolor' ), filter: utils._trimFilter, - validate: utils._colorValidator, + validate: (input)=>{ + if( !input.trim() ) { + return true; + } + return utils._colorValidator(input); + }, message: prefix()+'What color has your lightning node?'+utils._getHelp('lightning_nodecolor'), }]; }, diff --git a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config index 79aa05e18..2ced26c0a 100644 --- a/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config +++ b/install/generator-cyphernode/generators/app/templates/lightning/c-lightning/config @@ -4,8 +4,12 @@ network=testnet <% } else if (net === 'mainnet') { %> network=bitcoin <% } %> +<% if( lightning_nodename ) { %> alias=<%= lightning_nodename %> +<% } %> +<% if( lightning_nodecolor ) { %> rgb=<%= lightning_nodecolor %> +<% } %> bitcoin-rpcconnect=<%= (bitcoin_mode === 'internal')?'bitcoin':bitcoin_node_ip %> bitcoin-rpcuser=<%= bitcoin_rpcuser %> bitcoin-rpcpassword=<%= bitcoin_rpcpassword %> From 08d437af78bbfbc3a5da2c34e8fd7508508119bd Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 20 Dec 2018 14:57:56 +0100 Subject: [PATCH 242/268] added extended sudo check --- dist/setup.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 8f430f8be..05776a565 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -357,11 +357,11 @@ install_docker() { if [ -d $GATEKEEPER_DATAPATH ]; then if [[ ! -d $GATEKEEPER_DATAPATH/certs ]]; then - sudo_if_required mkdir $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 + sudo_if_required mkdir -p $GATEKEEPER_DATAPATH/certs > /dev/null 2>&1 fi if [[ ! -d $GATEKEEPER_DATAPATH/private ]]; then - sudo_if_required mkdir $GATEKEEPER_DATAPATH/private > /dev/null 2>&1 + 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 @@ -520,7 +520,17 @@ check_directory_owner() { status=1 break; fi - # TODO: does parent exist and do we have rw on that? + else + # does parent exist and do we have rw on that? + local parentDir=$(dirname $d) + while [[ ! $parentDir == '/' && ! -e $parentDir ]]; do + if [[ ! -r $parentDir || ! -w $parentDir ]]; then + status=1 + break; + fi + parentDir=$(dirname $parentDir) + echo $parentDir + done fi done echo $status From 8eafcc96643310970e23b80b8b6794decbbf6b8d Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 20 Dec 2018 15:06:02 +0100 Subject: [PATCH 243/268] added predefined data path selection with optional custom path --- .../generators/app/index.js | 14 ++ .../generators/app/prompters/999_installer.js | 157 +++++++++++++++--- 2 files changed, 150 insertions(+), 21 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 8bd80bb49..38545f475 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -314,6 +314,20 @@ module.exports = class extends Generator { console.log(chalk.bold.red( 'error! Config archive was not written' )); } + const pathProps = [ + 'gatekeeper_datapath', + 'proxy_datapath', + 'bitcoin_datapath', + 'lightning_datapath', + 'otsclient_datapath' + ]; + + for( let pathProp of pathProps ) { + if( this.props[pathProp] === '_custom' ) { + this.props[pathProp] = this.props[pathProp+'_custom'] || ''; + } + } + for( let m of prompters ) { const name = m.name(); for( let t of m.templates(this.props) ) { diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index d2231ba68..9823a6b8f 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -15,10 +15,6 @@ const installerDocker = function(props) { return props.installer_mode === 'docker' }; -const installerLunanode = function(props) { - return props.installer_mode === 'lunanode' -}; - module.exports = { name: function() { return name; @@ -32,58 +28,177 @@ module.exports = { choices: [{ name: "Docker", value: "docker" - } - /*, - { - name: "Lunanode (not implemented)", - value: "lunanode" - }*/ - ] + }] }, { when: installerDocker, - type: 'input', + type: 'list', name: 'gatekeeper_datapath', default: utils._getDefault( 'gatekeeper_datapath' ), + choices: [ + { + name: "/var/run/cyphernode/gatekeeper (needs sudo)", + value: "/var/run/cyphernode/gatekeeper" + }, + { + name: "~/.cyphernode/gatekeeper", + value: "~/.cyphernode/gatekeeper" + }, + { + name: "~/gatekeeper", + value: "~/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', + name: 'gatekeeper_datapath_custom', + default: utils._getDefault( 'gatekeeper_datapath_custom' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where to store your gatekeeper data?'+utils._getHelp('gatekeeper_datapath'), + message: prefix()+'Custom path for gatekeeper data?'+utils._getHelp('gatekeeper_datapath_custom'), }, { when: installerDocker, - type: 'input', + type: 'list', name: 'proxy_datapath', default: utils._getDefault( 'proxy_datapath' ), + choices: [ + { + name: "/var/run/cyphernode/proxy (needs sudo)", + value: "/var/run/cyphernode/proxy" + }, + { + name: "~/.cyphernode/proxy", + value: "~/.cyphernode/proxy" + }, + { + name: "~/proxy", + value: "~/proxy" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your proxy data?'+utils._getHelp('proxy_datapath'), + }, + { + when: (props)=>{ return installerDocker(props) && (props.proxy_datapath === '_custom') }, + type: 'input', + name: 'proxy_datapath_custom', + default: utils._getDefault( 'proxy_datapath_custom' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where to store your proxy db?'+utils._getHelp('proxy_datapath'), + message: prefix()+'Custom path for your proxy data?'+utils._getHelp('proxy_datapath_custom'), }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, - type: 'input', + type: 'list', name: 'bitcoin_datapath', default: utils._getDefault( 'bitcoin_datapath' ), + choices: [ + { + name: "/var/run/cyphernode/bitcoin (needs sudo)", + value: "/var/run/cyphernode/bitcoin" + }, + { + name: "~/.cyphernode/bitcoin", + value: "~/.cyphernode/bitcoin" + }, + { + name: "~/bitcoin", + value: "~/bitcoin" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your bitcoin full node data?'+utils._getHelp('bitcoin_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' && props.bitcoin_datapath === '_custom' }, + type: 'input', + name: 'bitcoin_datapath_custom', + default: utils._getDefault( 'bitcoin_datapath_custom' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your blockchain data?'+utils._getHelp('bitcoin_datapath'), + message: prefix()+'Custom path for your bitcoin full node data?'+utils._getHelp('bitcoin_datapath_custom'), }, { when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, - type: 'input', + type: 'list', name: 'lightning_datapath', default: utils._getDefault( 'lightning_datapath' ), + choices: [ + { + name: "/var/run/cyphernode/lightning (needs sudo)", + value: "/var/run/cyphernode/lightning" + }, + { + name: "~/.cyphernode/lightning", + value: "~/.cyphernode/lightning" + }, + { + name: "~/lightning", + value: "~/lightning" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your lightning node data?'+utils._getHelp('lightning_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 && props.lightning_datapath === '_custom'}, + type: 'input', + name: 'lightning_datapath_custom', + default: utils._getDefault( 'lightning_datapath_custom' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your lightning node data?'+utils._getHelp('lightning_datapath'), + message: prefix()+'Custom path for your lightning node data?'+utils._getHelp('lightning_datapath_custom'), }, { when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 }, - type: 'input', + type: 'list', name: 'otsclient_datapath', default: utils._getDefault( 'otsclient_datapath' ), + choices: [ + { + name: "/var/run/cyphernode/otsclient (needs sudo)", + value: "/var/run/cyphernode/otsclient" + }, + { + name: "~/.cyphernode/otsclient", + value: "~/.cyphernode/otsclient" + }, + { + name: "~/otsclient", + value: "~/otsclient" + }, + { + name: "Custom path", + value: "_custom" + } + ], + message: prefix()+'Where do you want to store your lightning node data?'+utils._getHelp('otsclient_datapath'), + }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 && props.otsclient_datapath === '_custom' }, + type: 'input', + name: 'otsclient_datapath_custom', + default: utils._getDefault( 'otsclient_datapath_custom' ), filter: utils._trimFilter, validate: utils._pathValidator, - message: prefix()+'Where is your otsclient data?'+utils._getHelp('otsclient_datapath'), + message: prefix()+'Where is your otsclient data?'+utils._getHelp('otsclient_datapath_custom'), }, { when: function(props) { return installerDocker(props) && props.bitcoin_mode === 'internal' }, From 7dc232ed84afa55f4e2888c904cbcfe11a470e55 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 20 Dec 2018 18:00:04 +0100 Subject: [PATCH 244/268] added installation.json touch command to prevent mounting it as a directory --- .../generators/app/templates/installer/start.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 602157742..fbe205b29 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -15,6 +15,11 @@ export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) export ARCH=$(uname -m) current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" +if [[ ! -e <%= gatekeeper_datapath %>/installation.json ]]; then + # prevent mounting installation.json as a directory + touch <%= gatekeeper_datapath %>/installation.json +fi + <% if (docker_mode == 'swarm') { %> docker stack deploy -c $current_path/docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> From 81cf6f4e4afc123062cd6f810b61b00b3e5f4e36 Mon Sep 17 00:00:00 2001 From: jash Date: Thu, 20 Dec 2018 18:20:10 +0100 Subject: [PATCH 245/268] fixed sudo check for creating data directories where parent folder is missing --- dist/setup.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 05776a565..4b1e968f9 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -523,14 +523,14 @@ check_directory_owner() { else # does parent exist and do we have rw on that? local parentDir=$(dirname $d) + echo $parentDir while [[ ! $parentDir == '/' && ! -e $parentDir ]]; do - if [[ ! -r $parentDir || ! -w $parentDir ]]; then - status=1 - break; - fi parentDir=$(dirname $parentDir) - echo $parentDir done + echo $parentDir + if [[ ! -r $parentDir || ! -w $parentDir ]]; then + status=1 + fi fi done echo $status From f9e86fd11603cd2a4ba58912abe8ba7ecd55c9e0 Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Thu, 20 Dec 2018 18:23:30 +0100 Subject: [PATCH 246/268] Update setup.sh removed uneeded echos --- dist/setup.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 4b1e968f9..86102619b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -523,11 +523,9 @@ check_directory_owner() { else # does parent exist and do we have rw on that? local parentDir=$(dirname $d) - echo $parentDir while [[ ! $parentDir == '/' && ! -e $parentDir ]]; do parentDir=$(dirname $parentDir) done - echo $parentDir if [[ ! -r $parentDir || ! -w $parentDir ]]; then status=1 fi From 9ef48fea2c7bbe30ff55a9f1b818dfca310b5ebd Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 21 Dec 2018 12:21:28 -0500 Subject: [PATCH 247/268] Various versioning fixes --- api_auth_docker/README.md | 6 ++--- api_auth_docker/auth.sh | 4 +-- api_auth_docker/default-ssl.conf | 4 +-- api_auth_docker/default.conf | 4 +-- build.sh | 25 +++++++++++++------ cron_docker/README.md | 6 ++--- dist/setup.sh | 19 +++++++++++++- .../generators/app/index.js | 7 ++++++ .../installer/docker/docker-compose.yaml | 14 +++++------ .../app/templates/installer/start.sh | 2 -- .../app/templates/installer/testfeatures.sh | 12 ++++----- otsclient_docker/README.md | 6 ++--- proxy_docker/README.md | 6 ++--- pycoin_docker/README.md | 6 ++--- 14 files changed, 76 insertions(+), 45 deletions(-) diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index 9de7336d3..399f83f42 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -5,13 +5,13 @@ So all the other containers are in the Docker Swarm and we want to expose a real ## Pull our Cyphernode image ```shell -docker pull cyphernode/gatekeeper:cyphernode-0.05 +docker pull cyphernode/gatekeeper:latest ``` ## Build yourself the image ```shell -docker build -t cyphernode/gatekeeper:cyphernode-0.05 . +docker build -t cyphernode/gatekeeper:latest . ``` ## Run image @@ -19,7 +19,7 @@ docker build -t cyphernode/gatekeeper:cyphernode-0.05 . If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: ```shell -docker run -d --rm --name gatekeeper -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode-ssl/certs:/etc/ssl/certs" -v "~/cyphernode-ssl/private:/etc/ssl/private" --env-file env.properties cyphernode/gatekeeper:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` +docker run -d --rm --name gatekeeper -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode-ssl/certs:/etc/ssl/certs" -v "~/cyphernode-ssl/private:/etc/ssl/private" --env-file env.properties cyphernode/gatekeeper:latest `id -u cyphernode`:`id -g cyphernode` ``` ## Prepare diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh index 6900f71ea..8604f3abe 100644 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -87,8 +87,8 @@ verify_group() trace "[verify_group] Verifying group..." local id=${1} - # REQUEST_URI should look like this: /watch/2blablabla - local action=$(echo "${REQUEST_URI#\/}" | cut -d '/' -f1) + # REQUEST_URI should look like this: /v0/watch/2blablabla + local action=$(echo "${REQUEST_URI#\/}" | cut -d '/' -f2) trace "[verify_group] action=${action}" # Check for code injection diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index 65f40bc48..69c7dc12c 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -14,9 +14,9 @@ server { index statuspage.html; } - location / { + location /v0/ { auth_request /auth; - proxy_pass http://proxy:8888; + proxy_pass http://proxy:8888/; } location /auth { diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index b3da9a144..d9da37ad4 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -4,9 +4,9 @@ server { #include /etc/nginx/conf.d/ip-whitelist.conf; - location / { + location /v0/ { auth_request /auth; - proxy_pass http://proxy:8888; + proxy_pass http://proxy:8888/; } location /auth { diff --git a/build.sh b/build.sh index e7776e888..a03dd622d 100755 --- a/build.sh +++ b/build.sh @@ -48,19 +48,28 @@ build_docker_images() { fi trace "Creating cyphernodeconf image" - build_docker_image install/ cyphernode/cyphernodeconf:cyphernode-0.05 + build_docker_image install/ cyphernode/cyphernodeconf:$CN_VERSION trace "Creating SatoshiPortal images" - build_docker_image install/SatoshiPortal/dockers/bitcoin-core cyphernode/bitcoin:cyphernode-0.05 $bitcoin_dockerfile - build_docker_image install/SatoshiPortal/dockers/c-lightning cyphernode/clightning:cyphernode-0.05 $clightning_dockerfile + 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 trace "Creating cyphernode images" - build_docker_image api_auth_docker/ cyphernode/gatekeeper:cyphernode-0.05 - build_docker_image proxy_docker/ cyphernode/proxy:cyphernode-0.05 $proxy_dockerfile - build_docker_image cron_docker/ cyphernode/proxycron:cyphernode-0.05 - build_docker_image pycoin_docker/ cyphernode/pycoin:cyphernode-0.05 - build_docker_image otsclient_docker/ cyphernode/otsclient:cyphernode-0.05 + 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 } +# 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/cron_docker/README.md b/cron_docker/README.md index a510d87d3..4de2c6444 100644 --- a/cron_docker/README.md +++ b/cron_docker/README.md @@ -3,13 +3,13 @@ ## Pull our Cyphernode image ```shell -docker pull cyphernode/proxycron:cyphernode-0.05 +docker pull cyphernode/proxycron:latest ``` ## Build yourself the image ```shell -docker build -t cyphernode/proxycron:cyphernode-0.05 . +docker build -t cyphernode/proxycron:latest . ``` ## Run image @@ -17,7 +17,7 @@ docker build -t cyphernode/proxycron:cyphernode-0.05 . If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: ```shell -docker run --rm -d --network cyphernodenet --env-file env.properties cyphernode/proxycron:cyphernode-0.05 +docker run --rm -d --network cyphernodenet --env-file env.properties cyphernode/proxycron:latest ``` ## Configure your container by modifying `env.properties` file diff --git a/dist/setup.sh b/dist/setup.sh index 86102619b..e41a04a00 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -184,9 +184,16 @@ configure() { docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ -e DEFAULT_CERT_HOSTNAME=$(hostname) \ + -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ + -e PROXY_VERSION=$PROXY_VERSION \ + -e PROXYCRON_VERSION=$PROXYCRON_VERSION \ + -e OTSCLIENT_VERSION=$OTSCLIENT_VERSION \ + -e PYCOIN_VERSION=$PYCOIN_VERSION \ + -e BITCOIN_VERSION=$BITCOIN_VERSION \ + -e LIGHTNING_VERSION=$LIGHTNING_VERSION \ --log-driver=none$pw_env \ --network none \ - --rm$interactive cyphernode/cyphernodeconf:cyphernode-0.05 $user yo --no-insight cyphernode$gen_options $recreate + --rm$interactive cyphernode/cyphernodeconf:$CONF_VERSION $user yo --no-insight cyphernode$gen_options $recreate if [[ -f $current_path/exitStatus.sh ]]; then . $current_path/exitStatus.sh rm $current_path/exitStatus.sh @@ -625,6 +632,16 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 +# CYPHERNODE VERSION "v0.1.0rc1" +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" + # 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 38545f475..ad39d7ea1 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -404,6 +404,13 @@ module.exports = class extends Generator { installer_cleanup: false }, this.props ); this.props.default_username = process.env.DEFAULT_USER || ''; + this.props.gatekeeper_version = process.env.GATEKEEPER_VERSION || 'latest'; + this.props.proxy_version = process.env.PROXY_VERSION || 'latest'; + this.props.proxycron_version = process.env.PROXYCRON_VERSION || 'latest'; + this.props.pycoin_version = process.env.PYCOIN_VERSION || 'latest'; + this.props.otsclient_version = process.env.OTSCLIENT_VERSION || 'latest'; + this.props.bitcoin_version = process.env.BITCOIN_VERSION || 'latest'; + this.props.lightning_version = process.env.LIGHTNING_VERSION || 'latest'; } _isChecked( name, value ) { 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 96476d50d..a050c4304 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 @@ -5,7 +5,7 @@ services: # HTTP authentication API gate environment: - "TRACING=1" - image: cyphernode/gatekeeper:cyphernode-0.05 + image: cyphernode/gatekeeper:<%= gatekeeper_version %> ports: - "443:443" volumes: @@ -47,7 +47,7 @@ services: - "WATCHER_BTC_NODE_PRUNED=<%= bitcoin_prune?'true':'false' %>" - "OTSCLIENT_CONTAINER=otsclient:6666" - "OTS_FILES=/proxy/otsfiles" - image: cyphernode/proxy:cyphernode-0.05 + image: cyphernode/proxy:<%= proxy_version %> <% if ( devmode ) { %> ports: - "8888:8888" @@ -70,7 +70,7 @@ services: proxycron: environment: - "PROXY_URL=proxy:8888/executecallbacks" - image: cyphernode/proxycron:cyphernode-0.05 + image: cyphernode/proxycron:<%= proxycron_version %> # deploy: # placement: # constraints: [node.hostname==dev] @@ -80,7 +80,7 @@ services: pycoin: # Pycoin command: $USER ./startpycoin.sh - image: cyphernode/pycoin:cyphernode-0.05 + image: cyphernode/pycoin:<%= pycoin_version %> environment: - "TRACING=1" - "PYCOIN_LISTENING_PORT=7777" @@ -97,7 +97,7 @@ services: <% if ( features.indexOf('lightning') !== -1 && lightning_implementation === 'c-lightning' ) { %> lightning: command: $USER lightningd - image: cyphernode/clightning:v0.6.2 + image: cyphernode/clightning:<%= lightning_version %> <% if( lightning_expose ) { %> ports: @@ -118,7 +118,7 @@ services: environment: - "TRACING=1" - "OTSCLIENT_LISTENING_PORT=6666" - image: cyphernode/otsclient:cyphernode-0.05 + image: cyphernode/otsclient:<%= otsclient_version %> # deploy: # placement: # constraints: [node.hostname==dev] @@ -133,7 +133,7 @@ services: <% if( bitcoin_mode === 'internal' ) { %> bitcoin: command: $USER bitcoind - image: cyphernode/bitcoin:v0.17.0 + image: cyphernode/bitcoin:<%= bitcoin_version %> <% if( bitcoin_expose ) { %> ports: - "<%= (net === 'mainnet')?'8332:8332':'18332:18332' %>" diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index fbe205b29..81a132a09 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -28,8 +28,6 @@ docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ --v $current_path/gatekeeper/keys.properties:/keys.properties \ --v $current_path/gatekeeper/cert.pem:/cert.pem \ -v <%= gatekeeper_datapath %>:/gatekeeper \ --network cyphernodenet alpine:3.8 /testfeatures.sh diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index 43173bb33..b1fb1c89b 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -2,7 +2,7 @@ apk add --update --no-cache openssl curl jq > /dev/null -. keys.properties +. /gatekeeper/keys.properties checkgatekeeper() { echo -e "\r\n\e[1;36mTesting Gatekeeper...\e[0;32m" > /dev/console @@ -24,7 +24,7 @@ checkgatekeeper() { sleep 2 echo " Testing expired request... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getblockinfo) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getblockinfo) [ "${rc}" -ne "403" ] && return 10 # Let's test authentication (signature) @@ -34,7 +34,7 @@ checkgatekeeper() { token="$h64.$p64.a$s" echo " Testing bad signature... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getblockinfo) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getblockinfo) [ "${rc}" -ne "403" ] && return 30 # Let's test authorization (action access for groups) @@ -42,7 +42,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing watcher trying to do a spender action... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/getbalance) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/getbalance) [ "${rc}" -ne "403" ] && return 40 id="002" @@ -52,7 +52,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing spender trying to do an internal action call... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/conf) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/conf) [ "${rc}" -ne "403" ] && return 50 @@ -63,7 +63,7 @@ checkgatekeeper() { token="$h64.$p64.$s" echo " Testing admin trying to do an internal action call... " > /dev/console - rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /cert.pem https://gatekeeper/conf) + rc=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" --cacert /gatekeeper/certs/cert.pem https://gatekeeper/v0/conf) [ "${rc}" -ne "403" ] && return 60 echo -e "\e[1;36mGatekeeper rocks!" > /dev/console diff --git a/otsclient_docker/README.md b/otsclient_docker/README.md index 964a2ed57..7b501ad79 100644 --- a/otsclient_docker/README.md +++ b/otsclient_docker/README.md @@ -3,13 +3,13 @@ ## Pull our Cyphernode image ```shell -docker pull cyphernode/ots:cyphernode-0.05 +docker pull cyphernode/otsclient:latest ``` ## Build yourself the image ```shell -docker build -t cyphernode/ots:cyphernode-0.05 . +docker build -t cyphernode/otsclient:latest . ``` ## OTS files directory... @@ -25,7 +25,7 @@ sudo find ~/otsfiles -type d -exec chmod 2775 {} \; ; sudo find ~/otsfiles -type If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: ```shell -docker run --rm -d -p 6666:6666 --network cyphernodenet --env-file env.properties cyphernode/ots:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startotsclient.sh +docker run --rm -d -p 6666:6666 --network cyphernodenet --env-file env.properties cyphernode/otsclient:latest `id -u cyphernode`:`id -g cyphernode` ./startotsclient.sh ``` ## Usefull examples diff --git a/proxy_docker/README.md b/proxy_docker/README.md index bf6fac3f4..f83388256 100644 --- a/proxy_docker/README.md +++ b/proxy_docker/README.md @@ -3,13 +3,13 @@ ## Pull our Cyphernode image ```shell -docker pull cyphernode/proxy:cyphernode-0.05 +docker pull cyphernode/proxy:latest ``` ## Build yourself the image ```shell -docker build -t cyphernode/proxy:cyphernode-0.05 . +docker build -t cyphernode/proxy:latest . ``` ## Run image @@ -17,7 +17,7 @@ docker build -t cyphernode/proxy:cyphernode-0.05 . If you want to run this container independently from Cyphernode: ```shell -docker run --rm -d -p 8888:8888 --network cyphernodenet --env-file env.properties cyphernode/proxy:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startproxy.sh +docker run --rm -d -p 8888:8888 --network cyphernodenet --env-file env.properties cyphernode/proxy:latest `id -u cyphernode`:`id -g cyphernode` ./startproxy.sh ``` ## Configure your container by modifying `env.properties` file diff --git a/pycoin_docker/README.md b/pycoin_docker/README.md index 971aec37b..b45112b70 100644 --- a/pycoin_docker/README.md +++ b/pycoin_docker/README.md @@ -3,13 +3,13 @@ ## Pull our Cyphernode image ```shell -docker pull cyphernode/pycoin:cyphernode-0.05 +docker pull cyphernode/pycoin:latest ``` ## Build yourself the image ```shell -docker build -t cyphernode/pycoin:cyphernode-0.05 . +docker build -t cyphernode/pycoin:latest . ``` ## Run image @@ -17,7 +17,7 @@ docker build -t cyphernode/pycoin:cyphernode-0.05 . If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: ```shell -docker run --rm -d -p 7777:7777 --network cyphernodenet --env-file env.properties cyphernode/pycoin:cyphernode-0.05 `id -u cyphernode`:`id -g cyphernode` ./startpycoin.sh +docker run --rm -d -p 7777:7777 --network cyphernodenet --env-file env.properties cyphernode/pycoin:latest `id -u cyphernode`:`id -g cyphernode` ./startpycoin.sh ``` ## Usefull examples From 7d8196469e4a9789095e52a2f55d0cbc7da2029f Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 21 Dec 2018 12:43:31 -0500 Subject: [PATCH 248/268] Version -rc1 --- dist/setup.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index e41a04a00..fc7efe5fa 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -633,12 +633,12 @@ SUDO_REQUIRED=0 AUTOSTART=0 # CYPHERNODE VERSION "v0.1.0rc1" -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-rc1" +GATEKEEPER_VERSION="v0.1-rc1" +PROXY_VERSION="v0.1-rc1" +PROXYCRON_VERSION="v0.1-rc1" +OTSCLIENT_VERSION="v0.1-rc1" +PYCOIN_VERSION="v0.1-rc1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" From 64c682ab7c49b23b699377cc5442fd196b5e1e46 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 21 Dec 2018 14:36:48 -0500 Subject: [PATCH 249/268] Fixed nginx file rights and moved touch installation.json in setup --- api_auth_docker/entrypoint.sh | 3 ++- dist/setup.sh | 2 ++ .../generators/app/templates/installer/start.sh | 5 ----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh index 158b281cd..b46418548 100644 --- a/api_auth_docker/entrypoint.sh +++ b/api_auth_docker/entrypoint.sh @@ -11,6 +11,7 @@ if [[ $1 ]]; then fi +chmod -R g+rw /var/run/fcgiwrap.socket /etc/nginx/conf.d/* +chown -R :nginx /etc/nginx/conf.d/* spawn-fcgi -M 0660 -s /var/run/fcgiwrap.socket -u $user -g nginx -U $user -- `which fcgiwrap` -chmod g+rw /var/run/fcgiwrap.socket nginx -g "daemon off;" diff --git a/dist/setup.sh b/dist/setup.sh index fc7efe5fa..6fb197724 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -359,6 +359,8 @@ install_docker() { if [ ! -d $GATEKEEPER_DATAPATH ]; then step " create $GATEKEEPER_DATAPATH" sudo_if_required mkdir -p $GATEKEEPER_DATAPATH + # prevent mounting installation.json as a directory + sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json next fi diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 81a132a09..5ba6fad43 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -15,11 +15,6 @@ export USER=$(id -u <%= default_username %>):$(id -g <%= default_username %>) export ARCH=$(uname -m) current_path="$(cd "$(dirname "$0")" >/dev/null && pwd)" -if [[ ! -e <%= gatekeeper_datapath %>/installation.json ]]; then - # prevent mounting installation.json as a directory - touch <%= gatekeeper_datapath %>/installation.json -fi - <% if (docker_mode == 'swarm') { %> docker stack deploy -c $current_path/docker-compose.yaml cyphernode <% } else if(docker_mode == 'compose') { %> From 6562b8670a34653b32a164878c549f70614192e7 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 21 Dec 2018 14:59:31 -0500 Subject: [PATCH 250/268] We need to chmod after spawn-fcgi executed... --- api_auth_docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh index b46418548..56e93fb6e 100644 --- a/api_auth_docker/entrypoint.sh +++ b/api_auth_docker/entrypoint.sh @@ -11,7 +11,7 @@ if [[ $1 ]]; then fi +spawn-fcgi -M 0660 -s /var/run/fcgiwrap.socket -u $user -g nginx -U $user -- `which fcgiwrap` chmod -R g+rw /var/run/fcgiwrap.socket /etc/nginx/conf.d/* chown -R :nginx /etc/nginx/conf.d/* -spawn-fcgi -M 0660 -s /var/run/fcgiwrap.socket -u $user -g nginx -U $user -- `which fcgiwrap` nginx -g "daemon off;" From 686c874f52f8e62a78f577ddf8b65552fd63b510 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 21 Dec 2018 16:01:47 -0500 Subject: [PATCH 251/268] Fixed timeouts, messages and sync --- dist/setup.sh | 7 +++++-- install/generator-cyphernode/generators/app/help.json | 5 +++++ .../generators/app/prompters/999_installer.js | 2 +- .../generators/app/templates/installer/start.sh | 7 +++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 6fb197724..bb504dd87 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -359,12 +359,15 @@ install_docker() { if [ ! -d $GATEKEEPER_DATAPATH ]; then step " create $GATEKEEPER_DATAPATH" sudo_if_required mkdir -p $GATEKEEPER_DATAPATH - # prevent mounting installation.json as a directory - sudo_if_required touch $GATEKEEPER_DATAPATH/installation.json 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 diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 5e9b339ce..136d7ae00 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -7,11 +7,13 @@ "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", + "proxy_datapath_custom": "Provide the full path name where the Proxy's files will be saved.", "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", + "gatekeeper_datapath_custom": "Provide the full path name where the Gatekeeper's files will be saved.", "gatekeeper_edit_apiproperties": "Not recommended. If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization.", "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", @@ -25,14 +27,17 @@ "bitcoin_prune_size": "Minimum size is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", + "bitcoin_datapath_custom": "Provide the full path name where the Bitcoin Core's files will be saved.", "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", + "lightning_datapath_custom": "Provide the full path name where the LN's files will be saved.", "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", + "otsclient_datapath_custom": "Provide the full path name where the OTS files will be saved.", "installer_mode": "As of today, only one installation mode is supported: local docker (self-hosted). In the next release, we will support trivially installing Cyphernode on a Lunanode VM (hosted at lunanode.com).", "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc. If you don't know and the last Bitcoin block mined has an odd height number, choose Swarm... or not.", diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index 9823a6b8f..aca41c1a3 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -189,7 +189,7 @@ module.exports = { value: "_custom" } ], - message: prefix()+'Where do you want to store your lightning node data?'+utils._getHelp('otsclient_datapath'), + message: prefix()+'Where do you want to store your OTS data?'+utils._getHelp('otsclient_datapath'), }, { when: function(props) { return installerDocker(props) && props.features.indexOf('otsclient') !== -1 && props.otsclient_datapath === '_custom' }, diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 5ba6fad43..755ab73e6 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -21,6 +21,13 @@ docker stack deploy -c $current_path/docker-compose.yaml cyphernode docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans <% } %> +arch=$(uname -m) +case "${arch}" in arm*) + printf "\r\n\033[1;31mSince we're on a slow RPi, let's give Docker 30 more seconds before performing our tests...\033[0m\r\n" + sleep 30 +;; +esac + # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v <%= gatekeeper_datapath %>:/gatekeeper \ From fb33e1cd2c679d503578ed50740c1978e5a53be4 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 17:45:47 +0100 Subject: [PATCH 252/268] if cyphernode tests fail, stop the stack and don't show the "visit local url"-message --- .../generators/app/templates/installer/start.sh | 15 ++++++++++++++- .../app/templates/installer/testfeatures.sh | 2 ++ 2 files changed, 16 insertions(+), 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 755ab73e6..be03bd7f0 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -30,9 +30,22 @@ esac # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ --v <%= gatekeeper_datapath %>:/gatekeeper \ +-v ~/.cyphernode/gatekeeper:/gatekeeper \ +-v $current_path/exitStatus.sh:/exitStatus.sh \ --network cyphernodenet alpine:3.8 /testfeatures.sh +if [[ -f $current_path/exitStatus.sh ]]; then + . $current_path/exitStatus.sh + rm $current_path/exitStatus.sh +fi + +if [[ ! $EXIT_STATUS == 0 ]]; then + exec ./stop.sh + exit 1 +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" diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index b1fb1c89b..ef71edf27 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -313,3 +313,5 @@ result="{${result}]}" echo "${result}" > /gatekeeper/installation.json echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console + +echo "EXIT_STATUS=${returncode}" > /exitStatus.sh From e54366d8bbd3b5664546ebcfe000dba774674ef1 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 17:46:28 +0100 Subject: [PATCH 253/268] changed some help texts. --- .../generators/app/help.json | 77 +++++++++---------- .../generators/app/index.js | 3 +- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 136d7ae00..a1a6b5270 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -1,45 +1,42 @@ { - "features": "What optional features do you want me to activate? Select multiple choices using the space bar.", - "net": "You want Cyphernode to run on what Bitcoin network?", - "run_as_different_user": "We recommend running Cyphernode as a different user when possible. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer.", - "username": "Run Cyphernode as what user? We recommend user cyphernode. If the user does not exist, I will create it for you.", - "use_xpub": "Cyphernode can derive Bitcoin addresses from an xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path on each call.", - "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", - "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", - "proxy_datapath": "The proxy container (through where all the requests to Cyphernode goes) uses a sqlite3 database for its tasks. The DB will be mounted from a local path, easy to back up from outside Docker. Please provide where you want the proxy DB to be stored locally.", - "proxy_datapath_custom": "Provide the full path name where the Proxy's files will be saved.", - "gatekeeper_clientkeyspassword": "The Gatekeeper authenticates and authorizes (or not) all the requests to Cyphernode before delegating them (or not) to the proxy. Following the JWT (JSON Web Tokens) standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. We are going to generate the secret keys and keep them in an encrypted file. Please provide the encryption passphrase.", - "gatekeeper_clientkeyspassword_c": "Confirm encryption passphrase for the Gatekeeper's keys file.", - "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", - "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", - "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", + "features": "What optional features do you want me to activate?", + "net": "Which Bitcoin network do you want Cyphernode to run on?", + "run_as_different_user": "I recommend running Cyphernode as a different user when possible. Using your current user would give Cyphernode your current access rights, which could be a security issue especially if you are a sudoer. Please note that this feature is not supported on OSX at runtime, but you will be fine activating it in case you want to use the configuration file on another machine.", + "username": "Run Cyphernode as what user? I recommend user cyphernode. If the user does not exist, I will create it for you.", + "use_xpub": "Cyphernode can derive Bitcoin addresses from an xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path on each call.", + "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", + "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", + "proxy_datapath": "The Cyphernode proxy container, which routes all the requests to the right services uses a sqlite3 database to keep track of some things. This DB will be mounted from a local path, easy to back up from outside Docker.", + "proxy_datapath_custom": " ", + "gatekeeper_clientkeyspassword": "The Gatekeeper checks all the incoming requests for the right permissions before delegating them to the proxy. Following the JWT standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. I am going to generate the secret keys and keep them in an encrypted file. You will be able to download this encrypted file later. Please provide the encryption passphrase.", + "gatekeeper_clientkeyspassword_c": " ", + "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", + "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", "gatekeeper_datapath_custom": "Provide the full path name where the Gatekeeper's files will be saved.", - "gatekeeper_edit_apiproperties": "Not recommended. If you know what you are doing, it is possible to manually edit the API endpoints/groups authorization.", - "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 Swarm, 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_sslcert": "** gatekeeper_sslcert **", - "gatekeeper_sslkey": "** gatekeeper_sslkey **", - "gatekeeper_cns": "Domain names and/or IP addresses used when calling Cyphernode. Used to create valid TLS certificates. For example, if https://cyphernodehost/getbestblockhash and https://192.168.7.44/getbestblockhash will be used, provide 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 it.", - "bitcoin_node_ip": "Cyphernode uses Bitcoin Core RPC interface for its tasks. Please provide the IP address of your current Bitcoin Core node.", + "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_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.", - "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", - "bitcoin_prune_size": "Minimum size is 550 MB. Is the number of MB Bitcoin Core will allot for raw block & undo data.", - "bitcoin_uacomment": "User Agent string used by Bitcoin Core.", - "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. Will be mounted in the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", - "bitcoin_datapath_custom": "Provide the full path name where the Bitcoin Core's files will be saved.", - "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", - "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", - "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: That won't make your router forward needed LN ports; you still have the responsability to configure and manage that part.", - "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", - "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", - "lightning_datapath": "Path name to where LN's data files are stored. Will be mounted in the LN node's container.", - "lightning_datapath_custom": "Provide the full path name where the LN's files will be saved.", - "lightning_expose": "By default, LN node ports won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", - "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted to the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", - "otsclient_datapath_custom": "Provide the full path name where the OTS files will be saved.", - "installer_mode": "As of today, only one installation mode is supported: local docker (self-hosted). In the next release, we will support trivially installing Cyphernode on a Lunanode VM (hosted at lunanode.com).", - "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", - "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc. If you don't know and the last Bitcoin block mined has an odd height number, choose Swarm... or not.", - "__default__": "Key missing!
There is no help text for this entry. :-(" + "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", + "bitcoin_prune_size": "Minimum size is 550. This option specifies the maximum number in MB Bitcoin Core will allocate for raw block & undo data.", + "bitcoin_uacomment": "User Agent string used by Bitcoin Core. (Optional)", + "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. This directory will be mounted into the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", + "bitcoin_datapath_custom": " ", + "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", + "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", + "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: In case you are running Cyphernode at home. This option won't make your router forward needed LN ports; you still need to configure and manage that part yourself in your router configuration.", + "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", + "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", + "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container.", + "lightning_datapath_custom": " ", + "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted into the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", + "otsclient_datapath_custom": " ", + "installer_mode": "Only one installation mode is supported, right now: local docker (self-hosted). Choose wisely ;-)", + "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", + "docker_mode": "Cyphernode Docker services can be run using Docker Swarm (https://docs.docker.com/engine/swarm/) or docker-compose (https://docs.docker.com/compose/overview/). Both will work, some users prefer one to another depending on deployment types, scalability, current framework, etc.", + "__default__": "" } diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index ad39d7ea1..64c43c1e0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -503,14 +503,13 @@ module.exports = class extends Generator { return ''; } - // TODO: remove default later: const helpText = this.help[topic] || this.help['__default__']; if( !helpText ||helpText === '' ) { return ''; } - return "\n\n"+chalk.reset.cyan(wrap( html2ansi(helpText),82 ))+"\n\n"; + return "\n\n"+wrap( html2ansi(helpText),82 )+"\n\n"; } }; From f8a2889b2bcb2a6243409c3af7967a6b38d64564 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 17:47:05 +0100 Subject: [PATCH 254/268] shortened the automatic name generated by name.js --- install/generator-cyphernode/generators/app/lib/name.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/generator-cyphernode/generators/app/lib/name.js b/install/generator-cyphernode/generators/app/lib/name.js index 82602e303..7d7f187e8 100644 --- a/install/generator-cyphernode/generators/app/lib/name.js +++ b/install/generator-cyphernode/generators/app/lib/name.js @@ -1,4 +1,4 @@ -const MAXLENGTH = 32; +const MAXLENGTH = 30; const ADJECTIVES = [ /*a*/ ["Abiding", @@ -2363,7 +2363,7 @@ module.exports.generate = function() { ' '+ ANIMALS[index][Math.round(Math.random()*(ANIMALS[index].length-1))]+' 🚀' ).replace(/\s+/g, ' '); - } while( name.length > MAXLENGTH ); + } while( name.length >= MAXLENGTH ); return name } From c1d83a9fa2700586b1e08a5644557bc1abbd45e7 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 17:47:37 +0100 Subject: [PATCH 255/268] changed some prompter questions --- .../generators/app/prompters/000_cyphernode.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js index f0d9265c9..374b93603 100644 --- a/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js +++ b/install/generator-cyphernode/generators/app/prompters/000_cyphernode.js @@ -57,7 +57,7 @@ module.exports = { type: 'confirm', name: 'use_xpub', default: utils._getDefault( 'use_xpub' )||false, - message: prefix()+'Use an xpub key to watch or generate adresses?'+utils._getHelp('use_xpub'), + message: prefix()+'Use a default xpub key to watch or generate adresses?'+utils._getHelp('use_xpub'), }, { when: function( props ) { @@ -66,7 +66,7 @@ module.exports = { type: 'input', name: 'xpub', default: utils._getDefault( 'xpub' ), - message: prefix()+'What is your xpub key?'+utils._getHelp('xpub'), + message: prefix()+'What is your default xpub key?'+utils._getHelp('xpub'), filter: utils._trimFilter, validate: utils._xkeyValidator }, @@ -77,7 +77,7 @@ module.exports = { type: 'input', name: 'derivation_path', default: utils._getDefault( 'derivation_path' ), - message: prefix()+'What is your address derivation path?'+utils._getHelp('derivation_path'), + message: prefix()+'What is your default derivation path?'+utils._getHelp('derivation_path'), filter: utils._trimFilter, validate: utils._derivationPathValidator }]; From f8431cb39fdc73930cb97ceaeb5d23e4046a66ac Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 17:48:22 +0100 Subject: [PATCH 256/268] exposing lightning port is not optional --- install/generator-cyphernode/generators/app/index.js | 1 - .../generators/app/prompters/999_installer.js | 7 ------- .../app/templates/installer/docker/docker-compose.yaml | 2 -- 3 files changed, 10 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 64c43c1e0..5bed7153b 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -388,7 +388,6 @@ module.exports = class extends Generator { bitcoin_node_ip: '', bitcoin_mode: 'internal', bitcoin_expose: false, - lightning_expose: false, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index aca41c1a3..d6ef06265 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -207,13 +207,6 @@ module.exports = { default: utils._getDefault( 'bitcoin_expose' ), message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils._getHelp('bitcoin_expose'), }, - { - when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, - type: 'confirm', - name: 'lightning_expose', - default: utils._getDefault( 'lightning_expose' ), - message: prefix()+'Expose lightning node outside of the docker network?'+utils._getHelp('lightning_expose'), - }, { when: installerDocker, type: 'list', 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 a050c4304..7f1220b75 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 @@ -99,10 +99,8 @@ services: command: $USER lightningd image: cyphernode/clightning:<%= lightning_version %> -<% if( lightning_expose ) { %> ports: - "9735:9735" -<% } %> volumes: - "<%= lightning_datapath%>:/.lightning" - "<%= lightning_datapath%>/bitcoin.conf:/.bitcoin/bitcoin.conf" From 4754de562e60cfcd9400b684ac999c5e8ef5cb00 Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 18:20:48 +0100 Subject: [PATCH 257/268] moved some stuff around --- .../generators/app/index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 5bed7153b..51ea22b08 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -400,16 +400,16 @@ module.exports = class extends Generator { lightning_nodename: name.generate(), lightning_nodecolor: '', otsclient_datapath: '', - installer_cleanup: false + installer_cleanup: false, + default_username: process.env.DEFAULT_USER || '', + gatekeeper_version: process.env.GATEKEEPER_VERSION || 'latest', + proxy_version: process.env.PROXY_VERSION || 'latest', + proxycron_version: process.env.PROXYCRON_VERSION || 'latest', + pycoin_version: process.env.PYCOIN_VERSION || 'latest', + otsclient_version: process.env.OTSCLIENT_VERSION || 'latest', + bitcoin_version: process.env.BITCOIN_VERSION || 'latest', + lightning_version: process.env.LIGHTNING_VERSION || 'latest' }, this.props ); - this.props.default_username = process.env.DEFAULT_USER || ''; - this.props.gatekeeper_version = process.env.GATEKEEPER_VERSION || 'latest'; - this.props.proxy_version = process.env.PROXY_VERSION || 'latest'; - this.props.proxycron_version = process.env.PROXYCRON_VERSION || 'latest'; - this.props.pycoin_version = process.env.PYCOIN_VERSION || 'latest'; - this.props.otsclient_version = process.env.OTSCLIENT_VERSION || 'latest'; - this.props.bitcoin_version = process.env.BITCOIN_VERSION || 'latest'; - this.props.lightning_version = process.env.LIGHTNING_VERSION || 'latest'; } _isChecked( name, value ) { From 434ff43cafdeca81c704ef324d09603936cfb57a Mon Sep 17 00:00:00 2001 From: jash Date: Sun, 23 Dec 2018 18:21:14 +0100 Subject: [PATCH 258/268] added missing file creation --- .../generators/app/templates/installer/start.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index be03bd7f0..bf9937106 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -28,6 +28,8 @@ case "${arch}" in arm*) ;; esac +echo "EXIT_STATUS=1" > $current_path/exitStatus.sh + # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v ~/.cyphernode/gatekeeper:/gatekeeper \ From af682cae4a79b19f21582c343e19bbdf75e633f5 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 24 Dec 2018 14:02:14 +0100 Subject: [PATCH 259/268] renamed status page user to admin --- .../generators/app/templates/gatekeeper/htpasswd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd index 8857f927c..7cf938311 100644 --- a/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd +++ b/install/generator-cyphernode/generators/app/templates/gatekeeper/htpasswd @@ -1 +1 @@ -cyphernode:<%- gatekeeper_statuspw %> +admin:<%- gatekeeper_statuspw %> From 377cf86bec3164f1802a6406edbdb286d3ca01c0 Mon Sep 17 00:00:00 2001 From: jash Date: Mon, 24 Dec 2018 14:04:01 +0100 Subject: [PATCH 260/268] tell the user what username and password they need to take a peek at the status page --- .../generators/app/templates/installer/start.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index bf9937106..f49cd5236 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -51,3 +51,4 @@ 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 "\r\n\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\r\n" From b025ec68b77d937f84e46ad626e9319f8e549d6c Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 26 Dec 2018 13:57:20 -0500 Subject: [PATCH 261/268] Put back LN port exposition but changed default value to true --- install/generator-cyphernode/generators/app/help.json | 1 + install/generator-cyphernode/generators/app/index.js | 1 + .../generators/app/prompters/999_installer.js | 7 +++++++ .../app/templates/installer/docker/docker-compose.yaml | 2 ++ 4 files changed, 11 insertions(+) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index a1a6b5270..dcb337816 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -33,6 +33,7 @@ "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container.", "lightning_datapath_custom": " ", + "lightning_expose": "By default, LN node port will be published outside of Docker. Do you want to hide it so that your node can't be accessed from outside of the Docker network?", "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted into the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", "otsclient_datapath_custom": " ", "installer_mode": "Only one installation mode is supported, right now: local docker (self-hosted). Choose wisely ;-)", diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 51ea22b08..177d453d7 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -388,6 +388,7 @@ module.exports = class extends Generator { bitcoin_node_ip: '', bitcoin_mode: 'internal', bitcoin_expose: false, + lightning_expose: true, gatekeeper_apiproperties: defaultAPIProperties, gatekeeper_ipwhitelist: '', gatekeeper_keys: { configEntries: [], clientInformation: [] }, diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index d6ef06265..aca41c1a3 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -207,6 +207,13 @@ module.exports = { default: utils._getDefault( 'bitcoin_expose' ), message: prefix()+'Expose bitcoin full node outside of the docker network?'+utils._getHelp('bitcoin_expose'), }, + { + when: function(props) { return installerDocker(props) && props.features.indexOf('lightning') !== -1 }, + type: 'confirm', + name: 'lightning_expose', + default: utils._getDefault( 'lightning_expose' ), + message: prefix()+'Expose lightning node outside of the docker network?'+utils._getHelp('lightning_expose'), + }, { when: installerDocker, type: 'list', 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 7f1220b75..d39a559f6 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 @@ -99,8 +99,10 @@ services: command: $USER lightningd image: cyphernode/clightning:<%= lightning_version %> + <% if( lightning_expose ) { %> ports: - "9735:9735" + <% } %> volumes: - "<%= lightning_datapath%>:/.lightning" - "<%= lightning_datapath%>/bitcoin.conf:/.bitcoin/bitcoin.conf" From e1a903bfdbc366dc32f7bf56ffb0c64a67ce2ee0 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 26 Dec 2018 14:25:02 -0500 Subject: [PATCH 262/268] Removed LN IP address and fixed gatekeeper_datapath in start --- dist/config.json.sample | 3 +-- install/generator-cyphernode/generators/app/help.json | 1 - .../generators/app/prompters/200_lightning.js | 11 +---------- .../generators/app/templates/installer/start.sh | 6 +++--- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/dist/config.json.sample b/dist/config.json.sample index 28446e69c..c0abc2770 100644 --- a/dist/config.json.sample +++ b/dist/config.json.sample @@ -14,7 +14,6 @@ "bitcoin_prune": false, "bitcoin_uacomment": "", "lightning_implementation": "c-lightning", - "lightning_external_ip": "clightning.nodes.com", "lightning_nodename": "SatoshiPortal", "lightning_nodecolor": "ff00ff", "electrum_implementation": "eps", @@ -24,4 +23,4 @@ "lightning_datapath": "/tmp/l", "bitcoin_expose": false, "devmode": true -} \ No newline at end of file +} diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index dcb337816..9a5bd05fd 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -28,7 +28,6 @@ "bitcoin_datapath_custom": " ", "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", - "lightning_external_ip": "If you want you LN node to be accessible from the Internet, provide the IP address that other LN nodes will use to connect to it. This is usually your router's public IP. NOTE: In case you are running Cyphernode at home. This option won't make your router forward needed LN ports; you still need to configure and manage that part yourself in your router configuration.", "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container.", diff --git a/install/generator-cyphernode/generators/app/prompters/200_lightning.js b/install/generator-cyphernode/generators/app/prompters/200_lightning.js index afc0ef337..5da99a970 100644 --- a/install/generator-cyphernode/generators/app/prompters/200_lightning.js +++ b/install/generator-cyphernode/generators/app/prompters/200_lightning.js @@ -46,15 +46,6 @@ module.exports = { ] }, */ - { - when: featureCondition, - type: 'input', - name: 'lightning_external_ip', - default: utils._getDefault( 'lightning_external_ip' ), - filter: utils._trimFilter, - validate: utils._ipOrFQDNValidator, - message: prefix()+'What external ip does your lightning node have?'+utils._getHelp('lightning_external_ip'), - }, { when: featureCondition, type: 'input', @@ -87,4 +78,4 @@ module.exports = { templates: function( props ) { return templates[props.lightning_implementation] } -}; \ 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 f49cd5236..df9474494 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -32,16 +32,16 @@ echo "EXIT_STATUS=1" > $current_path/exitStatus.sh # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ --v ~/.cyphernode/gatekeeper:/gatekeeper \ +-v <%= gatekeeper_datapath %>:/gatekeeper \ -v $current_path/exitStatus.sh:/exitStatus.sh \ --network cyphernodenet alpine:3.8 /testfeatures.sh -if [[ -f $current_path/exitStatus.sh ]]; then +if [ -f $current_path/exitStatus.sh ]; then . $current_path/exitStatus.sh rm $current_path/exitStatus.sh fi -if [[ ! $EXIT_STATUS == 0 ]]; then +if [ "$EXIT_STATUS" -ne "0" ]; then exec ./stop.sh exit 1 fi From f5e3f5adc13859eb014b2e3f8376e30d9a9f3676 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 26 Dec 2018 15:02:44 -0500 Subject: [PATCH 263/268] semver --- dist/setup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index bb504dd87..321fe69e9 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -637,13 +637,13 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 -# CYPHERNODE VERSION "v0.1.0rc1" -CONF_VERSION="v0.1-rc1" -GATEKEEPER_VERSION="v0.1-rc1" -PROXY_VERSION="v0.1-rc1" -PROXYCRON_VERSION="v0.1-rc1" -OTSCLIENT_VERSION="v0.1-rc1" -PYCOIN_VERSION="v0.1-rc1" +# CYPHERNODE VERSION "0.1.0-rc.1" +CONF_VERSION="0.1-rc.1" +GATEKEEPER_VERSION="0.1-rc.1" +PROXY_VERSION="0.1-rc.1" +PROXYCRON_VERSION="0.1-rc.1" +OTSCLIENT_VERSION="0.1-rc.1" +PYCOIN_VERSION="0.1-rc.1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" From 1cb146de717cd02749e10b2dc0734b753c27f2e9 Mon Sep 17 00:00:00 2001 From: kexkey Date: Wed, 26 Dec 2018 15:24:08 -0500 Subject: [PATCH 264/268] v --- dist/setup.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index 321fe69e9..de6259d57 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -638,12 +638,12 @@ SUDO_REQUIRED=0 AUTOSTART=0 # CYPHERNODE VERSION "0.1.0-rc.1" -CONF_VERSION="0.1-rc.1" -GATEKEEPER_VERSION="0.1-rc.1" -PROXY_VERSION="0.1-rc.1" -PROXYCRON_VERSION="0.1-rc.1" -OTSCLIENT_VERSION="0.1-rc.1" -PYCOIN_VERSION="0.1-rc.1" +CONF_VERSION="v0.1-rc.1" +GATEKEEPER_VERSION="v0.1-rc.1" +PROXY_VERSION="v0.1-rc.1" +PROXYCRON_VERSION="v0.1-rc.1" +OTSCLIENT_VERSION="v0.1-rc.1" +PYCOIN_VERSION="v0.1-rc.1" BITCOIN_VERSION="v0.17.0" LIGHTNING_VERSION="v0.6.2" From e445b2fd2bca328b6ba51dce122d64a2e65d0530 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 28 Dec 2018 13:53:39 -0500 Subject: [PATCH 265/268] More info on mounting on OSX and yeoman package versions --- install/generator-cyphernode/generators/app/help.json | 10 +++++----- .../generators/app/prompters/999_installer.js | 10 +++++----- install/generator-cyphernode/package.json | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/install/generator-cyphernode/generators/app/help.json b/install/generator-cyphernode/generators/app/help.json index 9a5bd05fd..36032e75f 100644 --- a/install/generator-cyphernode/generators/app/help.json +++ b/install/generator-cyphernode/generators/app/help.json @@ -6,13 +6,13 @@ "use_xpub": "Cyphernode can derive Bitcoin addresses from an xPub and the derivation path you want. If you want, you can provide your xPub and derivation path right now and call 'derive' with only the index instead of having to pass your xPub and derivation path on each call.", "xpub": "Cyphernode can derive addresses from your default xPub key. With that functionality, you don't have to provide your xPub every time you call the derivation endpoints.", "derivation_path": "Cyphernode can derive addresses from your default derivation path. With that functionality, you don't have to provide your derivation path every time you call the derivation endpoints.", - "proxy_datapath": "The Cyphernode proxy container, which routes all the requests to the right services uses a sqlite3 database to keep track of some things. This DB will be mounted from a local path, easy to back up from outside Docker.", + "proxy_datapath": "The Cyphernode proxy container, which routes all the requests to the right services uses a sqlite3 database to keep track of some things. This DB will be mounted from a local path, easy to back up from outside Docker. If running on OSX, check mountable directories in Docker's File Sharing configs.", "proxy_datapath_custom": " ", "gatekeeper_clientkeyspassword": "The Gatekeeper checks all the incoming requests for the right permissions before delegating them to the proxy. Following the JWT standard, it uses HMAC signature verification to allow or deny access. Signatures are created and verified using secret keys. I am going to generate the secret keys and keep them in an encrypted file. You will be able to download this encrypted file later. Please provide the encryption passphrase.", "gatekeeper_clientkeyspassword_c": " ", "gatekeeper_recreatekeys": "The Gatekeeper keys already exist, do you want to regenerate them? This will overwrite existing ones.", "gatekeeper_recreatecert": "The Gatekeeper TLS (SSL) certificates already exist, do you want to regenerate them? This will overwrite existing ones.", - "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) will be stored in a container's mounted directory. Please provide the local mounted path to that directory.", + "gatekeeper_datapath": "The Gatekeeper's files (TLS certs, HMAC keys, Groups/API) 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.", "gatekeeper_datapath_custom": "Provide the full path name where the Gatekeeper's files will be saved.", "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.", @@ -24,16 +24,16 @@ "bitcoin_prune": "If you don't have at least 350GB of disk space, you should run Bitcoin Core in prune mode. NOTE: when running Bitcoin Core in prune mode, the incoming transactions' fees cannot be computed by Cyphernode and won't be part of the addresses watching's callbacks payload.", "bitcoin_prune_size": "Minimum size is 550. This option specifies the maximum number in MB Bitcoin Core will allocate for raw block & undo data.", "bitcoin_uacomment": "User Agent string used by Bitcoin Core. (Optional)", - "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. This directory will be mounted into the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents.", + "bitcoin_datapath": "Path name to where Bitcoin Core's data files (blockchain data, wallets, configs, etc.) are stored. This directory will be mounted into the Bitcoin node's container. If you already have a sync'ed node, you can copy data there to be used by the node, instead of resyncing everything. NOTE: only copy chainstate/ and blocks/ contents. If running on OSX, check mountable directories in Docker's File Sharing configs.", "bitcoin_datapath_custom": " ", "bitcoin_expose": "By default, Bitcoin node ports (RPC and protocol) won't be published outside of Docker. Do you want to expose them so that your node can be accessed from outside of the Docker network?", "lightning_implementation": "Multiple LN implementations exist. Please choose the one you want to use with Cyphernode.", "lightning_nodename": "LN nodes have names. Choose the name you want for yours.", "lightning_nodecolor": "LN nodes have colors. Choose the color you want for yours in RGB format (RRGGBB). For example, pure red would be ff0000.", - "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container.", + "lightning_datapath": "Path name to where LN's data files are stored. This directory will be mounted into the LN node's container. If running on OSX, check mountable directories in Docker's File Sharing configs.", "lightning_datapath_custom": " ", "lightning_expose": "By default, LN node port will be published outside of Docker. Do you want to hide it so that your node can't be accessed from outside of the Docker network?", - "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted into the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients.", + "otsclient_datapath": "Full path where the OTS files will be stored. This path will be mounted into the otsclient container which will create the OTS files when stamping and update them when upgrading stamps. It will also be mounted to the proxy container so that it can serve the ots_getfile and send the OTS files to clients. If running on OSX, check mountable directories in Docker's File Sharing configs.", "otsclient_datapath_custom": " ", "installer_mode": "Only one installation mode is supported, right now: local docker (self-hosted). Choose wisely ;-)", "installer_cleanup": "Do you want to remove this configurator Docker image after installation? This would free about 150MB of disk space.", diff --git a/install/generator-cyphernode/generators/app/prompters/999_installer.js b/install/generator-cyphernode/generators/app/prompters/999_installer.js index aca41c1a3..a8e5ea77d 100644 --- a/install/generator-cyphernode/generators/app/prompters/999_installer.js +++ b/install/generator-cyphernode/generators/app/prompters/999_installer.js @@ -37,7 +37,7 @@ module.exports = { default: utils._getDefault( 'gatekeeper_datapath' ), choices: [ { - name: "/var/run/cyphernode/gatekeeper (needs sudo)", + name: "/var/run/cyphernode/gatekeeper (needs sudo and "+chalk.red('incompatible with OSX')+")", value: "/var/run/cyphernode/gatekeeper" }, { @@ -71,7 +71,7 @@ module.exports = { default: utils._getDefault( 'proxy_datapath' ), choices: [ { - name: "/var/run/cyphernode/proxy (needs sudo)", + name: "/var/run/cyphernode/proxy (needs sudo and "+chalk.red('incompatible with OSX')+")", value: "/var/run/cyphernode/proxy" }, { @@ -105,7 +105,7 @@ module.exports = { default: utils._getDefault( 'bitcoin_datapath' ), choices: [ { - name: "/var/run/cyphernode/bitcoin (needs sudo)", + name: "/var/run/cyphernode/bitcoin (needs sudo and "+chalk.red('incompatible with OSX')+")", value: "/var/run/cyphernode/bitcoin" }, { @@ -139,7 +139,7 @@ module.exports = { default: utils._getDefault( 'lightning_datapath' ), choices: [ { - name: "/var/run/cyphernode/lightning (needs sudo)", + name: "/var/run/cyphernode/lightning (needs sudo - "+chalk.red('incompatible with OSX')+")", value: "/var/run/cyphernode/lightning" }, { @@ -173,7 +173,7 @@ module.exports = { default: utils._getDefault( 'otsclient_datapath' ), choices: [ { - name: "/var/run/cyphernode/otsclient (needs sudo)", + name: "/var/run/cyphernode/otsclient (needs sudo and "+chalk.red('incompatible with OSX')+")", value: "/var/run/cyphernode/otsclient" }, { diff --git a/install/generator-cyphernode/package.json b/install/generator-cyphernode/package.json index a5c047d48..668451c28 100644 --- a/install/generator-cyphernode/package.json +++ b/install/generator-cyphernode/package.json @@ -26,7 +26,8 @@ "parse5": "^5.1.0", "validator": "^10.8.0", "wrap-ansi": "^4.0.0", - "yeoman-generator": "^2.0.1" + "yeoman-environment": "2.3.3", + "yeoman-generator": "2.0.5" }, "repository": "git@github.com:schulterklopfer/cyphernode.git", "license": "MIT" From fa54b54ed2b170757f475e6646ce2330d8f61ea1 Mon Sep 17 00:00:00 2001 From: jash Date: Fri, 28 Dec 2018 21:10:11 +0100 Subject: [PATCH 266/268] override versions with new ones --- .../generator-cyphernode/generators/app/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install/generator-cyphernode/generators/app/index.js b/install/generator-cyphernode/generators/app/index.js index 177d453d7..6053f7ee0 100644 --- a/install/generator-cyphernode/generators/app/index.js +++ b/install/generator-cyphernode/generators/app/index.js @@ -124,6 +124,7 @@ module.exports = class extends Generator { } async _initConfig() { + const versionOverride = process.env.VERSION_OVERRIDE==='true'; if( fs.existsSync(this.destinationPath('config.7z')) ) { let r = {}; @@ -200,10 +201,24 @@ module.exports = class extends Generator { this.props.gatekeeper_statuspw = await new Cert().passwd(this.configurationPassword); + if( versionOverride ) { + delete this.props.gatekeeper_version; + delete this.props.proxy_version; + delete this.props.proxycron_version; + delete this.props.pycoin_version; + delete this.props.otsclient_version; + delete this.props.bitcoin_version; + delete this.props.lightning_version; + delete this.props.grafana_version; + } + this._assignConfigDefaults(); for( let c of this.featureChoices ) { c.checked = this._isChecked( 'features', c.value ); } + + + } async prompting() { From 01bbd5dd4a62bddb75bd245afb5d8095dafcbc26 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 28 Dec 2018 15:27:03 -0500 Subject: [PATCH 267/268] finalreturncode from tests and override versions in setup --- dist/setup.sh | 4 +++- .../generators/app/templates/installer/start.sh | 6 ++---- .../generators/app/templates/installer/testfeatures.sh | 8 +++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/dist/setup.sh b/dist/setup.sh index de6259d57..b3d0d055b 100755 --- a/dist/setup.sh +++ b/dist/setup.sh @@ -184,6 +184,7 @@ configure() { docker run -v $current_path:/data \ -e DEFAULT_USER=$USER \ -e DEFAULT_CERT_HOSTNAME=$(hostname) \ + -e VERSION_OVERRIDE=$VERSION_OVERRIDE \ -e GATEKEEPER_VERSION=$GATEKEEPER_VERSION \ -e PROXY_VERSION=$PROXY_VERSION \ -e PROXYCRON_VERSION=$PROXYCRON_VERSION \ @@ -637,7 +638,8 @@ ALWAYSYES=0 SUDO_REQUIRED=0 AUTOSTART=0 -# CYPHERNODE VERSION "0.1.0-rc.1" +# CYPHERNODE VERSION "v0.1.0-rc.1" +VERSION_OVERRIDE="true" CONF_VERSION="v0.1-rc.1" GATEKEEPER_VERSION="v0.1-rc.1" PROXY_VERSION="v0.1-rc.1" diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index df9474494..670c2a3e2 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -42,13 +42,11 @@ if [ -f $current_path/exitStatus.sh ]; then fi if [ "$EXIT_STATUS" -ne "0" ]; then - exec ./stop.sh + 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 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 "\r\n\033[0;92mUse 'admin' as the username with the configuration password you selected at the beginning of the configuration process.\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" diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index ef71edf27..a7afff2bf 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -234,6 +234,7 @@ feature_status() { brokenproxy="false" containers=$(checkservice) returncode=$? +finalreturncode=${returncode} if [ "${returncode}" -ne "0" ]; then echo -e "\e[1;31mCyphernode could not fully start properly within delay." > /dev/console status=$(echo "{${containers}}" | jq ".containers[] | select(.name == \"proxy\") | .active") @@ -262,6 +263,7 @@ if [ "${status}" = "true" ]; then else returncode=1 fi +finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Gatekeeper error!')}" result="${result},{\"name\":\"pycoin\",\"working\":" @@ -272,6 +274,7 @@ if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then else returncode=1 fi +finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Pycoin error!')}" <% if (features.indexOf('otsclient') != -1) { %> @@ -283,6 +286,7 @@ if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then else returncode=1 fi +finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'OTSclient error!')}" <% } %> @@ -294,6 +298,7 @@ if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then else returncode=1 fi +finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Bitcoin error!')}" <% if (features.indexOf('lightning') != -1) { %> @@ -305,6 +310,7 @@ if [[ "${brokenproxy}" != "true" && "${status}" = "true" ]]; then else returncode=1 fi +finalreturncode=$((${returncode} | ${finalreturncode})) result="${result}$(feature_status ${returncode} 'Lightning error!')}" <% } %> @@ -314,4 +320,4 @@ echo "${result}" > /gatekeeper/installation.json echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console -echo "EXIT_STATUS=${returncode}" > /exitStatus.sh +echo "EXIT_STATUS=${finalreturncode}" > /exitStatus.sh From 50a4c805f6ab0865f9f0c2e4600f5de0385c1550 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 28 Dec 2018 17:03:12 -0500 Subject: [PATCH 268/268] exitStatus mounting as dir, delays, are we ready? --- .../generators/app/templates/installer/start.sh | 10 ++++------ .../generators/app/templates/installer/testfeatures.sh | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/install/generator-cyphernode/generators/app/templates/installer/start.sh b/install/generator-cyphernode/generators/app/templates/installer/start.sh index 670c2a3e2..8fed59f9f 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/start.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/start.sh @@ -23,22 +23,20 @@ docker-compose -f $current_path/docker-compose.yaml up -d --remove-orphans arch=$(uname -m) case "${arch}" in arm*) - printf "\r\n\033[1;31mSince we're on a slow RPi, let's give Docker 30 more seconds before performing our tests...\033[0m\r\n" - sleep 30 + 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" + sleep 60 ;; esac -echo "EXIT_STATUS=1" > $current_path/exitStatus.sh - # Will test if Cyphernode is fully up and running... docker run --rm -it -v $current_path/testfeatures.sh:/testfeatures.sh \ -v <%= gatekeeper_datapath %>:/gatekeeper \ --v $current_path/exitStatus.sh:/exitStatus.sh \ +-v $current_path:/dist \ --network cyphernodenet alpine:3.8 /testfeatures.sh if [ -f $current_path/exitStatus.sh ]; then . $current_path/exitStatus.sh - rm $current_path/exitStatus.sh + rm -f $current_path/exitStatus.sh fi if [ "$EXIT_STATUS" -ne "0" ]; then diff --git a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh index a7afff2bf..e9ef7189f 100644 --- a/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh +++ b/install/generator-cyphernode/generators/app/templates/installer/testfeatures.sh @@ -231,6 +231,8 @@ feature_status() { # Let's first see if everything is up. +echo "EXIT_STATUS=1" > /dist/exitStatus.sh + brokenproxy="false" containers=$(checkservice) returncode=$? @@ -320,4 +322,4 @@ echo "${result}" > /gatekeeper/installation.json echo -e "\r\n\e[1;32mTests finished.\e[0m" > /dev/console -echo "EXIT_STATUS=${finalreturncode}" > /exitStatus.sh +echo "EXIT_STATUS=${finalreturncode}" > /dist/exitStatus.sh