From 39845238fc0ef671b2f54bdb231302afb041505d Mon Sep 17 00:00:00 2001 From: dsc Date: Wed, 7 Apr 2021 01:24:46 +0200 Subject: [PATCH] Add receivePIN and lookupPIN commands for Wowlet VR --- wowlet_backend/factory.py | 2 +- wowlet_backend/tasks/rpc_nodes.py | 2 + wowlet_backend/utils.py | 4 ++ wowlet_backend/wsparse.py | 108 ++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/wowlet_backend/factory.py b/wowlet_backend/factory.py index 188d47f..a1f1633 100644 --- a/wowlet_backend/factory.py +++ b/wowlet_backend/factory.py @@ -26,7 +26,7 @@ async def _setup_nodes(app: Quart): global cache with open('data/nodes.json', 'r') as f: nodes = json.loads(f.read()).get(settings.COIN_SYMBOL) - cache.execute('JSON.SET', 'nodes', '.', json.dumps(nodes)) + await cache.set('nodes', json.dumps(nodes).encode()) async def _setup_user_agents(app: Quart): diff --git a/wowlet_backend/tasks/rpc_nodes.py b/wowlet_backend/tasks/rpc_nodes.py index 889122a..13a070e 100644 --- a/wowlet_backend/tasks/rpc_nodes.py +++ b/wowlet_backend/tasks/rpc_nodes.py @@ -32,6 +32,8 @@ class RPCNodeCheckTask(FeatherTask): heights = {} rpc_nodes = await self.cache_json_get("nodes") + if not rpc_nodes: + rpc_nodes = {} nodes = [] for network_type_coin, _ in rpc_nodes.items(): diff --git a/wowlet_backend/utils.py b/wowlet_backend/utils.py index ba48afc..151c742 100644 --- a/wowlet_backend/utils.py +++ b/wowlet_backend/utils.py @@ -2,6 +2,7 @@ # Copyright (c) 2020, The Monero Project. # Copyright (c) 2020, dsc@xmr.pm +import re import json import asyncio import os @@ -20,6 +21,9 @@ from PIL import Image import settings +RE_ADDRESS = r"^[a-zA-Z0-9]{97}$" + + def print_banner(): print(f"""\033[91m _______________ |*\_/*|________ diff --git a/wowlet_backend/wsparse.py b/wowlet_backend/wsparse.py index 2f160e2..6c72dda 100644 --- a/wowlet_backend/wsparse.py +++ b/wowlet_backend/wsparse.py @@ -2,12 +2,31 @@ # Copyright (c) 2020, The Monero Project. # Copyright (c) 2020, dsc@xmr.pm +import random +import time +import json +from datetime import datetime, timedelta +from typing import Dict, Union, Optional +from copy import deepcopy +import asyncio +import re + +from wowlet_backend.utils import RE_ADDRESS + + +PIN_SPACE_AMOUNT = list(range(1, 10000)) +PIN_CODES = {str(k): None for k in PIN_SPACE_AMOUNT} + class WebsocketParse: @staticmethod async def parser(cmd: str, data=None): if cmd == "txFiatHistory": return await WebsocketParse.txFiatHistory(data) + elif cmd == "requestPIN": + return await WebsocketParse.requestPIN(data) + elif cmd == "lookupPIN": + return await WebsocketParse.lookupPIN(data) @staticmethod async def txFiatHistory(data=None): @@ -23,3 +42,92 @@ class WebsocketParse: from wowlet_backend.tasks.historical_prices import HistoricalPriceTask return await HistoricalPriceTask.get(year, month) + + @staticmethod + async def requestPIN(data=None) -> str: + from wowlet_backend.factory import cache + if not data or not isinstance(data, dict): + return "" + if "address" not in data or not isinstance(data['address'], str): + return "" + if "signature" not in data or not isinstance(data['signature'], str): + return "" + signature = data.get('signature') + address = data.get('address') + if not re.match(RE_ADDRESS, address) or not signature.startswith("Sig"): + return "" + + cache_key_address = f"pin_{address}" + cache_key_lookups = "pin_space" + ttl = 600 + + lock = asyncio.Lock() + async with lock: + result = await cache.get(cache_key_address) + if result: + return result.decode().zfill(4) + + lookups = await cache.get(cache_key_lookups) + if not lookups: + await cache.set(cache_key_lookups, json.dumps(PIN_CODES).encode()) + lookups: Dict[str, Optional[dict]] = PIN_CODES + else: + lookups: Dict[str, Optional[dict]] = json.loads(lookups) + + space = deepcopy(PIN_SPACE_AMOUNT) + random.shuffle(space) + now = int(time.time()) + + for number in space: + _blob = lookups[str(number)] + if _blob: + until = _blob.get('until') + if now > until: + _blob = None # expired, mark as writeable + + if not _blob: + valid_until = int(time.time()) + ttl + lookups[str(number)] = { + "address": address, + "until": valid_until + } + + await cache.setex(cache_key_address, ttl, number) + await cache.set(cache_key_lookups, json.dumps(lookups).encode()) + return str(number).zfill(4) + return "" + + @staticmethod + async def lookupPIN(data=None) -> dict: + from wowlet_backend.factory import cache + if not data or not isinstance(data, dict): + return {} + if "PIN" not in data or not isinstance(data['PIN'], str) or not len(data['PIN']) == 4: + return {} + PIN = data['PIN'] + if not re.match("^\d{4}$", PIN): + return {} + + cache_key_lookups = "pin_space" + lookups = await cache.get(cache_key_lookups) + if not lookups: + await cache.set(cache_key_lookups, json.dumps(PIN_CODES).encode()) + lookups: Dict[str, Optional[dict]] = PIN_CODES + else: + lookups: Dict[str, Optional[dict]] = json.loads(lookups) + + blob = lookups.get(str(int(PIN))) + if not blob: + return {"address": "", "PIN": PIN} # undefined behavior + + address = blob.get('address') + until = blob.get('until') + + now = int(time.time()) + if now > until: + return {"address": "", "PIN": PIN} # entry expired + + return { + "address": address, + "PIN": PIN + }