Merge pull request #1361 from frennkie/ip2tor-blitz-error

ip2tor: add blitz error + refactor
This commit is contained in:
Christian Rotzoll 2020-07-19 16:50:52 +02:00 committed by GitHub
commit fd4647a605
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 474 additions and 414 deletions

View file

@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.3.0] - 2020-07-19
### Added
- add BlitzError Class
## [0.2.0] - 2020-05-23
### Added
- add write() to BlitzPy config Classes

View file

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from .config import RaspiBlitzConfig, RaspiBlitzInfo
from .exceptions import BlitzError
__all__ = [
'RaspiBlitzConfig',
'RaspiBlitzInfo',
'BlitzError'
]

View file

@ -0,0 +1,16 @@
from datetime import datetime
TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
class BlitzError(Exception):
def __init__(self, short: str, details: dict = None, org: Exception = None):
self.short: str = str(short)
if details:
self.details: dict = details
self.details.update({'timestamp': datetime.utcnow().strftime(TS_FORMAT)})
else:
self.details = dict()
self.details['timestamp'] = datetime.utcnow().strftime(TS_FORMAT)
self.org: Exception = org

View file

@ -4,5 +4,5 @@
# 3) we can import it into your module module
"""
__version_info__ = ('0', '2', '0')
__version_info__ = ('0', '3', '0')
__version__ = '.'.join(__version_info__)

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +1,5 @@
#!/usr/bin/python3
import ast
import codecs
import json
import math
@ -13,7 +12,7 @@ from pathlib import Path
import grpc
import requests
import toml
from blitzpy import RaspiBlitzConfig
from blitzpy import RaspiBlitzConfig, BlitzError
from lndlibs import rpc_pb2 as lnrpc
from lndlibs import rpc_pb2_grpc as rpcstub
@ -35,6 +34,12 @@ if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "help":
print("# blitz.subscriptions.ip2tor.py ip-by-tor onionaddress")
sys.exit(1)
# constants for standard services
SERVICE_LND_REST_API = "LND-REST-API"
SERVICE_LND_GRPC_API = "LND-GRPC-API"
SERVICE_LNBITS = "LNBITS"
SERVICE_BTCPAY = "BTCPAY"
#####################
# BASIC SETTINGS
#####################
@ -70,17 +75,6 @@ else:
is_testnet = False
#####################
# HELPER CLASSES
#####################
class BlitzError(Exception):
def __init__(self, errorShort, errorLong="", errorException=None):
self.errorShort = str(errorShort)
self.errorLong = str(errorLong)
self.errorException = errorException
#####################
# HELPER FUNCTIONS
#####################
@ -91,9 +85,9 @@ def eprint(*args, **kwargs):
def handleException(e):
if isinstance(e, BlitzError):
eprint(e.errorLong)
eprint(e.errorException)
print("error='{0}'".format(e.errorShort))
eprint(e.details)
eprint(e.org)
print("error='{0}'".format(e.short))
else:
eprint(e)
print("error='{0}'".format(str(e)))
@ -150,17 +144,17 @@ def apiGetHosts(session, shopurl):
try:
response = session.get(url)
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
raise BlitzError("failed HTTP request", {'url': url}, e)
if response.status_code != 200:
raise BlitzError("failed HTTP code", response.status_code, )
raise BlitzError("failed HTTP code", {'status_code': response.status_code})
# parse & validate data
try:
jData = json.loads(response.content)
except Exception as e:
raise BlitzError("failed JSON parsing", response.content, e)
raise BlitzError("failed JSON parsing", {'content': response.content}, e)
if not isinstance(jData, list):
raise BlitzError("hosts not list", response.content)
raise BlitzError("hosts not list", {'content': response.content})
for idx, hostEntry in enumerate(jData):
try:
# ignore if not offering tor bridge
@ -188,7 +182,7 @@ def apiGetHosts(session, shopurl):
# shorten names to 20 chars max
hostEntry['name'] = hostEntry['name'][:20]
except Exception as e:
raise BlitzError("failed host entry pasring", str(hostEntry), e)
raise BlitzError("failed host entry pasring", hostEntry, e)
hosts.append(hostEntry)
@ -211,11 +205,11 @@ def apiPlaceOrderNew(session, shopurl, hostid, toraddressWithPort):
try:
response = session.post(url, data=postData)
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
raise BlitzError("failed HTTP request", {'url': url}, e)
if response.status_code == 420:
raise BlitzError("forwarding this address was rejected", response.status_code)
raise BlitzError("forwarding this address was rejected", {'status_code': response.status_code})
if response.status_code != 201:
raise BlitzError("failed HTTP code", response.status_code)
raise BlitzError("failed HTTP code", {'status_code': response.status_code})
# parse & validate data
try:
@ -224,7 +218,7 @@ def apiPlaceOrderNew(session, shopurl, hostid, toraddressWithPort):
print("error='MISSING ID'")
return
except Exception as e:
raise BlitzError("failed JSON parsing", response.status_code, e)
raise BlitzError("failed JSON parsing", {'status_code': response.status_code}, e)
return jData['id']
@ -236,11 +230,11 @@ def apiPlaceOrderExtension(session, shopurl, bridgeid):
try:
response = session.post(url)
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
raise BlitzError("failed HTTP request", {'url': url}, e)
if response.status_code == 420:
raise BlitzError("forwarding this address was rejected", response.status_code)
raise BlitzError("forwarding this address was rejected", {'status_code': response.status_code})
if response.status_code != 200 and response.status_code != 201:
raise BlitzError("failed HTTP code", response.status_code)
raise BlitzError("failed HTTP code", {'status_code': response.status_code})
# parse & validate data
print("# parse")
@ -250,12 +244,12 @@ def apiPlaceOrderExtension(session, shopurl, bridgeid):
print("error='MISSING ID'")
return
except Exception as e:
raise BlitzError("failed JSON parsing", response.content, e)
raise BlitzError("failed JSON parsing", {'content': response.content}, e)
return jData['po_id']
def apiGetOrder(session, shopurl, orderid):
def apiGetOrder(session, shopurl, orderid) -> dict:
print("# apiGetOrder")
# make HTTP request
@ -263,19 +257,19 @@ def apiGetOrder(session, shopurl, orderid):
try:
response = session.get(url)
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
raise BlitzError("failed HTTP request", {'url': url}, e)
if response.status_code != 200:
raise BlitzError("failed HTTP code", response.status_code)
raise BlitzError("failed HTTP code", {'status_code': response.status_code})
# parse & validate data
try:
jData = json.loads(response.content)
if len(jData['item_details']) == 0:
raise BlitzError("missing item", response.content)
raise BlitzError("missing item", {'content': response.content})
if len(jData['ln_invoices']) > 1:
raise BlitzError("more than one invoice", response.content)
raise BlitzError("more than one invoice", {'content': response.content})
except Exception as e:
raise BlitzError("failed JSON parsing", response.content, e)
raise BlitzError("failed JSON parsing", {'content': response.content}, e)
return jData
@ -288,16 +282,16 @@ def apiGetBridgeStatus(session, shopurl, bridgeid):
try:
response = session.get(url)
except Exception as e:
raise BlitzError("failed HTTP request", url, e)
raise BlitzError("failed HTTP request", {'url': url}, e)
if response.status_code != 200:
raise BlitzError("failed HTTP code", response.status_code)
raise BlitzError("failed HTTP code", {'status_code': response.status_code})
# parse & validate data
try:
jData = json.loads(response.content)
if len(jData['id']) == 0:
raise BlitzError("missing id", response.content)
raise BlitzError("missing id", {'content': response.content})
except Exception as e:
raise BlitzError("failed JSON parsing", response.content, e)
raise BlitzError("failed JSON parsing", {'content': response.content}, e)
return jData
@ -322,10 +316,10 @@ def lndDecodeInvoice(lnInvoiceString):
# validate results
if response.num_msat <= 0:
raise BlitzError("zero invoice not allowed", lnInvoiceString)
raise BlitzError("zero invoice not allowed", {'invoice': lnInvoiceString})
except Exception as e:
raise BlitzError("failed LND invoice decoding", lnInvoiceString, e)
raise BlitzError("failed LND invoice decoding", {'invoice': lnInvoiceString}, e)
return response
@ -346,10 +340,10 @@ def lndPayInvoice(lnInvoiceString):
# validate results
if len(response.payment_error) > 0:
raise BlitzError(response.payment_error, lnInvoiceString)
raise BlitzError(response.payment_error, {'invoice': lnInvoiceString})
except Exception as e:
raise BlitzError("payment failed", lnInvoiceString, e)
raise BlitzError("payment failed", {'invoice': lnInvoiceString}, e)
return response
@ -480,7 +474,7 @@ def shopOrder(shopUrl, hostid, servicename, torTarget, duration, msatsFirst, msa
except Exception as e:
eprint(e)
raise BlitzError("fail on subscription storage", str(subscription), e)
raise BlitzError("fail on subscription storage", subscription, e)
print("# OK - BRIDGE READY: {0}:{1} -> {2}".format(bridge_ip, bridge_port, torTarget))
return subscription
@ -578,7 +572,7 @@ def subscriptionExtend(shopUrl, bridgeid, durationAdvertised, msatsNext, bridge_
except Exception as e:
eprint(e)
raise BlitzError("fail on subscription storage", "", e)
raise BlitzError("fail on subscription storage", org=e)
print("# BRIDGE GOT EXTENDED: {0} -> {1}".format(bridge_suspendafter, bridge['suspend_after']))
@ -664,7 +658,8 @@ Try again later, enter another address or cancel.
choices=choices, title="Available Subscriptions")
# if user cancels
if code != d.OK: sys.exit(0)
if code != d.OK:
sys.exit(0)
# get data of selected
seletedIndex = int(tag)
@ -712,7 +707,8 @@ More information on the service you can find under:
height=30)
# if user AGREED break loop and continue with selected host
if code == "extra": break
if code == "extra":
break
############################
# PHASE 3: Make Subscription
@ -729,16 +725,15 @@ More information on the service you can find under:
exitcode = 0
order = ast.literal_eval(be.errorLong)
try :
message = order['message']
except Exception as e:
message = "n/a"
try:
message = be.details['message']
except KeyError:
message = ""
if (be.errorShort == "timeout on waiting for extending bridge" or
be.errorShort == "fail on subscription storage" or
be.errorShort == "invalid port" or
be.errorShort == "timeout bridge not getting ready"):
if (be.short == "timeout on waiting for extending bridge" or
be.short == "fail on subscription storage" or
be.short == "invalid port" or
be.short == "timeout bridge not getting ready"):
# error happened after payment
exitcode = Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
@ -748,7 +743,7 @@ Subscription will be ignored.
Error: {0}
Message: {1}
'''.format(be.errorShort, message), title="Error on Subscription", extra_button=True, extra_label="Details")
'''.format(be.short, message), title="Error on Subscription", extra_button=True, extra_label="Details")
else:
# error happened before payment
@ -759,7 +754,7 @@ Subscription will be ignored.
Error: {0}
Message: {1}
'''.format(be.errorShort, message), title="Error on Subscription", extra_button=True, extra_label="Details")
'''.format(be.short, message), title="Error on Subscription", extra_button=True, extra_label="Details")
# show more details (when user used extra button)
if exitcode == Dialog.EXTRA:
@ -767,13 +762,13 @@ Message: {1}
print('###### ERROR DETAIL FOR DEBUG #######')
print("")
print("Error Short:")
print(be.errorShort)
print(be.short)
print('Shop:')
print(shopurl)
print('Bridge:')
print(str(host))
print("Error Detail:")
print(be.errorLong)
print(be.details)
print("")
input("Press Enter to continue ...")
@ -796,7 +791,7 @@ Message: {1}
sys.exit(1)
# if LND REST or LND GRPC service ... add bridge IP to TLS
if servicename == "LND-REST-API" or servicename == "LND-GRPC-API":
if blitzServiceName == SERVICE_LND_REST_API or blitzServiceName == SERVICE_LND_GRPC_API:
os.system("sudo /home/admin/config.scripts/lnd.tlscert.sh ip-add {0}".format(subscription['ip']))
os.system("sudo /home/admin/config.scripts/lnd.credentials.sh reset tls")
os.system("sudo /home/admin/config.scripts/lnd.credentials.sh sync")
@ -811,7 +806,7 @@ You may want to consider to cancel the subscription later.
# decide if https:// address
protocol = ""
if blitzServiceName == "LNBITS":
if blitzServiceName == SERVICE_LNBITS:
protocol = "https://"
# Give final result feedback to user
@ -847,13 +842,11 @@ MAIN MENU > Manage Subscriptions > My Subscriptions
# CREATE SSH DIALOG
# use for ssh shell menu
###############
if sys.argv[1] == "create-ssh-dialog":
def create_ssh_dialog():
# check parameters
try:
if len(sys.argv) <= 4:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
@ -865,17 +858,16 @@ if sys.argv[1] == "create-ssh-dialog":
sys.exit()
###############
# SHOP LIST
# call from web interface
###############
if sys.argv[1] == "shop-list":
def shop_list():
# check parameters
try:
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
@ -891,17 +883,16 @@ if sys.argv[1] == "shop-list":
sys.exit(0)
##########################
# SHOP ORDER
# call from web interface
##########################
if sys.argv[1] == "shop-order":
def shop_order():
# check parameters
try:
if len(sys.argv) <= 8:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
@ -926,13 +917,12 @@ if sys.argv[1] == "shop-order":
except Exception as e:
handleException(e)
#######################
# SUBSCRIPTIONS LIST
# call in intervals from background process
#######################
if sys.argv[1] == "subscriptions-list":
def subscriptions_list():
try:
if Path(SUBSCRIPTIONS_FILE).is_file():
@ -947,15 +937,12 @@ if sys.argv[1] == "subscriptions-list":
except Exception as e:
handleException(e)
sys.exit(0)
#######################
# SUBSCRIPTIONS RENEW
# call in intervals from background process
#######################
if sys.argv[1] == "subscriptions-renew":
def subscriptions_renew():
print("# RUNNING subscriptions-renew")
# check parameters
@ -1002,16 +989,16 @@ if sys.argv[1] == "subscriptions-renew":
subs = toml.load(SUBSCRIPTIONS_FILE)
for sub in subs['subscriptions_ip2tor']:
if sub['id'] == subscription['id']:
sub['warning'] = "Exception on Renew: {0}".format(be.errorShort)
if be.errorShort == "invoice bigger amount than advertised":
sub['warning'] = "Exception on Renew: {0}".format(be.short)
if be.short == "invoice bigger amount than advertised":
sub['contract_breached'] = True
sub['active'] = False
with open(SUBSCRIPTIONS_FILE, 'w') as writer:
writer.write(toml.dumps(subs))
writer.close()
break
print("# BLITZERROR on subscriptions-renew of subscription index {0}: {1}".format(idx, be.errorShort))
print("# {0}".format(be.errorShort))
print("# BLITZERROR on subscriptions-renew of subscription index {0}: {1}".format(idx, be.short))
print("# {0}".format(be.short))
except Exception as e:
print("# EXCEPTION on subscriptions-renew of subscription index {0}".format(idx))
@ -1022,18 +1009,17 @@ if sys.argv[1] == "subscriptions-renew":
# output - not needed only for debug logs
print("# DONE subscriptions-renew")
sys.exit(1)
#######################
# SUBSCRIPTION CANCEL
# call in intervalls from background process
# call in intervals from background process
#######################
if sys.argv[1] == "subscription-cancel":
def subscription_cancel():
# check parameters
try:
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
@ -1058,30 +1044,28 @@ if sys.argv[1] == "subscription-cancel":
except Exception as e:
handleException(e)
sys.exit(0)
#######################
# GET ADDRESS BY SERVICENAME
# GET ADDRESS BY SERVICE NAME
# gets called by other scripts to check if service has a ip2tor bridge address
# output is bash key/value style so that it can be imported with source
#######################
if sys.argv[1] == "subscription-by-service":
def subscription_by_service():
# check parameters
try:
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
servicename = sys.argv[2]
service_name = sys.argv[2]
try:
if os.path.isfile(SUBSCRIPTIONS_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'] and sub['name'] == servicename:
if sub['active'] and sub['name'] == service_name:
print("type='{0}'".format(sub['type']))
print("ip='{0}'".format(sub['ip']))
print("port='{0}'".format(sub['port']))
@ -1089,24 +1073,22 @@ if sys.argv[1] == "subscription-by-service":
sys.exit(0)
print("error='not found'")
sys.exit(0)
except Exception as e:
handleException(e)
sys.exit(1)
#######################
# GET IP BY ONIONADDRESS
# GET IP BY ONION ADDRESS
# gets called by other scripts to check if a onion address as a IP2TOR bridge
# output is bash key/value style so that it can be imported with source
#######################
if sys.argv[1] == "ip-by-tor":
def ip_by_tor():
# check parameters
try:
if len(sys.argv) <= 2:
raise BlitzError("incorrect parameters", "")
raise BlitzError("incorrect parameters")
except Exception as e:
handleException(e)
@ -1126,12 +1108,41 @@ if sys.argv[1] == "ip-by-tor":
sys.exit(0)
print("error='not found'")
sys.exit(0)
except Exception as e:
handleException(e)
sys.exit(1)
# unknown command
print("# unknown command")
def main():
if sys.argv[1] == "create-ssh-dialog":
create_ssh_dialog()
elif sys.argv[1] == "shop-list":
shop_list()
elif sys.argv[1] == "shop-order":
shop_order()
elif sys.argv[1] == "subscriptions-list":
subscriptions_list()
elif sys.argv[1] == "subscriptions-renew":
subscriptions_renew()
elif sys.argv[1] == "subscription-cancel":
subscription_cancel()
elif sys.argv[1] == "subscription-by-service":
subscription_by_service()
elif sys.argv[1] == "ip-by-tor":
ip_by_tor()
else:
# unknown command
print("# unknown command")
if __name__ == '__main__':
main()

View file

@ -10,7 +10,7 @@ from pathlib import Path
import requests
import toml
from blitzpy import RaspiBlitzConfig
from blitzpy import RaspiBlitzConfig,BlitzError
#####################
# SCRIPT INFO
@ -30,6 +30,12 @@ if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "help":
print("# blitz.subscriptions.ip2tor.py subscription-cancel <id>")
sys.exit(1)
# constants for standard services
SERVICE_LND_REST_API = "LND-REST-API"
SERVICE_LND_GRPC_API = "LND-GRPC-API"
SERVICE_LNBITS = "LNBITS"
SERVICE_BTCPAY = "BTCPAY"
#####################
# BASIC SETTINGS
#####################
@ -49,6 +55,7 @@ if cfg.run_behind_tor:
# HELPER CLASSES
#####################
# ToDo(frennkie) replace this with updated BlitzError from blitzpy
class BlitzError(Exception):
def __init__(self, errorShort, errorLong="", errorException=None):
self.errorShort = str(errorShort)
@ -75,19 +82,19 @@ def handleException(e):
sys.exit(1)
def getsubdomain(fulldomainstring):
return fulldomainstring.split('.')[0]
def get_subdomain(fulldomain_str):
return fulldomain_str.split('.')[0]
############################
# API Calls to DNS Servcies
# API Calls to DNS Services
############################
def duckDNSupdate(domain, token, ip):
def duckdns_update(domain, token, ip):
print("# duckDNS update IP API call for {0}".format(domain))
# make HTTP request
url = "https://www.duckdns.org/update?domains={0}&token={1}&ip={2}".format(getsubdomain(domain), token, ip)
url = "https://www.duckdns.org/update?domains={0}&token={1}&ip={2}".format(get_subdomain(domain), token, ip)
try:
response = session.get(url)
if response.status_code != 200:
@ -102,31 +109,33 @@ def duckDNSupdate(domain, token, ip):
# PROCESS FUNCTIONS
#####################
def subscriptionsNew(ip, dnsservice, id, token, target):
# id needs to the full domain name
def subscriptions_new(ip, dnsservice, id, token, target):
# id needs to be the full domain name
if id.find(".") == -1:
raise BlitzError("not a fully qualified domainname", dnsservice_id)
# ToDo(frennkie) dnsservice_id doesn't exit
raise BlitzError("not a fully qualified domain name", dnsservice_id)
# check if id already exists
if len(getSubscription(id)) > 0:
if len(get_subscription(id)) > 0:
raise BlitzError("id already exists", id)
# make sure lets encrypt client is installed
os.system("/home/admin/config.scripts/bonus.letsencrypt.sh on")
# dyndns
realip = ip
real_ip = ip
if ip == "dyndns":
updateURL = ""
update_url = ""
if dnsservice == "duckdns":
updateURL = "https://www.duckdns.org/update?domains={0}&token={1}".format(getsubdomain(domain), token, ip)
subprocess.run(['/home/admin/config.scriprs/internet.dyndomain.sh', 'on', id, updateURL],
# ToDo(frennkie) domain doesn't exit
update_url = "https://www.duckdns.org/update?domains={0}&token={1}".format(get_subdomain(domain), token, ip)
subprocess.run(['/home/admin/config.scriprs/internet.dyndomain.sh', 'on', id, update_url],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
realip = cfg.public_ip
real_ip = cfg.public_ip
# update DNS with actual IP
if dnsservice == "duckdns":
duckDNSupdate(getsubdomain(id), token, realip)
duckdns_update(get_subdomain(id), token, real_ip)
# create subscription data for storage
subscription = dict()
@ -164,10 +173,10 @@ def subscriptionsNew(ip, dnsservice, id, token, target):
# run the ACME script
print("# Running letsencrypt ACME script ...")
acmeResult = subprocess.Popen(
acme_result = subprocess.Popen(
["/home/admin/config.scripts/bonus.letsencrypt.sh", "issue-cert", dnsservice, id, token, target],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
out, err = acmeResult.communicate()
out, err = acme_result.communicate()
eprint(str(out))
eprint(str(err))
if out.find("error=") > -1:
@ -178,26 +187,24 @@ def subscriptionsNew(ip, dnsservice, id, token, target):
return subscription
def subscriptionsCancel(id):
# ToDo(frennkie) id is not used..
def subscriptions_cancel(s_id):
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
subs = toml.load(SUBSCRIPTIONS_FILE)
newList = []
removedCert = None
new_list = []
removed_cert = None
for idx, sub in enumerate(subs['subscriptions_letsencrypt']):
if sub['id'] != subscriptionID:
newList.append(sub)
if sub['id'] != s_id:
new_list.append(sub)
else:
removedCert = sub
subs['subscriptions_letsencrypt'] = newList
removed_cert = sub
subs['subscriptions_letsencrypt'] = new_list
# run the ACME script to remove cert
if removedCert:
acmeResult = subprocess.Popen(
["/home/admin/config.scripts/bonus.letsencrypt.sh", "remove-cert", removedCert['id'],
removedCert['target']], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')
out, err = acmeResult.communicate()
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()
if out.find("error=") > -1:
time.sleep(6)
raise BlitzError("letsencrypt acme failed", out)
@ -212,7 +219,7 @@ def subscriptionsCancel(id):
# todo: deinstall letsencrypt if this was last subscription
def getSubscription(subscriptionID):
def get_subscription(subscription_id):
try:
if Path(SUBSCRIPTIONS_FILE).is_file():
@ -223,7 +230,7 @@ def getSubscription(subscriptionID):
if "subscriptions_letsencrypt" not in subs:
return []
for idx, sub in enumerate(subs['subscriptions_letsencrypt']):
if sub['id'] == subscriptionID:
if sub['id'] == subscription_id:
return sub
return []
@ -231,7 +238,7 @@ def getSubscription(subscriptionID):
return []
def getDomainByIP(ip):
def get_domain_by_ip(ip):
# does subscriptin file exists
if Path(SUBSCRIPTIONS_FILE).is_file():
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
@ -253,7 +260,7 @@ def getDomainByIP(ip):
raise BlitzError("no match")
def menuMakeSubscription():
def menu_make_subscription():
# late imports - so that rest of script can run also if dependency is not available
from dialog import Dialog
@ -299,7 +306,7 @@ If you havent already go to https://duckdns.org
title="DuckDNS Domain")
subdomain = text.strip()
subdomain = subdomain.split(' ')[0]
subdomain = getsubdomain(subdomain)
subdomain = get_subdomain(subdomain)
domain = "{0}.duckdns.org".format(subdomain)
os.system("clear")
@ -326,7 +333,7 @@ This looks not like a valid subdomain.
if len(token) < 20:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
This looks not like a valid token.
''', title="Unvalid Input")
''', title="Invalid Input")
sys.exit(0)
else:
@ -350,7 +357,7 @@ This looks not like a valid token.
"\nChoose the kind of IP you want to use:",
choices=choices, width=60, height=10, title="Select Service")
# if user chosses CANCEL
# if user chooses CANCEL
os.system("clear")
if code != d.OK:
sys.exit(0)
@ -362,16 +369,16 @@ This looks not like a valid token.
if tag == "IP2TOR":
# get all active IP2TOR subscriptions (just in case)
ip2torSubs = []
ip2tor_subs = []
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']:
ip2torSubs.append(sub)
ip2tor_subs.append(sub)
# when user has no IP2TOR subs yet
if len(ip2torSubs) == 0:
if len(ip2tor_subs) == 0:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
You have no active IP2TOR subscriptions.
Create one first and try again.
@ -380,7 +387,7 @@ Create one first and try again.
# let user select a IP2TOR subscription
choices = []
for idx, sub in enumerate(ip2torSubs):
for idx, sub in enumerate(ip2tor_subs):
choices.append(("{0}".format(idx), "IP2TOR {0} {1}:{2}".format(sub['name'], sub['ip'], sub['port'])))
d = Dialog(dialog="dialog", autowidgetsize=True)
@ -394,8 +401,8 @@ Create one first and try again.
sys.exit(0)
# get the slected IP2TOR bridge
ip2torSelect = ip2torSubs[int(tag)]
ip = ip2torSelect["ip"]
ip2tor_select = ip2tor_subs[int(tag)]
ip = ip2tor_select["ip"]
target = "tor"
elif tag == "DYNDNS":
@ -421,13 +428,13 @@ Create one first and try again.
if len(ip) == 0:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
This looks not like a valid IP.
''', title="Unvalid Input")
''', title="Invalid Input")
sys.exit(0)
# create the letsencrypt subscription
try:
os.system("clear")
subscription = subscriptionsNew(ip, dnsservice, domain, token, target)
subscription = subscriptions_new(ip, dnsservice, domain, token, target)
# success dialog
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
@ -455,19 +462,15 @@ Unknown Error happened - please report to developers:
# CREATE SSH DIALOG
# use for ssh shell menu
###############
def create_ssh_dialog():
menu_make_subscription()
if sys.argv[1] == "create-ssh-dialog":
menuMakeSubscription()
sys.exit()
##########################
# SUBSCRIPTIONS NEW
# call from web interface
##########################
if sys.argv[1] == "subscription-new":
def subscription_new():
# check parameters
try:
if len(sys.argv) <= 5:
@ -486,7 +489,7 @@ if sys.argv[1] == "subscription-new":
# create the subscription
try:
subscription = subscriptionsNew(ip, dnsservice_type, dnsservice_id, dnsservice_token, target)
subscription = subscriptions_new(ip, dnsservice_type, dnsservice_id, dnsservice_token, target)
# output json ordered bridge
print(json.dumps(subscription, indent=2))
@ -495,12 +498,11 @@ if sys.argv[1] == "subscription-new":
except Exception as e:
handleException(e)
#######################
# SUBSCRIPTIONS LIST
#######################
if sys.argv[1] == "subscriptions-list":
def subscriptions_list():
try:
if Path(SUBSCRIPTIONS_FILE).is_file():
@ -515,13 +517,11 @@ if sys.argv[1] == "subscriptions-list":
except Exception as e:
handleException(e)
sys.exit(0)
#######################
# SUBSCRIPTION DETAIL
#######################
if sys.argv[1] == "subscription-detail":
def subscription_detail():
# check parameters
try:
if len(sys.argv) <= 2:
@ -529,22 +529,20 @@ if sys.argv[1] == "subscription-detail":
except Exception as e:
handleException(e)
subscriptionID = sys.argv[2]
subscription_id = sys.argv[2]
try:
sub = getSubscription(subscriptionID)
sub = get_subscription(subscription_id)
print(json.dumps(sub, indent=2))
except Exception as e:
handleException(e)
sys.exit(0)
#######################
# DOMAIN BY IP
# to check if an ip has a domain mapping
#######################
if sys.argv[1] == "domain-by-ip":
def domain_by_ip():
# check parameters
try:
if len(sys.argv) <= 2:
@ -556,19 +554,17 @@ if sys.argv[1] == "domain-by-ip":
ip = sys.argv[2]
try:
domain = getDomainByIP(ip)
domain = get_domain_by_ip(ip)
print("domain='{0}'".format(domain))
except Exception as e:
handleException(e)
sys.exit(0)
#######################
# SUBSCRIPTION CANCEL
#######################
if sys.argv[1] == "subscription-cancel":
def subscription_cancel():
# check parameters
try:
if len(sys.argv) <= 2:
@ -576,13 +572,36 @@ if sys.argv[1] == "subscription-cancel":
except Exception as e:
handleException(e)
subscriptionID = sys.argv[2]
subscription_id = sys.argv[2]
try:
subscriptionsCancel(subscriptionID)
subscriptions_cancel(subscription_id)
except Exception as e:
handleException(e)
sys.exit(0)
# unknown command
print("# unknown command")
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()

View file

@ -15,10 +15,10 @@ from blitzpy import RaspiBlitzConfig
from dialog import Dialog
# constants for standard services
LND_REST_API = "LND-REST-API"
LND_GRPC_API = "LND-GRPC-API"
LNBITS = "LNBITS"
BTCPAY = "BTCPAY"
SERVICE_LND_REST_API = "LND-REST-API"
SERVICE_LND_GRPC_API = "LND-GRPC-API"
SERVICE_LNBITS = "LNBITS"
SERVICE_BTCPAY = "BTCPAY"
# load config
cfg = RaspiBlitzConfig()
@ -32,35 +32,38 @@ SUBSCRIPTIONS_FILE = "/mnt/hdd/app-data/subscriptions/subscriptions.toml"
# HELPER FUNCTIONS
#######################
# ToDo(frennkie) these are not being used!
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def parseDateIP2TORSERVER(datestr):
return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%S.%fZ")
def parse_date_ip2tor(date_str):
return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
def secondsLeft(dateObj):
return round((dateObj - datetime.utcnow()).total_seconds())
def seconds_left(date_obj):
return round((date_obj - datetime.utcnow()).total_seconds())
#######################
# SSH MENU FUNCTIONS
#######################
def mySubscriptions():
def my_subscriptions():
# check if any subscriptions are available
countSubscriptions = 0
count_subscriptions = 0
try:
os.system("sudo chown admin:admin {0}".format(SUBSCRIPTIONS_FILE))
subs = toml.load(SUBSCRIPTIONS_FILE)
if 'subscriptions_ip2tor' in subs:
countSubscriptions += len(subs['subscriptions_ip2tor'])
count_subscriptions += len(subs['subscriptions_ip2tor'])
if 'subscriptions_letsencrypt' in subs:
countSubscriptions += len(subs['subscriptions_letsencrypt'])
count_subscriptions += len(subs['subscriptions_letsencrypt'])
except Exception as e:
pass
if countSubscriptions == 0:
print(f"warning: {e}")
if count_subscriptions == 0:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
You have no active or inactive subscriptions.
''', title="Info")
@ -69,36 +72,36 @@ You have no active or inactive subscriptions.
# load subscriptions and make dialog choices out of it
choices = []
lookup = {}
lookupIndex = 0
lookup_index = 0
subs = toml.load(SUBSCRIPTIONS_FILE)
# list ip2tor subscriptions
if 'subscriptions_ip2tor' in subs:
for sub in subs['subscriptions_ip2tor']:
# remember subscription under lookupindex
lookupIndex += 1
lookup[str(lookupIndex)] = sub
lookup_index += 1
lookup[str(lookup_index)] = sub
# add to dialog choices
if sub['active']:
activeState = "active"
active_state = "active"
else:
activeState = "in-active"
active_state = "in-active"
name = "IP2TOR Bridge (P:{1}) for {0}".format(sub['name'], sub['port'])
choices.append(("{0}".format(lookupIndex), "{0} ({1})".format(name.ljust(30), activeState)))
choices.append(("{0}".format(lookup_index), "{0} ({1})".format(name.ljust(30), active_state)))
# list letsencrypt subscriptions
if 'subscriptions_letsencrypt' in subs:
for sub in subs['subscriptions_letsencrypt']:
# remember subscription under lookupindex
lookupIndex += 1
lookup[str(lookupIndex)] = sub
lookup_index += 1
lookup[str(lookup_index)] = sub
# add to dialog choices
if sub['active']:
activeState = "active"
active_state = "active"
else:
activeState = "in-active"
active_state = "in-active"
name = "LETSENCRYPT {0}".format(sub['id'])
choices.append(("{0}".format(lookupIndex), "{0} ({1})".format(name.ljust(30), activeState)))
choices.append(("{0}".format(lookup_index), "{0} ({1})".format(name.ljust(30), active_state)))
# show menu with options
d = Dialog(dialog="dialog", autowidgetsize=True)
@ -111,15 +114,15 @@ You have no active or inactive subscriptions.
if code != d.OK:
return
# get data of selected subscrption
selectedSub = lookup[str(tag)]
# get data of selected subscription
selected_sub = lookup[str(tag)]
# show details of selected
d = Dialog(dialog="dialog", autowidgetsize=True)
d.set_background_title("My Subscriptions")
if selectedSub['type'] == "letsencrypt-v1":
if len(selectedSub['warning']) > 0:
selectedSub['warning'] = "\n{0}".format(selectedSub['warning'])
if selected_sub['type'] == "letsencrypt-v1":
if len(selected_sub['warning']) > 0:
selected_sub['warning'] = "\n{0}".format(selected_sub['warning'])
text = '''
This is a LetsEncrypt subscription using the free DNS service
{dnsservice}
@ -135,17 +138,17 @@ The state of the subscription is: {active} {warning}
The following additional information is available:
{description}
'''.format(dnsservice=selectedSub['dnsservice_type'],
domain=selectedSub['id'],
ip=selectedSub['ip'],
active="ACTIVE" if selectedSub['active'] else "NOT ACTIVE",
warning=selectedSub['warning'],
description=selectedSub['description']
'''.format(dnsservice=selected_sub['dnsservice_type'],
domain=selected_sub['id'],
ip=selected_sub['ip'],
active="ACTIVE" if selected_sub['active'] else "NOT ACTIVE",
warning=selected_sub['warning'],
description=selected_sub['description']
)
elif selectedSub['type'] == "ip2tor-v1":
if len(selectedSub['warning']) > 0:
selectedSub['warning'] = "\n{0}".format(selectedSub['warning'])
elif selected_sub['type'] == "ip2tor-v1":
if len(selected_sub['warning']) > 0:
selected_sub['warning'] = "\n{0}".format(selected_sub['warning'])
text = '''
This is a IP2TOR subscription bought on {initdate} at
{shop}
@ -161,26 +164,26 @@ The state of the subscription is: {active} {warning}
The following additional information is available:
{description}
'''.format(initdate=selectedSub['time_created'],
shop=selectedSub['shop'],
publicaddress="{0}:{1}".format(selectedSub['ip'], selectedSub['port']),
toraddress=selectedSub['tor'],
renewhours=(round(int(selectedSub['duration']) / 3600)),
renewsats=(round(int(selectedSub['price_extension']) / 1000)),
totalsats=(round(int(selectedSub['price_total']) / 1000)),
active="ACTIVE" if selectedSub['active'] else "NOT ACTIVE",
warning=selectedSub['warning'],
description=selectedSub['description'],
service=selectedSub['name']
'''.format(initdate=selected_sub['time_created'],
shop=selected_sub['shop'],
publicaddress="{0}:{1}".format(selected_sub['ip'], selected_sub['port']),
toraddress=selected_sub['tor'],
renewhours=(round(int(selected_sub['duration']) / 3600)),
renewsats=(round(int(selected_sub['price_extension']) / 1000)),
totalsats=(round(int(selected_sub['price_total']) / 1000)),
active="ACTIVE" if selected_sub['active'] else "NOT ACTIVE",
warning=selected_sub['warning'],
description=selected_sub['description'],
service=selected_sub['name']
)
else:
text = "no text?! FIXME"
if selectedSub['active']:
extraLable = "CANCEL SUBSCRIPTION"
if selected_sub['active']:
extra_label = "CANCEL SUBSCRIPTION"
else:
extraLable = "DELETE SUBSCRIPTION"
code = d.msgbox(text, title="Subscription Detail", ok_label="Back", extra_button=True, extra_label=extraLable,
extra_label = "DELETE SUBSCRIPTION"
code = d.msgbox(text, title="Subscription Detail", ok_label="Back", extra_button=True, extra_label=extra_label,
width=75, height=30)
# user wants to delete this subscription
@ -188,15 +191,15 @@ The following additional information is available:
# api calls when canceling
if code == "extra":
os.system("clear")
if selectedSub['type'] == "letsencrypt-v1":
if selected_sub['type'] == "letsencrypt-v1":
cmd = "python /home/admin/config.scripts/blitz.subscriptions.letsencrypt.py subscription-cancel {0}".format(
selectedSub['id'])
selected_sub['id'])
print("# running: {0}".format(cmd))
os.system(cmd)
time.sleep(2)
elif selectedSub['type'] == "ip2tor-v1":
elif selected_sub['type'] == "ip2tor-v1":
cmd = "python /home/admin/config.scripts/blitz.subscriptions.ip2tor.py subscription-cancel {0}".format(
selectedSub['id'])
selected_sub['id'])
print("# running: {0}".format(cmd))
os.system(cmd)
time.sleep(2)
@ -205,41 +208,42 @@ The following additional information is available:
time.sleep(3)
# loop until no more subscriptions or user chooses CANCEL on subscription list
mySubscriptions()
my_subscriptions()
#######################
# SSH MENU
#######################
def main():
#######################
# SSH MENU
#######################
choices = list()
choices.append(("LIST", "My Subscriptions"))
choices.append(("NEW1", "+ IP2TOR Bridge (paid)"))
choices.append(("NEW2", "+ LetsEncrypt HTTPS Domain (free)"))
choices = list()
choices.append(("LIST", "My Subscriptions"))
choices.append(("NEW1", "+ IP2TOR Bridge (paid)"))
choices.append(("NEW2", "+ LetsEncrypt HTTPS Domain (free)"))
d = Dialog(dialog="dialog", autowidgetsize=True)
d.set_background_title("RaspiBlitz Subscriptions")
code, tag = d.menu(
d = Dialog(dialog="dialog", autowidgetsize=True)
d.set_background_title("RaspiBlitz Subscriptions")
code, tag = d.menu(
"\nCheck existing subscriptions or create new:",
choices=choices, width=50, height=10, title="Subscription Management")
# if user chosses CANCEL
if code != d.OK:
# if user chosses CANCEL
if code != d.OK:
sys.exit(0)
#######################
# MANAGE SUBSCRIPTIONS
#######################
#######################
# MANAGE SUBSCRIPTIONS
#######################
if tag == "LIST":
mySubscriptions()
if tag == "LIST":
my_subscriptions()
sys.exit(0)
###############################
# NEW LETSENCRYPT HTTPS DOMAIN
###############################
###############################
# NEW LETSENCRYPT HTTPS DOMAIN
###############################
if tag == "NEW2":
if tag == "NEW2":
# run creating a new IP2TOR subscription
os.system("clear")
cmd = "python /home/admin/config.scripts/blitz.subscriptions.letsencrypt.py create-ssh-dialog"
@ -247,18 +251,18 @@ if tag == "NEW2":
os.system(cmd)
sys.exit(0)
###############################
# NEW IP2TOR BRIDGE
###############################
###############################
# NEW IP2TOR BRIDGE
###############################
if tag == "NEW1":
if tag == "NEW1":
# check if Blitz is running behind TOR
cfg.reload()
if not cfg.run_behind_tor.value:
Dialog(dialog="dialog", autowidgetsize=True).msgbox('''
The IP2TOR service just makes sense if you run
your RaspiBlitz behind TOR.
The IP2TOR service just makes sense if you run
your RaspiBlitz behind TOR.
''', title="Info")
sys.exit(1)
@ -277,31 +281,31 @@ your RaspiBlitz behind TOR.
for sub in subs['subscriptions_ip2tor']:
if not sub['active']:
continue
if sub['active'] and sub['name'] == LND_REST_API:
if sub['active'] and sub['name'] == SERVICE_LND_REST_API:
lnd_rest_api = True
if sub['active'] and sub['name'] == LND_GRPC_API:
if sub['active'] and sub['name'] == SERVICE_LND_GRPC_API:
lnd_grpc_api = True
if sub['active'] and sub['name'] == LNBITS:
if sub['active'] and sub['name'] == SERVICE_LNBITS:
lnbits = True
if sub['active'] and sub['name'] == BTCPAY:
if sub['active'] and sub['name'] == SERVICE_BTCPAY:
btcpay = True
except Exception as e:
print(e)
# check if BTCPayserver is installed
btcPayServer = False
statusData = subprocess.run(['/home/admin/config.scripts/bonus.btcpayserver.sh', 'status'],
# check if BTCPayServer is installed
btc_pay_server = False
status_data = subprocess.run(['/home/admin/config.scripts/bonus.btcpayserver.sh', 'status'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
if statusData.find("installed=1") > -1:
btcPayServer = True
if status_data.find("installed=1") > -1:
btc_pay_server = True
# ask user for which RaspiBlitz service the bridge should be used
choices = list()
choices.append(("REST", "LND REST API {0}".format("--> ALREADY BRIDGED" if lnd_rest_api else "")))
choices.append(("GRPC", "LND gRPC API {0}".format("--> ALREADY BRIDGED" if lnd_grpc_api else "")))
if cfg.lnbits:
choices.append(("LNBITS", "LNbits Webinterface {0}".format("--> ALREADY BRIDGED" if lnd_grpc_api else "")))
if btcPayServer:
choices.append(("LNBITS", "LNbits Webinterface {0}".format("--> ALREADY BRIDGED" if lnbits else "")))
if btc_pay_server:
choices.append(("BTCPAY", "BTCPay Server Webinterface {0}".format("--> ALREADY BRIDGED" if btcpay else "")))
choices.append(("SELF", "Create a custom IP2TOR Bridge"))
@ -315,35 +319,35 @@ your RaspiBlitz behind TOR.
if code != d.OK:
sys.exit(0)
servicename = None
torAddress = None
torPort = None
service_name = None
tor_address = None
tor_port = None
if tag == "REST":
# get TOR address for REST
servicename = LND_REST_API
torAddress = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lndrest8080/hostname'],
service_name = SERVICE_LND_REST_API
tor_address = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lndrest8080/hostname'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
torPort = 8080
tor_port = 8080
if tag == "GRPC":
# get TOR address for GRPC
servicename = LND_GRPC_API
torAddress = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lndrpc10009/hostname'],
service_name = SERVICE_LND_GRPC_API
tor_address = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lndrpc10009/hostname'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
torPort = 10009
tor_port = 10009
if tag == "LNBITS":
# get TOR address for LNBits
servicename = LNBITS
torAddress = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lnbits/hostname'],
service_name = SERVICE_LNBITS
tor_address = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/lnbits/hostname'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
torPort = 443
tor_port = 443
if tag == "BTCPAY":
# get TOR address for BTCPAY
servicename = BTCPAY
torAddress = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/btcpay/hostname'],
service_name = SERVICE_BTCPAY
tor_address = subprocess.run(['sudo', 'cat', '/mnt/hdd/tor/btcpay/hostname'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
torPort = 443
tor_port = 443
if tag == "SELF":
servicename = "CUSTOM"
service_name = "CUSTOM"
try:
# get custom TOR address
code, text = d.inputbox(
@ -360,7 +364,7 @@ your RaspiBlitz behind TOR.
print("Not a TOR Onion Address")
time.sleep(3)
sys.exit(0)
torAddress = text
tor_address = text
# get custom TOR port
code, text = d.inputbox(
"Enter TOR Port Number:",
@ -372,7 +376,7 @@ your RaspiBlitz behind TOR.
sys.exit(0)
if len(text) == 0:
sys.exit(0)
torPort = int(text)
tor_port = int(text)
except Exception as e:
print(e)
time.sleep(3)
@ -381,7 +385,11 @@ your RaspiBlitz behind TOR.
# run creating a new IP2TOR subscription
os.system("clear")
cmd = "python /home/admin/config.scripts/blitz.subscriptions.ip2tor.py create-ssh-dialog {0} {1} {2}".format(
servicename, torAddress, torPort)
service_name, tor_address, tor_port)
print("# running: {0}".format(cmd))
os.system(cmd)
sys.exit(0)
if __name__ == '__main__':
main()