autobahn-python icon indicating copy to clipboard operation
autobahn-python copied to clipboard

SSL error on connectWS

Open Roms1383 opened this issue 6 years ago • 5 comments

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 OS-configuration

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.

Roms1383 avatar Nov 11 '19 07:11 Roms1383

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
// ...

Roms1383 avatar Nov 11 '19 07:11 Roms1383

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 :)

Roms1383 avatar Nov 11 '19 08:11 Roms1383

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...

Roms1383 avatar Nov 11 '19 09:11 Roms1383

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.

Roms1383 avatar Nov 11 '19 09:11 Roms1383

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)

Roms1383 avatar Nov 11 '19 10:11 Roms1383

@oberstet

Pborz avatar Jun 18 '23 08:06 Pborz