diff --git a/dist/setup.sh b/dist/setup.sh index 6f27b27..86f0ea3 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 b5fcc6b..2ac473f 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 8f2986f..e7444fc 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 bacd2b5..6c400b6 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 189451e..04092d2 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 0000000..b3e94d6 --- /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