SSL error on connectWS
Hi everybody,
I am having an issue on connecting to JEX Websocket API while deriving it from python-binance which is really similar to JEX.
Following example with Partial Book Depth Streams.
I already tested my rest.py implementation and it works properly for all the endpoints.
Steps to reproduce
environment
Python 3.7.4
pip 19.3.1 from /Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/pip (python 3.7)
exceptions.py
class JEXAPIException(Exception):
def __init__(self, response):
self.code = 0
try:
json_res = response.json()
except ValueError:
self.message = 'Invalid JSON error message from JEX: {}'.format(response.text)
else:
self.code = json_res['code']
self.message = json_res['msg']
self.status_code = response.status_code
self.response = response
self.request = getattr(response, 'request', None)
def __str__(self): # pragma: no cover
return 'APIError(code=%s): %s' % (self.code, self.message)
class JEXRequestException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return 'JEXRequestException: %s' % self.message
rest.py
# coding=utf-8
import hashlib
import hmac
import requests
import time
from binance.helpers import date_to_milliseconds, interval_to_milliseconds
from exceptions import JEXAPIException, JEXRequestException
class Rest(object):
def __init__(self, api_key=None, api_secret=None):
self.API_KEY = api_key
self.API_SECRET = api_secret
self.session = self._init_session()
# init DNS and SSL cert
self.ping()
def _init_session(self):
session = requests.Session()
session.headers.update({'Accept': 'application/json',
'User-Agent': 'jex/python',
'X-JEX-APIKEY': self.API_KEY})
return session
def _url(self, domain, version, path):
return domain + '/' + 'api' + '/' + version + '/' + path
def _encrypt(self, message):
return hmac.new(self.API_SECRET.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
def _stringify(self, data):
return '&'.join(["{}={}".format(k, v) for k, v in data.items()])
def _sign(self, data):
stringified = self._stringify(data)
encrypted = self._encrypt(stringified)
return encrypted
def _nonce(self):
return int(time.time() * 1000)
def _handle(self, response):
if not str(response.status_code).startswith('2'):
raise JEXAPIException(response)
try:
return response.json()
except ValueError:
raise JEXRequestException('Invalid Response: %s' % response.text)
def _request(self, method, domain, version, path, signed, **kwargs):
url = self._url(domain, version, path)
# beware of the order of the parameters
if signed:
kwargs['timestamp'] = self._nonce()
kwargs['signature'] = self._sign(kwargs)
call = getattr(self.session, method)
if method == 'get' or method == 'delete':
if len(kwargs) > 0:
url += '?' + self._stringify(kwargs)
response = call(url)
else:
response = call(url, kwargs)
print(method.upper(), url)
print('params', kwargs)
json = self._handle(response)
print('response', json)
print('')
return json
def _public(self, method, path, domain = 'https://www.jex.com', version = 'v1', **kwargs):
return self._request(method, domain, version, path, signed = False, **kwargs)
def _private(self, method, path, domain = 'https://www.jex.com', version = 'v1', **kwargs):
return self._request(method, domain, version, path, signed = True, **kwargs)
# beware of the order of the parameters in each methods below
# PUBLIC
def ping(self):
return self._public(method = 'get', path = 'ping')
def ticker(self, symbol):
return self._public(method = 'get', path = 'contract/ticker/24hr', symbol = symbol)
def depth(self, symbol):
return self._public(method = 'get', path = 'contract/depth', symbol = symbol, limit = 60)
# PRIVATE
def open_orders(self, symbol):
return self._private(method = 'get', path = 'contract/openOrders', symbol = symbol)
def create_order(self, symbol, side, quantity, price, type = 'LIMIT', recvWindow = 5000):
path = 'contract/order'
return self._private(
method = 'post',
path = path,
symbol = symbol,
side = side,
type = type,
quantity = quantity,
price = price,
recvWindow = recvWindow)
def delete_order(self, symbol, orderId, recvWindow = 5000):
return self._private(
method = 'delete',
path = 'contract/order',
symbol = symbol,
orderId = orderId,
recvWindow = recvWindow)
def position(self, symbol, recvWindow = 5000):
return self._private(
method = 'get',
path = 'contract/position',
symbol = symbol,
recvWindow = recvWindow)
def open_stream(self):
return self._private(method = 'post', path = 'userDataStream')
def keep_stream(self, listenKey):
return self._private(method = 'put', path = 'userDataStream', listenKey = listenKey)
def close_stream(self, listenKey):
return self._private(method = 'delete', path = 'userDataStream', listenKey = listenKey)
ws.py
from twisted.internet import ssl
from binance.client import Client
from binance.websockets import BinanceSocketManager, BinanceClientFactory, BinanceClientProtocol, BinanceReconnectingClientFactory
from autobahn.twisted.websocket import connectWS, WebSocketClientFactory
from rest import Rest
from os import getenv
from dotenv import load_dotenv
load_dotenv()
def process_message(msg):
print("message type: {}".format(msg['e']))
print(msg)
# do something
class MyClientProtocol(BinanceClientProtocol):
def onConnect(self, response):
print("Server connected: {0}".format(response.peer))
def onConnecting(self, transport_details):
print("Connecting; transport details: {}".format(transport_details))
return None
def onClose(self, wasClean, code, reason):
print("WebSocket connection closed: {}".format(reason))
class MySocketManager(BinanceSocketManager):
def _start_socket(self, path, callback, prefix='ws/'):
if path in self._conns:
return False
factory_url = self.STREAM_URL + prefix + path
print(factory_url)
factory = BinanceClientFactory(factory_url)
factory.protocol = MyClientProtocol
factory.protocol.logRxFrame = True
factory.callback = callback
factory.reconnect = True
context_factory = ssl.ClientContextFactory()
self._conns[path] = connectWS(factory, context_factory)
return path
def test_stream(self, symbol, callback):
return self._start_socket(symbol.lower() + '@spotDepth5', callback)
client = Rest(api_key=getenv('apiKey'), api_secret=getenv('secret'))
bm = MySocketManager(client)
bm.STREAM_URL ='wss://ws.jex.com/'
bm.test_stream('LTCBTC', process_message)
bm.start()
Expected output
I would expect to be able to connect and receive data from the Partial Book Depth Streams properly
Real output
GET https://www.jex.com/api/v1/ping
params {}
response {}
wss://ws.jex.com/ws/ltcbtc@spotDepth5
Connecting; transport details: {"peer": "tcp4:SOME_IP:443", "is_secure": true, "secure_channel_id": {"tls-unique": null}} // replaced actual IP with SOME_IP as it's not relevant
WebSocket connection closed: connection was closed uncleanly (SSL error: sslv3 alert handshake failure (in ssl3_read_bytes))
Further notes
I also gave a try with a simple rust implementation using websocket crate and another Python lib called websocket-client and both work as expected, hence my confusion.
Also, past this simple example my ws.py implementation wouldn't work as there's some inner method which calls different rest.py method, but it's sufficient enough to test and of course I would rebuild it from scratch once being able to correctly connect with the Websocket.
As a side note, I also tried using both given and upgraded python-binance dependencies like so :
requirements.txt
autobahn==19.11.1
certifi==2019.9.11
chardet==3.0.4
cryptography==2.8
dateparser==0.7.2
numpy==1.17.3
pyOpenSSL==19.0.0
python-binance==0.7.4
python-dateutil==2.8.1
python-dotenv==0.10.3
regex==2019.11.1
requests==2.22.0
service-identity==18.1.0
six==1.12.0
Twisted==19.7.0
urllib3==1.25.6
// ...
Actually comparing logs I end up with these :
using websocket-client
GET /ws/ltcbtc@spotDepth5 HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: ws.jex.com
Origin: http://ws.jex.com
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
HTTP/1.1 101
Date: Mon, 11 Nov 2019 08:05:36 GMT
Connection: upgrade
Set-Cookie: __cfduid=...; expires=Tue, 10-Nov-20 08:05:36 GMT; path=/; domain=.jex.com; HttpOnly
Set-Cookie: JSESSIONID=...; Path=/; HttpOnly
Upgrade: websocket
Sec-WebSocket-Accept: ...
CF-Cache-Status: DYNAMIC
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
CF-RAY: ...
using autobahn-python
GET /ws/ltcbtc@spotDepth5 HTTP/1.1
User-Agent: AutobahnPython/19.11.1
Host: ws.jex.com:443
Upgrade: WebSocket
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
Connection made to tcp4:...:443
SSL error: sslv3 alert handshake failure (in ssl3_read_bytes)
My guess here is that I should prevent trying to connect to port : 443, any suggestion is welcome :)
Getting the headers from autobahn-python to match those of websocket-client ends up like this :
GET /ws/ltcbtc@spotDepth5 HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: ws.jex.com
Origin: http://ws.jex.com
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
2019-11-11T16:03:49+0700 Connection made to tcp4:...:443
2019-11-11T16:03:49+0700 SSL error: sslv3 alert handshake failure (in ssl3_read_bytes)
2019-11-11T16:03:49+0700 _connectionLost: [Failure instance: Traceback: <class 'OpenSSL.SSL.Error'>: [('SSL routines', 'ssl3_read_bytes', 'sslv3 alert handshake failure')]
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/twisted/internet/selectreactor.py:149:_doReadOrWrite
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/twisted/internet/tcp.py:243:doRead
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/twisted/internet/tcp.py:249:_dataReceived
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/twisted/protocols/tls.py:315:dataReceived
--- <exception caught here> ---
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/twisted/protocols/tls.py:235:_checkHandshakeStatus
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/OpenSSL/SSL.py:1915:do_handshake
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/OpenSSL/SSL.py:1647:_raise_ssl_error
/Users/me/.pyenv/versions/3.7.4/lib/python3.7/site-packages/OpenSSL/_util.py:54:exception_from_error_queue
]
2019-11-11T16:03:49+0700 WebSocket connection closed: connection was closed uncleanly (SSL error: sslv3 alert handshake failure (in ssl3_read_bytes))
2019-11-11T16:03:49+0700 <twisted.internet.tcp.Connector instance at 0x10a5a32d0 disconnected IPv4Address(type='TCP', host='ws.jex.com', port=443)> will retry in 8 seconds
Still investigating...
I tried to do the opposite and update the websocket-client header to Host: ws.jex.com:443 but it keeps working properly so my guess now is that it's purely related to the SSL handshake somehow.
Got stuck on this error :
2019-11-11T17:09:47+0700 doRead b'\x15\x03\x01\x00\x02\x02('
2019-11-11T17:09:47+0700 SSL error: sslv3 alert handshake failure (in ssl3_read_bytes)
@oberstet
