From 31a7de33b12e5e941a9025de86a66d67a7e27f61 Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Thu, 22 Feb 2024 17:09:34 +0100 Subject: Better questions using a python script --- regional-currency/ask_questions.py | 238 +++++++++++++++++++++++++++++++++++++ regional-currency/functions.sh | 100 ---------------- regional-currency/main.sh | 17 +-- 3 files changed, 241 insertions(+), 114 deletions(-) create mode 100755 regional-currency/ask_questions.py diff --git a/regional-currency/ask_questions.py b/regional-currency/ask_questions.py new file mode 100755 index 0000000..8c36311 --- /dev/null +++ b/regional-currency/ask_questions.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 + +import base64 +import os +import subprocess +import uuid +from typing import Callable, Dict, TypeVar + +log = open("setup.log", "ab", buffering=0) +CONFIG_FILE = "config/user.conf" + + +def load_conf() -> Dict[str, str]: + conf = {} + with open(CONFIG_FILE, "r") as f: + for kv in f.read().splitlines(): + if len(kv) != 0: + [k, v] = [part.strip() for part in kv.split("=", 1)] + conf[k] = v.strip('"').replace('\\"', '"') + return conf + + +conf = load_conf() + + +def add_conf(name: str, value: str): + conf[name] = value + content = "" + for key, value in conf.items(): + escaped = value.replace('"', '\\"') + content += f'{key}="{escaped}"\n' + with open(CONFIG_FILE, "w") as f: + f.write(content) + + +def run_cmd( + cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None +) -> int: + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + input=input.encode() if input is not None else None, + stdin=subprocess.DEVNULL if input is None else None, + env=env, + ) + log.write(result.stdout) + if result.returncode != 0: + print(result.stdout.decode("utf-8"), end="") + return result.returncode + + +def try_cmd( + cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None +) -> bool: + return run_cmd(cmd, input, env) == 0 + + +A = TypeVar("A") +T = TypeVar("T") + + +def custom( + name: str | None, + action: Callable[[], str | None], + default: T | None = None, + check: Callable[[str], T | None] = lambda it: it, + fmt: Callable[[T], str] = lambda it: str(it), +): + if name is not None: + prev = conf.get(name, None) + if prev is not None: # If got previous value + checked = check(prev) + if checked is not None: # And previous value is valid + add_conf(name, fmt(checked)) # Add again in case normalization changed + return checked + + while True: + raw = action() + if raw is None: + if default is not None: + if name is not None: + add_conf(name, fmt(default)) + return default + continue + else: + checked = check(raw) + if checked is not None: + if name is not None: + add_conf(name, fmt(checked)) + return checked + + +def ask( + name: str | None, + msg: str, + default: T | None = None, + check: Callable[[str], T | None] = lambda it: it, + fmt: Callable[[T], str] = lambda it: str(it), +) -> T: + def do_ask(): + log.write(msg.encode()) + log.write("\n".encode()) + raw = input(msg).strip() + if raw == "": + if default is None: + print("You must enter a value") + return None + return raw + + return custom(name, do_ask, default, check, fmt) + + +def ask_str(name: str | None, msg: str, default: str | None = None) -> str: + return ask(name, msg, default) + + +def ask_currency(name: str, msg: str, default: str | None = None) -> str: + def check_currency(currency: str) -> str | None: + currency = currency.upper() + if not currency.isascii() or not currency.isalpha(): + print("The currency name must be an ASCII alphabetic string") + elif len(currency) < 3 or 11 < len(currency): + print("The currency name had to be between 3 and 11 characters long") + else: + return currency + return None + + return ask(name, msg, default, check_currency) + + +def ask_host(name: str, msg: str, default: str | None = None) -> str: + def check_host(host: str) -> str | None: + success = True + for subdomain in ["backend", "bank", "exchange"]: + success = try_cmd(["ping", "-c", "1", f"{subdomain}.{host}"]) and success + if success: + return host + else: + return None + + return ask(name, msg, default, check_host) + + +def ask_yes_no(name: str | None, msg: str, default: bool | None = None) -> bool: + def check_yes_no(raw: str) -> bool | None: + raw = raw.lower() + if raw == "y" or raw == "yes": + return True + elif raw == "n" or raw == "no": + return False + else: + print("Expected 'y' or 'n'") + return None + + return ask(name, msg, default, check_yes_no, lambda it: "y" if it else "n") + + +ask_currency( + "CURRENCY", + "1. Enter the name of the regional currency (e.g. 'NETZBON'): ", + "NETZBON", +) +ask_currency( + "FIAT_CURRENCY", "2. Enter the name of the fiat currency (e.g. 'CHF'): ", "CHF" +) +ask_str( + "BANK_NAME", + "3. Enter the human-readable name of the bank (e.g. 'Taler Bank'): ", + "Taler Bank", +) +ask_host("DOMAIN_NAME", "4. Enter the domain name (e.g. 'example.com'): ") +if ask_yes_no("ENABLE_TLS", "5. Setup TLS using Let's Encrypt? (Y/n): ", True): + ask_str("TLS_EMAIL", "5.1. Enter an email address for Let's Encrypt: ") + + def ask_tos(): + print( + "5.2. Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf." + ) + if not ask_yes_no( + None, + "5.2. You must agree in order to register with the ACME server. Do you agree? (y/n): ", + False, + ): + print("You must agree in order to register with the ACME server") + return None + else: + return "y" + + custom("TLS_TOS", ask_tos) + add_conf("PROTO", "https") +else: + add_conf("PROTO", "http") + +if not ask_yes_no( + "DO_OFFLINE", + "6. Do you want Taler Exchange keys on this server (Y) or externally on another server (n): ", + True, +): + ask_str("MASTER_PUBLIC_KEY", "6.1. Enter the exchange-offline master public key: ") + +if ask_yes_no( + "DO_TELESIGN", + "7. Setup sms two-factor authentication using Telesign https://www.telesign.com? (Y/n): ", + True, +): + + def ask_telesign(): + customer_id = ask_str(None, "7.1. Enter your Telesign Customer ID: ") + api_key = ask_str(None, "7.2. Enter your Telesign API Key: ") + phone_number = ask_str( + None, + "7.3. Enter a phone number to test your API key (e.g. '+447911123456'): ", + ) + auth_token = base64.b64encode(f"{customer_id}:{api_key}".encode()).decode() + if not try_cmd( + ["libeufin-tan-sms.sh", phone_number], + "12345", + {**os.environ, "AUTH_TOKEN": auth_token}, + ): + print( + "Failed to send an sms using Telesign API, check your credentials and phone number" + ) + return None + code = ask_str(None, f"7.4. Enter the code received by {phone_number} : ") + if code != "12345": + print( + f"Wrong code got '{code}' expected '12345', check your credentials and phone number" + ) + return None + return auth_token + + custom("TELESIGN_AUTH_TOKEN", ask_telesign) +ask_str( + "BANK_ADMIN_PASSWORD", + "8. Enter the admin password for the bank (or press enter to autogenerate password): ", + str(uuid.uuid4()), +) diff --git a/regional-currency/functions.sh b/regional-currency/functions.sh index 77616ba..749d1bd 100755 --- a/regional-currency/functions.sh +++ b/regional-currency/functions.sh @@ -22,106 +22,6 @@ function check_user() { fi } -function ask_questions() { - if test -z "${CURRENCY:-}"; then - read -r -p "1. Enter the name of the regional currency (e.g. 'NETZBON'): " CURRENCY - CURRENCY=$(normalize_currency "${CURRENCY}") - echo "CURRENCY=${CURRENCY}" >>config/user.conf - fi - if test -z "${FIAT_CURRENCY:-}"; then - read -r -p "2. Enter the name of the fiat currency (e.g. 'CHF'): " FIAT_CURRENCY - FIAT_CURRENCY=$(normalize_currency "${FIAT_CURRENCY}") - echo "FIAT_CURRENCY=${FIAT_CURRENCY}" >>config/user.conf - fi - if test -z "${BANK_NAME:-}"; then - read -r -p "3. Enter the human-readable name of the bank (e.g. 'Taler Bank'): " BANK_NAME - echo "BANK_NAME=\"${BANK_NAME}\"" >>config/user.conf - fi - if test -z "${DOMAIN_NAME:-}"; then - read -r -p "4. Enter the domain name: " DOMAIN_NAME - # convert to lower-case - DOMAIN_NAME=$(echo "${DOMAIN_NAME}" | tr A-Z a-z) - check_dns - echo "DOMAIN_NAME=${DOMAIN_NAME}" >>config/user.conf - fi - if test -z "${ENABLE_TLS:-}"; then - read -r -p "5. Setup TLS using Let's Encrypt? (y/n): " ENABLE_TLS - echo "ENABLE_TLS=${ENABLE_TLS}" >>config/user.conf - fi - if test -z "${TLS_EMAIL:-}"; then - if test "${ENABLE_TLS:-}" == y; then - read -r -p "5.1. Enter an email address for Let's Encrypt: " TLS_EMAIL - echo "TLS_EMAIL=${TLS_EMAIL}" >>config/user.conf - fi - fi - if test -z "${TLS_TOS:-}"; then - if test "${ENABLE_TLS:-}" == y; then - echo "5.2. Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf." - read -r -p "5.2. You must agree in order to register with the ACME server. Do you agree? (y/n): " TLS_TOS - if test "${TLS_TOS:-y}" != y; then - say "You must agree in order to register with the ACME server" - exit 1 - fi - echo "TLS_TOS=${TLS_TOS}" >>config/user.conf - fi - fi - if test -z "${DO_OFFLINE:-}"; then - read -r -p "6. Do you want Taler Exchange keys on this server (y) or externally on another server (n): " DO_OFFLINE - echo "DO_OFFLINE=${DO_OFFLINE}" >>config/user.conf - fi - if test -z "${MASTER_PUBLIC_KEY:-}"; then - if test "${DO_OFFLINE:-y}" == n; then - read -r -p "6.1. Enter the exchange-offline master public key: " MASTER_PUBLIC_KEY - echo "MASTER_PUBLIC_KEY=${MASTER_PUBLIC_KEY}" >>config/user.conf - fi - fi - if test -z "${DO_TELESIGN:-}"; then - read -r -p "7. Setup sms two-factor authentication using Telesign https://www.telesign.com? (y/n): " DO_TELESIGN - if test "${DO_TELESIGN:-y}" != n; then - read -r -p "7.1. Enter your Telesign Customer ID: " TELESIGN_CUSTOMER_ID - read -r -s -p "7.2. Enter your Telesign API Key: " TELESIGN_API_KEY - echo "" # force new line - read -r -p "7.3. Enter a phone number to test your API key (e.g. '+447911123456'): " TELESIGN_PHONE - TELESIGN_AUTH_TOKEN=$(echo "$TELESIGN_CUSTOMER_ID:$TELESIGN_API_KEY" | base64 -w 0) - export AUTH_TOKEN=$TELESIGN_AUTH_TOKEN - echo "12345" | libeufin-tan-sms.sh $TELESIGN_PHONE - read -r -p "7.4. Enter the code received by $TELESIGN_PHONE : " TELESIGN_TEST_CODE - if test "${TELESIGN_TEST_CODE:-y}" != "12345"; then - say "Wrong code got '$TELESIGN_TEST_CODE' expected '12345', rerun this script to enter the right Telesign auth info" - exit 1 - fi - echo "TELESIGN_AUTH_TOKEN=\"${TELESIGN_AUTH_TOKEN}\"" >>config/user.conf - fi - echo "DO_TELESIGN=${DO_TELESIGN}" >>config/user.conf - fi - if test -z "${BANK_ADMIN_PASSWORD:-}"; then - read -r -s -p "8. Enter the admin password for the bank (or press enter to autogenerate password): " BANK_ADMIN_PASSWORD - echo "BANK_ADMIN_PASSWORD=$(printf '%q' "${BANK_ADMIN_PASSWORD}")" >>config/user.conf - echo "" # force new line - fi -} - -function normalize_currency() { - # convert to all-caps - local CURRENCY=$(echo "$1" | tr a-z A-Z) - # libeufin currenly doesn't like currency names less than 3 letters. - if [[ ${#CURRENCY} -lt 3 || ${#CURRENCY} -gt 11 ]]; then - say "Currency name must be between 3 and 10 letters" - exit 1 - fi - echo "${CURRENCY}" -} - -function check_dns() { - for prefix in "exchange" "bank" "backend"; do - if ! ping -c1 "${prefix}.${DOMAIN_NAME}" &>>setup.log; then - say "Could not ping ${prefix}.${DOMAIN_NAME}." - say "Please make sure your DNS/network are working." - exit 1 - fi - done -} - # Set DISTRO to the detected distro or return non-zero # status if distro not supported. function detect_distro() { diff --git a/regional-currency/main.sh b/regional-currency/main.sh index 144fe5a..f7bc9e3 100755 --- a/regional-currency/main.sh +++ b/regional-currency/main.sh @@ -44,9 +44,9 @@ say "Installing packages (step 1 of 6)" say "" say "Interactive configuration (step 2 of 6)" -ask_questions +./ask_questions.py +source config/user.conf -check_dns # Remove when libeufin currencies.conf is in sync with exchange cat >>/usr/share/libeufin/config.d/netzbon.conf <>config/internal.conf fi -if test -z "${BANK_ADMIN_PASSWORD:-}"; then - BANK_ADMIN_PASSWORD=$(uuidgen) - echo "BANK_ADMIN_PASSWORD=\"${BANK_ADMIN_PASSWORD}\"" >>config/user.conf -fi - if test -z "${BANK_PORT:-}"; then echo "BANK_PORT=8080" >>config/user.conf fi -if test "${ENABLE_TLS:-}" == "y"; then - PROTO="https" -else - PROTO="http" -fi -echo "PROTO=${PROTO}" >>config/internal.conf - say "" say "Configuring nginx (step 3 of 6)" ./config_nginx.sh @@ -100,6 +88,7 @@ say "Setting up merchant (step 6 of 6)" say "" say "Congratulations, you have successfully installed GNU Taler" say "Your bank is at ${PROTO}://bank.${DOMAIN_NAME}/" +say "You can connect to the bank web UI as 'admin' using '${BANK_ADMIN_PASSWORD}'" say "A merchant is at ${PROTO}://backend.${DOMAIN_NAME}/" say "You should set credentials for the merchant soon." -- cgit v1.2.3