From f5fc0766a1beb5644e5b278f063fd35e2955b936 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Mon, 27 Jan 2025 23:42:46 +1100 Subject: [PATCH 01/15] feat: Add batching plugin --- account.py | 50 ++++ plugins/batching.py | 435 +++++++++++++++++++++++++++++++++++ templates/components/tx.html | 3 +- 3 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 plugins/batching.py diff --git a/account.py b/account.py index 0844c8c..37a85c5 100644 --- a/account.py +++ b/account.py @@ -659,6 +659,56 @@ def revoke(account,domain): } } +def sendBatch(account, batch): + account_name = check_account(account) + password = ":".join(account.split(":")[1:]) + + if account_name == False: + return { + "error": { + "message": "Invalid account" + } + } + + try: + response = hsw.rpc_selectWallet(account_name) + if response['error'] is not None: + return { + "error": { + "message": response['error']['message'] + } + } + response = hsw.rpc_walletPassphrase(password,10) + if response['error'] is not None: + return { + "error": { + "message": response['error']['message'] + } + } + response = requests.post(f"http://x:{APIKEY}@{ip}:12039",json={ + "method": "sendbatch", + "params": [batch] + }).json() + if response['error'] is not None: + return { + "error": { + "message": response['error']['message'] + } + } + if 'result' not in response: + return { + "error": { + "message": "No result" + } + } + + return response['result'] + except Exception as e: + return { + "error": { + "message": str(e) + } + } #region settingsAPIs diff --git a/plugins/batching.py b/plugins/batching.py new file mode 100644 index 0000000..c2379f8 --- /dev/null +++ b/plugins/batching.py @@ -0,0 +1,435 @@ +import json +import account +import requests + + +# Plugin Data +info = { + "name": "Batching Functions", + "description": "This is a plugin that provides multiple functions to batch transactions", + "version": "1.0", + "author": "Nathan.Woodburn/" +} +# https://hsd-dev.org/api-docs/?shell--cli#sendbatch + + +# Functions +functions = { + "transfer":{ + "name": "Batch transfer", + "type": "default", + "description": "Transfer a ton of domains", + "params": { + "domains": { + "name":"List of domains to transfer (one per line)", + "type":"longText" + }, + "address": { + "name":"Address to transfer to", + "type":"address" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "finalize":{ + "name": "Batch finalize a transfer", + "type": "default", + "description": "Finalize transferring a ton of domains", + "params": { + "domains": { + "name":"List of domains to finalize (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "cancel":{ + "name": "Batch cancel a transfer", + "type": "default", + "description": "Cancel transferring a ton of domains", + "params": { + "domains": { + "name":"List of domains to cancel (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "open":{ + "name": "Batch open auctions", + "type": "default", + "description": "Open auctions for a ton of domains", + "params": { + "domains": { + "name":"List of domains to open (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "bid":{ + "name": "Batch bid on auctions", + "type": "default", + "description": "Bid on auctions for a ton of domains", + "params": { + "domains": { + "name":"List of domains to bid on (one per line)", + "type":"longText" + }, + "bid": { + "name":"Bid amount", + "type":"text" + }, + "blind": { + "name":"Blind amount", + "type":"text" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "reveal":{ + "name": "Batch reveal bids", + "type": "default", + "description": "Reveal bids for tons of auctions", + "params": { + "domains": { + "name":"List of domains to reveal (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "redeem":{ + "name": "Batch redeem bids", + "type": "default", + "description": "Redeem lost bids to get funds back", + "params": { + "domains": { + "name":"List of domains to redeem (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "register":{ + "name": "Batch register domains", + "type": "default", + "description": "Register domains won in auction", + "params": { + "domains": { + "name":"List of domains to redeem (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "advancedBid":{ + "name": "Bid on domains with csv", + "type": "default", + "description": "Bid on domains using a csv format", + "params": { + "bids": { + "name":"List of bids in format `domain,bid,blind` (one per line)", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, + "advancedBatch":{ + "name": "Batch transactions with csv", + "type": "default", + "description": "Batch transactions using a csv format", + "params": { + "transactions": { + "name":"List of transactions in format `type,domain,param1,param2` (one per line) Eg.
TRANSFER,woodburn1,hs1q4rkfe5df7ss6wzhnw388hv27we0hp7ha2np0hk
OPEN,woodburn2", + "type":"longText" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + } +} + +def sendBatch(batch, authentication): + response = account.sendBatch(authentication, batch) + return response + + +def transfer(params, authentication): + domains = params["domains"] + address = params["address"] + domains = domains.splitlines() + + wallet = authentication.split(":")[0] + owned = account.getDomains(wallet) + # Only keep owned domains ["name"] + ownedNames = [domain["name"] for domain in owned] + + for domain in domains: + if domain not in ownedNames: + return { + "status":f"Domain {domain} not owned", + "transaction":None + } + + batch = [] + for domain in domains: + batch.append(['TRANSFER', domain, address]) + + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } + +def simple(batchType,params, authentication): + domains = params["domains"] + domains = domains.splitlines() + + batch = [] + for domain in domains: + batch.append([batchType, domain]) + + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } + +def finalize(params, authentication): + return simple("FINALIZE",params,authentication) + +def cancel(params, authentication): + return simple("CANCEL",params,authentication) + +def open(params, authentication): + return simple("OPEN",params,authentication) + +def bid(params, authentication): + domains = params["domains"] + domains = domains.splitlines() + try: + bid = float(params["bid"]) + blind = float(params["blind"]) + blind+=bid + except: + return { + "status":"Invalid bid amount", + "transaction":None + } + + batch = [] + for domain in domains: + batch.append(['BID', domain, bid, blind]) + + print(batch) + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } + +def reveal(params, authentication): + return simple("REVEAL",params,authentication) + +def redeem(params, authentication): + return simple("REDEEM",params,authentication) + +def register(params, authentication): + domains = params["domains"] + domains = domains.splitlines() + batch = [] + for domain in domains: + batch.append(['UPDATE', domain,{"records": []}]) + + print(batch) + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } + + +def advancedBid(params, authentication): + bids = params["bids"] + bids = bids.splitlines() + + batch = [] + for bid in bids: + # Split the bid + line = bid.split(",") + domain = line[0] + bid = float(line[1]) + blind = float(line[2]) + blind+=bid + batch.append(['BID', domain, bid, blind]) + + print(batch) + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } + +def advancedBatch(params, authentication): + transactions = params["transactions"] + transactions = transactions.splitlines() + + batch = [] + for transaction in transactions: + # Split the bid + line = transaction.split(",") + line[0] = line[0].upper() + batch.append(line) + + print(batch) + response = sendBatch(batch, authentication) + if 'error' in response: + return { + "status":response['error']['message'], + "transaction":None + } + + return { + "status":"Sent batch successfully", + "transaction":response['hash'] + } diff --git a/templates/components/tx.html b/templates/components/tx.html index c1e0c1e..88f3575 100644 --- a/templates/components/tx.html +++ b/templates/components/tx.html @@ -1,4 +1,5 @@ TX: {{tx}} Check your transaction on a block explorer Niami -3xpl \ No newline at end of file +3xpl +Cymon.de \ No newline at end of file From 4b7b9f991b1f3ac0984e0f645687d6e2a5636d99 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 00:03:10 +1100 Subject: [PATCH 02/15] feat: Add batch renewals to batching plugin --- plugins/batching.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/batching.py b/plugins/batching.py index c2379f8..3a7321f 100644 --- a/plugins/batching.py +++ b/plugins/batching.py @@ -211,6 +211,28 @@ functions = { } } }, + "renew":{ + "name": "Batch renew domains", + "type": "default", + "description": "Renew a ton of domain", + "params": { + "domains": { + "name": "Domains to renew (one per line)", + "type": "longText" + } + }, + "returns": { + "status": { + "name": "Status", + "type": "text" + }, + "transaction": + { + "name": "Hash of the transaction", + "type": "tx" + } + } + }, "advancedBid":{ "name": "Bid on domains with csv", "type": "default", @@ -382,6 +404,8 @@ def register(params, authentication): "transaction":response['hash'] } +def renew(params, authentication): + return simple("RENEW", params, authentication) def advancedBid(params, authentication): bids = params["bids"] From 2b6447fd1207ec4fbbaf893b42f88ed397a5b59f Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 10:56:24 +1100 Subject: [PATCH 03/15] fix: Add rounding to bid display and strip batch inputs --- main.py | 15 ++++++++------- plugins/batching.py | 14 ++++++++++++++ render.py | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 7bf2f37..4d864ea 100644 --- a/main.py +++ b/main.py @@ -278,6 +278,8 @@ def auctions(): balance = account_module.getBalance(account) locked = balance['locked'] + # Round to 2 decimals + locked = round(locked, 2) # Add commas to the numbers locked = "{:,}".format(locked) @@ -289,7 +291,7 @@ def auctions(): # Sort sort = request.args.get("sort") if sort == None: - sort = "domain" + sort = "state" sort = sort.lower() sort_price = "" sort_price_next = "⬇" @@ -301,7 +303,10 @@ def auctions(): direction = request.args.get("direction") if direction == None: - direction = "⬇" + if sort == "state": + direction = "⬆" + else: + direction = "⬇" if direction == "⬆": reverse = True @@ -342,10 +347,6 @@ def auctions(): pending_reveals += 1 plugins = "" - # dashFunctions = plugins_module.getDashboardFunctions() - # for function in dashFunctions: - # functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{},request.cookies.get("account")) - # plugins += render.plugin_output_dash(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"])) message = '' if 'message' in request.args: @@ -377,7 +378,7 @@ def revealAllBids(): return redirect("/auctions?message=No reveals pending") return redirect("/auctions?message=" + response['error']['message']) - return redirect("/success?tx=" + response['hash']) + return redirect("/success?tx=" + response['result']['hash']) @app.route('/search') diff --git a/plugins/batching.py b/plugins/batching.py index 3a7321f..fa944c7 100644 --- a/plugins/batching.py +++ b/plugins/batching.py @@ -290,6 +290,8 @@ def transfer(params, authentication): domains = params["domains"] address = params["address"] domains = domains.splitlines() + domains = [x.strip() for x in domains] + domains = [x for x in domains if x != ""] wallet = authentication.split(":")[0] owned = account.getDomains(wallet) @@ -322,6 +324,8 @@ def transfer(params, authentication): def simple(batchType,params, authentication): domains = params["domains"] domains = domains.splitlines() + domains = [x.strip() for x in domains] + domains = [x for x in domains if x != ""] batch = [] for domain in domains: @@ -351,6 +355,9 @@ def open(params, authentication): def bid(params, authentication): domains = params["domains"] domains = domains.splitlines() + domains = [x.strip() for x in domains] + domains = [x for x in domains if x != ""] + try: bid = float(params["bid"]) blind = float(params["blind"]) @@ -387,6 +394,9 @@ def redeem(params, authentication): def register(params, authentication): domains = params["domains"] domains = domains.splitlines() + domains = [x.strip() for x in domains] + domains = [x for x in domains if x != ""] + batch = [] for domain in domains: batch.append(['UPDATE', domain,{"records": []}]) @@ -410,6 +420,8 @@ def renew(params, authentication): def advancedBid(params, authentication): bids = params["bids"] bids = bids.splitlines() + bids = [x.strip() for x in bids] + bids = [x for x in bids if x != ""] batch = [] for bid in bids: @@ -437,6 +449,8 @@ def advancedBid(params, authentication): def advancedBatch(params, authentication): transactions = params["transactions"] transactions = transactions.splitlines() + transactions = [x.strip() for x in transactions] + transactions = [x for x in transactions if x != ""] batch = [] for transaction in transactions: diff --git a/render.py b/render.py index 2229394..81fe830 100644 --- a/render.py +++ b/render.py @@ -189,6 +189,7 @@ def bidDomains(bids,domains, sortState=False): bidValue = round(bidValue, 2) blind = lockup - bidValue bidValue = "{:,}".format(bidValue) + blind = round(blind, 2) blind = "{:,}".format(blind) bidDisplay = f'{bidValue} HNS + {blind} HNS blind' From d39f433738e341e4008f35a3a5705db7890f6cce Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 11:09:08 +1100 Subject: [PATCH 04/15] fix: Add debugging to simple batches --- plugins/batching.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/batching.py b/plugins/batching.py index fa944c7..309fdd5 100644 --- a/plugins/batching.py +++ b/plugins/batching.py @@ -331,6 +331,7 @@ def simple(batchType,params, authentication): for domain in domains: batch.append([batchType, domain]) + print(batch) response = sendBatch(batch, authentication) if 'error' in response: return { From f7968fc21886c119903e61cb5b8e6daa2cc4983e Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 13:20:44 +1100 Subject: [PATCH 05/15] feat: Add change lookahead to plugin --- account.py | 6 +----- plugins/batching.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/account.py b/account.py index 37a85c5..5408164 100644 --- a/account.py +++ b/account.py @@ -690,11 +690,7 @@ def sendBatch(account, batch): "params": [batch] }).json() if response['error'] is not None: - return { - "error": { - "message": response['error']['message'] - } - } + return response if 'result' not in response: return { "error": { diff --git a/plugins/batching.py b/plugins/batching.py index 309fdd5..4d24414 100644 --- a/plugins/batching.py +++ b/plugins/batching.py @@ -1,6 +1,8 @@ import json import account import requests +import os + # Plugin Data @@ -278,6 +280,24 @@ functions = { "type": "tx" } } + }, + "advancedChangeLookahead":{ + "name": "Change wallet lookahead", + "type": "default", + "description": "Change the lookahead of the wallet", + "params": { + "lookahead": { + "name":"Lookahead (default 200)", + "type":"number" + } + }, + "returns": { + "status": + { + "name": "Status", + "type": "text" + } + } } } @@ -334,6 +354,7 @@ def simple(batchType,params, authentication): print(batch) response = sendBatch(batch, authentication) if 'error' in response: + print(response) return { "status":response['error']['message'], "transaction":None @@ -472,3 +493,30 @@ def advancedBatch(params, authentication): "status":"Sent batch successfully", "transaction":response['hash'] } + + +def advancedChangeLookahead(params, authentication): + lookahead = params["lookahead"] + lookahead = int(lookahead) + wallet = authentication.split(":")[0] + password = ":".join(authentication.split(":")[1:]) + # curl http://x:api-key@127.0.0.1:14039/wallet/$id/account/$name \ + # -X PATCH \ + # --data '{"lookahead": $lookahead}' + + APIKEY = os.getenv("hsd_api") + ip = os.getenv("hsd_ip") + if ip is None: + ip = "localhost" + + # Unlock wallet + response = requests.post(f"http://x:{APIKEY}@{ip}:12039/wallet/{wallet}/unlock", + json={"passphrase": password, "timeout": 10}) + + response = requests.patch(f"http://x:{APIKEY}@{ip}:12039/wallet/{wallet}/account/default", + json={"lookahead": lookahead}) + + + return { + "status":f"Status: {'Success' if response.status_code == 200 else 'Error'}" + } \ No newline at end of file From 2b895a524a746d71a412c5d5fa15157a7c0df083 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 16:16:26 +1100 Subject: [PATCH 06/15] fix: Stop bids without known blind from causing crashes --- account.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/account.py b/account.py index 5408164..9ecb02e 100644 --- a/account.py +++ b/account.py @@ -402,11 +402,16 @@ def getWalletStatus(): def getBids(account, domain="NONE"): if domain == "NONE": - return hsw.getWalletBids(account) - - - response = hsw.getWalletBidsByName(domain,account) - return response + response = hsw.getWalletBids(account) + else: + response = hsw.getWalletBidsByName(domain,account) + # Add backup for bids with no value + bids = [] + for bid in response: + if 'value' not in bid: + bid['value'] = -1000000 + bids.append(bid) + return bids def getReveals(account,domain): return hsw.getWalletRevealsByName(domain,account) From 35d3ccd0c0d2bb6429ff6e0ba94781df3ff4942a Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 16:16:46 +1100 Subject: [PATCH 07/15] feat: Sort bids by block to make it more intuitive --- main.py | 15 +++++++++++---- render.py | 1 + templates/auctions.html | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 4d864ea..badcda9 100644 --- a/main.py +++ b/main.py @@ -291,7 +291,7 @@ def auctions(): # Sort sort = request.args.get("sort") if sort == None: - sort = "state" + sort = "time" sort = sort.lower() sort_price = "" sort_price_next = "⬇" @@ -299,11 +299,13 @@ def auctions(): sort_state_next = "⬇" sort_domain = "" sort_domain_next = "⬇" + sort_time = "" + sort_time_next = "⬇" reverse = False direction = request.args.get("direction") if direction == None: - if sort == "state": + if sort == "time": direction = "⬆" else: direction = "⬇" @@ -320,6 +322,10 @@ def auctions(): sort_state = direction sort_state_next = reverseDirection(direction) domains = sorted(domains, key=lambda k: k['state'],reverse=reverse) + elif sort == "time": + sort_time = direction + sort_time_next = reverseDirection(direction) + bids = sorted(bids, key=lambda k: k['height'],reverse=reverse) else: # Sort by domain bids = sorted(bids, key=lambda k: k['name'],reverse=reverse) @@ -358,7 +364,8 @@ def auctions(): sort_price=sort_price,sort_state=sort_state, sort_domain=sort_domain,sort_price_next=sort_price_next, sort_state_next=sort_state_next,sort_domain_next=sort_domain_next, - bids=len(bids),reveal=pending_reveals,message=message) + bids=len(bids),reveal=pending_reveals,message=message, + sort_time=sort_time,sort_time_next=sort_time_next) @app.route('/reveal') def revealAllBids(): @@ -1092,7 +1099,7 @@ def settings_action(action): resp = account_module.rescan() if 'error' in resp: return redirect("/settings?error=" + str(resp['error'])) - return redirect("/settings?success=Resent transactions") + return redirect("/settings?success=Rescan started") elif action == "resend": resp = account_module.resendTXs() if 'error' in resp: diff --git a/render.py b/render.py index 81fe830..1629e6e 100644 --- a/render.py +++ b/render.py @@ -199,6 +199,7 @@ def bidDomains(bids,domains, sortState=False): html += f"{domain['name']}" html += f"{domain['state']}" html += f"{bidDisplay}" + html += f"{bid['height']}" html += "" else: for domain in domains: diff --git a/templates/auctions.html b/templates/auctions.html index b02626d..4e3caec 100644 --- a/templates/auctions.html +++ b/templates/auctions.html @@ -125,6 +125,7 @@ Domain{{sort_domain}} State{{sort_state}} Bid{{sort_price}} + Block{{sort_time}} From 5c61bad9a211f053c77cd1b798e18cf3d97a06a2 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 16:26:24 +1100 Subject: [PATCH 08/15] fix: Update renewal plugin to get IP from env --- plugins/batching.py | 4 ---- plugins/renewal.py | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/batching.py b/plugins/batching.py index 4d24414..e1770b2 100644 --- a/plugins/batching.py +++ b/plugins/batching.py @@ -500,10 +500,6 @@ def advancedChangeLookahead(params, authentication): lookahead = int(lookahead) wallet = authentication.split(":")[0] password = ":".join(authentication.split(":")[1:]) - # curl http://x:api-key@127.0.0.1:14039/wallet/$id/account/$name \ - # -X PATCH \ - # --data '{"lookahead": $lookahead}' - APIKEY = os.getenv("hsd_api") ip = os.getenv("hsd_ip") if ip is None: diff --git a/plugins/renewal.py b/plugins/renewal.py index ac4ef44..349fa8f 100644 --- a/plugins/renewal.py +++ b/plugins/renewal.py @@ -51,10 +51,11 @@ def main(params, authentication): # Unlock wallet api_key = os.getenv("hsd_api") + ip = os.getenv("hsd_ip") if api_key is None: print("API key not set") return {"status": "API key not set", "transaction": "None"} - response = requests.post(f'http://x:{api_key}@127.0.0.1:12039/wallet/{wallet}/unlock', + response = requests.post(f'http://x:{api_key}@{ip}:12039/wallet/{wallet}/unlock', json={'passphrase': password, 'timeout': 600}) if response.status_code != 200: print("Failed to unlock wallet") @@ -73,7 +74,7 @@ def main(params, authentication): batchTX = "[" + ", ".join(batch) + "]" responseContent = f'{{"method": "sendbatch","params":[ {batchTX} ]}}' - response = requests.post(f'http://x:{api_key}@127.0.0.1:12039', data=responseContent) + response = requests.post(f'http://x:{api_key}@{ip}:12039', data=responseContent) if response.status_code != 200: print("Failed to create batch") print(f'Status code: {response.status_code}') From 8c61a09e5b80de8bb89fe99cc9fcdadcbc1f285f Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 16:30:30 +1100 Subject: [PATCH 09/15] fix: Add more logging for batch errors --- plugins/renewal.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/renewal.py b/plugins/renewal.py index 349fa8f..fe9b2d8 100644 --- a/plugins/renewal.py +++ b/plugins/renewal.py @@ -76,9 +76,9 @@ def main(params, authentication): responseContent = f'{{"method": "sendbatch","params":[ {batchTX} ]}}' response = requests.post(f'http://x:{api_key}@{ip}:12039', data=responseContent) if response.status_code != 200: - print("Failed to create batch") - print(f'Status code: {response.status_code}') - print(f'Response: {response.text}') + print("Failed to create batch",flush=True) + print(f'Status code: {response.status_code}',flush=True) + print(f'Response: {response.text}',flush=True) return {"status": "Failed", "transaction": "None"} batch = response.json() @@ -86,8 +86,8 @@ def main(params, authentication): print("Verifying tx...") if batch["error"]: if batch["error"] != "": - print("Failed to verify batch") - print(batch["error"]["message"]) + print("Failed to verify batch",flush=True) + print(batch["error"]["message"],flush=True) return {"status": "Failed", "transaction": "None"} if 'result' in batch: From cfb814d006dca9737f1635773f73660eccec9ffc Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 16:46:06 +1100 Subject: [PATCH 10/15] feat: Add public info and troubleshooting modules --- plugins/public_info.py | 41 ++++ plugins/renewal.py | 2 +- plugins/troubleshooting.py | 237 +++++++++++++++++++++ templates/components/dashboard-plugin.html | 2 +- 4 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 plugins/public_info.py create mode 100644 plugins/troubleshooting.py diff --git a/plugins/public_info.py b/plugins/public_info.py new file mode 100644 index 0000000..54012da --- /dev/null +++ b/plugins/public_info.py @@ -0,0 +1,41 @@ +import json +import account +import requests + +# Plugin Data +info = { + "name": "Public Node Dashboard", + "description": "Dashboard modules for public nodes", + "version": "1.0", + "author": "Nathan.Woodburn/" +} + +# Functions +functions = { + "main":{ + "name": "Info Dashboard widget", + "type": "dashboard", + "description": "This creates the widget that shows on the dashboard", + "params": {}, + "returns": { + "status": + { + "name": "Status of Node", + "type": "text" + } + } + } +} + +def main(params, authentication): + info = account.hsd.getInfo() + + status = f"Version: {info['version']}
Inbound Connections: {info['pool']['inbound']}
Outbound Connections: {info['pool']['outbound']}
" + if info['pool']['public']['listen']: + status += f"Public Node: Yes
Host: {info['pool']['public']['host']}
Port: {info['pool']['public']['port']}
" + else: + status += f"Public Node: No
" + status += f"Agent: {info['pool']['agent']}
Services: {info['pool']['services']}
" + + return {"status": status} + \ No newline at end of file diff --git a/plugins/renewal.py b/plugins/renewal.py index fe9b2d8..9a9f850 100644 --- a/plugins/renewal.py +++ b/plugins/renewal.py @@ -88,7 +88,7 @@ def main(params, authentication): if batch["error"] != "": print("Failed to verify batch",flush=True) print(batch["error"]["message"],flush=True) - return {"status": "Failed", "transaction": "None"} + return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"} if 'result' in batch: if batch['result'] != None: diff --git a/plugins/troubleshooting.py b/plugins/troubleshooting.py new file mode 100644 index 0000000..a5325e8 --- /dev/null +++ b/plugins/troubleshooting.py @@ -0,0 +1,237 @@ +import json +import account +import requests + +import dns.resolver +import dns.message +import dns.query +import dns.rdatatype +import dns.rrset +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +import tempfile +import subprocess +import binascii +import datetime +import dns.asyncresolver +import httpx +from requests_doh import DNSOverHTTPSSession, add_dns_provider +import domainLookup + +doh_url = "https://hnsdoh.com/dns-query" + +# Plugin Data +info = { + "name": "Troubleshooting", + "description": "Various troubleshooting functions", + "version": "1.0", + "author": "Nathan.Woodburn/" +} + +# Functions +functions = { + "dig":{ + "name": "DNS Lookup", + "type": "default", + "description": "Do DNS lookups on a domain", + "params": { + "domain": { + "name":"Domain to lookup (eg. woodburn)", + "type":"text" + }, + "type": { + "name":"Type of lookup (A,TXT,NS,DS,TLSA)", + "type":"text" + } + }, + "returns": { + "result": + { + "name": "Result", + "type": "list" + } + } + }, + "https_check":{ + "name": "HTTPS Check", + "type": "default", + "description": "Check if a domain has an HTTPS certificate", + "params": { + "domain": { + "name":"Domain to lookup (eg. woodburn)", + "type":"text" + } + }, + "returns": { + "result": + { + "name": "Result", + "type": "text" + } + } + }, + "hip_lookup": { + "name": "Hip Lookup", + "type": "default", + "description": "Look up a domain's hip address", + "params": { + "domain": { + "name": "Domain to lookup", + "type": "text" + } + }, + "returns": { + "result": { + "name": "Result", + "type": "text" + } + } + } +} + +def dns_request(domain: str, rType:str) -> list[dns.rrset.RRset]: + if rType == "": + rType = "A" + rType = dns.rdatatype.from_text(rType.upper()) + + + with httpx.Client() as client: + q = dns.message.make_query(domain, rType) + r = dns.query.https(q, doh_url, session=client) + return r.answer + + +def dig(params, authentication): + domain = params["domain"] + type = params["type"] + result: list[dns.rrset.RRset] = dns_request(domain, type) + print(result) + if result: + if len(result) == 1: + result: dns.rrset.RRset = result[0] + result = result.items + return {"result": result} + + else: + return {"result": result} + else: + return {"result": ["No result"]} + + + +def https_check(params, authentication): + domain = params["domain"] + domain_check = False + try: + # Get the IP + ip = list(dns_request(domain,"A")[0].items.keys()) + if len(ip) == 0: + return {"result": "No IP found"} + ip = ip[0] + print(ip) + + # Run the openssl s_client command + s_client_command = ["openssl","s_client","-showcerts","-connect",f"{ip}:443","-servername",domain,] + + s_client_process = subprocess.Popen(s_client_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + s_client_output, _ = s_client_process.communicate(input=b"\n") + + certificates = [] + current_cert = "" + for line in s_client_output.split(b"\n"): + current_cert += line.decode("utf-8") + "\n" + if "-----END CERTIFICATE-----" in line.decode("utf-8"): + certificates.append(current_cert) + current_cert = "" + + # Remove anything before -----BEGIN CERTIFICATE----- + certificates = [cert[cert.find("-----BEGIN CERTIFICATE-----"):] for cert in certificates] + + if certificates: + cert = certificates[0] + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_cert_file: + temp_cert_file.write(cert) + temp_cert_file.seek(0) # Move back to the beginning of the temporary file + + tlsa_command = ["openssl","x509","-in",temp_cert_file.name,"-pubkey","-noout","|","openssl","pkey","-pubin","-outform","der","|","openssl","dgst","-sha256","-binary",] + + tlsa_process = subprocess.Popen(" ".join(tlsa_command), shell=True, stdout=subprocess.PIPE) + tlsa_output, _ = tlsa_process.communicate() + + tlsa_server = "3 1 1 " + binascii.hexlify(tlsa_output).decode("utf-8") + print(f"TLSA Server: {tlsa_server}") + + + # Get domains + cert_obj = x509.load_pem_x509_certificate(cert.encode("utf-8"), default_backend()) + + domains = [] + for ext in cert_obj.extensions: + if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: + san_list = ext.value.get_values_for_type(x509.DNSName) + domains.extend(san_list) + + # Extract the common name (CN) from the subject + common_name = cert_obj.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) + if common_name: + if common_name[0].value not in domains: + domains.append(common_name[0].value) + + if domains: + if domain in domains: + domain_check = True + else: + # Check if matching wildcard domain exists + for d in domains: + if d.startswith("*"): + if domain.split(".")[1:] == d.split(".")[1:]: + domain_check = True + break + + + expiry_date = cert_obj.not_valid_after + # Check if expiry date is past + if expiry_date < datetime.datetime.now(): + return {"result": "Certificate is expired"} + else: + return {"result": "No certificate found"} + + try: + # Check for TLSA record + tlsa = dns_request(f"_443._tcp.{domain}","TLSA") + tlsa = list(tlsa[0].items.keys()) + if len(tlsa) == 0: + return {"result": "No TLSA record found"} + tlsa = tlsa[0] + print(f"TLSA: {tlsa}") + + if not tlsa: + return {"result": "TLSA lookup failed"} + else: + if tlsa_server == str(tlsa): + if domain_check: + add_dns_provider("HNSDoH", "https://hnsdoh.com/dns-query") + + session = DNSOverHTTPSSession("HNSDoH") + r = session.get(f"https://{domain}/",verify=False) + if r.status_code != 200: + return {"result": "Webserver returned status code: " + str(r.status_code)} + return {"result": "HTTPS check successful"} + else: + return {"result": "TLSA record matches certificate, but domain does not match certificate"} + + else: + return {"result": "TLSA record does not match certificate"} + + except Exception as e: + return {"result": "TLSA lookup failed with error: " + str(e)} + + # Catch all exceptions + except Exception as e: + return {"result": "Lookup failed.

Error: " + str(e)} + +def hip_lookup(params, authentication): + domain = params["domain"] + hip = domainLookup.hip2(domain) + return {"result": hip} \ No newline at end of file diff --git a/templates/components/dashboard-plugin.html b/templates/components/dashboard-plugin.html index c6fdb75..12aeb15 100644 --- a/templates/components/dashboard-plugin.html +++ b/templates/components/dashboard-plugin.html @@ -2,7 +2,7 @@
{{name}}
-
{{output}}
+
{{output | safe}}
\ No newline at end of file From 2ae618c68a1af24b6ca84dabbebcc00350633aca Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 28 Jan 2025 17:26:59 +1100 Subject: [PATCH 11/15] feat: Add custom plugins --- .gitignore | 3 +- main.py | 21 ++-- plugin.py | 64 ++++++++-- plugins/customPlugins.py | 114 ++++++++++++++++++ plugins/public_info.py | 41 ------- plugins/troubleshooting.py | 237 ------------------------------------- templates/plugin.html | 2 +- 7 files changed, 184 insertions(+), 298 deletions(-) create mode 100644 plugins/customPlugins.py delete mode 100644 plugins/public_info.py delete mode 100644 plugins/troubleshooting.py diff --git a/.gitignore b/.gitignore index 33a7451..01a374d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ plugins/signatures.json .venv/ -user_data/ \ No newline at end of file +user_data/ +customPlugins/ \ No newline at end of file diff --git a/main.py b/main.py index badcda9..239615b 100644 --- a/main.py +++ b/main.py @@ -1300,8 +1300,8 @@ def plugins_index(): wallet_status=account_module.getWalletStatus(), plugins=plugins) -@app.route('/plugin/') -def plugin(plugin): +@app.route('/plugin//') +def plugin(ptype,plugin): # Check if the user is logged in if request.cookies.get("account") is None: return redirect("/login") @@ -1310,7 +1310,10 @@ def plugin(plugin): if not account: return redirect("/logout") + plugin = f"{ptype}/{plugin}" + if not plugins_module.pluginExists(plugin): + print(f"Plugin {plugin} not found") return redirect("/plugins") data = plugins_module.getPluginData(plugin) @@ -1330,10 +1333,10 @@ def plugin(plugin): wallet_status=account_module.getWalletStatus(), name=data['name'],description=data['description'], author=data['author'],version=data['version'], - functions=functions,error=error) + source=data['source'],functions=functions,error=error) -@app.route('/plugin//verify') -def plugin_verify(plugin): +@app.route('/plugin///verify') +def plugin_verify(ptype,plugin): # Check if the user is logged in if request.cookies.get("account") is None: return redirect("/login") @@ -1341,6 +1344,8 @@ def plugin_verify(plugin): account = account_module.check_account(request.cookies.get("account")) if not account: return redirect("/logout") + + plugin = f"{ptype}/{plugin}" if not plugins_module.pluginExists(plugin): return redirect("/plugins") @@ -1352,8 +1357,8 @@ def plugin_verify(plugin): return redirect("/plugin/" + plugin) -@app.route('/plugin//', methods=["POST"]) -def plugin_function(plugin,function): +@app.route('/plugin///', methods=["POST"]) +def plugin_function(ptype,plugin,function): # Check if the user is logged in if request.cookies.get("account") is None: return redirect("/login") @@ -1362,6 +1367,8 @@ def plugin_function(plugin,function): if not account: return redirect("/logout") + plugin = f"{ptype}/{plugin}" + if not plugins_module.pluginExists(plugin): return redirect("/plugins") diff --git a/plugin.py b/plugin.py index c2d5edc..6230c9a 100644 --- a/plugin.py +++ b/plugin.py @@ -3,10 +3,12 @@ import json import importlib import sys import hashlib +import subprocess def listPlugins(): plugins = [] + customPlugins = [] for file in os.listdir("plugins"): if file.endswith(".py"): if file != "main.py": @@ -14,9 +16,40 @@ def listPlugins(): if "info" not in dir(plugin): continue details = plugin.info - details["link"] = file[:-3] + details["source"] = "built-in" + details["link"] = f"plugins/{file[:-3]}" plugins.append(details) + # Check for imported plugins + if not os.path.exists("user_data/plugins.json"): + with open("user_data/plugins.json", "w") as f: + json.dump([], f) + + with open("user_data/plugins.json", "r") as f: + importurls = json.load(f) + + for importurl in importurls: + # Get only repo name + importPath = importurl.split("/")[-1].removesuffix(".git") + + # Git clone into customPlugins/ + if not os.path.exists(f"customPlugins/{importPath}"): + os.system(f"git clone {importurl} customPlugins/{importPath}") + else: + os.system(f"cd customPlugins/{importPath} && git pull") + + # Import plugins from customPlugins/ + for file in os.listdir(f"customPlugins/{importPath}"): + if file.endswith(".py"): + if file != "main.py": + plugin = importlib.import_module(f"customPlugins.{importPath}."+file[:-3]) + if "info" not in dir(plugin): + continue + details = plugin.info + details["source"] = importPath + details["link"] = f"customPlugins/{importPath}/{file[:-3]}" + plugins.append(details) + # Verify plugin signature signatures = [] try: @@ -39,10 +72,7 @@ def listPlugins(): def pluginExists(plugin: str): - for file in os.listdir("plugins"): - if file == plugin+".py": - return True - return False + return os.path.exists(plugin+".py") def verifyPlugin(plugin: str): @@ -66,7 +96,7 @@ def verifyPlugin(plugin: str): def hashPlugin(plugin: str): BUF_SIZE = 65536 sha256 = hashlib.sha256() - with open("plugins/"+plugin+".py", 'rb') as f: + with open(plugin+".py", 'rb') as f: while True: data = f.read(BUF_SIZE) if not data: @@ -76,7 +106,7 @@ def hashPlugin(plugin: str): def getPluginData(pluginStr: str): - plugin = importlib.import_module("plugins."+pluginStr) + plugin = importlib.import_module(pluginStr.replace("/",".")) # Check if the plugin is verified signatures = [] @@ -89,6 +119,18 @@ def getPluginData(pluginStr: str): json.dump(signatures, f) info = plugin.info + info["source"] = "built-in" + + # Check if the plugin is in customPlugins + if pluginStr.startswith("customPlugins"): + # Get git url for dir + print(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin") + url = subprocess.check_output(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin", shell=True).decode("utf-8").strip() + info["source"] = url + + + + # Hash the plugin file pluginHash = hashPlugin(pluginStr) if pluginHash not in signatures: @@ -100,12 +142,12 @@ def getPluginData(pluginStr: str): def getPluginFunctions(plugin: str): - plugin = importlib.import_module("plugins."+plugin) + plugin = importlib.import_module(plugin.replace("/",".")) return plugin.functions def runPluginFunction(plugin: str, function: str, params: dict, authentication: str): - plugin_module = importlib.import_module("plugins."+plugin) + plugin_module = importlib.import_module(plugin.replace("/",".")) if function not in plugin_module.functions: return {"error": "Function not found"} @@ -141,12 +183,12 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication: def getPluginFunctionInputs(plugin: str, function: str): - plugin = importlib.import_module("plugins."+plugin) + plugin = importlib.import_module(plugin.replace("/",".")) return plugin.functions[function]["params"] def getPluginFunctionReturns(plugin: str, function: str): - plugin = importlib.import_module("plugins."+plugin) + plugin = importlib.import_module(plugin.replace("/",".")) return plugin.functions[function]["returns"] diff --git a/plugins/customPlugins.py b/plugins/customPlugins.py new file mode 100644 index 0000000..1b9058d --- /dev/null +++ b/plugins/customPlugins.py @@ -0,0 +1,114 @@ +import json +import account +import requests +import os + +# Plugin Data +info = { + "name": "Custom Plugin Manager", + "description": "Import custom plugins from git repositories", + "version": "1.0", + "author": "Nathan.Woodburn/" +} + +# Functions +functions = { + "add":{ + "name": "Add Plugin repo", + "type": "default", + "description": "Add a plugin repo", + "params": { + "url": { + "name":"URL", + "type":"text" + } + }, + "returns": { + "status": + { + "name": "Status of the function", + "type": "text" + } + } + }, + "remove":{ + "name": "Remove Plugins", + "type": "default", + "description": "Remove a plugin repo from the list", + "params": { + "url": { + "name":"URL", + "type":"text" + } + }, + "returns": { + "status": + { + "name": "Status of the function", + "type": "text" + } + } + }, + "list":{ + "name": "List Plugins", + "type": "default", + "description": "List all imported plugins", + "params": {}, + "returns": { + "plugins": + { + "name": "List of plugins", + "type": "list" + } + } + } +} + +def add(params, authentication): + url = params["url"] + if not os.path.exists("user_data/plugins.json"): + with open("user_data/plugins.json", "w") as f: + json.dump([], f) + + with open("user_data/plugins.json", "r") as f: + importurls = json.load(f) + + # Check if the plugin is already imported + if url in importurls: + return {"status": "Plugin already imported"} + + importurls.append(url) + with open("user_data/plugins.json", "w") as f: + json.dump(importurls, f) + + return {"status": "Imported"} + + +def remove(params, authentication): + url = params["url"] + if not os.path.exists("user_data/plugins.json"): + with open("user_data/plugins.json", "w") as f: + json.dump([], f) + + with open("user_data/plugins.json", "r") as f: + importurls = json.load(f) + + # Check if the plugin is already imported + if url not in importurls: + return {"status": "Plugin not imported"} + + importurls.remove(url) + with open("user_data/plugins.json", "w") as f: + json.dump(importurls, f) + + return {"status": "Removed"} + +def list(params, authentication): + if not os.path.exists("user_data/plugins.json"): + with open("user_data/plugins.json", "w") as f: + json.dump([], f) + + with open("user_data/plugins.json", "r") as f: + importurls = json.load(f) + + return {"plugins": importurls} \ No newline at end of file diff --git a/plugins/public_info.py b/plugins/public_info.py deleted file mode 100644 index 54012da..0000000 --- a/plugins/public_info.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -import account -import requests - -# Plugin Data -info = { - "name": "Public Node Dashboard", - "description": "Dashboard modules for public nodes", - "version": "1.0", - "author": "Nathan.Woodburn/" -} - -# Functions -functions = { - "main":{ - "name": "Info Dashboard widget", - "type": "dashboard", - "description": "This creates the widget that shows on the dashboard", - "params": {}, - "returns": { - "status": - { - "name": "Status of Node", - "type": "text" - } - } - } -} - -def main(params, authentication): - info = account.hsd.getInfo() - - status = f"Version: {info['version']}
Inbound Connections: {info['pool']['inbound']}
Outbound Connections: {info['pool']['outbound']}
" - if info['pool']['public']['listen']: - status += f"Public Node: Yes
Host: {info['pool']['public']['host']}
Port: {info['pool']['public']['port']}
" - else: - status += f"Public Node: No
" - status += f"Agent: {info['pool']['agent']}
Services: {info['pool']['services']}
" - - return {"status": status} - \ No newline at end of file diff --git a/plugins/troubleshooting.py b/plugins/troubleshooting.py deleted file mode 100644 index a5325e8..0000000 --- a/plugins/troubleshooting.py +++ /dev/null @@ -1,237 +0,0 @@ -import json -import account -import requests - -import dns.resolver -import dns.message -import dns.query -import dns.rdatatype -import dns.rrset -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -import tempfile -import subprocess -import binascii -import datetime -import dns.asyncresolver -import httpx -from requests_doh import DNSOverHTTPSSession, add_dns_provider -import domainLookup - -doh_url = "https://hnsdoh.com/dns-query" - -# Plugin Data -info = { - "name": "Troubleshooting", - "description": "Various troubleshooting functions", - "version": "1.0", - "author": "Nathan.Woodburn/" -} - -# Functions -functions = { - "dig":{ - "name": "DNS Lookup", - "type": "default", - "description": "Do DNS lookups on a domain", - "params": { - "domain": { - "name":"Domain to lookup (eg. woodburn)", - "type":"text" - }, - "type": { - "name":"Type of lookup (A,TXT,NS,DS,TLSA)", - "type":"text" - } - }, - "returns": { - "result": - { - "name": "Result", - "type": "list" - } - } - }, - "https_check":{ - "name": "HTTPS Check", - "type": "default", - "description": "Check if a domain has an HTTPS certificate", - "params": { - "domain": { - "name":"Domain to lookup (eg. woodburn)", - "type":"text" - } - }, - "returns": { - "result": - { - "name": "Result", - "type": "text" - } - } - }, - "hip_lookup": { - "name": "Hip Lookup", - "type": "default", - "description": "Look up a domain's hip address", - "params": { - "domain": { - "name": "Domain to lookup", - "type": "text" - } - }, - "returns": { - "result": { - "name": "Result", - "type": "text" - } - } - } -} - -def dns_request(domain: str, rType:str) -> list[dns.rrset.RRset]: - if rType == "": - rType = "A" - rType = dns.rdatatype.from_text(rType.upper()) - - - with httpx.Client() as client: - q = dns.message.make_query(domain, rType) - r = dns.query.https(q, doh_url, session=client) - return r.answer - - -def dig(params, authentication): - domain = params["domain"] - type = params["type"] - result: list[dns.rrset.RRset] = dns_request(domain, type) - print(result) - if result: - if len(result) == 1: - result: dns.rrset.RRset = result[0] - result = result.items - return {"result": result} - - else: - return {"result": result} - else: - return {"result": ["No result"]} - - - -def https_check(params, authentication): - domain = params["domain"] - domain_check = False - try: - # Get the IP - ip = list(dns_request(domain,"A")[0].items.keys()) - if len(ip) == 0: - return {"result": "No IP found"} - ip = ip[0] - print(ip) - - # Run the openssl s_client command - s_client_command = ["openssl","s_client","-showcerts","-connect",f"{ip}:443","-servername",domain,] - - s_client_process = subprocess.Popen(s_client_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) - s_client_output, _ = s_client_process.communicate(input=b"\n") - - certificates = [] - current_cert = "" - for line in s_client_output.split(b"\n"): - current_cert += line.decode("utf-8") + "\n" - if "-----END CERTIFICATE-----" in line.decode("utf-8"): - certificates.append(current_cert) - current_cert = "" - - # Remove anything before -----BEGIN CERTIFICATE----- - certificates = [cert[cert.find("-----BEGIN CERTIFICATE-----"):] for cert in certificates] - - if certificates: - cert = certificates[0] - - with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_cert_file: - temp_cert_file.write(cert) - temp_cert_file.seek(0) # Move back to the beginning of the temporary file - - tlsa_command = ["openssl","x509","-in",temp_cert_file.name,"-pubkey","-noout","|","openssl","pkey","-pubin","-outform","der","|","openssl","dgst","-sha256","-binary",] - - tlsa_process = subprocess.Popen(" ".join(tlsa_command), shell=True, stdout=subprocess.PIPE) - tlsa_output, _ = tlsa_process.communicate() - - tlsa_server = "3 1 1 " + binascii.hexlify(tlsa_output).decode("utf-8") - print(f"TLSA Server: {tlsa_server}") - - - # Get domains - cert_obj = x509.load_pem_x509_certificate(cert.encode("utf-8"), default_backend()) - - domains = [] - for ext in cert_obj.extensions: - if ext.oid == x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME: - san_list = ext.value.get_values_for_type(x509.DNSName) - domains.extend(san_list) - - # Extract the common name (CN) from the subject - common_name = cert_obj.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) - if common_name: - if common_name[0].value not in domains: - domains.append(common_name[0].value) - - if domains: - if domain in domains: - domain_check = True - else: - # Check if matching wildcard domain exists - for d in domains: - if d.startswith("*"): - if domain.split(".")[1:] == d.split(".")[1:]: - domain_check = True - break - - - expiry_date = cert_obj.not_valid_after - # Check if expiry date is past - if expiry_date < datetime.datetime.now(): - return {"result": "Certificate is expired"} - else: - return {"result": "No certificate found"} - - try: - # Check for TLSA record - tlsa = dns_request(f"_443._tcp.{domain}","TLSA") - tlsa = list(tlsa[0].items.keys()) - if len(tlsa) == 0: - return {"result": "No TLSA record found"} - tlsa = tlsa[0] - print(f"TLSA: {tlsa}") - - if not tlsa: - return {"result": "TLSA lookup failed"} - else: - if tlsa_server == str(tlsa): - if domain_check: - add_dns_provider("HNSDoH", "https://hnsdoh.com/dns-query") - - session = DNSOverHTTPSSession("HNSDoH") - r = session.get(f"https://{domain}/",verify=False) - if r.status_code != 200: - return {"result": "Webserver returned status code: " + str(r.status_code)} - return {"result": "HTTPS check successful"} - else: - return {"result": "TLSA record matches certificate, but domain does not match certificate"} - - else: - return {"result": "TLSA record does not match certificate"} - - except Exception as e: - return {"result": "TLSA lookup failed with error: " + str(e)} - - # Catch all exceptions - except Exception as e: - return {"result": "Lookup failed.

Error: " + str(e)} - -def hip_lookup(params, authentication): - domain = params["domain"] - hip = domainLookup.hip2(domain) - return {"result": hip} \ No newline at end of file diff --git a/templates/plugin.html b/templates/plugin.html index ca9291f..ab82fe2 100644 --- a/templates/plugin.html +++ b/templates/plugin.html @@ -67,7 +67,7 @@

{{name}}

{{description}}

-
Author: {{author}}
Version: {{version}}
{{functions|safe}} +
Author: {{author}}
Version: {{version}}
Source: {{source}}
{{functions|safe}}