mirror of
https://github.com/basicswap/basicswap.git
synced 2024-11-16 15:58:17 +00:00
ui: Add websocket notifications.
This commit is contained in:
parent
6cc54d9c61
commit
1601a57aed
21 changed files with 830 additions and 215 deletions
|
@ -34,8 +34,10 @@ RUN cd basicswap-master; \
|
|||
RUN useradd -ms /bin/bash swap_user && \
|
||||
mkdir /coindata && chown swap_user -R /coindata
|
||||
|
||||
# Expose html port
|
||||
# html port
|
||||
EXPOSE 12700
|
||||
# websocket port
|
||||
EXPOSE 11700
|
||||
|
||||
VOLUME /coindata
|
||||
|
||||
|
|
|
@ -129,7 +129,9 @@ from .basicswap_util import (
|
|||
replaceAddrPrefix,
|
||||
getOfferProofOfFundsHash,
|
||||
getLastBidState,
|
||||
isActiveBidState)
|
||||
isActiveBidState,
|
||||
NotificationTypes as NT,
|
||||
)
|
||||
from .protocols.xmr_swap_1 import (
|
||||
addLockRefundSigs,
|
||||
recoverNoScriptTxnWithKey)
|
||||
|
@ -200,6 +202,8 @@ class WatchedTransaction():
|
|||
|
||||
|
||||
class BasicSwap(BaseApp):
|
||||
ws_server = None
|
||||
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
|
||||
super().__init__(fp, data_dir, settings, chain, log_name)
|
||||
|
||||
|
@ -920,6 +924,25 @@ class BasicSwap(BaseApp):
|
|||
if (coin_from == Coins.PART_BLIND or coin_to == Coins.PART_BLIND) and swap_type != SwapTypes.XMR_SWAP:
|
||||
raise ValueError('Invalid swap type for PART_BLIND')
|
||||
|
||||
def notify(self, event_type, event_data):
|
||||
if event_type == NT.OFFER_RECEIVED:
|
||||
self.log.debug('Received new offer %s', event_data['offer_id'])
|
||||
if self.ws_server:
|
||||
event_data['event'] = 'new_offer'
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_RECEIVED:
|
||||
self.log.info('Received valid bid %s for %s offer %s', event_data['bid_id'], event_data['type'], event_data['offer_id'])
|
||||
if self.ws_server:
|
||||
event_data['event'] = 'new_bid'
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
elif event_type == NT.BID_ACCEPTED:
|
||||
self.log.info('Received valid bid accept for %s', event_data['bid_id'])
|
||||
if self.ws_server:
|
||||
event_data['event'] = 'bid_accepted'
|
||||
self.ws_server.send_message_to_all(json.dumps(event_data))
|
||||
else:
|
||||
self.log.warning(f'Unknown notification {event_type}')
|
||||
|
||||
def validateOfferAmounts(self, coin_from, coin_to, amount, rate, min_bid_amount):
|
||||
ci_from = self.ci(coin_from)
|
||||
ci_to = self.ci(coin_to)
|
||||
|
@ -3625,6 +3648,8 @@ class BasicSwap(BaseApp):
|
|||
try:
|
||||
self.receiveXmrBidAccept(bid, session)
|
||||
except Exception as ex:
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
self.log.info('Verify xmr bid accept {} failed: {}'.format(bid.bid_id.hex(), str(ex)))
|
||||
bid.setState(BidStates.BID_ERROR, 'Failed accept validation: ' + str(ex))
|
||||
session.add(bid)
|
||||
|
@ -3738,7 +3763,7 @@ class BasicSwap(BaseApp):
|
|||
|
||||
session.add(xmr_offer)
|
||||
|
||||
self.log.debug('Received new offer %s', offer_id.hex())
|
||||
self.notify(NT.OFFER_RECEIVED, {'offer_id': offer_id.hex()})
|
||||
else:
|
||||
existing_offer.setState(OfferStates.OFFER_RECEIVED)
|
||||
session.add(existing_offer)
|
||||
|
@ -3972,8 +3997,8 @@ class BasicSwap(BaseApp):
|
|||
|
||||
bid.setState(BidStates.BID_RECEIVED)
|
||||
|
||||
self.log.info('Received valid bid %s for offer %s', bid_id.hex(), bid_data.offer_msg_id.hex())
|
||||
self.saveBid(bid_id, bid)
|
||||
self.notify(NT.BID_RECEIVED, {'type': 'atomic', 'bid_id': bid_id.hex(), 'offer_id': bid_data.offer_msg_id.hex()})
|
||||
|
||||
if self.shouldAutoAcceptBid(offer, bid):
|
||||
delay = random.randrange(self.min_delay_event, self.max_delay_event)
|
||||
|
@ -4048,10 +4073,11 @@ class BasicSwap(BaseApp):
|
|||
bid.setState(BidStates.BID_ACCEPTED)
|
||||
bid.setITxState(TxStates.TX_NONE)
|
||||
|
||||
self.log.info('Received valid bid accept %s for bid %s', bid.accept_msg_id.hex(), bid_id.hex())
|
||||
bid.offer_id.hex()
|
||||
|
||||
self.saveBid(bid_id, bid)
|
||||
self.swaps_in_progress[bid_id] = (bid, offer)
|
||||
self.notify(NT.BID_ACCEPTED, {'bid_id': bid_id.hex()})
|
||||
|
||||
def receiveXmrBid(self, bid, session):
|
||||
self.log.debug('Receiving xmr bid %s', bid.bid_id.hex())
|
||||
|
@ -4091,7 +4117,7 @@ class BasicSwap(BaseApp):
|
|||
ensure(ci_to.verifyKey(xmr_swap.vkbvf), 'Invalid key, vkbvf')
|
||||
ensure(ci_from.verifyPubkey(xmr_swap.pkaf), 'Invalid pubkey, pkaf')
|
||||
|
||||
self.log.info('Received valid bid %s for xmr offer %s', bid.bid_id.hex(), bid.offer_id.hex())
|
||||
self.notify(NT.BID_RECEIVED, {'type': 'xmr', 'bid_id': bid.bid_id.hex(), 'offer_id': bid.offer_id.hex()})
|
||||
|
||||
bid.setState(BidStates.BID_RECEIVED)
|
||||
|
||||
|
@ -4153,6 +4179,7 @@ class BasicSwap(BaseApp):
|
|||
|
||||
bid.setState(BidStates.BID_ACCEPTED) # XMR
|
||||
self.saveBidInSession(bid.bid_id, bid, session, xmr_swap)
|
||||
self.notify(NT.BID_ACCEPTED, {'bid_id': bid.bid_id.hex()})
|
||||
|
||||
delay = random.randrange(self.min_delay_event, self.max_delay_event)
|
||||
self.log.info('Responding to xmr bid accept %s in %d seconds', bid.bid_id.hex(), delay)
|
||||
|
|
|
@ -190,6 +190,13 @@ def strOfferState(state):
|
|||
return 'Unknown'
|
||||
|
||||
|
||||
class NotificationTypes(IntEnum):
|
||||
NONE = 0
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
|
||||
|
||||
def strBidState(state):
|
||||
if state == BidStates.BID_SENT:
|
||||
return 'Sent'
|
||||
|
|
1
basicswap/contrib/websocket_server/__init__.py
Normal file
1
basicswap/contrib/websocket_server/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .websocket_server import *
|
38
basicswap/contrib/websocket_server/thread.py
Normal file
38
basicswap/contrib/websocket_server/thread.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import threading
|
||||
|
||||
|
||||
class ThreadWithLoggedException(threading.Thread):
|
||||
"""
|
||||
Similar to Thread but will log exceptions to passed logger.
|
||||
|
||||
Args:
|
||||
logger: Logger instance used to log any exception in child thread
|
||||
|
||||
Exception is also reachable via <thread>.exception from the main thread.
|
||||
"""
|
||||
|
||||
DIVIDER = "*"*80
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.logger = kwargs.pop("logger")
|
||||
except KeyError:
|
||||
raise Exception("Missing 'logger' in kwargs")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exception = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self._target is not None:
|
||||
self._target(*self._args, **self._kwargs)
|
||||
except Exception as exception:
|
||||
thread = threading.current_thread()
|
||||
self.exception = exception
|
||||
self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}")
|
||||
finally:
|
||||
del self._target, self._args, self._kwargs
|
||||
|
||||
|
||||
class WebsocketServerThread(ThreadWithLoggedException):
|
||||
"""Dummy wrapper to make debug messages a bit more readable"""
|
||||
pass
|
495
basicswap/contrib/websocket_server/websocket_server.py
Normal file
495
basicswap/contrib/websocket_server/websocket_server.py
Normal file
|
@ -0,0 +1,495 @@
|
|||
# Author: Johan Hanssen Seferidis
|
||||
# License: MIT
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import ssl
|
||||
from base64 import b64encode
|
||||
from hashlib import sha1
|
||||
import logging
|
||||
from socket import error as SocketError
|
||||
import errno
|
||||
import threading
|
||||
from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
|
||||
|
||||
from .thread import WebsocketServerThread
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
|
||||
'''
|
||||
+-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||
|I|S|S|S| (4) |A| (7) | (16/64) |
|
||||
|N|V|V|V| |S| | (if payload len==126/127) |
|
||||
| |1|2|3| |K| | |
|
||||
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
||||
| Extended payload length continued, if payload len == 127 |
|
||||
+ - - - - - - - - - - - - - - - +-------------------------------+
|
||||
| Payload Data continued ... |
|
||||
+---------------------------------------------------------------+
|
||||
'''
|
||||
|
||||
FIN = 0x80
|
||||
OPCODE = 0x0f
|
||||
MASKED = 0x80
|
||||
PAYLOAD_LEN = 0x7f
|
||||
PAYLOAD_LEN_EXT16 = 0x7e
|
||||
PAYLOAD_LEN_EXT64 = 0x7f
|
||||
|
||||
OPCODE_CONTINUATION = 0x0
|
||||
OPCODE_TEXT = 0x1
|
||||
OPCODE_BINARY = 0x2
|
||||
OPCODE_CLOSE_CONN = 0x8
|
||||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xA
|
||||
|
||||
CLOSE_STATUS_NORMAL = 1000
|
||||
DEFAULT_CLOSE_REASON = bytes('', encoding='utf-8')
|
||||
|
||||
|
||||
class API():
|
||||
|
||||
def run_forever(self, threaded=False):
|
||||
return self._run_forever(threaded)
|
||||
|
||||
def new_client(self, client, server):
|
||||
pass
|
||||
|
||||
def client_left(self, client, server):
|
||||
pass
|
||||
|
||||
def message_received(self, client, server, message):
|
||||
pass
|
||||
|
||||
def set_fn_new_client(self, fn):
|
||||
self.new_client = fn
|
||||
|
||||
def set_fn_client_left(self, fn):
|
||||
self.client_left = fn
|
||||
|
||||
def set_fn_message_received(self, fn):
|
||||
self.message_received = fn
|
||||
|
||||
def send_message(self, client, msg):
|
||||
self._unicast(client, msg)
|
||||
|
||||
def send_message_to_all(self, msg):
|
||||
self._multicast(msg)
|
||||
|
||||
def deny_new_connections(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
self._deny_new_connections(status, reason)
|
||||
|
||||
def allow_new_connections(self):
|
||||
self._allow_new_connections()
|
||||
|
||||
def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
self._shutdown_gracefully(status, reason)
|
||||
|
||||
def shutdown_abruptly(self):
|
||||
self._shutdown_abruptly()
|
||||
|
||||
def disconnect_clients_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
self._disconnect_clients_gracefully(status, reason)
|
||||
|
||||
def disconnect_clients_abruptly(self):
|
||||
self._disconnect_clients_abruptly()
|
||||
|
||||
|
||||
class WebsocketServer(ThreadingMixIn, TCPServer, API):
|
||||
"""
|
||||
A websocket server waiting for clients to connect.
|
||||
|
||||
Args:
|
||||
port(int): Port to bind to
|
||||
host(str): Hostname or IP to listen for connections. By default 127.0.0.1
|
||||
is being used. To accept connections from any client, you should use
|
||||
0.0.0.0.
|
||||
loglevel: Logging level from logging module to use for logging. By default
|
||||
warnings and errors are being logged.
|
||||
|
||||
Properties:
|
||||
clients(list): A list of connected clients. A client is a dictionary
|
||||
like below.
|
||||
{
|
||||
'id' : id,
|
||||
'handler' : handler,
|
||||
'address' : (addr, port)
|
||||
}
|
||||
"""
|
||||
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True # comment to keep threads alive until finished
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=0, loglevel=logging.WARNING, key=None, cert=None):
|
||||
logger.setLevel(loglevel)
|
||||
TCPServer.__init__(self, (host, port), WebSocketHandler)
|
||||
self.host = host
|
||||
self.port = self.socket.getsockname()[1]
|
||||
self.url = f'ws://{self.host}:{self.port}/'
|
||||
|
||||
self.key = key
|
||||
self.cert = cert
|
||||
|
||||
self.clients = []
|
||||
self.id_counter = 0
|
||||
self.thread = None
|
||||
|
||||
self._deny_clients = False
|
||||
|
||||
def _run_forever(self, threaded):
|
||||
cls_name = self.__class__.__name__
|
||||
try:
|
||||
logger.info("Listening on port %d for clients.." % self.port)
|
||||
if threaded:
|
||||
self.daemon = True
|
||||
self.thread = WebsocketServerThread(target=super().serve_forever, daemon=True, logger=logger)
|
||||
if sys.version_info[0] > 3 or (sys.version_info[0] == 3 and sys.version_info[1] >= 10):
|
||||
logger.info(f"Starting {cls_name} on thread {self.thread.name}.")
|
||||
else:
|
||||
logger.info(f"Starting {cls_name} on thread {self.thread.getName()}.")
|
||||
self.thread.start()
|
||||
else:
|
||||
self.thread = threading.current_thread()
|
||||
logger.info(f"Starting {cls_name} on main thread.")
|
||||
super().serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
self.server_close()
|
||||
logger.info("Server terminated.")
|
||||
except Exception as e:
|
||||
logger.error(str(e), exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
def _message_received_(self, handler, msg):
|
||||
self.message_received(self.handler_to_client(handler), self, msg)
|
||||
|
||||
def _ping_received_(self, handler, msg):
|
||||
handler.send_pong(msg)
|
||||
|
||||
def _pong_received_(self, handler, msg):
|
||||
pass
|
||||
|
||||
def _new_client_(self, handler):
|
||||
if self._deny_clients:
|
||||
status = self._deny_clients["status"]
|
||||
reason = self._deny_clients["reason"]
|
||||
handler.send_close(status, reason)
|
||||
self._terminate_client_handler(handler)
|
||||
return
|
||||
|
||||
self.id_counter += 1
|
||||
client = {
|
||||
'id': self.id_counter,
|
||||
'handler': handler,
|
||||
'address': handler.client_address
|
||||
}
|
||||
self.clients.append(client)
|
||||
self.new_client(client, self)
|
||||
|
||||
def _client_left_(self, handler):
|
||||
client = self.handler_to_client(handler)
|
||||
self.client_left(client, self)
|
||||
if client in self.clients:
|
||||
self.clients.remove(client)
|
||||
|
||||
def _unicast(self, receiver_client, msg):
|
||||
receiver_client['handler'].send_message(msg)
|
||||
|
||||
def _multicast(self, msg):
|
||||
for client in self.clients:
|
||||
self._unicast(client, msg)
|
||||
|
||||
def handler_to_client(self, handler):
|
||||
for client in self.clients:
|
||||
if client['handler'] == handler:
|
||||
return client
|
||||
|
||||
def _terminate_client_handler(self, handler):
|
||||
handler.keep_alive = False
|
||||
handler.finish()
|
||||
handler.connection.close()
|
||||
|
||||
def _terminate_client_handlers(self):
|
||||
"""
|
||||
Ensures request handler for each client is terminated correctly
|
||||
"""
|
||||
for client in self.clients:
|
||||
self._terminate_client_handler(client["handler"])
|
||||
|
||||
def _shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
"""
|
||||
Send a CLOSE handshake to all connected clients before terminating server
|
||||
"""
|
||||
self.keep_alive = False
|
||||
self._disconnect_clients_gracefully(status, reason)
|
||||
self.server_close()
|
||||
self.shutdown()
|
||||
|
||||
def _shutdown_abruptly(self):
|
||||
"""
|
||||
Terminate server without sending a CLOSE handshake
|
||||
"""
|
||||
self.keep_alive = False
|
||||
self._disconnect_clients_abruptly()
|
||||
self.server_close()
|
||||
self.shutdown()
|
||||
|
||||
def _disconnect_clients_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
"""
|
||||
Terminate clients gracefully without shutting down the server
|
||||
"""
|
||||
for client in self.clients:
|
||||
client["handler"].send_close(status, reason)
|
||||
self._terminate_client_handlers()
|
||||
|
||||
def _disconnect_clients_abruptly(self):
|
||||
"""
|
||||
Terminate clients abruptly (no CLOSE handshake) without shutting down the server
|
||||
"""
|
||||
self._terminate_client_handlers()
|
||||
|
||||
def _deny_new_connections(self, status, reason):
|
||||
self._deny_clients = {
|
||||
"status": status,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
def _allow_new_connections(self):
|
||||
self._deny_clients = False
|
||||
|
||||
|
||||
class WebSocketHandler(StreamRequestHandler):
|
||||
|
||||
def __init__(self, socket, addr, server):
|
||||
self.server = server
|
||||
self.timeout = 1000 # Must set a timeout or rfile.read timesout in the tests
|
||||
assert not hasattr(self, "_send_lock"), "_send_lock already exists"
|
||||
self._send_lock = threading.Lock()
|
||||
if server.key and server.cert:
|
||||
try:
|
||||
socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key)
|
||||
except: # Not sure which exception it throws if the key/cert isn't found
|
||||
logger.warning("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert))
|
||||
StreamRequestHandler.__init__(self, socket, addr, server)
|
||||
|
||||
def setup(self):
|
||||
StreamRequestHandler.setup(self)
|
||||
self.keep_alive = True
|
||||
self.handshake_done = False
|
||||
self.valid_client = False
|
||||
|
||||
def handle(self):
|
||||
while self.keep_alive:
|
||||
if not self.handshake_done:
|
||||
self.handshake()
|
||||
elif self.valid_client:
|
||||
self.read_next_message()
|
||||
|
||||
def read_bytes(self, num):
|
||||
return self.rfile.read(num)
|
||||
|
||||
def read_next_message(self):
|
||||
try:
|
||||
b1, b2 = self.read_bytes(2)
|
||||
except TimeoutError:
|
||||
return
|
||||
except SocketError as e: # to be replaced with ConnectionResetError for py3
|
||||
if e.errno == errno.ECONNRESET:
|
||||
logger.info("Client closed connection.")
|
||||
self.keep_alive = 0
|
||||
return
|
||||
b1, b2 = 0, 0
|
||||
except ValueError as e:
|
||||
b1, b2 = 0, 0
|
||||
|
||||
fin = b1 & FIN
|
||||
opcode = b1 & OPCODE
|
||||
masked = b2 & MASKED
|
||||
payload_length = b2 & PAYLOAD_LEN
|
||||
|
||||
if opcode == OPCODE_CLOSE_CONN:
|
||||
logger.info("Client asked to close connection.")
|
||||
self.keep_alive = 0
|
||||
return
|
||||
if not masked:
|
||||
logger.warning("Client must always be masked.")
|
||||
self.keep_alive = 0
|
||||
return
|
||||
if opcode == OPCODE_CONTINUATION:
|
||||
logger.warning("Continuation frames are not supported.")
|
||||
return
|
||||
elif opcode == OPCODE_BINARY:
|
||||
logger.warning("Binary frames are not supported.")
|
||||
return
|
||||
elif opcode == OPCODE_TEXT:
|
||||
opcode_handler = self.server._message_received_
|
||||
elif opcode == OPCODE_PING:
|
||||
opcode_handler = self.server._ping_received_
|
||||
elif opcode == OPCODE_PONG:
|
||||
opcode_handler = self.server._pong_received_
|
||||
else:
|
||||
logger.warning("Unknown opcode %#x." % opcode)
|
||||
self.keep_alive = 0
|
||||
return
|
||||
|
||||
if payload_length == 126:
|
||||
payload_length = struct.unpack(">H", self.rfile.read(2))[0]
|
||||
elif payload_length == 127:
|
||||
payload_length = struct.unpack(">Q", self.rfile.read(8))[0]
|
||||
|
||||
masks = self.read_bytes(4)
|
||||
message_bytes = bytearray()
|
||||
for message_byte in self.read_bytes(payload_length):
|
||||
message_byte ^= masks[len(message_bytes) % 4]
|
||||
message_bytes.append(message_byte)
|
||||
opcode_handler(self, message_bytes.decode('utf8'))
|
||||
|
||||
def send_message(self, message):
|
||||
self.send_text(message)
|
||||
|
||||
def send_pong(self, message):
|
||||
self.send_text(message, OPCODE_PONG)
|
||||
|
||||
def send_close(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON):
|
||||
"""
|
||||
Send CLOSE to client
|
||||
|
||||
Args:
|
||||
status: Status as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1
|
||||
reason: Text with reason of closing the connection
|
||||
"""
|
||||
if status < CLOSE_STATUS_NORMAL or status > 1015:
|
||||
raise Exception(f"CLOSE status must be between 1000 and 1015, got {status}")
|
||||
|
||||
header = bytearray()
|
||||
payload = struct.pack('!H', status) + reason
|
||||
payload_length = len(payload)
|
||||
assert payload_length <= 125, "We only support short closing reasons at the moment"
|
||||
|
||||
# Send CLOSE with status & reason
|
||||
header.append(FIN | OPCODE_CLOSE_CONN)
|
||||
header.append(payload_length)
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def send_text(self, message, opcode=OPCODE_TEXT):
|
||||
"""
|
||||
Important: Fragmented(=continuation) messages are not supported since
|
||||
their usage cases are limited - when we don't know the payload length.
|
||||
"""
|
||||
|
||||
# Validate message
|
||||
if isinstance(message, bytes):
|
||||
message = try_decode_UTF8(message) # this is slower but ensures we have UTF-8
|
||||
if not message:
|
||||
logger.warning("Can\'t send message, message is not valid UTF-8")
|
||||
return False
|
||||
elif not isinstance(message, str):
|
||||
logger.warning('Can\'t send message, message has to be a string or bytes. Got %s' % type(message))
|
||||
return False
|
||||
|
||||
header = bytearray()
|
||||
payload = encode_to_UTF8(message)
|
||||
payload_length = len(payload)
|
||||
|
||||
# Normal payload
|
||||
if payload_length <= 125:
|
||||
header.append(FIN | opcode)
|
||||
header.append(payload_length)
|
||||
|
||||
# Extended payload
|
||||
elif payload_length >= 126 and payload_length <= 65535:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT16)
|
||||
header.extend(struct.pack(">H", payload_length))
|
||||
|
||||
# Huge extended payload
|
||||
elif payload_length < 18446744073709551616:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT64)
|
||||
header.extend(struct.pack(">Q", payload_length))
|
||||
|
||||
else:
|
||||
raise Exception("Message is too big. Consider breaking it into chunks.")
|
||||
return
|
||||
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def read_http_headers(self):
|
||||
headers = {}
|
||||
# first line should be HTTP GET
|
||||
http_get = self.rfile.readline().decode().strip()
|
||||
assert http_get.upper().startswith('GET')
|
||||
# remaining should be headers
|
||||
while True:
|
||||
header = self.rfile.readline().decode().strip()
|
||||
if not header:
|
||||
break
|
||||
head, value = header.split(':', 1)
|
||||
headers[head.lower().strip()] = value.strip()
|
||||
return headers
|
||||
|
||||
def handshake(self):
|
||||
headers = self.read_http_headers()
|
||||
|
||||
try:
|
||||
assert headers['upgrade'].lower() == 'websocket'
|
||||
except AssertionError:
|
||||
self.keep_alive = False
|
||||
return
|
||||
|
||||
try:
|
||||
key = headers['sec-websocket-key']
|
||||
except KeyError:
|
||||
logger.warning("Client tried to connect but was missing a key")
|
||||
self.keep_alive = False
|
||||
return
|
||||
|
||||
response = self.make_handshake_response(key)
|
||||
with self._send_lock:
|
||||
self.handshake_done = self.request.send(response.encode())
|
||||
self.valid_client = True
|
||||
self.server._new_client_(self)
|
||||
|
||||
@classmethod
|
||||
def make_handshake_response(cls, key):
|
||||
return \
|
||||
'HTTP/1.1 101 Switching Protocols\r\n'\
|
||||
'Upgrade: websocket\r\n' \
|
||||
'Connection: Upgrade\r\n' \
|
||||
'Sec-WebSocket-Accept: %s\r\n' \
|
||||
'\r\n' % cls.calculate_response_key(key)
|
||||
|
||||
@classmethod
|
||||
def calculate_response_key(cls, key):
|
||||
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
hash = sha1(key.encode() + GUID.encode())
|
||||
response_key = b64encode(hash.digest()).strip()
|
||||
return response_key.decode('ASCII')
|
||||
|
||||
def finish(self):
|
||||
self.server._client_left_(self)
|
||||
|
||||
|
||||
def encode_to_UTF8(data):
|
||||
try:
|
||||
return data.encode('UTF-8')
|
||||
except UnicodeEncodeError as e:
|
||||
logger.error("Could not encode data to UTF-8 -- %s" % e)
|
||||
return False
|
||||
except Exception as e:
|
||||
raise(e)
|
||||
return False
|
||||
|
||||
|
||||
def try_decode_UTF8(data):
|
||||
try:
|
||||
return data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except Exception as e:
|
||||
raise(e)
|
|
@ -141,6 +141,17 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
self.server.last_form_id[name] = form_id
|
||||
return form_data
|
||||
|
||||
def render_template(self, template, args_dict):
|
||||
swap_client = self.server.swap_client
|
||||
if swap_client.ws_server:
|
||||
args_dict['ws_url'] = swap_client.ws_server.url
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
form_id=os.urandom(8).hex(),
|
||||
**args_dict,
|
||||
), 'UTF-8')
|
||||
|
||||
def page_explorers(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
|
@ -174,16 +185,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
result = str(ex)
|
||||
|
||||
template = env.get_template('explorers.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
explorers=listAvailableExplorers(swap_client),
|
||||
explorer=explorer,
|
||||
actions=listExplorerActions(swap_client),
|
||||
action=action,
|
||||
result=result,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'explorers': listAvailableExplorers(swap_client),
|
||||
'explorer': explorer,
|
||||
'actions': listExplorerActions(swap_client),
|
||||
'action': action,
|
||||
'result': result
|
||||
})
|
||||
|
||||
def page_rpc(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -237,15 +245,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
coins.append((-3, 'Monero JSON'))
|
||||
coins.append((-4, 'Monero Wallet'))
|
||||
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
coins=coins,
|
||||
coin_type=coin_id,
|
||||
result=result,
|
||||
messages=messages,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'coins': coins,
|
||||
'coin_type': coin_id,
|
||||
'result': result,
|
||||
'messages': messages,
|
||||
})
|
||||
|
||||
def page_debug(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -262,25 +267,20 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
messages.append('Failed.')
|
||||
|
||||
template = env.get_template('debug.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
messages=messages,
|
||||
result=result,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'result': result,
|
||||
})
|
||||
|
||||
def page_active(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
active_swaps = swap_client.listSwapsInProgress()
|
||||
|
||||
template = env.get_template('active.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
refresh=30,
|
||||
h2=self.server.title,
|
||||
active_swaps=[(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'active_swaps': [(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
|
||||
})
|
||||
|
||||
def page_settings(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -347,13 +347,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
chains_formatted[-1]['can_disable'] = True
|
||||
|
||||
template = env.get_template('settings.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
messages=messages,
|
||||
chains=chains_formatted,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'chains': chains_formatted,
|
||||
})
|
||||
|
||||
def page_bid(self, url_split, post_string):
|
||||
ensure(len(url_split) > 2, 'Bid ID not specified')
|
||||
|
@ -428,16 +425,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
data['addr_from_label'] = '(' + data['addr_from_label'] + ')'
|
||||
|
||||
template = env.get_template('bid_xmr.html') if offer.swap_type == SwapTypes.XMR_SWAP else env.get_template('bid.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
bid_id=bid_id.hex(),
|
||||
messages=messages,
|
||||
data=data,
|
||||
edit_bid=edit_bid,
|
||||
form_id=os.urandom(8).hex(),
|
||||
old_states=old_states,
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'bid_id': bid_id.hex(),
|
||||
'messages': messages,
|
||||
'data': data,
|
||||
'edit_bid': edit_bid,
|
||||
'old_states': old_states,
|
||||
})
|
||||
|
||||
def page_bids(self, url_split, post_string, sent=False, available=False):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -486,30 +480,25 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
}
|
||||
|
||||
template = env.get_template('bids.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
page_type='Sent' if sent else 'Received',
|
||||
messages=messages,
|
||||
filters=filters,
|
||||
data=page_data,
|
||||
bids=[(format_timestamp(b[0]),
|
||||
b[2].hex(), b[3].hex(), strBidState(b[5]), strTxState(b[7]), strTxState(b[8]), b[11]) for b in bids],
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'page_type': 'Sent' if sent else 'Received',
|
||||
'messages': messages,
|
||||
'filters': filters,
|
||||
'data': page_data,
|
||||
'bids': [(format_timestamp(b[0]),
|
||||
b[2].hex(), b[3].hex(), strBidState(b[5]), strTxState(b[7]), strTxState(b[8]), b[11]) for b in bids],
|
||||
})
|
||||
|
||||
def page_watched(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
||||
|
||||
template = env.get_template('watched.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
refresh=30,
|
||||
h2=self.server.title,
|
||||
last_scanned=[(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
||||
watched_outputs=[(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'last_scanned': [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
||||
'watched_outputs': [(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
|
||||
})
|
||||
|
||||
def page_smsgaddresses(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -575,15 +564,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
addr['type'] = strAddressType(addr['type'])
|
||||
|
||||
template = env.get_template('smsgaddresses.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
messages=messages,
|
||||
data=page_data,
|
||||
form_id=os.urandom(8).hex(),
|
||||
smsgaddresses=smsgaddresses,
|
||||
network_addr=network_addr,
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'data': page_data,
|
||||
'smsgaddresses': smsgaddresses,
|
||||
'network_addr': network_addr,
|
||||
})
|
||||
|
||||
def page_identity(self, url_split, post_string):
|
||||
ensure(len(url_split) > 2, 'Address not specified')
|
||||
|
@ -620,13 +606,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
messages.append(e)
|
||||
|
||||
template = env.get_template('identity.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
messages=messages,
|
||||
data=page_data,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'data': page_data,
|
||||
})
|
||||
|
||||
def page_shutdown(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
@ -649,15 +632,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
self.server.session_tokens['shutdown'] = shutdown_token
|
||||
|
||||
template = env.get_template('index.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
refresh=30,
|
||||
h2=self.server.title,
|
||||
version=__version__,
|
||||
summary=summary,
|
||||
use_tor_proxy=swap_client.use_tor_proxy,
|
||||
shutdown_token=shutdown_token
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'version': __version__,
|
||||
'summary': summary,
|
||||
'use_tor_proxy': swap_client.use_tor_proxy,
|
||||
'shutdown_token': shutdown_token
|
||||
})
|
||||
|
||||
def page_404(self, url_split):
|
||||
template = env.get_template('404.html')
|
||||
|
@ -708,10 +689,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
elif len(url_split) > 3 and url_split[2] == 'images':
|
||||
filename = os.path.join(*url_split[3:])
|
||||
_, extension = os.path.splitext(filename)
|
||||
mime_type = {'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
}.get(extension, '')
|
||||
mime_type = {
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
}.get(extension, '')
|
||||
if mime_type == '':
|
||||
raise ValueError('Unknown file type ' + filename)
|
||||
with open(os.path.join(static_path, 'images', filename), 'rb') as fp:
|
||||
|
@ -737,51 +719,52 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
try:
|
||||
self.putHeaders(status_code, 'text/html')
|
||||
if len(url_split) > 1:
|
||||
if url_split[1] == 'active':
|
||||
page = url_split[1]
|
||||
if page == 'active':
|
||||
return self.page_active(url_split, post_string)
|
||||
if url_split[1] == 'wallets':
|
||||
if page == 'wallets':
|
||||
return page_wallets(self, url_split, post_string)
|
||||
if url_split[1] == 'wallet':
|
||||
if page == 'wallet':
|
||||
return page_wallet(self, url_split, post_string)
|
||||
if url_split[1] == 'settings':
|
||||
if page == 'settings':
|
||||
return self.page_settings(url_split, post_string)
|
||||
if url_split[1] == 'rpc':
|
||||
if page == 'rpc':
|
||||
return self.page_rpc(url_split, post_string)
|
||||
if url_split[1] == 'debug':
|
||||
if page == 'debug':
|
||||
return self.page_debug(url_split, post_string)
|
||||
if url_split[1] == 'explorers':
|
||||
if page == 'explorers':
|
||||
return self.page_explorers(url_split, post_string)
|
||||
if url_split[1] == 'offer':
|
||||
if page == 'offer':
|
||||
return page_offer(self, url_split, post_string)
|
||||
if url_split[1] == 'offers':
|
||||
if page == 'offers':
|
||||
return page_offers(self, url_split, post_string)
|
||||
if url_split[1] == 'newoffer':
|
||||
if page == 'newoffer':
|
||||
return page_newoffer(self, url_split, post_string)
|
||||
if url_split[1] == 'sentoffers':
|
||||
if page == 'sentoffers':
|
||||
return page_offers(self, url_split, post_string, sent=True)
|
||||
if url_split[1] == 'bid':
|
||||
if page == 'bid':
|
||||
return self.page_bid(url_split, post_string)
|
||||
if url_split[1] == 'bids':
|
||||
if page == 'bids':
|
||||
return self.page_bids(url_split, post_string)
|
||||
if url_split[1] == 'sentbids':
|
||||
if page == 'sentbids':
|
||||
return self.page_bids(url_split, post_string, sent=True)
|
||||
if url_split[1] == 'availablebids':
|
||||
if page == 'availablebids':
|
||||
return self.page_bids(url_split, post_string, available=True)
|
||||
if url_split[1] == 'watched':
|
||||
if page == 'watched':
|
||||
return self.page_watched(url_split, post_string)
|
||||
if url_split[1] == 'smsgaddresses':
|
||||
if page == 'smsgaddresses':
|
||||
return self.page_smsgaddresses(url_split, post_string)
|
||||
if url_split[1] == 'identity':
|
||||
if page == 'identity':
|
||||
return self.page_identity(url_split, post_string)
|
||||
if url_split[1] == 'tor':
|
||||
if page == 'tor':
|
||||
return page_tor(self, url_split, post_string)
|
||||
if url_split[1] == 'automation':
|
||||
if page == 'automation':
|
||||
return page_automation_strategies(self, url_split, post_string)
|
||||
if url_split[1] == 'automationstrategy':
|
||||
if page == 'automationstrategy':
|
||||
return page_automation_strategy(self, url_split, post_string)
|
||||
if url_split[1] == 'newautomationstrategy':
|
||||
if page == 'newautomationstrategy':
|
||||
return page_automation_strategy_new(self, url_split, post_string)
|
||||
if url_split[1] == 'shutdown':
|
||||
if page == 'shutdown':
|
||||
return self.page_shutdown(url_split, post_string)
|
||||
return self.page_index(url_split)
|
||||
except Exception as ex:
|
||||
|
|
|
@ -13,3 +13,12 @@
|
|||
{
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
.floatright
|
||||
{
|
||||
position:fixed;
|
||||
top:10px;
|
||||
right:18px;
|
||||
margin: 0;
|
||||
width:calc(33.33% - 25px);
|
||||
}
|
||||
|
|
|
@ -12,3 +12,33 @@
|
|||
{% if h2 %}
|
||||
<h2>{{ h2 }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if ws_url %}
|
||||
<script>
|
||||
var ws = new WebSocket("{{ ws_url }}"),
|
||||
floating_div = document.createElement('div');
|
||||
floating_div.classList.add('floatright');
|
||||
messages = document.createElement('ul');
|
||||
ws.onmessage = function (event) {
|
||||
let json = JSON.parse(event.data);
|
||||
|
||||
let event_message = 'Unknown event';
|
||||
if (json['event'] == 'new_offer') {
|
||||
event_message = '<a href=/offer/' + json['offer_id'] + '>New offer</a>';
|
||||
} else
|
||||
if (json['event'] == 'new_bid') {
|
||||
event_message = '<a href=/bid/' + json['bid_id'] + '>New bid</a> on offer <a href=/offer/' + json['offer_id'] + '>' + json['offer_id'] + '</a>';
|
||||
} else
|
||||
if (json['event'] == 'bid_accepted') {
|
||||
event_message = '<a href=/bid/' + json['bid_id'] + '>Bid accepted</a>';
|
||||
}
|
||||
|
||||
let messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li');
|
||||
message.innerHTML = event_message;
|
||||
messages.appendChild(message);
|
||||
};
|
||||
floating_div.appendChild(messages);
|
||||
document.body.appendChild(floating_div);
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
|
||||
from .util import (
|
||||
PAGE_LIMIT,
|
||||
get_data_entry,
|
||||
|
@ -51,14 +49,11 @@ def page_automation_strategies(self, url_split, post_string):
|
|||
formatted_strategies.append((s[0], s[1], strConcepts(s[2])))
|
||||
|
||||
template = server.env.get_template('automation_strategies.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
filters=filters,
|
||||
strategies=formatted_strategies,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'filters': filters,
|
||||
'strategies': formatted_strategies,
|
||||
})
|
||||
|
||||
|
||||
def page_automation_strategy_new(self, url_split, post_string):
|
||||
|
@ -69,12 +64,9 @@ def page_automation_strategy_new(self, url_split, post_string):
|
|||
form_data = self.checkForm(post_string, 'automationstrategynew', messages)
|
||||
|
||||
template = server.env.get_template('automation_strategy_new.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
})
|
||||
|
||||
|
||||
def page_automation_strategy(self, url_split, post_string):
|
||||
|
@ -101,10 +93,7 @@ def page_automation_strategy(self, url_split, post_string):
|
|||
}
|
||||
|
||||
template = server.env.get_template('automation_strategy.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
strategy=formatted_strategy,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'strategy': formatted_strategy,
|
||||
})
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from .util import (
|
||||
|
@ -308,18 +307,15 @@ def page_newoffer(self, url_split, post_string):
|
|||
automation_filters['type_ind'] = Concepts.OFFER
|
||||
automation_strategies = swap_client.listAutomationStrategies(automation_filters)
|
||||
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
coins_from=coins_from,
|
||||
coins=coins_to,
|
||||
addrs=swap_client.listSmsgAddresses('offer_send_from'),
|
||||
addrs_to=swap_client.listSmsgAddresses('offer_send_to'),
|
||||
data=page_data,
|
||||
automation_strategies=automation_strategies,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'coins_from': coins_from,
|
||||
'coins': coins_to,
|
||||
'addrs': swap_client.listSmsgAddresses('offer_send_from'),
|
||||
'addrs_to': swap_client.listSmsgAddresses('offer_send_to'),
|
||||
'data': page_data,
|
||||
'automation_strategies': automation_strategies,
|
||||
})
|
||||
|
||||
|
||||
def page_offer(self, url_split, post_string):
|
||||
|
@ -469,17 +465,14 @@ def page_offer(self, url_split, post_string):
|
|||
data['amt_swapped'] = ci_from.format_amount(amt_swapped)
|
||||
|
||||
template = server.env.get_template('offer.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
offer_id=offer_id.hex(),
|
||||
sent_bid_id=sent_bid_id,
|
||||
messages=messages,
|
||||
data=data,
|
||||
bids=formatted_bids,
|
||||
addrs=None if show_bid_form is None else swap_client.listSmsgAddresses('bid'),
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'offer_id': offer_id.hex(),
|
||||
'sent_bid_id': sent_bid_id,
|
||||
'messages': messages,
|
||||
'data': data,
|
||||
'bids': formatted_bids,
|
||||
'addrs': None if show_bid_form is None else swap_client.listSmsgAddresses('bid'),
|
||||
})
|
||||
|
||||
|
||||
def page_offers(self, url_split, post_string, sent=False):
|
||||
|
@ -540,12 +533,10 @@ def page_offers(self, url_split, post_string, sent=False):
|
|||
ci_from.format_amount(completed_amount)))
|
||||
|
||||
template = server.env.get_template('offers.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
coins=listAvailableCoins(swap_client),
|
||||
messages=messages,
|
||||
filters=filters,
|
||||
offers=formatted_offers,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'coins': listAvailableCoins(swap_client),
|
||||
'messages': messages,
|
||||
'filters': filters,
|
||||
'offers': formatted_offers,
|
||||
})
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def extract_data(bytes_in):
|
||||
str_in = bytes_in.decode('utf-8')
|
||||
start = str_in.find('=')
|
||||
|
@ -37,10 +34,7 @@ def page_tor(self, url_split, post_string):
|
|||
messages = []
|
||||
|
||||
template = self.server.env.get_template('tor.html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
messages=messages,
|
||||
data=page_data,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'data': page_data,
|
||||
})
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from .util import (
|
||||
|
@ -151,13 +150,10 @@ def page_wallets(self, url_split, post_string):
|
|||
wallets_formatted.append(wf)
|
||||
|
||||
template = server.env.get_template('wallets.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
wallets=wallets_formatted,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'wallets': wallets_formatted,
|
||||
})
|
||||
|
||||
|
||||
def page_wallet(self, url_split, post_string):
|
||||
|
@ -304,10 +300,7 @@ def page_wallet(self, url_split, post_string):
|
|||
wallet_data['utxo_groups'] = utxo_groups
|
||||
|
||||
template = server.env.get_template('wallet.html')
|
||||
return bytes(template.render(
|
||||
title=server.title,
|
||||
h2=server.title,
|
||||
messages=messages,
|
||||
w=wallet_data,
|
||||
form_id=os.urandom(8).hex(),
|
||||
), 'UTF-8')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'w': wallet_data,
|
||||
})
|
||||
|
|
|
@ -80,6 +80,7 @@ if not len(logger.handlers):
|
|||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
UI_HTML_PORT = int(os.getenv('UI_HTML_PORT', 12700))
|
||||
UI_WS_PORT = int(os.getenv('UI_WS_PORT', 11700))
|
||||
COINS_RPCBIND_IP = os.getenv('COINS_RPCBIND_IP', '127.0.0.1')
|
||||
|
||||
PART_ZMQ_PORT = int(os.getenv('PART_ZMQ_PORT', 20792))
|
||||
|
@ -748,7 +749,8 @@ def printHelp():
|
|||
logger.info('--nocores Don\'t download and extract any coin clients.')
|
||||
logger.info('--usecontainers Expect each core to run in a unique container.')
|
||||
logger.info('--portoffset=n Raise all ports by n.')
|
||||
logger.info('--htmlhost= Interface to host on, default:127.0.0.1.')
|
||||
logger.info('--htmlhost= Interface to host html server on, default:127.0.0.1.')
|
||||
logger.info('--wshost= Interface to host websocket server on, disable by setting to "none", default:127.0.0.1.')
|
||||
logger.info('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
|
||||
logger.info('--noextractover Prevent extracting cores if files exist. Speeds up tests')
|
||||
logger.info('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.')
|
||||
|
@ -853,6 +855,7 @@ def main():
|
|||
add_coin = ''
|
||||
disable_coin = ''
|
||||
htmlhost = '127.0.0.1'
|
||||
wshost = '127.0.0.1'
|
||||
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
|
||||
prepare_bin_only = False
|
||||
no_cores = False
|
||||
|
@ -955,6 +958,9 @@ def main():
|
|||
if name == 'htmlhost':
|
||||
htmlhost = s[1].strip('"')
|
||||
continue
|
||||
if name == 'wshost':
|
||||
wshost = s[1].strip('"')
|
||||
continue
|
||||
if name == 'xmrrestoreheight':
|
||||
xmr_restore_height = int(s[1])
|
||||
continue
|
||||
|
@ -1200,6 +1206,10 @@ def main():
|
|||
'check_expired_seconds': 60
|
||||
}
|
||||
|
||||
if wshost != 'none':
|
||||
settings['wshost'] = wshost
|
||||
settings['wsport'] = UI_WS_PORT + port_offset
|
||||
|
||||
if use_tor_proxy:
|
||||
tor_control_password = generate_salt(24)
|
||||
addTorSettings(settings, tor_control_password)
|
||||
|
|
|
@ -19,6 +19,7 @@ import basicswap.config as cfg
|
|||
from basicswap import __version__
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
@ -93,6 +94,25 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
|||
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir)
|
||||
|
||||
|
||||
def ws_new_client(client, server):
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_new_client {client["id"]}')
|
||||
|
||||
|
||||
def ws_client_left(client, server):
|
||||
if client is None:
|
||||
return
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_client_left {client["id"]}')
|
||||
|
||||
|
||||
def ws_message_received(client, server, message):
|
||||
if len(message) > 200:
|
||||
message = message[:200] + '..'
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
|
||||
|
||||
|
||||
def runClient(fp, data_dir, chain):
|
||||
global swap_client
|
||||
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||
|
@ -158,24 +178,45 @@ def runClient(fp, data_dir, chain):
|
|||
swap_client.start()
|
||||
|
||||
if 'htmlhost' in settings:
|
||||
swap_client.log.info('Starting server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
|
||||
swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
|
||||
allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
|
||||
tS1 = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
|
||||
threads.append(tS1)
|
||||
tS1.start()
|
||||
thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
|
||||
threads.append(thread_http)
|
||||
thread_http.start()
|
||||
|
||||
if 'wshost' in settings:
|
||||
ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport'])
|
||||
swap_client.log.info(f'Starting ws server at {ws_url}.')
|
||||
|
||||
swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport'])
|
||||
swap_client.ws_server.set_fn_new_client(ws_new_client)
|
||||
swap_client.ws_server.set_fn_client_left(ws_client_left)
|
||||
swap_client.ws_server.set_fn_message_received(ws_message_received)
|
||||
swap_client.ws_server.run_forever(threaded=True)
|
||||
|
||||
logger.info('Exit with Ctrl + c.')
|
||||
while swap_client.is_running:
|
||||
time.sleep(0.5)
|
||||
swap_client.update()
|
||||
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
if swap_client.ws_server:
|
||||
try:
|
||||
swap_client.log.info('Stopping websocket server.')
|
||||
swap_client.ws_server.shutdown_gracefully()
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.finalise()
|
||||
swap_client.log.info('Stopping HTTP threads.')
|
||||
for t in threads:
|
||||
t.stop()
|
||||
t.join()
|
||||
try:
|
||||
t.stop()
|
||||
t.join()
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
|
|
|
@ -7,16 +7,16 @@ Update only the code:
|
|||
|
||||
basicswap]$ git pull
|
||||
$ cd docker
|
||||
$ docker-compose build
|
||||
$ export COINDATA_PATH=[PATH_TO]
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
|
||||
If the dependencies have changed the container must be built with `--no-cache`:
|
||||
|
||||
basicswap]$ git pull
|
||||
$ cd docker
|
||||
$ docker-compose build --no-cache
|
||||
$ export COINDATA_PATH=[PATH_TO]
|
||||
$ docker-compose build --no-cache
|
||||
$ docker-compose up
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
HTML_PORT=127.0.0.1:12700:12700
|
||||
WS_PORT=127.0.0.1:11700:11700
|
||||
#COINDATA_PATH=/var/data/coinswaps
|
||||
TZ=UTC
|
||||
|
|
|
@ -9,6 +9,7 @@ services:
|
|||
- ${COINDATA_PATH}:/coindata
|
||||
ports:
|
||||
- "${HTML_PORT}" # Expose only to localhost, see .env
|
||||
- "${WS_PORT}" # Expose only to localhost, see .env
|
||||
environment:
|
||||
- TZ
|
||||
logging:
|
||||
|
|
|
@ -11,6 +11,7 @@ services:
|
|||
- ${COINDATA_PATH}:/coindata
|
||||
ports:
|
||||
- "${HTML_PORT}" # Expose only to localhost, see .env
|
||||
- "${WS_PORT}" # Expose only to localhost, see .env
|
||||
environment:
|
||||
- TZ
|
||||
- TOR_PROXY_HOST
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
- ${DATA_PATH}/swapclient:/data
|
||||
ports:
|
||||
- "${HTML_PORT}" # Expose only to localhost, see .env
|
||||
- "${WS_PORT}" # Expose only to localhost, see .env
|
||||
environment:
|
||||
- TZ
|
||||
logging:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
HTML_PORT=127.0.0.1:12700:12700
|
||||
WS_PORT=127.0.0.1:11700:11700
|
||||
TZ=UTC
|
||||
|
||||
DATA_PATH=/var/swapdata/
|
||||
|
|
Loading…
Reference in a new issue