raspiblitz/home.admin/config.scripts/blitz.subscriptions.letsencrypt.py

814 lines
26 KiB
Python
Raw Normal View History

2020-07-13 12:35:56 +02:00
#!/usr/bin/python3
import json
2020-07-17 19:16:06 +01:00
import os
2020-07-14 15:35:59 +02:00
import subprocess
2020-07-17 19:16:06 +01:00
import sys
import time
import re
2020-07-17 19:16:06 +01:00
from datetime import datetime
2020-07-13 12:35:56 +02:00
from pathlib import Path
2020-07-17 19:16:06 +01:00
import requests
2020-07-13 12:35:56 +02:00
import toml
2020-07-19 10:32:17 +01:00
from blitzpy import RaspiBlitzConfig,BlitzError
2020-07-13 12:35:56 +02:00
2020-07-17 19:16:06 +01:00
#####################
# SCRIPT INFO
#####################
2020-07-13 12:35:56 +02:00
# - this subscription does not require any payments
# - the recurring part is managed by the lets encrypt ACME script
# display config script info
if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "help":
print("# manage letsencrypt HTTPS certificates for raspiblitz")
print("# blitz.subscriptions.letsencrypt.py create-ssh-dialog")
print("# blitz.subscriptions.letsencrypt.py subscriptions-list")
print("# blitz.subscriptions.letsencrypt.py subscription-new <dyndns|ip> <duckdns> <domain> <token> [ip|tor|ip&tor]")
print("# blitz.subscriptions.letsencrypt.py subscription-new <ip> <dynu> <domain> <clientid:secret> [ip|tor|ip&tor]")
print("# blitz.subscriptions.letsencrypt.py subscription-detail <id>")
print("# blitz.subscriptions.letsencrypt.py subscription-cancel <id>")
print("# blitz.subscriptions.letsencrypt.py domain-by-ip <ip>")
2020-07-13 12:35:56 +02:00
sys.exit(1)
2020-07-17 19:16:06 +01:00
#####################
# BASIC SETTINGS
#####################
2020-07-13 12:35:56 +02:00
2020-07-17 19:16:06 +01:00
SUBSCRIPTIONS_FILE = "/mnt/hdd/app-data/subscriptions/subscriptions.toml"
2020-07-13 12:35:56 +02:00
cfg = RaspiBlitzConfig()
cfg.reload()
# todo: make sure that also ACME script uses TOR if activated
session = requests.session()
2020-08-13 15:42:31 +02:00
if cfg.run_behind_tor.value:
2020-07-17 19:16:06 +01:00
session.proxies = {'http': 'socks5h://127.0.0.1:9050', 'https': 'socks5h://127.0.0.1:9050'}
2020-07-13 12:35:56 +02:00
2020-07-17 19:16:06 +01:00
#####################
# HELPER CLASSES
#####################
2020-07-13 12:35:56 +02:00
2020-07-19 10:32:17 +01:00
# ToDo(frennkie) replace this with updated BlitzError from blitzpy
2020-07-13 12:35:56 +02:00
class BlitzError(Exception):
def __init__(self, errorShort, errorLong="", errorException=None):
self.errorShort = str(errorShort)
self.errorLong = str(errorLong)
self.errorException = errorException
2020-07-17 19:16:06 +01:00
#####################
# HELPER FUNCTIONS
#####################
2020-07-13 12:35:56 +02:00
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
2020-07-17 19:16:06 +01:00
2020-07-13 12:35:56 +02:00
def handleException(e):
if isinstance(e, BlitzError):
eprint(e.errorLong)
eprint(e.errorException)
print("error='{0}'".format(e.errorShort))
else:
eprint(e)
print("error='{0}'".format(str(e)))
sys.exit(1)
2020-07-17 19:16:06 +01:00
############################
2020-07-19 10:32:17 +01:00
# API Calls to DNS Services
2020-07-17 19:16:06 +01:00
############################
2020-07-13 12:35:56 +02:00
2020-07-19 10:32:17 +01:00
def duckdns_update(domain, token, ip):
2020-07-14 17:58:35 +02:00
print("# duckDNS update IP API call for {0}".format(domain))
2020-07-17 19:16:06 +01:00
2020-07-13 12:35:56 +02:00
# make HTTP request
url = "https://www.duckdns.org/update?domains={0}&token={1}&ip={2}".format(domain.split('.')[0], token, ip)
2020-08-04 17:13:14 +02:00
print("# calling URL: {0}".format(url))
2020-07-13 12:35:56 +02:00
try:
response = session.get(url)
2020-07-17 19:16:06 +01:00
if response.status_code != 200:
raise BlitzError("failed HTTP code", str(response.status_code))
2020-08-04 17:13:52 +02:00
print("# response-code: {0}".format(response.status_code))
2020-07-13 12:35:56 +02:00
except Exception as e:
2020-07-17 19:16:06 +01:00
raise BlitzError("failed HTTP request", url, e)
2020-07-13 12:35:56 +02:00
return response.content
def dynu_update(domain, token, ip):
print("# dynu update IP API call")
print("# domain({0})".format(domain))
print("# token({0})".format(token))
print("# ip({0})".format(ip))
# split token to oAuth username and password
try:
print("Splitting oAuth user & pass:")
username = token.split(":")[0]
password = token.split(":")[1]
print(username)
print(password)
except Exception as e:
raise BlitzError("failed to split token", token, e)
# get API token from oAuth data
url="https://api.dynu.com/v2/oauth2/token"
headers = {'accept': 'application/json'}
print("# calling URL: {0}".format(url))
print("# headers: {0}".format(headers))
try:
response = session.get(url, headers=headers, auth=(username, password))
if response.status_code != 200:
raise BlitzError("failed HTTP request", url + " ErrorCode:" + str(response.status_code))
print("# response-code: {0}".format(response.status_code))
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
# parse data
apitoken=""
try:
print(response.content)
data = json.loads(response.content)
apitoken = data["access_token"];
except Exception as e:
raise BlitzError("failed parsing data", response.content, e)
if len(apitoken) == 0:
raise BlitzError("access_token not found", response.content)
print("# apitoken({0})".format(apitoken))
apitoken = re.sub("[^0-9a-zA-Z]", "", apitoken)
print("# cleaning API token:")
print("# apitoken({0})".format(apitoken))
# get id for domain
print("# API CALL --> Getting ID for Domain (list all domains and search thru)")
url = "https://api.dynu.com/v2/dns"
headers = {'accept': 'application/json', 'Authorization': "Bearer {0}".format(apitoken)}
print("# calling URL: {0}".format(url))
print("# headers: {0}".format(headers))
try:
response = session.get(url, headers=headers)
if response.status_code == 401:
raise BlitzError("failed oAuth Service", url + " ErrorCode:" + str(response.status_code))
if response.status_code != 200:
raise BlitzError("failed HTTP request", url + " ErrorCode:" + str(response.status_code))
print("# response-code: {0}".format(response.status_code))
except Exception as e:
print(e)
time.sleep(4)
raise BlitzError("failed HTTP request", url, e)
# parse data
id_for_domain=0
try:
print(response.content)
data = json.loads(response.content)
for entry in data["domains"]:
if entry['name'] == domain:
id_for_domain = entry['id']
break
except Exception as e:
print(e)
time.sleep(4)
raise BlitzError("failed parsing data", response.content, e)
if id_for_domain == 0:
raise BlitzError("domain not found", response.content)
# update ip address
print("# API CALL --> Update IP for Domain-ID")
url = "https://api.dynu.com/v2/dns/{0}".format(id_for_domain)
print("# calling URL: {0}".format(url))
headers = {'accept': 'application/json', 'Authorization': "Bearer {0}".format(apitoken)}
print("# headers: {0}".format(headers))
data = {
"name": domain,
"ipv4Address": ip,
"ipv4": True,
"ipv6": False
}
data = json.dumps(data)
print("# post data: {0}".format(data))
try:
response = session.post(url, headers=headers, data=data)
if response.status_code != 200:
raise BlitzError("failed HTTP request", url + " ErrorCode:" + str(response.status_code))
print("# response-code: {0}".format(response.status_code))
except Exception as e:
print(e)
time.sleep(4)
raise BlitzError("failed HTTP request", url, e)
return response.content
2020-07-13 12:35:56 +02:00
2020-07-17 19:16:06 +01:00
#####################
# PROCESS FUNCTIONS
#####################
2020-07-13 12:35:56 +02:00
2020-07-20 00:12:01 +02:00
def subscriptions_new(ip, dnsservice, domain, token, target):
# domain needs to be the full domain name
if domain.find(".") == -1:
raise BlitzError("not a fully qualified domain name", domain)
2020-07-14 17:44:32 +02:00
2020-07-20 00:12:01 +02:00
# check if domain already exists
if len(get_subscription(domain)) > 0:
raise BlitzError("domain already exists", domain)
2020-07-13 13:12:06 +02:00
2020-07-14 00:43:15 +02:00
# make sure lets encrypt client is installed
os.system("/home/admin/config.scripts/bonus.letsencrypt.sh on")
2020-07-14 00:07:28 +02:00
2020-07-13 16:24:31 +02:00
# dyndns
2020-07-19 10:32:17 +01:00
real_ip = ip
2020-07-13 16:24:31 +02:00
if ip == "dyndns":
2020-07-19 10:32:17 +01:00
update_url = ""
2020-07-14 23:30:36 +02:00
if dnsservice == "duckdns":
update_url = "https://www.duckdns.org/update?domains={0}&token={1}".format(domain, token, ip)
2020-08-02 16:59:24 +02:00
subprocess.run(['/home/admin/config.scripts/internet.dyndomain.sh', 'on', domain, update_url],
2020-07-17 19:16:06 +01:00
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
2020-07-19 10:32:17 +01:00
real_ip = cfg.public_ip
if dnsservice == "dynu":
raise BlitzError("not implemented", "dynamic ip updating for dynu.com not implemented yet ", e)
sys.exit(0)
2020-07-13 12:35:56 +02:00
2020-07-13 16:24:31 +02:00
# update DNS with actual IP
if dnsservice == "duckdns":
print("# dnsservice=duckdns --> update {0}".format(domain))
2020-08-04 19:59:04 +02:00
duckdns_update(domain, token, real_ip)
2020-10-18 15:31:58 +02:00
elif dnsservice == "dynu":
print("# dnsservice=dynu --> update {0}".format(domain))
dynu_update(domain, token, real_ip)
2020-07-13 16:18:22 +02:00
2020-07-13 12:35:56 +02:00
# create subscription data for storage
2020-07-17 19:16:06 +01:00
subscription = dict()
2020-07-13 12:35:56 +02:00
subscription['type'] = "letsencrypt-v1"
2020-07-20 00:12:01 +02:00
subscription['id'] = domain
2020-07-13 12:35:56 +02:00
subscription['active'] = True
2020-07-20 00:12:01 +02:00
subscription['name'] = "{0} for {1}".format(dnsservice, domain)
2020-07-13 12:35:56 +02:00
subscription['dnsservice_type'] = dnsservice
subscription['dnsservice_token'] = token
subscription['ip'] = ip
2020-07-14 17:22:40 +02:00
subscription['target'] = target
2020-07-14 16:18:16 +02:00
subscription['description'] = "For {0}".format(target)
2020-07-17 19:16:06 +01:00
subscription['time_created'] = str(datetime.now().strftime("%Y-%m-%d %H:%M"))
2020-07-13 12:35:56 +02:00
subscription['warning'] = ""
# load, add and store subscriptions
try:
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
if Path(SUBSCRIPTIONS_FILE).is_file():
print("# load toml file")
subscriptions = toml.load(SUBSCRIPTIONS_FILE)
else:
print("# new toml file")
subscriptions = {}
2020-07-13 12:58:35 +02:00
if "subscriptions_letsencrypt" not in subscriptions:
2020-07-13 12:35:56 +02:00
subscriptions['subscriptions_letsencrypt'] = []
subscriptions['subscriptions_letsencrypt'].append(subscription)
with open(SUBSCRIPTIONS_FILE, 'w') as writer:
writer.write(toml.dumps(subscriptions))
writer.close()
except Exception as e:
eprint(e)
2020-07-17 19:16:06 +01:00
raise BlitzError("fail on subscription storage", str(subscription), e)
2020-07-13 12:35:56 +02:00
2020-07-18 16:47:42 +02:00
# run the ACME script
print("# Running letsencrypt ACME script ...")
2020-07-19 10:32:17 +01:00
acme_result = subprocess.Popen(
2020-07-20 00:12:01 +02:00
["/home/admin/config.scripts/bonus.letsencrypt.sh", "issue-cert", dnsservice, domain, token, target],
2020-07-18 16:47:42 +02:00
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
2020-07-19 10:32:17 +01:00
out, err = acme_result.communicate()
2020-07-18 16:47:42 +02:00
eprint(str(out))
eprint(str(err))
if out.find("error=") > -1:
time.sleep(6)
raise BlitzError("letsancrypt acme failed", out)
2020-07-13 12:58:35 +02:00
print("# OK - LETSENCRYPT DOMAIN IS READY")
2020-07-13 12:35:56 +02:00
return subscription
2020-07-17 19:16:06 +01:00
2020-07-19 10:32:17 +01:00
def subscriptions_cancel(s_id):
2020-07-13 12:35:56 +02:00
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
subs = toml.load(SUBSCRIPTIONS_FILE)
2020-07-19 10:32:17 +01:00
new_list = []
removed_cert = None
2020-07-13 12:35:56 +02:00
for idx, sub in enumerate(subs['subscriptions_letsencrypt']):
2020-07-19 10:32:17 +01:00
if sub['id'] != s_id:
new_list.append(sub)
2020-07-14 17:22:40 +02:00
else:
2020-07-19 10:32:17 +01:00
removed_cert = sub
subs['subscriptions_letsencrypt'] = new_list
2020-07-13 12:35:56 +02:00
2020-07-14 17:22:40 +02:00
# run the ACME script to remove cert
2020-07-19 10:32:17 +01:00
if removed_cert:
acme_result = subprocess.Popen(
["/home/admin/config.scripts/bonus.letsencrypt.sh", "remove-cert", removed_cert['id'],
removed_cert['target']], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
out, err = acme_result.communicate()
2020-07-14 17:49:50 +02:00
if out.find("error=") > -1:
2020-07-14 17:22:40 +02:00
time.sleep(6)
2020-07-17 19:16:06 +01:00
raise BlitzError("letsencrypt acme failed", out)
2020-07-14 17:22:40 +02:00
2020-07-13 12:35:56 +02:00
# persist change
with open(SUBSCRIPTIONS_FILE, 'w') as writer:
writer.write(toml.dumps(subs))
writer.close()
print(json.dumps(subs, indent=2))
# todo: deinstall letsencrypt if this was last subscription
2020-07-13 13:12:06 +02:00
2020-07-19 10:32:17 +01:00
def get_subscription(subscription_id):
2020-07-13 13:12:06 +02:00
try:
if Path(SUBSCRIPTIONS_FILE).is_file():
subs = toml.load(SUBSCRIPTIONS_FILE)
else:
return []
if "subscriptions_letsencrypt" not in subs:
return []
for idx, sub in enumerate(subs['subscriptions_letsencrypt']):
2020-07-19 10:32:17 +01:00
if sub['id'] == subscription_id:
2020-07-13 13:12:06 +02:00
return sub
return []
2020-07-17 19:16:06 +01:00
2020-07-13 13:12:06 +02:00
except Exception as e:
return []
2020-07-14 19:07:06 +02:00
2020-07-19 10:32:17 +01:00
def get_domain_by_ip(ip):
2020-07-14 19:10:40 +02:00
# does subscriptin file exists
if Path(SUBSCRIPTIONS_FILE).is_file():
subs = toml.load(SUBSCRIPTIONS_FILE)
else:
2020-07-14 19:07:06 +02:00
raise BlitzError("no match")
2020-07-14 19:10:40 +02:00
# section with letsencrypt subs exists
if "subscriptions_letsencrypt" not in subs:
raise BlitzError("no match")
# go thru subscription and check of a match
for idx, sub in enumerate(subs['subscriptions_letsencrypt']):
# if IP is a direct match
if sub['ip'] == ip:
return sub['id']
# if IP is a dynamicIP - check with the publicIP from the config
if sub['ip'] == "dyndns":
if cfg.public_ip == ip:
return sub['id']
raise BlitzError("no match")
2020-07-14 19:07:06 +02:00
2020-07-19 10:32:17 +01:00
def menu_make_subscription():
2020-07-17 19:16:06 +01:00
# late imports - so that rest of script can run also if dependency is not available
from dialog import Dialog
2020-07-13 12:35:56 +02:00
# todo ... copy parts of IP2TOR dialogs
############################
# PHASE 1: Choose DNS service
2020-07-13 15:53:12 +02:00
# ask user for which RaspiBlitz service the bridge should be used
choices = []
2020-07-17 19:16:06 +01:00
choices.append(("DUCKDNS", "Use duckdns.org"))
choices.append(("DYNU", "Use dynu.com"))
2020-07-13 15:53:12 +02:00
2020-07-17 19:16:06 +01:00
d = Dialog(dialog="dialog", autowidgetsize=True)
2020-07-13 15:53:12 +02:00
d.set_background_title("LetsEncrypt Subscription")
code, tag = d.menu(
"\nChoose a free DNS service to work with:",
choices=choices, width=60, height=10, title="Select Service")
# if user chosses CANCEL
if code != d.OK:
sys.exit(0)
# get the fixed dnsservice string
2020-07-17 19:16:06 +01:00
dnsservice = tag.lower()
2020-07-13 15:53:12 +02:00
2020-07-13 12:35:56 +02:00
############################
# PHASE 2: Enter ID & API token for service
2020-07-13 15:53:12 +02:00
if dnsservice == "duckdns":
# show basic info on duck dns
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 16:03:01 +02:00
If you havent already go to https://duckdns.org
2020-07-13 16:18:22 +02:00
- consider using the TOR browser
- create an account or login
- make sure you have a subdomain added
2020-07-17 19:16:06 +01:00
''', title="DuckDNS Account needed")
2020-07-13 15:53:12 +02:00
# enter the subdomain
code, text = d.inputbox(
"Enter your duckDNS subdomain:",
2020-07-17 19:16:06 +01:00
height=10, width=40, init="",
title="DuckDNS Domain")
2020-07-13 15:53:12 +02:00
subdomain = text.strip()
subdomain = subdomain.split(' ')[0]
subdomain = subdomain.split('.')[0]
2020-07-13 15:53:12 +02:00
domain = "{0}.duckdns.org".format(subdomain)
os.system("clear")
# check for valid input
if len(subdomain) == 0:
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 15:53:12 +02:00
This looks not like a valid subdomain.
2020-07-17 19:16:06 +01:00
''', title="Unvalid Input")
2020-07-13 15:53:12 +02:00
sys.exit(0)
# enter the token
code, text = d.inputbox(
2020-07-17 19:16:06 +01:00
"Enter the duckDNS token of your account:",
height=10, width=50, init="",
title="DuckDNS Token")
2020-07-13 15:53:12 +02:00
token = text.strip()
token = token.split(' ')[0]
# check for valid input
try:
token.index("-")
except Exception as e:
2020-07-17 19:16:06 +01:00
token = ""
2020-07-13 16:18:22 +02:00
if len(token) < 20:
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 15:53:12 +02:00
This looks not like a valid token.
2020-07-19 10:32:17 +01:00
''', title="Invalid Input")
2020-07-13 15:53:12 +02:00
sys.exit(0)
2020-10-18 15:31:58 +02:00
elif dnsservice == "dynu":
# show basic info on duck dns
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
If you havent already go to https://dynu.com
- consider using the TOR browser
- create an account or login
- DDNS Services -> create new
''', title="dynu.com Account needed")
# enter the subdomain
code, text = d.inputbox(
"Enter the complete DDNS name:",
height=10, width=40, init="",
title="dynu.com DDNS Domain")
domain = text.strip()
if len(domain) < 6:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
This looks not like a valid DDNS.
''', title="Invalid Input")
sys.exit(0)
os.system("clear")
# show basic info on duck dns
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
Continue in your dynu.com account:
- open 'Control Panel' > 'API Credentials'
- see listed 'OAuth2' ClientID & Secret
- click glasses icon to view values
''', title="dynu.com API Key needed")
# enter the CLIENTID
code, text = d.inputbox(
"Enter the OAuth2 CLIENTID:",
height=10, width=50, init="",
title="dynu.com OAuth2 ClientID")
clientid = text.strip()
clientid = clientid.split(' ')[0]
if len(clientid) < 20 or len(clientid.split('-'))<2:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
This looks not like valid ClientID.
''', title="Invalid Input")
sys.exit(0)
# enter the SECRET
code, text = d.inputbox(
"Enter the OAuth2 SECRET:",
height=10, width=50, init="",
title="dynu.com OAuth2 SECRET")
secret = text.strip()
secret = secret.split(' ')[0]
if len(secret) < 10:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
This looks not like valid.
''', title="Invalid Input")
sys.exit(0)
token = "{}:{}".format(clientid, secret)
2020-10-18 15:31:58 +02:00
2020-07-13 15:53:12 +02:00
else:
os.system("clear")
print("Not supported yet: {0}".format(dnsservice))
time.sleep(4)
2020-07-17 19:16:06 +01:00
sys.exit(0)
2020-10-18 15:31:58 +02:00
############################
2020-07-13 12:35:56 +02:00
# PHASE 3: Choose what kind of IP: dynDNS, IP2TOR, fixedIP
2020-07-13 15:53:12 +02:00
# ask user for which RaspiBlitz service the bridge should be used
2020-07-17 19:16:06 +01:00
choices = list()
choices.append(("IP2TOR", "HTTPS for a IP2TOR Bridge"))
choices.append(("DYNDNS", "HTTPS for {0} DynamicIP DNS".format(dnsservice.upper())))
choices.append(("STATIC", "HTTPS for a static IP"))
2020-07-13 15:53:12 +02:00
2020-07-17 19:16:06 +01:00
d = Dialog(dialog="dialog", autowidgetsize=True)
2020-07-13 15:53:12 +02:00
d.set_background_title("LetsEncrypt Subscription")
code, tag = d.menu(
"\nChoose the kind of IP you want to use:",
choices=choices, width=60, height=10, title="Select Service")
2020-07-19 10:32:17 +01:00
# if user chooses CANCEL
2020-07-14 16:24:08 +02:00
os.system("clear")
2020-07-13 15:53:12 +02:00
if code != d.OK:
sys.exit(0)
2020-07-14 16:18:16 +02:00
# default target are the nginx ip ports
2020-07-17 19:16:06 +01:00
target = "ip"
ip = ""
serviceName = ""
2020-07-14 16:18:16 +02:00
2020-07-13 15:53:12 +02:00
if tag == "IP2TOR":
# get all active IP2TOR subscriptions (just in case)
2020-07-19 10:32:17 +01:00
ip2tor_subs = []
2020-07-13 15:53:12 +02:00
if Path(SUBSCRIPTIONS_FILE).is_file():
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
subs = toml.load(SUBSCRIPTIONS_FILE)
for idx, sub in enumerate(subs['subscriptions_ip2tor']):
if sub['active']:
2020-07-19 10:32:17 +01:00
ip2tor_subs.append(sub)
2020-07-17 19:16:06 +01:00
2020-07-13 15:53:12 +02:00
# when user has no IP2TOR subs yet
2020-07-19 10:32:17 +01:00
if len(ip2tor_subs) == 0:
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 15:53:12 +02:00
You have no active IP2TOR subscriptions.
Create one first and try again.
2020-07-17 19:16:06 +01:00
''', title="No IP2TOR available")
sys.exit(0)
2020-07-13 15:53:12 +02:00
2020-07-17 19:16:06 +01:00
# let user select a IP2TOR subscription
2020-07-13 15:53:12 +02:00
choices = []
2020-07-19 10:32:17 +01:00
for idx, sub in enumerate(ip2tor_subs):
2020-07-17 19:16:06 +01:00
choices.append(("{0}".format(idx), "IP2TOR {0} {1}:{2}".format(sub['name'], sub['ip'], sub['port'])))
d = Dialog(dialog="dialog", autowidgetsize=True)
2020-07-13 15:53:12 +02:00
d.set_background_title("LetsEncrypt Subscription")
code, tag = d.menu(
"\nChoose the IP2TOR subscription:",
choices=choices, width=60, height=10, title="Select")
# if user chosses CANCEL
if code != d.OK:
sys.exit(0)
# get the slected IP2TOR bridge
2020-07-19 10:32:17 +01:00
ip2tor_select = ip2tor_subs[int(tag)]
ip = ip2tor_select["ip"]
serviceName = ip2tor_select["name"]
2020-07-17 19:16:06 +01:00
target = "tor"
2020-07-13 15:53:12 +02:00
elif tag == "DYNDNS":
2020-07-13 16:18:22 +02:00
# the subscriptioNew method will handle acrivating the dnydns part
2020-07-17 19:16:06 +01:00
ip = "dyndns"
2020-07-13 15:53:12 +02:00
elif tag == "STATIC":
# enter the static IP
code, text = d.inputbox(
2020-07-17 19:16:06 +01:00
"Enter the static public IP of this RaspiBlitz:",
height=10, width=40, init="",
title="Static IP")
2020-07-13 15:53:12 +02:00
ip = text.strip()
2020-08-02 17:06:56 +02:00
ip = ip.split(' ')[0]
2020-07-13 15:53:12 +02:00
# check for valid input
try:
ip.index(".")
except Exception as e:
2020-07-17 19:16:06 +01:00
ip = ""
2020-07-13 15:53:12 +02:00
if len(ip) == 0:
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 15:53:12 +02:00
This looks not like a valid IP.
2020-07-19 10:32:17 +01:00
''', title="Invalid Input")
2020-07-13 15:53:12 +02:00
sys.exit(0)
2020-07-17 19:16:06 +01:00
# create the letsencrypt subscription
2020-07-13 15:53:12 +02:00
try:
2020-07-14 16:44:28 +02:00
os.system("clear")
2020-07-19 10:32:17 +01:00
subscription = subscriptions_new(ip, dnsservice, domain, token, target)
2020-07-13 16:18:22 +02:00
# restart certain services to update urls
if "SPHINX" in serviceName:
print("# restarting Sphinx Relay to pickup new public url (please wait) ...")
os.system("sudo systemctl restart sphinxrelay")
2020-07-13 16:18:22 +02:00
# success dialog
2020-07-17 19:16:06 +01:00
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
2020-07-13 16:18:22 +02:00
OK your LetsEncrypt subscription is now ready.
Go to SUBSCRIBE > LIST to see details.
Use the correct port on {0}
to reach the service you wanted.
2020-07-17 19:16:06 +01:00
'''.format(domain), title="OK LetsEncrypt Created")
2020-07-13 16:18:22 +02:00
2020-07-13 15:53:12 +02:00
except Exception as e:
# service flaky
# https://github.com/rootzoll/raspiblitz/issues/1772
if "failed oAuth Service" in str(e):
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
A temporary error with the DYNU API happend:\nUnvalid OAuth Bearer Token
Please try again later or choose another dynamic domain service.
''', title="Exception on Subscription")
sys.exit(1)
2020-07-17 19:16:06 +01:00
# unknown error happened
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
Unknown Error happened - please report to developers:
2020-07-13 15:53:12 +02:00
{0}
2020-07-17 19:16:06 +01:00
'''.format(str(e)), title="Exception on Subscription")
sys.exit(1)
2020-07-13 12:41:52 +02:00
2020-07-17 19:16:06 +01:00
##################
# COMMANDS
##################
2020-07-13 12:35:56 +02:00
###############
# CREATE SSH DIALOG
# use for ssh shell menu
###############
2020-07-19 10:32:17 +01:00
def create_ssh_dialog():
menu_make_subscription()
2020-07-13 12:35:56 +02:00
2020-07-17 19:16:06 +01:00
##########################
2020-07-13 12:35:56 +02:00
# SUBSCRIPTIONS NEW
# call from web interface
2020-07-17 19:16:06 +01:00
##########################
2020-07-19 10:32:17 +01:00
def subscription_new():
2020-07-13 12:35:56 +02:00
# check parameters
try:
2020-07-17 19:16:06 +01:00
if len(sys.argv) <= 5:
raise BlitzError("incorrect parameters", "")
2020-07-13 12:35:56 +02:00
except Exception as e:
handleException(e)
2020-07-17 19:16:06 +01:00
ip = sys.argv[2]
dnsservice_type = sys.argv[3]
dnsservice_id = sys.argv[4]
dnsservice_token = sys.argv[5]
if len(sys.argv) <= 6:
target = "ip&tor"
else:
target = sys.argv[6]
2020-07-13 15:53:12 +02:00
# create the subscription
2020-07-13 12:35:56 +02:00
try:
2020-07-19 10:32:17 +01:00
subscription = subscriptions_new(ip, dnsservice_type, dnsservice_id, dnsservice_token, target)
2020-07-17 19:16:06 +01:00
# output json ordered bridge
print(json.dumps(subscription, indent=2))
sys.exit()
2020-07-13 12:35:56 +02:00
except Exception as e:
handleException(e)
2020-07-19 10:32:17 +01:00
2020-07-13 12:35:56 +02:00
#######################
# SUBSCRIPTIONS LIST
#######################
2020-07-19 10:32:17 +01:00
def subscriptions_list():
2020-07-13 12:35:56 +02:00
try:
if Path(SUBSCRIPTIONS_FILE).is_file():
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
subs = toml.load(SUBSCRIPTIONS_FILE)
else:
subs = {}
2020-07-13 12:52:15 +02:00
if "subscriptions_letsencrypt" not in subs:
2020-07-13 12:35:56 +02:00
subs['subscriptions_letsencrypt'] = []
print(json.dumps(subs['subscriptions_letsencrypt'], indent=2))
2020-07-17 19:16:06 +01:00
2020-07-13 12:35:56 +02:00
except Exception as e:
handleException(e)
2020-07-13 13:12:06 +02:00
#######################
# SUBSCRIPTION DETAIL
#######################
2020-07-19 10:32:17 +01:00
def subscription_detail():
2020-07-13 13:12:06 +02:00
# check parameters
try:
2020-07-17 19:16:06 +01:00
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
2020-07-13 13:12:06 +02:00
except Exception as e:
handleException(e)
2020-07-19 10:32:17 +01:00
subscription_id = sys.argv[2]
httpsTestport = ""
if len(sys.argv) > 3:
httpsTestport = sys.argv[3]
2020-07-13 13:12:06 +02:00
try:
2020-07-19 10:32:17 +01:00
sub = get_subscription(subscription_id)
# use unix 'getent' to resolve DNS to IP
dns_result = subprocess.Popen(
["getent", "hosts", subscription_id],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
out, err = dns_result.communicate()
sub['dns_response'] = "unknown"
if subscription_id in out:
sub['dns_response'] = out.split(" ")[0]
if sub['dns_response']!=sub['ip'] and len(sub['warning'])==0:
sub['warning'] = "Domain resolves not to target IP yet."
# when https testport is set - check if you we get a https response
sub['https_response'] = -1
if len(httpsTestport) > 0:
url = "https://{0}:{1}".format(subscription_id, httpsTestport)
try:
response = session.get(url)
sub['https_response'] = response.status_code
except Exception as e:
sub['https_response'] = 0
if sub['https_response']!=200 and len(sub['warning'])==0:
sub['warning'] = "Not able to get HTTPS response."
2020-07-13 13:12:06 +02:00
print(json.dumps(sub, indent=2))
except Exception as e:
handleException(e)
2020-07-17 19:16:06 +01:00
2020-07-14 19:07:06 +02:00
#######################
# DOMAIN BY IP
# to check if an ip has a domain mapping
#######################
2020-07-19 10:32:17 +01:00
def domain_by_ip():
2020-07-14 19:07:06 +02:00
# check parameters
try:
2020-07-17 19:16:06 +01:00
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
2020-07-14 19:07:06 +02:00
except Exception as e:
handleException(e)
2020-07-17 19:16:06 +01:00
ip = sys.argv[2]
2020-07-14 19:07:06 +02:00
try:
2020-07-19 10:32:17 +01:00
domain = get_domain_by_ip(ip)
2020-07-14 19:07:06 +02:00
print("domain='{0}'".format(domain))
except Exception as e:
handleException(e)
2020-07-13 12:35:56 +02:00
#######################
# SUBSCRIPTION CANCEL
#######################
2020-07-19 10:32:17 +01:00
def subscription_cancel():
2020-07-13 12:35:56 +02:00
# check parameters
try:
2020-07-17 19:16:06 +01:00
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
2020-07-13 12:35:56 +02:00
except Exception as e:
handleException(e)
2020-07-19 10:32:17 +01:00
subscription_id = sys.argv[2]
2020-07-13 12:35:56 +02:00
try:
2020-07-19 10:32:17 +01:00
subscriptions_cancel(subscription_id)
2020-07-13 12:35:56 +02:00
except Exception as e:
handleException(e)
2020-07-19 10:32:17 +01:00
def main():
if sys.argv[1] == "create-ssh-dialog":
create_ssh_dialog()
elif sys.argv[1] == "domain-by-ip":
domain_by_ip()
elif sys.argv[1] == "subscriptions-list":
subscriptions_list()
elif sys.argv[1] == "subscription-cancel":
subscription_cancel()
elif sys.argv[1] == "subscription-detail":
subscription_detail()
elif sys.argv[1] == "subscription-new":
subscription_new()
else:
# unknown command
print("# unknown command")
if __name__ == '__main__':
main()