raspiblitz/home.admin/config.scripts/blitz.ip2tor.py

750 lines
24 KiB
Python
Raw Normal View History

2020-05-22 19:24:46 +02:00
#!/usr/bin/python3
import sys
import locale
2020-05-22 20:06:59 +02:00
import requests
import json
2020-05-23 02:44:08 +02:00
import math
2020-05-24 13:11:31 +02:00
import time
import datetime, time
2020-05-23 13:49:13 +02:00
import codecs, grpc, os
2020-05-24 13:11:31 +02:00
from pathlib import Path
from blitzpy import RaspiBlitzConfig
from lndlibs import rpc_pb2 as lnrpc
from lndlibs import rpc_pb2_grpc as rpcstub
2020-05-23 13:49:13 +02:00
2020-05-25 00:19:32 +02:00
####### SCRIPT INFO #########
2020-05-22 19:24:46 +02:00
# display config script info
if len(sys.argv) <= 1 or sys.argv[1] == "-h" or sys.argv[1] == "help":
2020-05-23 02:44:08 +02:00
print("# manage ip2tor subscriptions for raspiblitz")
print("# blitz.ip2tor.py menu")
2020-05-25 00:19:32 +02:00
print("# blitz.ip2tor.py shop-list [shopurl]")
print("# blitz.ip2tor.py shop-order [shopurl] [hostid] [toraddress:port] [duration] [msats]")
print("# blitz.ip2tor.py subscriptions-list")
print("# blitz.ip2tor.py subscriptions-renew [secondsBeforeSuspend]")
2020-05-25 19:18:34 +02:00
print("# blitz.ip2tor.py subscription-cancel [id]")
2020-05-22 19:24:46 +02:00
sys.exit(1)
2020-05-25 00:19:32 +02:00
####### BASIC SETTINGS #########
2020-05-24 13:11:31 +02:00
cfg = RaspiBlitzConfig()
2020-05-24 18:04:56 +02:00
session = requests.session()
2020-05-24 13:11:31 +02:00
if Path("/mnt/hdd/raspiblitz.conf").is_file():
print("# blitz.ip2tor.py")
cfg.reload()
2020-05-24 18:38:09 +02:00
#DEFAULT_SHOPURL="shopdeu2vdhazvmllyfagdcvlpflzdyt5gwftmn4hjj3zw2oyelksaid.onion"
DEFAULT_SHOPURL="shop.ip2t.org"
2020-05-24 13:11:31 +02:00
LND_IP="127.0.0.1"
2020-05-25 19:18:34 +02:00
LND_ADMIN_MACAROON_PATH="/mnt/hdd/app-data/lnd/data/chain/{0}/{1}net/admin.macaroon".format(fg.network.value,cfg.chain.value)
2020-05-24 13:11:31 +02:00
LND_TLS_PATH="/mnt/hdd/app-data/lnd/tls.cert"
2020-05-24 18:38:09 +02:00
# make sure to make requests thru TOR 127.0.0.1:9050
session.proxies = {'http': 'socks5h://127.0.0.1:9050', 'https': 'socks5h://127.0.0.1:9050'}
2020-05-24 13:11:31 +02:00
else:
print("# blitz.ip2tor.py (development env)")
DEFAULT_SHOPURL="shop.ip2t.org"
LND_IP="192.168.178.95"
LND_ADMIN_MACAROON_PATH="/Users/rotzoll/Downloads/RaspiBlitzCredentials/admin.macaroon"
LND_TLS_PATH="/Users/rotzoll/Downloads/RaspiBlitzCredentials/tls.cert"
2020-05-25 19:18:34 +02:00
####### HELPER CLASSES #########
class BlitzError(Exception):
def __init__(self, errorShort, errorLong="", errorException=None):
self.errorShort = str(errorShort)
self.errorLong = str(errorLong)
self.errorException = errorException
2020-05-25 00:19:32 +02:00
####### HELPER FUNCTIONS #########
2020-05-24 13:11:31 +02:00
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
2020-05-25 19:18:34 +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-05-24 13:11:31 +02:00
def parseDate(datestr):
return datetime.datetime.strptime(datestr,"%Y-%m-%dT%H:%M:%S.%fZ")
def secondsLeft(dateObj):
2020-05-24 13:46:47 +02:00
return round((dateObj - datetime.datetime.utcnow()).total_seconds())
2020-05-24 13:11:31 +02:00
# takes a shopurl from user input and turns it into the format needed
# also makes sure that .onion addresses run just with http not https
def normalizeShopUrl(shopurlUserInput):
# basic checks and formats
2020-05-25 19:18:34 +02:00
if len(shopurlUserInput) < 3: return ""
2020-05-24 13:11:31 +02:00
shopurlUserInput = shopurlUserInput.lower()
shopurlUserInput = shopurlUserInput.replace(" ", "")
shopurlUserInput = shopurlUserInput.replace("\n", "")
shopurlUserInput = shopurlUserInput.replace("\r", "")
# remove protocol from the beginning (if needed)
if shopurlUserInput.find("://") > 0:
shopurlUserInput = shopurlUserInput[shopurlUserInput.find("://")+3:]
# remove all path after domain
if shopurlUserInput.find("/") > 0:
shopurlUserInput = shopurlUserInput[:shopurlUserInput.find("/")]
# add correct protocol again
if ( not shopurlUserInput.startswith("http://") and not shopurlUserInput.startswith("https://") ):
if shopurlUserInput.endswith(".onion"):
shopurlUserInput = "http://{0}".format(shopurlUserInput)
else:
shopurlUserInput = "https://{0}".format(shopurlUserInput)
return shopurlUserInput
2020-05-22 19:24:46 +02:00
2020-05-25 00:19:32 +02:00
####### IP2TOR API CALLS #########
2020-05-23 13:49:13 +02:00
def apiGetHosts(session, shopurl):
2020-05-23 02:44:08 +02:00
print("# apiGetHosts")
hosts=[]
# make HTTP request
try:
2020-05-24 18:00:48 +02:00
url="{0}/api/v1/public/hosts/".format(shopurl)
response = session.get(url)
2020-05-23 02:44:08 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("falied HTTP request",url,e)
2020-05-23 02:44:08 +02:00
if response.status_code != 200:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP code",response.status_code,)
2020-05-23 02:44:08 +02:00
# parse & validate data
try:
jData = json.loads(response.content)
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed JSON parsing",response.content,e)
2020-05-23 02:44:08 +02:00
if not isinstance(jData, list):
2020-05-25 19:18:34 +02:00
raise BlitzError("hosts not list",response.content)
2020-05-23 02:44:08 +02:00
for idx, hostEntry in enumerate(jData):
try:
# ignore if not offering tor bridge
if not hostEntry['offers_tor_bridges']: continue
# ignore if duration is less than an hour
if hostEntry['tor_bridge_duration'] < 3600: continue
# add duration per hour value
hostEntry['tor_bridge_duration_hours'] = math.floor(hostEntry['tor_bridge_duration']/3600)
# ignore if prices are negative or below one sat (maybe msats later)
if hostEntry['tor_bridge_price_initial'] < 1000: continue
if hostEntry['tor_bridge_price_extension'] < 1000: continue
# add price in sats
hostEntry['tor_bridge_price_initial_sats'] = math.ceil(hostEntry['tor_bridge_price_initial']/1000)
hostEntry['tor_bridge_price_extension_sats'] = math.ceil(hostEntry['tor_bridge_price_extension']/1000)
# ignore name is less then 3 chars
if len(hostEntry['name']) < 3: continue
# ignore id with zero value
if len(hostEntry['id']) < 1: continue
# shorten names to 20 chars max
hostEntry['name'] = hostEntry['name'][:20]
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed host entry pasring",str(hostEntry),e)
2020-05-23 02:44:08 +02:00
hosts.append(hostEntry)
print("# found {0} valid torbridge hosts".format(len(hosts)))
return hosts
2020-05-24 13:11:31 +02:00
def apiPlaceOrderNew(session, shopurl, hostid, toraddressWithPort):
2020-05-23 02:44:08 +02:00
2020-05-24 13:11:31 +02:00
print("# apiPlaceOrderNew")
2020-05-23 02:44:08 +02:00
try:
2020-05-25 19:18:34 +02:00
url="{0}/api/v1/public/order/".format(shopurl)
2020-05-24 13:11:31 +02:00
postData={
'product': "tor_bridge",
'host_id': hostid,
'tos_accepted': True,
'comment': 'test',
'target': toraddressWithPort,
'public_key': ''
}
2020-05-25 19:18:34 +02:00
response = session.post(url, data=postData)
2020-05-23 02:44:08 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP request",url,e)
2020-05-23 02:44:08 +02:00
if response.status_code != 201:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP code",response.status_code)
2020-05-23 02:44:08 +02:00
# parse & validate data
try:
jData = json.loads(response.content)
2020-05-23 13:49:13 +02:00
if len(jData['id']) == 0:
print("error='MISSING ID'")
return
2020-05-23 02:44:08 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed JSON parsing",response.status_code,e)
2020-05-23 02:44:08 +02:00
2020-05-23 13:49:13 +02:00
return jData['id']
2020-05-24 13:11:31 +02:00
def apiPlaceOrderExtension(session, shopurl, bridgeid):
print("# apiPlaceOrderExtension")
try:
url="{0}/api/v1/public/tor_bridges/{1}/extend/".format(shopurl, bridgeid)
response = session.post(url)
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP request",url,e)
2020-05-24 13:11:31 +02:00
if response.status_code != 200 and response.status_code != 201:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP code",response.status_code)
2020-05-24 13:11:31 +02:00
# parse & validate data
try:
jData = json.loads(response.content)
if len(jData['po_id']) == 0:
print("error='MISSING ID'")
return
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed JSON parsing",response.content, e)
2020-05-24 13:11:31 +02:00
return jData['po_id']
2020-05-23 13:49:13 +02:00
def apiGetOrder(session, shopurl, orderid):
print("# apiGetOrder")
# make HTTP request
try:
2020-05-25 19:18:34 +02:00
url="{0}/api/v1/public/pos/{1}/".format(shopurl,orderid)
response = session.get(url)
2020-05-23 13:49:13 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP request",url, e)
2020-05-23 13:49:13 +02:00
if response.status_code != 200:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP code",response.status_code)
2020-05-23 13:49:13 +02:00
# parse & validate data
try:
jData = json.loads(response.content)
if len(jData['item_details']) == 0:
2020-05-25 19:18:34 +02:00
raise BlitzError("missing item",response.content)
2020-05-23 13:49:13 +02:00
if len(jData['ln_invoices']) > 1:
2020-05-25 19:18:34 +02:00
raise BlitzError("more than one invoice",response.content)
2020-05-23 13:49:13 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed JSON parsing",response.content, e)
2020-05-24 13:11:31 +02:00
return jData
def apiGetBridgeStatus(session, shopurl, bridgeid):
print("# apiGetBridgeStatus")
# make HTTP request
try:
2020-05-25 19:18:34 +02:00
url="{0}/api/v1/public/tor_bridges/{1}/".format(shopurl,bridgeid)
response = session.get(url)
2020-05-24 13:11:31 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP request",url, e)
2020-05-24 13:11:31 +02:00
if response.status_code != 200:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed HTTP code",response.status_code)
2020-05-24 13:11:31 +02:00
# parse & validate data
try:
jData = json.loads(response.content)
if len(jData['id']) == 0:
2020-05-25 19:18:34 +02:00
raise BlitzError("missing id",response.content)
2020-05-24 13:11:31 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed JSON parsing",response.content, e)
2020-05-23 02:44:08 +02:00
2020-05-23 13:49:13 +02:00
return jData
2020-05-25 00:19:32 +02:00
####### LND API CALLS #########
2020-05-23 13:49:13 +02:00
def lndDecodeInvoice(lnInvoiceString):
2020-05-24 13:11:31 +02:00
try:
# call LND GRPC API
macaroon = codecs.encode(open(LND_ADMIN_MACAROON_PATH, 'rb').read(), 'hex')
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
cert = open(LND_TLS_PATH, 'rb').read()
ssl_creds = grpc.ssl_channel_credentials(cert)
channel = grpc.secure_channel("{0}:10009".format(LND_IP), ssl_creds)
stub = rpcstub.LightningStub(channel)
request = lnrpc.PayReqString(
pay_req=lnInvoiceString,
)
response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)])
# validate results
if response.num_msat <= 0:
2020-05-25 19:18:34 +02:00
raise BlitzError("zero invoice not allowed",lnInvoiceString)
2020-05-24 13:11:31 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("failed LND invoice decoding",lnInvoiceString,e)
2020-05-24 13:11:31 +02:00
return response
def lndPayInvoice(lnInvoiceString):
try:
# call LND GRPC API
macaroon = codecs.encode(open(LND_ADMIN_MACAROON_PATH, 'rb').read(), 'hex')
os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
cert = open(LND_TLS_PATH, 'rb').read()
ssl_creds = grpc.ssl_channel_credentials(cert)
channel = grpc.secure_channel("{0}:10009".format(LND_IP), ssl_creds)
stub = rpcstub.LightningStub(channel)
request = lnrpc.SendRequest(
payment_request=lnInvoiceString,
)
response = stub.SendPaymentSync(request, metadata=[('macaroon', macaroon)])
# validate results
if len(response.payment_error) > 0:
2020-05-25 19:18:34 +02:00
raise BlitzError(response.payment_error,lnInvoiceString)
2020-05-24 13:11:31 +02:00
except Exception as e:
2020-05-25 19:18:34 +02:00
raise BlitzError("payment failed",lnInvoiceString,e)
2020-05-23 13:49:13 +02:00
2020-05-24 13:11:31 +02:00
return response
2020-05-23 13:49:13 +02:00
2020-05-25 00:19:32 +02:00
####### PROCESS FUNCTIONS #########
def shopList(shopUrl):
print("#### GET HOSTS")
shopUrl=normalizeShopUrl(shopUrl)
return apiGetHosts(session, shopUrl)
def shopOrder(shopurl, hostid, toraddress, duration, msatsFirst):
print("#### PLACE ORDER")
shopUrl=normalizeShopUrl(shopUrl)
orderid = apiPlaceOrderNew(session, shopUrl, hostid, torTarget)
print("#### WAIT UNTIL INVOICE IS AVAILABLE")
loopCount=0
while True:
2020-05-25 19:18:34 +02:00
time.sleep(2)
2020-05-25 00:19:32 +02:00
loopCount+=1
print("# Loop {0}".format(loopCount))
order = apiGetOrder(session, shopUrl, orderid)
2020-05-25 19:18:34 +02:00
if len(order['ln_invoices']) > 0 and order['ln_invoices'][0]['payment_request'] is not None:
break
if loopCount > 30:
raise BlitzError("timeout on getting invoice", order)
2020-05-25 00:19:32 +02:00
# get data from now complete order
paymentRequestStr = order['ln_invoices'][0]['payment_request']
bridge_id = order['item_details'][0]['product']['id']
bridge_ip = order['item_details'][0]['product']['host']['ip']
bridge_port = order['item_details'][0]['product']['port']
print("#### DECODE INVOICE & CHECK)")
print("# invoice: {0}".format(paymentRequestStr))
paymentRequestDecoded = lndDecodeInvoice(paymentRequestStr)
if paymentRequestDecoded is None: sys.exit()
print("# amount as advertised: {0}".format(msatsFirst))
print("# amount in invoice is: {0}".format(paymentRequestDecoded.num_msat))
if msatsFirst < paymentRequestDecoded.num_msat:
2020-05-25 19:18:34 +02:00
raise BlitzError("invoice other amount than advertised", "advertised({0}) invoice({1})".format(msatsFirst, paymentRequestDecoded.num_msat))
2020-05-25 00:19:32 +02:00
print("#### PAY INVOICE")
payedInvoice = lndPayInvoice(paymentRequestStr)
print('# OK PAYMENT SENT')
print("#### CHECK IF BRIDGE IS READY")
loopCount=0
while True:
2020-05-25 19:18:34 +02:00
time.sleep(3)
2020-05-25 00:19:32 +02:00
loopCount+=1
print("## Loop {0}".format(loopCount))
bridge = apiGetBridgeStatus(session, shopUrl, bridge_id)
2020-05-25 19:18:34 +02:00
if bridge['status'] == "A":
break
2020-05-25 00:19:32 +02:00
if loopCount > 60:
2020-05-25 19:18:34 +02:00
raise BlitzError("timeout bridge not getting ready", bridge)
2020-05-25 00:19:32 +02:00
# get data from ready bride
bridge_suspendafter = bridge['suspend_after']
bridge_port = bridge['port']
print("#### CHECK IF DURATION DELIVERED AS PROMISED")
secondsDelivered=secondsLeft(parseDate(bridge_suspendafter))
print("# delivered({0}) promised({1})".format(secondsDelivered, duration))
if (secondsDelivered + 600) < duration:
2020-05-25 19:18:34 +02:00
bridge['contract_breached'] = True
bridge['warning'] = "delivered duration shorter than advertised"
else:
bridge['contract_breached'] = False
2020-05-25 00:19:32 +02:00
print("# OK - BRIDGE READY: {0}:{1} -> {2}".format(bridge_ip, bridge_port, torTarget))
2020-05-25 19:18:34 +02:00
return bridge
def subscriptionExtend(shopUrl, bridgeid, durationAdvertised, msatsFirst):
2020-05-25 00:19:32 +02:00
print("#### PLACE EXTENSION ORDER")
shopUrl=normalizeShopUrl(shopUrl)
orderid = apiPlaceOrderExtension(session, shopUrl, bridgeid)
print("#### WAIT UNTIL INVOICE IS AVAILABLE")
loopCount=0
while True:
2020-05-25 19:18:34 +02:00
time.sleep(2)
2020-05-25 00:19:32 +02:00
loopCount+=1
print("## Loop {0}".format(loopCount))
order = apiGetOrder(session, shopUrl, orderid)
2020-05-25 19:18:34 +02:00
if len(order['ln_invoices']) > 0 and order['ln_invoices'][0]['payment_request'] is not None:
break
if loopCount > 30:
raise BlitzError("timeout on getting invoice", order)
2020-05-25 00:19:32 +02:00
paymentRequestStr = order['ln_invoices'][0]['payment_request']
print("#### DECODE INVOICE & CHECK AMOUNT")
print("# invoice: {0}".format(paymentRequestStr))
paymentRequestDecoded = lndDecodeInvoice(paymentRequestStr)
if paymentRequestDecoded is None: sys.exit()
print("# amount as advertised: {0}".format(msatsNext))
print("# amount in invoice is: {0}".format(paymentRequestDecoded.num_msat))
if msatsNext < paymentRequestDecoded.num_msat:
2020-05-25 19:18:34 +02:00
raise BlitzError("invoice other amount than advertised", "advertised({0}) invoice({1})".format(msatsNext, paymentRequestDecoded.num_msat))
2020-05-25 00:19:32 +02:00
print("#### PAY INVOICE")
payedInvoice = lndPayInvoice(paymentRequestStr)
print("#### CHECK IF BRIDGE GOT EXTENDED")
loopCount=0
while True:
2020-05-25 19:18:34 +02:00
time.sleep(3)
2020-05-25 00:19:32 +02:00
loopCount+=1
print("## Loop {0}".format(loopCount))
bridge = apiGetBridgeStatus(session, shopUrl, bridgeid)
2020-05-25 19:18:34 +02:00
if bridge['suspend_after'] != bridge_suspendafter:
break
2020-05-25 00:19:32 +02:00
if loopCount > 60:
2020-05-25 19:18:34 +02:00
raise BlitzError("timeout on waiting for extending bridge", bridge)
2020-05-25 00:19:32 +02:00
print("#### CHECK IF DURATION DELIVERED AS PROMISED")
secondsLeftOld = secondsLeft(parseDate(bridge_suspendafter))
secondsLeftNew = secondsLeft(parseDate(bridge['suspend_after']))
secondsExtended = secondsLeftNew - secondsLeftOld
2020-05-25 19:18:34 +02:00
print("# secondsExtended({0}) promised({1})".format(secondsExtended, durationAdvertised))
if secondsExtended < durationAdvertised:
bridge['contract_breached'] = True
bridge['warning'] = "delivered duration shorter than advertised"
else:
bridge['contract_breached'] = False
2020-05-25 00:19:32 +02:00
print("# BRIDGE GOT EXTENDED: {0} -> {1}".format(bridge_suspendafter, bridge['suspend_after']))
2020-05-25 19:18:34 +02:00
return bridge
2020-05-23 13:49:13 +02:00
2020-05-25 19:18:34 +02:00
def menuMakeSubscription(blitzServiceName, torAddress, torPort):
2020-05-25 00:19:32 +02:00
2020-05-25 19:18:34 +02:00
torTarget = "{0}:{1}".format(torAddress, torPort)
2020-05-25 00:19:32 +02:00
2020-05-25 19:18:34 +02:00
############################
# PHASE 1: Enter Shop URL
2020-05-25 00:19:32 +02:00
shopurl = DEFAULT_SHOPURL
2020-05-25 01:10:03 +02:00
while True:
# input shop url
d = Dialog(dialog="dialog",autowidgetsize=True)
2020-05-25 02:47:19 +02:00
d.set_background_title("Select IP2TOR Bridge Shop (communication secured thru TOR)")
code, text = d.inputbox(
"Enter Address of a IP2TOR Shop (PRESS ENTER FOR DEFAULT):",
height=10, width=60, init=shopurl,
title="Shop Address")
2020-05-25 01:10:03 +02:00
# if user canceled
if code != d.OK: sys.exit(0)
# get host list from shop
2020-05-25 01:17:57 +02:00
shopurl = text
2020-05-25 01:27:16 +02:00
os.system('clear')
2020-05-25 19:18:34 +02:00
try:
hosts = shopList(shopurl)
except Exception as e:
2020-05-25 01:10:03 +02:00
# shopurl not working
2020-05-25 01:30:04 +02:00
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
Cannot reach a shop under that address.
Please check domain or cancel dialog.
2020-05-25 02:47:19 +02:00
''',title="ERROR")
2020-05-25 19:18:34 +02:00
else:
# when shop is empty
if len(hosts) == 0:
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
2020-05-25 02:47:19 +02:00
The shop has no available offers at the moment.
Try again later, enter another address or cancel.
''',title="ERROR")
2020-05-25 01:10:03 +02:00
# ok we got hosts - continue
2020-05-25 19:18:34 +02:00
else: break
###############################
# PHASE 2: SELECT SUBSCRIPTION
2020-05-25 00:19:32 +02:00
# create menu to select shop - TODO: also while loop list & detail until cancel or subscription
2020-05-25 01:10:03 +02:00
host=None
2020-05-25 00:19:32 +02:00
choices = []
for idx, hostEntry in enumerate(hosts):
2020-05-25 00:28:04 +02:00
choices.append( ("{0}".format(idx), "{0} ({1} hours, first: {2} sats, next: {3} sats)".format(hostEntry['name'].ljust(20), hostEntry['tor_bridge_duration_hours'], hostEntry['tor_bridge_price_initial_sats'], hostEntry['tor_bridge_price_extension_sats'])) )
2020-05-25 00:19:32 +02:00
2020-05-25 01:10:03 +02:00
while True:
2020-05-25 00:19:32 +02:00
2020-05-25 01:10:03 +02:00
# show menu with options
d = Dialog(dialog="dialog",autowidgetsize=True)
2020-05-25 02:47:19 +02:00
d.set_background_title("IP2TOR Bridge Shop: {0}".format(shopurl))
code, tag = d.menu(
"Following TOR bridge hosts are available. Select for details:",
choices=choices, title="Available Subscriptions")
2020-05-25 19:18:34 +02:00
# if user cancels
if code != d.OK: sys.exit(0)
2020-05-25 01:10:03 +02:00
# get data of selected
seletedIndex = int(tag)
2020-05-25 02:47:19 +02:00
host = hosts[seletedIndex]
2020-05-25 01:10:03 +02:00
2020-05-25 02:47:19 +02:00
# optimize content for display
2020-05-25 02:49:25 +02:00
if len(host['terms_of_service']) == 0: host['terms_of_service'] = "-"
if len(host['terms_of_service_url']) == 0: host['terms_of_service_url'] = "-"
2020-05-25 02:47:19 +02:00
2020-05-25 01:10:03 +02:00
# show details of selected
2020-05-25 02:47:19 +02:00
d = Dialog(dialog="dialog",autowidgetsize=True)
d.set_background_title("IP2TOR Bridge Offer Details: {0}".format(shopurl))
text='''
2020-05-25 03:19:38 +02:00
The subscription will renew every {0} hours.
The first time it will cost: {1} sats
Every next time it will cost: {2} sats
2020-05-25 02:47:19 +02:00
If you AGREE you will subscribe to this service.
2020-05-25 03:19:38 +02:00
You will get a port on the IP {3} that will
2020-05-25 02:47:19 +02:00
forward to your RaspiBlitz TOR address:
2020-05-25 03:19:38 +02:00
{4}
2020-05-25 02:47:19 +02:00
You can cancel the subscription anytime under
the "SUBSCRIPTONS" menu on your RaspiBlitz.
There will be no refunds for not used hours.
There is no guarantee for quality of service.
The service has the following additional terms:
2020-05-25 03:19:38 +02:00
{5}
2020-05-25 02:47:19 +02:00
More information on the service you can find under:
2020-05-25 03:19:38 +02:00
{6}
2020-05-25 02:47:19 +02:00
'''.format(
host['tor_bridge_duration_hours'],
host['tor_bridge_price_initial_sats'],
2020-05-25 03:15:58 +02:00
host['tor_bridge_price_extension_sats'],
2020-05-25 03:19:38 +02:00
host['ip'],
2020-05-25 19:18:34 +02:00
torTarget,
2020-05-25 03:13:31 +02:00
host['terms_of_service'],
2020-05-25 03:14:54 +02:00
host['terms_of_service_url'])
2020-05-25 00:19:32 +02:00
2020-05-25 04:08:16 +02:00
code = d.msgbox(text, title=host['name'], ok_label="Back", extra_button=True, extra_label="AGREE" ,width=60, height=30)
2020-05-25 03:44:30 +02:00
# if user AGREED break loop and continue with selected host
2020-05-25 03:46:19 +02:00
if code == "extra": break
2020-05-25 01:10:03 +02:00
2020-05-25 19:18:34 +02:00
############################
# PHASE 3: Make Subscription
try:
#bridge = shopOrder(shopurl, host['id'], torTarget, host['tor_bridge_duration'], host['tor_bridge_price_initial_sats'])
bridge=[]
bridge['contract_breached']=True
except BlitzError as be:
if be.errorShort == "timeout on waiting for extending bridge":
2020-05-25 01:10:03 +02:00
2020-05-25 19:18:34 +02:00
# error happend after payment
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
You DID PAY the initial fee.
But the service was not able to provide service.
Subscription will be ignored.
''',title="Error on Subscription")
sys.exit(1)
else:
# error happend before payment
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
You DID NOT PAY the initial fee.
The service was not able to provide service.
Subscription will be ignored.
Error: {0}
'''.format(be.errorShort),title="Error on Subscription")
sys.exit(1)
except Exception as e:
# unkown error happend
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
Unkown Error happend - please report to developers:
{0}
'''.format(str(e)),title="Exception on Subscription")
sys.exit(1)
# warn user if not delivered as advertised
if bridge['contract_breached']:
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
The service was payed & delivered, but RaspiBlitz detected:
{0}
You may want to consider to cancel the subscription later.
'''.format(bridge['warning'],title="Warning")
# TODO: persist subscription in list
# Give final result feedback to user
Dialog(dialog="dialog",autowidgetsize=True).msgbox('''
OK your service is ready & your subscription is active.
You payed {0} sats for the first {1} hours.
Next AUTOMATED PAYMENT will be {2} sats.
Your service '{3}' should now publicly be reachable under:
{4}:{5}
Please test now if the service is performing as promised.
If not - dont forget to cancel the subscription under:
MAIN MENU > SUBSCRIPTIONS > MY SUBSCRIPTIONS
'''.format(be.errorShort),title="Subscription Active")
####### COMMANDS #########
###############
# MENU
# use for ssh shell menu
###############
if sys.argv[1] == "menu":
# late imports - so that rest of script can run also if dependency is not available
from dialog import Dialog
menuMakeSubscription("RTL", "s7foqiwcstnxmlesfsjt7nlhwb2o6w44hc7glv474n7sbyckf76wn6id.onion", "80")
2020-05-24 13:11:31 +02:00
sys.exit()
2020-05-25 00:19:32 +02:00
###############
# SHOP LIST
# call from web interface
###############
if sys.argv[1] == "shop-list":
# check parameters
try:
shopurl = sys.argv[2]
except Exception as e:
2020-05-25 19:18:34 +02:00
handleException(e)
2020-05-25 00:19:32 +02:00
# get data
2020-05-25 19:18:34 +02:00
try:
hosts = shopList(shopurl)
except Exception as e:
handleException(e)
2020-05-25 00:19:32 +02:00
# output is json list of hosts
print(hosts)
sys.exit(0)
###############
# SHOP ORDER
# call from web interface
###############
if sys.argv[1] == "shop-order":
2020-05-25 19:18:34 +02:00
# check parameters
try:
shopurl = sys.argv[2]
hostid = sys.argv[3]
toraddress = sys.argv[4]
duration = sys.argv[5]
msats = sys.argv[6]
except Exception as e:
handleException(e)
2020-05-25 00:19:32 +02:00
2020-05-25 19:18:34 +02:00
# get data
try:
bridge = shopOrder(shopurl, hostid, toraddress, duration, msats)
except Exception as e:
handleException(e)
2020-05-25 00:19:32 +02:00
2020-05-25 19:18:34 +02:00
# TODO: persist subscription
2020-05-24 13:11:31 +02:00
2020-05-25 19:18:34 +02:00
# output json ordered bridge
print(bridge)
2020-05-24 13:46:47 +02:00
sys.exit()
2020-05-25 00:19:32 +02:00
2020-05-25 19:18:34 +02:00
#######################
# SUBSCRIPTIONS LIST
# call in intervalls from background process
#######################
if sys.argv[1] == "subscriptions-list":
try:
# TODO: JSON output of list with all subscrptions
print("TODO: implement")
except Exception as e:
handleException(e)
sys.exit(0)
2020-05-25 00:19:32 +02:00
#######################
# SUBSCRIPTIONS RENEW
# call in intervalls from background process
#######################
if sys.argv[1] == "subscriptions-renew":
2020-05-25 19:18:34 +02:00
# check parameters
try:
secondsBeforeSuspend = sys.argv[2]
if secondsBeforeSuspend < 0: secondsBeforeSuspend = 0
except Exception as e:
handleException(e)
2020-05-25 00:19:32 +02:00
# TODO: check if any active subscrpitions are below the secondsBeforeSuspend - if yes extend
2020-05-25 19:18:34 +02:00
print("TODO: implement")
sys.exit(1)
# get date
try:
bridge = subscriptionExtend(shopUrl, bridgeid, durationAdvertised, msatsFirst)
except Exception as e:
handleException(e)
# TODO: persist subscription
# output json ordered bridge
print(bridge)
sys.exit()
#######################
# SUBSCRIPTION CANCEL
# call in intervalls from background process
#######################
if sys.argv[1] == "subscription-cancel":
try:
# TODO: JSON output of list with all subscrptions
print("TODO: implement")
except Exception as e:
handleException(e)
sys.exit(0)
2020-05-25 00:19:32 +02:00
# unkown command
2020-05-25 19:18:34 +02:00
print("# unkown command")