Compare commits

...

28 commits

Author SHA1 Message Date
Roy Hyunjin Han
8aa519e095 Order goals 2016-01-19 23:12:33 -05:00
Roy Hyunjin Han
d3a5839678 Process issues 2016-01-05 09:10:16 -05:00
Roy Hyunjin Han
7e9f3c7343 Use six.string_types 2015-11-10 17:42:40 -05:00
Roy Hyunjin Han
4f04e6ca62 Replace basestring with str 2015-11-02 16:13:35 -05:00
Roy Hyunjin Han
fd88b9e096 Prepare for updates 2015-10-22 17:41:21 -04:00
Roy Hyunjin Han
1cd63a031b Add classifier 2015-06-01 16:16:32 -04:00
Roy Hyunjin Han
db59b16e25 Add documentation 2015-06-01 16:06:48 -04:00
Roy Hyunjin Han
ba82ffe558 Fix #80 2015-06-01 15:58:49 -04:00
Roy Hyunjin Han
59dcd02a91 Prepare for minor release 2015-06-01 14:31:46 -04:00
Roy Hyunjin Han
605194d515 Merge pull request #78 from mattporritt/issue77
Fix #77
2015-06-01 12:57:07 -04:00
Matt Porritt
689648ba32 fixing close method that is called as part of __del__. No longer throws error trying to call an attribute that will not exist if object fails during instantiation 2015-05-22 17:11:21 +10:00
Roy Hyunjin Han
29a3e0ebf6 Support Python 2.6 & 2.7 2015-05-20 23:12:11 -04:00
Roy Hyunjin Han
aeaad0c330 Merge pull request #75 from mattporritt/python34
explicitly set file open to use utf8 encoding in setup.py
2015-05-20 22:59:56 -04:00
Matt Porritt
12fa6c523b explicitly set file open to user utf8 encoding when setup.py parses README and CHANGES file. If this is not done some systems will throw unicode can't decode byte error 2015-05-19 15:09:22 +10:00
Roy Hyunjin Han
829e669403 Fix link 2015-05-15 22:48:33 -04:00
Roy Hyunjin Han
fb3b66115e Fix #74 thanks to @Nabla128k 2015-05-15 22:47:33 -04:00
Roy Hyunjin Han
16b538d847 Bump version 2015-04-15 17:23:56 -04:00
Roy Hyunjin Han
e622312a79 Restore support for Python 2.6 2015-04-15 16:47:56 -04:00
Roy Hyunjin Han
eb0324cb68 Remove captureWarnings 2015-04-15 16:32:46 -04:00
Roy Hyunjin Han
fd106e623d Update error message 2015-04-15 16:05:00 -04:00
Roy Hyunjin Han
b3f61bb26d Update changes 2015-04-15 15:49:35 -04:00
Roy Hyunjin Han
9e0bb069ad Fix SSL compatibility issues 2015-04-15 15:48:45 -04:00
Roy Hyunjin Han
4b6c562825 Update CHANGES 2015-04-15 14:42:28 -04:00
Roy Hyunjin Han
a6f9260964 Add locks to fix concurrency issues 2015-04-15 14:38:25 -04:00
Roy Hyunjin Han
f5b157014d Use timeout to unblock recv in websocket transport 2015-04-15 08:41:11 -04:00
Roy Hyunjin Han
978a669d16 Add websocket transport 2015-04-15 07:17:23 -04:00
Roy Hyunjin Han
601ba59971 Add bountysource link 2015-03-02 08:59:47 -05:00
Roy Hyunjin Han
57da37e995 Update README 2015-02-23 11:48:19 -05:00
18 changed files with 487 additions and 174 deletions

View file

@ -8,13 +8,12 @@ before_install:
- sudo apt-get install nodejs - sudo apt-get install nodejs
install: install:
- npm install -G socket.io - npm install -G socket.io
- npm install -G http-proxy - npm install -G yargs
- pip install -U requests - pip install -U requests
- pip install -U six - pip install -U six
- pip install -U websocket-client - pip install -U websocket-client
- pip install -U coverage - pip install -U coverage
before_script: before_script:
- DEBUG=* node socketIO_client/tests/serve.js & - DEBUG=* node socketIO_client/tests/serve.js &
- DEBUG=* node socketIO_client/tests/proxy.js &
- sleep 3 - sleep 3
script: nosetests script: nosetests

View file

@ -1,7 +1,26 @@
0.6.5
-----
- Updated wait loop to be more responsive under websocket transport
0.6.4
-----
- Fixed support for Python 3
- Fixed thread cleanup
0.6.3
-----
- Upgraded to socket.io protocol 1.x for websocket transport
- Added locks to fix concurrency issues with polling transport
- Fixed SSL support
0.6.1 0.6.1
----- -----
- Upgraded to socket.io protocol 1.x thanks to Sean Arietta and Joe Palmer - Upgraded to socket.io protocol 1.x thanks to Sean Arietta and Joe Palmer
0.5.6
-----
- Backported to support requests 0.8.2
0.5.5 0.5.5
----- -----
- Fixed reconnection in the event of server restart - Fixed reconnection in the event of server restart

18
LICENSE
View file

@ -1,7 +1,19 @@
Copyright (c) 2013 Roy Hyunjin Han and contributors Copyright (c) 2013 Roy Hyunjin Han and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -6,6 +6,8 @@ socketIO-client
=============== ===============
Here is a `socket.io <http://socket.io>`_ client library for Python. You can use it to write test code for your socket.io server. Here is a `socket.io <http://socket.io>`_ client library for Python. You can use it to write test code for your socket.io server.
Please note that this version implements `socket.io protocol 1.x <https://github.com/automattic/socket.io-protocol>`_, which is not backwards compatible. If you want to communicate using `socket.io protocol 0.9 <https://github.com/learnboost/socket.io-spec>`_ (which is compatible with `gevent-socketio <https://github.com/abourget/gevent-socketio>`_), please use `socketIO-client 0.5.6 <https://pypi.python.org/pypi/socketIO-client/0.5.6>`_.
Installation Installation
------------ ------------
@ -33,7 +35,8 @@ Activate isolated environment. ::
Launch your socket.io server. :: Launch your socket.io server. ::
# Get package folder # Get package folder
PACKAGE_FOLDER=`python -c "import os, socketIO_client; print(os.path.dirname(socketIO_client.__file__))"` PACKAGE_FOLDER=`python -c "import os, socketIO_client;\
print(os.path.dirname(socketIO_client.__file__))"`
# Start socket.io server # Start socket.io server
DEBUG=* node $PACKAGE_FOLDER/tests/serve.js DEBUG=* node $PACKAGE_FOLDER/tests/serve.js
# Start proxy server in a separate terminal on the same machine # Start proxy server in a separate terminal on the same machine
@ -135,7 +138,8 @@ Specify params, headers, cookies, proxies thanks to the `requests <http://python
from socketIO_client import SocketIO from socketIO_client import SocketIO
from base64 import b64encode from base64 import b64encode
SocketIO('localhost', 8000, SocketIO(
localhost', 8000,
params={'q': 'qqq'}, params={'q': 'qqq'},
headers={'Authorization': 'Basic ' + b64encode('username:password')}, headers={'Authorization': 'Basic ' + b64encode('username:password')},
cookies={'a': 'aaa'}, cookies={'a': 'aaa'},
@ -156,11 +160,11 @@ This software is available under the MIT License.
Credits Credits
------- -------
- `Guillermo Rauch <https://github.com/rauchg>`_ wrote the `socket.io specification <https://github.com/LearnBoost/socket.io-spec>`_. - `Guillermo Rauch <https://github.com/rauchg>`_ wrote the `socket.io specification <https://github.com/automattic/socket.io-protocol>`_.
- `Hiroki Ohtani <https://github.com/liris>`_ wrote `websocket-client <https://github.com/liris/websocket-client>`_. - `Hiroki Ohtani <https://github.com/liris>`_ wrote `websocket-client <https://github.com/liris/websocket-client>`_.
- `rod <http://stackoverflow.com/users/370115/rod>`_ wrote a `prototype for a Python client to a socket.io server <http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client>`_. - `rod <http://stackoverflow.com/users/370115/rod>`_ wrote a `prototype for a Python client to a socket.io server <http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client>`_.
- `Alexandre Bourget <https://github.com/abourget>`_ wrote `gevent-socketio <https://github.com/abourget/gevent-socketio>`_, which is a socket.io server written in Python. - `Alexandre Bourget <https://github.com/abourget>`_ wrote `gevent-socketio <https://github.com/abourget/gevent-socketio>`_, which is a socket.io server written in Python.
- `Paul Kienzle <https://github.com/pkienzle>`_, `Zac Lee <https://github.com/zratic>`_, `Josh VanderLinden <https://github.com/codekoala>`_, `Ian Fitzpatrick <https://github.com/ifitzpatrick>`_, `Lucas Klein <https://github.com/lukasklein>`_, `Rui Chicoria <https://github.com/rchicoria>`_, `Travis Odom <https://github.com/burstaholic>`_, `Patrick Huber <https://github.com/stackmagic>`_, `Brad Campbell <https://github.com/bradjc>`_, `Daniel <https://github.com/dabidan>`_, `Sean Arietta <https://github.com/sarietta>`_ submitted code to expand support of the socket.io protocol. - `Paul Kienzle <https://github.com/pkienzle>`_, `Zac Lee <https://github.com/zratic>`_, `Josh VanderLinden <https://github.com/codekoala>`_, `Ian Fitzpatrick <https://github.com/ifitzpatrick>`_, `Lucas Klein <https://github.com/lukasklein>`_, `Rui Chicoria <https://github.com/rchicoria>`_, `Travis Odom <https://github.com/burstaholic>`_, `Patrick Huber <https://github.com/stackmagic>`_, `Brad Campbell <https://github.com/bradjc>`_, `Daniel <https://github.com/dabidan>`_, `Sean Arietta <https://github.com/sarietta>`_ submitted code to expand support of the socket.io protocol.
- `Bernard Pratz <https://github.com/guyzmo>`_, `Francis Bull <https://github.com/franbull>`_ wrote prototypes to support xhr-polling and jsonp-polling. - `Bernard Pratz <https://github.com/guyzmo>`_, `Francis Bull <https://github.com/franbull>`_ wrote prototypes to support xhr-polling and jsonp-polling.
- `Eric Chen <https://github.com/taiyangc>`_, `Denis Zinevich <https://github.com/dzinevich>`_, `Thiago Hersan <https://github.com/thiagohersan>`_, `Nayef Copty <https://github.com/nayefc>`_, `Jörgen Karlsson <https://github.com/jorgen-k>`_, `Branden Ghena <https://github.com/brghena>`_ suggested ways to make the connection more robust. - `Eric Chen <https://github.com/taiyangc>`_, `Denis Zinevich <https://github.com/dzinevich>`_, `Thiago Hersan <https://github.com/thiagohersan>`_, `Nayef Copty <https://github.com/nayefc>`_, `Jörgen Karlsson <https://github.com/jorgen-k>`_, `Branden Ghena <https://github.com/brghena>`_, `Tim Landscheidt <https://github.com/scfc>`_, `Matt Porritt <https://github.com/mattporritt>`_ suggested ways to make the connection more robust.
- `Merlijn van Deen <https://github.com/valhallasw>`_, `Frederic Sureau <https://github.com/fredericsureau>`_, `Marcus Cobden <https://github.com/leth>`_, `Drew Hutchison <https://github.com/drewhutchison>`_, `wuurrd <https://github.com/wuurrd>`_, `Adam Kecer <https://github.com/amfg>`_, `Alex Monk <https://github.com/Krenair>`_, `Vishal P R <https://github.com/vishalwy>`_, `John Vandenberg <https://github.com/jayvdb>`_, `Thomas Grainger <https://github.com/graingert>`_ proposed changes that make the library more friendly and practical for you! - `Merlijn van Deen <https://github.com/valhallasw>`_, `Frederic Sureau <https://github.com/fredericsureau>`_, `Marcus Cobden <https://github.com/leth>`_, `Drew Hutchison <https://github.com/drewhutchison>`_, `wuurrd <https://github.com/wuurrd>`_, `Adam Kecer <https://github.com/amfg>`_, `Alex Monk <https://github.com/Krenair>`_, `Vishal P R <https://github.com/vishalwy>`_, `John Vandenberg <https://github.com/jayvdb>`_, `Thomas Grainger <https://github.com/graingert>`_ proposed changes that make the library more friendly and practical for you!

View file

@ -1,9 +1,48 @@
Add Websocket transport = Consider supporting both protocols in the same library
Update proxy to include websocket depending on argument https://github.com/invisibleroads/socketIO-client/issues/95
Use prepared request to get headers from http_session Add binary support
Include https://github.com/invisibleroads/socketIO-client/issues/68 https://github.com/invisibleroads/socketIO-client/pull/85
Add test for on_reconnect using sarietta's bash scripts https://github.com/invisibleroads/socketIO-client/issues/70
Consider logging packets sent and received https://github.com/invisibleroads/socketIO-client/issues/71
Implement rooms #65 https://github.com/invisibleroads/socketIO-client/issues/91
Implement binary event Check requests dependency
Implement binary ack https://github.com/invisibleroads/socketIO-client/issues/92
https://github.com/invisibleroads/socketIO-client/pull/93
https://github.com/invisibleroads/socketIO-client/commit/b288d89c15d452a30bfeb00f38494f59f71f5a43
Check SSL
https://github.com/invisibleroads/socketIO-client/issues/86
https://github.com/invisibleroads/socketIO-client/pull/87
Look into invalid namespace handling
https://github.com/invisibleroads/socketIO-client/issues/84
Check unicode issues
https://github.com/invisibleroads/socketIO-client/issues/81
Check python3 support for socketIO-client 0.5.6
https://github.com/invisibleroads/socketIO-client/issues/83
Check OK assertion
https://github.com/invisibleroads/socketIO-client/issues/99
https://github.com/invisibleroads/socketIO-client/pull/103
Check why connected=True after termination
https://github.com/invisibleroads/socketIO-client/issues/80
https://github.com/invisibleroads/socketIO-client/issues/98
Consider catching heartbeat thread exception
https://github.com/invisibleroads/socketIO-client/issues/100
Check why it blocks when defining a namespace
https://github.com/invisibleroads/socketIO-client/issues/96
Check why transports are not being set
https://github.com/invisibleroads/socketIO-client/issues/102
Look at 404 not found error
https://github.com/invisibleroads/socketIO-client/issues/101
Look at socketio off and socketio once
https://github.com/invisibleroads/socketIO-client/pull/94
Implement rooms
https://github.com/invisibleroads/socketIO-client/issues/72
https://github.com/invisibleroads/socketIO-client/pull/65
Check tests
https://github.com/invisibleroads/socketIO-client/issues/90
Check whether it works on Windows 8
https://github.com/invisibleroads/socketIO-client/issues/97
Add debian packaging support
https://github.com/invisibleroads/socketIO-client/pull/89
+ Review issues and pull requests
+ Order issues

View file

@ -1,3 +1,4 @@
import io
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
from setuptools import find_packages, setup from setuptools import find_packages, setup
@ -10,13 +11,14 @@ REQUIREMENTS = [
HERE = dirname(abspath(__file__)) HERE = dirname(abspath(__file__))
DESCRIPTION = '\n\n'.join(open(join(HERE, _)).read() for _ in [ LOAD_TEXT = lambda name: io.open(join(HERE, name), encoding='UTF-8').read()
DESCRIPTION = '\n\n'.join(LOAD_TEXT(_) for _ in [
'README.rst', 'README.rst',
'CHANGES.rst', 'CHANGES.rst',
]) ])
setup( setup(
name='socketIO_client', name='socketIO_client',
version='0.6.1', version='0.6.5',
description='A socket.io client library', description='A socket.io client library',
long_description=DESCRIPTION, long_description=DESCRIPTION,
license='MIT', license='MIT',
@ -24,6 +26,7 @@ setup(
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Programming Language :: Python', 'Programming Language :: Python',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Development Status :: 5 - Production/Stable',
], ],
keywords='socket.io node.js', keywords='socket.io node.js',
author='Roy Hyunjin Han', author='Roy Hyunjin Han',

View file

@ -9,11 +9,12 @@ from .parsers import (
format_socketIO_packet_data, parse_socketIO_packet_data, format_socketIO_packet_data, parse_socketIO_packet_data,
get_namespace_path) get_namespace_path)
from .symmetries import get_character from .symmetries import get_character
from .transports import XHR_PollingTransport, prepare_http_session, TRANSPORTS from .transports import (
WebsocketTransport, XHR_PollingTransport, prepare_http_session, TRANSPORTS)
__all__ = 'SocketIO', 'SocketIONamespace' __all__ = 'SocketIO', 'SocketIONamespace'
__version__ = '0.6.1' __version__ = '0.6.3'
BaseNamespace = SocketIONamespace BaseNamespace = SocketIONamespace
LoggingNamespace = LoggingSocketIONamespace LoggingNamespace = LoggingSocketIONamespace
@ -39,10 +40,12 @@ class EngineIO(LoggingMixin):
self._wait_for_connection = wait_for_connection self._wait_for_connection = wait_for_connection
self._client_transports = transports self._client_transports = transports
self._hurry_interval_in_seconds = hurry_interval_in_seconds self._hurry_interval_in_seconds = hurry_interval_in_seconds
self._kw = kw self._http_session = prepare_http_session(kw)
self._log_name = self._url self._log_name = self._url
self._wants_to_close = False self._wants_to_close = False
self._opened = False self._opened = False
if Namespace: if Namespace:
self.define(Namespace) self.define(Namespace)
self._transport self._transport
@ -54,7 +57,7 @@ class EngineIO(LoggingMixin):
if self._opened: if self._opened:
return self._transport_instance return self._transport_instance
self._engineIO_session = self._get_engineIO_session() self._engineIO_session = self._get_engineIO_session()
self._transport_instance = self._negotiate_transport() self._negotiate_transport()
self._connect_namespaces() self._connect_namespaces()
self._opened = True self._opened = True
self._reset_heartbeat() self._reset_heartbeat()
@ -62,7 +65,6 @@ class EngineIO(LoggingMixin):
def _get_engineIO_session(self): def _get_engineIO_session(self):
warning_screen = self._yield_warning_screen() warning_screen = self._yield_warning_screen()
self._http_session = prepare_http_session(self._kw)
for elapsed_time in warning_screen: for elapsed_time in warning_screen:
transport = XHR_PollingTransport( transport = XHR_PollingTransport(
self._http_session, self._is_secure, self._url) self._http_session, self._is_secure, self._url)
@ -75,36 +77,58 @@ class EngineIO(LoggingMixin):
raise raise
warning = Exception('[waiting for connection] %s' % e) warning = Exception('[waiting for connection] %s' % e)
warning_screen.throw(warning) warning_screen.throw(warning)
assert engineIO_packet_type == 0 assert engineIO_packet_type == 0 # engineIO_packet_type == open
return parse_engineIO_session(engineIO_packet_data) return parse_engineIO_session(engineIO_packet_data)
def _negotiate_transport(self): def _negotiate_transport(self):
self._transport_name = 'xhr-polling' self._transport_instance = self._get_transport('xhr-polling')
return self._get_transport(self._transport_name) self.transport_name = 'xhr-polling'
is_ws_client = 'websocket' in self._client_transports
is_ws_server = 'websocket' in self._engineIO_session.transport_upgrades
if is_ws_client and is_ws_server:
try:
transport = self._get_transport('websocket')
transport.send_packet(2, 'probe')
for packet_type, packet_data in transport.recv_packet():
if packet_type == 3 and packet_data == b'probe':
transport.send_packet(5, '')
self._transport_instance = transport
self.transport_name = 'websocket'
else:
self._warn('unexpected engine.io packet')
except Exception:
pass
self._debug('[transport selected] %s', self.transport_name)
def _reset_heartbeat(self): def _reset_heartbeat(self):
try: try:
self._heartbeat_thread.halt() self._heartbeat_thread.halt()
hurried = self._heartbeat_thread.hurried
except AttributeError: except AttributeError:
pass hurried = False
ping_interval = self._engineIO_session.ping_interval ping_interval = self._engineIO_session.ping_interval
if self._transport_name.endswith('-polling'): if self.transport_name.endswith('-polling'):
# Use ping/pong to unblock recv for polling transport
hurry_interval_in_seconds = self._hurry_interval_in_seconds hurry_interval_in_seconds = self._hurry_interval_in_seconds
else: else:
# Use timeout to unblock recv for websocket transport
hurry_interval_in_seconds = ping_interval hurry_interval_in_seconds = ping_interval
self._heartbeat_thread = HeartbeatThread( self._heartbeat_thread = HeartbeatThread(
send_heartbeat=self._ping, send_heartbeat=self._ping,
relax_interval_in_seconds=ping_interval, relax_interval_in_seconds=ping_interval,
hurry_interval_in_seconds=hurry_interval_in_seconds) hurry_interval_in_seconds=hurry_interval_in_seconds)
self._heartbeat_thread.start() self._heartbeat_thread.start()
if hurried:
self._heartbeat_thread.hurry()
self._debug('[heartbeat reset]')
def _connect_namespaces(self): def _connect_namespaces(self):
pass pass
def _get_transport(self, transport_name): def _get_transport(self, transport_name):
self._debug('[transport selected] %s', transport_name)
SelectedTransport = { SelectedTransport = {
'xhr-polling': XHR_PollingTransport, 'xhr-polling': XHR_PollingTransport,
'websocket': WebsocketTransport,
}[transport_name] }[transport_name]
return SelectedTransport( return SelectedTransport(
self._http_session, self._is_secure, self._url, self._http_session, self._is_secure, self._url,
@ -143,51 +167,62 @@ class EngineIO(LoggingMixin):
def send(self, engineIO_packet_data): def send(self, engineIO_packet_data):
self._message(engineIO_packet_data) self._message(engineIO_packet_data)
@retry
def _open(self): def _open(self):
engineIO_packet_type = 0 engineIO_packet_type = 0
self._transport.send_packet(engineIO_packet_type, '') self._transport_instance.send_packet(engineIO_packet_type)
def _close(self): def _close(self):
self._wants_to_close = True self._wants_to_close = True
self._heartbeat_thread.halt() try:
self._heartbeat_thread.halt()
except AttributeError:
pass
if not self._opened: if not self._opened:
return return
engineIO_packet_type = 1 engineIO_packet_type = 1
self._transport.send_packet(engineIO_packet_type, '') try:
self._transport_instance.send_packet(engineIO_packet_type)
except (TimeoutError, ConnectionError):
pass
self._opened = False self._opened = False
@retry
def _ping(self, engineIO_packet_data=''): def _ping(self, engineIO_packet_data=''):
engineIO_packet_type = 2 engineIO_packet_type = 2
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) self._transport_instance.send_packet(
engineIO_packet_type, engineIO_packet_data)
@retry
def _pong(self, engineIO_packet_data=''): def _pong(self, engineIO_packet_data=''):
engineIO_packet_type = 3 engineIO_packet_type = 3
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) self._transport_instance.send_packet(
engineIO_packet_type, engineIO_packet_data)
@retry @retry
def _message(self, engineIO_packet_data): def _message(self, engineIO_packet_data, with_transport_instance=False):
engineIO_packet_type = 4 engineIO_packet_type = 4
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) if with_transport_instance:
transport = self._transport_instance
else:
transport = self._transport
transport.send_packet(engineIO_packet_type, engineIO_packet_data)
self._debug('[socket.io packet sent] %s', engineIO_packet_data) self._debug('[socket.io packet sent] %s', engineIO_packet_data)
@retry
def _upgrade(self): def _upgrade(self):
engineIO_packet_type = 5 engineIO_packet_type = 5
self._transport.send_packet(engineIO_packet_type, '') self._transport_instance.send_packet(engineIO_packet_type)
@retry
def _noop(self): def _noop(self):
engineIO_packet_type = 6 engineIO_packet_type = 6
self._transport.send_packet(engineIO_packet_type, '') self._transport_instance.send_packet(engineIO_packet_type)
# React # React
def wait(self, seconds=None, **kw): def wait(self, seconds=None, **kw):
'Wait in a loop and react to events as defined in the namespaces' 'Wait in a loop and react to events as defined in the namespaces'
# Use ping/pong to unblock recv for polling transport
self._heartbeat_thread.hurry() self._heartbeat_thread.hurry()
# Use timeout to unblock recv for websocket transport
self._transport.set_timeout(seconds=1)
# Listen
warning_screen = self._yield_warning_screen(seconds) warning_screen = self._yield_warning_screen(seconds)
for elapsed_time in warning_screen: for elapsed_time in warning_screen:
if self._should_stop_waiting(**kw): if self._should_stop_waiting(**kw):
@ -198,6 +233,7 @@ class EngineIO(LoggingMixin):
except TimeoutError: except TimeoutError:
pass pass
except ConnectionError as e: except ConnectionError as e:
self._opened = False
try: try:
warning = Exception('[connection error] %s' % e) warning = Exception('[connection error] %s' % e)
warning_screen.throw(warning) warning_screen.throw(warning)
@ -209,6 +245,7 @@ class EngineIO(LoggingMixin):
except PacketError: except PacketError:
pass pass
self._heartbeat_thread.relax() self._heartbeat_thread.relax()
self._transport.set_timeout()
def _should_stop_waiting(self): def _should_stop_waiting(self):
return self._wants_to_close return self._wants_to_close
@ -237,31 +274,31 @@ class EngineIO(LoggingMixin):
except KeyError: except KeyError:
raise PacketError( raise PacketError(
'unexpected engine.io packet type (%s)' % engineIO_packet_type) 'unexpected engine.io packet type (%s)' % engineIO_packet_type)
delegate(engineIO_packet_data, namespace._find_packet_callback) delegate(engineIO_packet_data, namespace)
if engineIO_packet_type is 4: if engineIO_packet_type is 4:
return engineIO_packet_data return engineIO_packet_data
def _on_open(self, data, find_packet_callback): def _on_open(self, data, namespace):
find_packet_callback('open')() namespace._find_packet_callback('open')()
def _on_close(self, data, find_packet_callback): def _on_close(self, data, namespace):
find_packet_callback('close')() namespace._find_packet_callback('close')()
def _on_ping(self, data, find_packet_callback): def _on_ping(self, data, namespace):
self._pong(data) self._pong(data)
find_packet_callback('ping')(data) namespace._find_packet_callback('ping')(data)
def _on_pong(self, data, find_packet_callback): def _on_pong(self, data, namespace):
find_packet_callback('pong')(data) namespace._find_packet_callback('pong')(data)
def _on_message(self, data, find_packet_callback): def _on_message(self, data, namespace):
find_packet_callback('message')(data) namespace._find_packet_callback('message')(data)
def _on_upgrade(self, data, find_packet_callback): def _on_upgrade(self, data, namespace):
find_packet_callback('upgrade')() namespace._find_packet_callback('upgrade')()
def _on_noop(self, data, find_packet_callback): def _on_noop(self, data, namespace):
find_packet_callback('noop')() namespace._find_packet_callback('noop')()
class SocketIO(EngineIO): class SocketIO(EngineIO):
@ -303,7 +340,7 @@ class SocketIO(EngineIO):
for path, namespace in self._namespace_by_path.items(): for path, namespace in self._namespace_by_path.items():
namespace._transport = self._transport_instance namespace._transport = self._transport_instance
if path: if path:
self.connect(path) self.connect(path, with_transport_instance=True)
def __exit__(self, *exception_pack): def __exit__(self, *exception_pack):
self.disconnect() self.disconnect()
@ -316,9 +353,10 @@ class SocketIO(EngineIO):
# Define # Define
def define(self, Namespace, path=''): def define(self, Namespace, path=''):
self._namespace_by_path[path] = namespace = Namespace(self, path)
if path: if path:
self.connect(path) self.connect(path)
self._namespace_by_path[path] = namespace = Namespace(self, path) self.wait(for_connect=True)
return namespace return namespace
def on(self, event, callback, path=''): def on(self, event, callback, path=''):
@ -336,20 +374,23 @@ class SocketIO(EngineIO):
# Act # Act
def connect(self, path): def connect(self, path, with_transport_instance=False):
socketIO_packet_type = 0 socketIO_packet_type = 0
socketIO_packet_data = format_socketIO_packet_data(path) socketIO_packet_data = format_socketIO_packet_data(path)
self._message(str(socketIO_packet_type) + socketIO_packet_data) self._message(
str(socketIO_packet_type) + socketIO_packet_data,
with_transport_instance)
def disconnect(self, path=''): def disconnect(self, path=''):
if not self._opened: if not path or not self._opened:
return self._close()
if path: elif path:
socketIO_packet_type = 1 socketIO_packet_type = 1
socketIO_packet_data = format_socketIO_packet_data(path) socketIO_packet_data = format_socketIO_packet_data(path)
self._message(str(socketIO_packet_type) + socketIO_packet_data) try:
else: self._message(str(socketIO_packet_type) + socketIO_packet_data)
self._close() except (TimeoutError, ConnectionError):
pass
try: try:
namespace = self._namespace_by_path.pop(path) namespace = self._namespace_by_path.pop(path)
namespace.on_disconnect() namespace.on_disconnect()
@ -379,13 +420,17 @@ class SocketIO(EngineIO):
# React # React
def wait(self, seconds=None, for_callbacks=False):
super(SocketIO, self).wait(seconds, for_callbacks=for_callbacks)
def wait_for_callbacks(self, seconds=None): def wait_for_callbacks(self, seconds=None):
self.wait(seconds, for_callbacks=True) self.wait(seconds, for_callbacks=True)
def _should_stop_waiting(self, for_callbacks): def _should_stop_waiting(self, for_connect=False, for_callbacks=False):
if for_connect:
for namespace in self._namespace_by_path.values():
is_namespace_connected = getattr(
namespace, '_connected', False)
if not is_namespace_connected:
return False
return True
if for_callbacks and not self._has_ack_callback: if for_callbacks and not self._has_ack_callback:
return True return True
return super(SocketIO, self)._should_stop_waiting() return super(SocketIO, self)._should_stop_waiting()
@ -413,16 +458,18 @@ class SocketIO(EngineIO):
except KeyError: except KeyError:
raise PacketError( raise PacketError(
'unexpected socket.io packet type (%s)' % socketIO_packet_type) 'unexpected socket.io packet type (%s)' % socketIO_packet_type)
delegate(socketIO_packet_data, namespace._find_packet_callback) delegate(socketIO_packet_data, namespace)
return socketIO_packet_data return socketIO_packet_data
def _on_connect(self, data, find_packet_callback): def _on_connect(self, data, namespace):
find_packet_callback('connect')() namespace._connected = True
namespace._find_packet_callback('connect')()
def _on_disconnect(self, data, find_packet_callback): def _on_disconnect(self, data, namespace):
find_packet_callback('disconnect')() namespace._connected = False
namespace._find_packet_callback('disconnect')()
def _on_event(self, data, find_packet_callback): def _on_event(self, data, namespace):
data_parsed = parse_socketIO_packet_data(data) data_parsed = parse_socketIO_packet_data(data)
args = data_parsed.args args = data_parsed.args
try: try:
@ -432,9 +479,9 @@ class SocketIO(EngineIO):
if data_parsed.ack_id is not None: if data_parsed.ack_id is not None:
args.append(self._prepare_to_send_ack( args.append(self._prepare_to_send_ack(
data_parsed.path, data_parsed.ack_id)) data_parsed.path, data_parsed.ack_id))
find_packet_callback(event)(*args) namespace._find_packet_callback(event)(*args)
def _on_ack(self, data, find_packet_callback): def _on_ack(self, data, namespace):
data_parsed = parse_socketIO_packet_data(data) data_parsed = parse_socketIO_packet_data(data)
try: try:
ack_callback = self._get_ack_callback(data_parsed.ack_id) ack_callback = self._get_ack_callback(data_parsed.ack_id)
@ -442,13 +489,13 @@ class SocketIO(EngineIO):
return return
ack_callback(*data_parsed.args) ack_callback(*data_parsed.args)
def _on_error(self, data, find_packet_callback): def _on_error(self, data, namespace):
find_packet_callback('error')(data) namespace._find_packet_callback('error')(data)
def _on_binary_event(self, data, find_packet_callback): def _on_binary_event(self, data, namespace):
self._warn('[not implemented] binary event') self._warn('[not implemented] binary event')
def _on_binary_ack(self, data, find_packet_callback): def _on_binary_ack(self, data, namespace):
self._warn('[not implemented] binary ack') self._warn('[not implemented] binary ack')
def _prepare_to_send_ack(self, path, ack_id): def _prepare_to_send_ack(self, path, ack_id):

View file

@ -1,3 +1,4 @@
import logging
from threading import Thread, Event from threading import Thread, Event
from .exceptions import ConnectionError, TimeoutError from .exceptions import ConnectionError, TimeoutError
@ -32,7 +33,7 @@ class HeartbeatThread(Thread):
interval_in_seconds = self._relax_interval_in_seconds interval_in_seconds = self._relax_interval_in_seconds
self._rest.wait(interval_in_seconds) self._rest.wait(interval_in_seconds)
except ConnectionError: except ConnectionError:
pass logging.debug('[heartbeat connection error]')
def relax(self): def relax(self):
self._adrenaline.clear() self._adrenaline.clear()
@ -42,6 +43,10 @@ class HeartbeatThread(Thread):
self._rest.set() self._rest.set()
self._rest.clear() self._rest.clear()
@property
def hurried(self):
return self._adrenaline.is_set()
def halt(self): def halt(self):
self._rest.set() self._rest.set()
self._halt.set() self._halt.set()

View file

@ -33,6 +33,10 @@ def _yield_elapsed_time(seconds=None):
start_time = time.time() start_time = time.time()
if seconds is None: if seconds is None:
while True: while True:
yield time.time() - start_time yield _get_elapsed_time(start_time)
while time.time() - start_time < seconds: while _get_elapsed_time(start_time) < seconds:
yield time.time() - start_time yield _get_elapsed_time(start_time)
def _get_elapsed_time(start_time):
return time.time() - start_time

View file

@ -141,31 +141,31 @@ class SocketIONamespace(EngineIONamespace):
class LoggingEngineIONamespace(EngineIONamespace): class LoggingEngineIONamespace(EngineIONamespace):
def on_open(self): def on_open(self):
self._debug('[open]') self._debug('[engine.io open]')
super(LoggingEngineIONamespace, self).on_open() super(LoggingEngineIONamespace, self).on_open()
def on_close(self): def on_close(self):
self._debug('[close]') self._debug('[engine.io close]')
super(LoggingEngineIONamespace, self).on_close() super(LoggingEngineIONamespace, self).on_close()
def on_ping(self, data): def on_ping(self, data):
self._debug('[ping] %s', data) self._debug('[engine.io ping] %s', data)
super(LoggingEngineIONamespace, self).on_ping(data) super(LoggingEngineIONamespace, self).on_ping(data)
def on_pong(self, data): def on_pong(self, data):
self._debug('[pong] %s', data) self._debug('[engine.io pong] %s', data)
super(LoggingEngineIONamespace, self).on_pong(data) super(LoggingEngineIONamespace, self).on_pong(data)
def on_message(self, data): def on_message(self, data):
self._debug('[message] %s', data) self._debug('[engine.io message] %s', data)
super(LoggingEngineIONamespace, self).on_message(data) super(LoggingEngineIONamespace, self).on_message(data)
def on_upgrade(self): def on_upgrade(self):
self._debug('[upgrade]') self._debug('[engine.io upgrade]')
super(LoggingEngineIONamespace, self).on_upgrade() super(LoggingEngineIONamespace, self).on_upgrade()
def on_noop(self): def on_noop(self):
self._debug('[noop]') self._debug('[engine.io noop]')
super(LoggingEngineIONamespace, self).on_noop() super(LoggingEngineIONamespace, self).on_noop()
def on_event(self, event, *args): def on_event(self, event, *args):
@ -173,22 +173,25 @@ class LoggingEngineIONamespace(EngineIONamespace):
arguments = [repr(_) for _ in args] arguments = [repr(_) for _ in args]
if callback: if callback:
arguments.append('callback(*args)') arguments.append('callback(*args)')
self._info('[event] %s(%s)', event, ', '.join(arguments)) self._info('[engine.io event] %s(%s)', event, ', '.join(arguments))
super(LoggingEngineIONamespace, self).on_event(event, *args) super(LoggingEngineIONamespace, self).on_event(event, *args)
class LoggingSocketIONamespace(SocketIONamespace): class LoggingSocketIONamespace(SocketIONamespace, LoggingEngineIONamespace):
def on_connect(self): def on_connect(self):
self._debug('%s[connect]', _make_logging_header(self.path)) self._debug(
'%s[socket.io connect]', _make_logging_header(self.path))
super(LoggingSocketIONamespace, self).on_connect() super(LoggingSocketIONamespace, self).on_connect()
def on_reconnect(self): def on_reconnect(self):
self._debug('%s[reconnect]', _make_logging_header(self.path)) self._debug(
'%s[socket.io reconnect]', _make_logging_header(self.path))
super(LoggingSocketIONamespace, self).on_reconnect() super(LoggingSocketIONamespace, self).on_reconnect()
def on_disconnect(self): def on_disconnect(self):
self._debug('%s[disconnect]', _make_logging_header(self.path)) self._debug(
'%s[socket.io disconnect]', _make_logging_header(self.path))
super(LoggingSocketIONamespace, self).on_disconnect() super(LoggingSocketIONamespace, self).on_disconnect()
def on_event(self, event, *args): def on_event(self, event, *args):
@ -197,12 +200,13 @@ class LoggingSocketIONamespace(SocketIONamespace):
if callback: if callback:
arguments.append('callback(*args)') arguments.append('callback(*args)')
self._info( self._info(
'%s[event] %s(%s)', _make_logging_header(self.path), event, '%s[socket.io event] %s(%s)', _make_logging_header(self.path),
', '.join(arguments)) event, ', '.join(arguments))
super(LoggingSocketIONamespace, self).on_event(event, *args) super(LoggingSocketIONamespace, self).on_event(event, *args)
def on_error(self, data): def on_error(self, data):
self._debug('%s[error] %s', _make_logging_header(self.path), data) self._debug(
'%s[socket.io error] %s', _make_logging_header(self.path), data)
super(LoggingSocketIONamespace, self).on_error() super(LoggingSocketIONamespace, self).on_error()

View file

@ -32,8 +32,8 @@ def parse_engineIO_session(engineIO_packet_data):
def encode_engineIO_content(engineIO_packets): def encode_engineIO_content(engineIO_packets):
content = bytearray() content = bytearray()
for packet_type, packet_data in engineIO_packets: for packet_type, packet_data in engineIO_packets:
packet_string = encode_string(str(packet_type) + packet_data) packet_text = format_packet_text(packet_type, packet_data)
content.extend(_make_packet_header(packet_string) + packet_string) content.extend(_make_packet_prefix(packet_text) + packet_text)
return content return content
@ -46,10 +46,10 @@ def decode_engineIO_content(content):
content, content_index) content, content_index)
except IndexError: except IndexError:
break break
content_index, packet_string = _read_packet_string( content_index, packet_text = _read_packet_text(
content, content_index, packet_length) content, content_index, packet_length)
engineIO_packet_type = int(get_character(packet_string, 0)) engineIO_packet_type, engineIO_packet_data = parse_packet_text(
engineIO_packet_data = packet_string[1:] packet_text)
yield engineIO_packet_type, engineIO_packet_data yield engineIO_packet_type, engineIO_packet_data
@ -85,6 +85,16 @@ def parse_socketIO_packet_data(socketIO_packet_data):
return SocketIOData(path=path, ack_id=ack_id, args=args) return SocketIOData(path=path, ack_id=ack_id, args=args)
def format_packet_text(packet_type, packet_data):
return encode_string(str(packet_type) + packet_data)
def parse_packet_text(packet_text):
packet_type = int(get_character(packet_text, 0))
packet_data = packet_text[1:]
return packet_type, packet_data
def get_namespace_path(socketIO_packet_data): def get_namespace_path(socketIO_packet_data):
if not socketIO_packet_data.startswith(b'/'): if not socketIO_packet_data.startswith(b'/'):
return '' return ''
@ -98,8 +108,8 @@ def get_namespace_path(socketIO_packet_data):
return ''.join(parts) return ''.join(parts)
def _make_packet_header(packet_string): def _make_packet_prefix(packet):
length_string = str(len(packet_string)) length_string = str(len(packet))
header_digits = bytearray([0]) header_digits = bytearray([0])
for i in range(len(length_string)): for i in range(len(length_string)):
header_digits.append(ord(length_string[i]) - 48) header_digits.append(ord(length_string[i]) - 48)
@ -120,8 +130,8 @@ def _read_packet_length(content, content_index):
return content_index, int(packet_length_string) return content_index, int(packet_length_string)
def _read_packet_string(content, content_index, packet_length): def _read_packet_text(content, content_index, packet_length):
while get_byte(content, content_index) == 255: while get_byte(content, content_index) == 255:
content_index += 1 content_index += 1
packet_string = content[content_index:content_index + packet_length] packet_text = content[content_index:content_index + packet_length]
return content_index + packet_length, packet_string return content_index + packet_length, packet_text

View file

@ -1,18 +1,26 @@
import six import six
try: try:
from urllib.parse import urlparse as parse_url from urllib import urlencode as format_query
except ImportError: except ImportError:
from urllib.parse import urlencode as format_query
try:
from urlparse import urlparse as parse_url from urlparse import urlparse as parse_url
except ImportError:
from urllib.parse import urlparse as parse_url
try:
memoryview = memoryview
except NameError:
memoryview = buffer
def get_character(x, index):
return chr(get_byte(x, index))
def get_byte(x, index): def get_byte(x, index):
return six.indexbytes(x, index) return six.indexbytes(x, index)
def get_character(x, index):
return chr(six.indexbytes(x, index))
def encode_string(x): def encode_string(x):
return x.encode('utf-8') return x.encode('utf-8')

View file

@ -6,7 +6,7 @@ from .. import SocketIO, LoggingNamespace, find_callback
HOST = 'localhost' HOST = 'localhost'
PORT = 8000 PORT = 9000
DATA = 'xxx' DATA = 'xxx'
PAYLOAD = {'xxx': 'yyy'} PAYLOAD = {'xxx': 'yyy'}
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@ -17,21 +17,18 @@ class BaseMixin(object):
def setUp(self): def setUp(self):
super(BaseMixin, self).setUp() super(BaseMixin, self).setUp()
self.called_on_response = False self.called_on_response = False
self.wait_time_in_seconds = 1
def tearDown(self): def tearDown(self):
super(BaseMixin, self).tearDown() super(BaseMixin, self).tearDown()
del self.socketIO self.socketIO.disconnect()
def test_disconnect(self): def test_disconnect(self):
'Disconnect' 'Disconnect'
namespace = self.socketIO.define(Namespace)
self.assertTrue(self.socketIO.connected) self.assertTrue(self.socketIO.connected)
self.assertFalse(namespace.called_on_disconnect)
self.socketIO.disconnect() self.socketIO.disconnect()
self.assertFalse(self.socketIO.connected)
# Use context manager
with SocketIO(HOST, PORT, Namespace) as self.socketIO:
namespace = self.socketIO.get_namespace()
self.assertFalse(namespace.called_on_disconnect)
self.assertTrue(self.socketIO.connected)
self.assertTrue(namespace.called_on_disconnect) self.assertTrue(namespace.called_on_disconnect)
self.assertFalse(self.socketIO.connected) self.assertFalse(self.socketIO.connected)
@ -116,7 +113,7 @@ class BaseMixin(object):
def test_wait_with_disconnect(self): def test_wait_with_disconnect(self):
'Exit loop when the client wants to disconnect' 'Exit loop when the client wants to disconnect'
self.socketIO.define(Namespace) self.socketIO.define(Namespace)
self.socketIO.emit('wait_with_disconnect') self.socketIO.disconnect()
timeout_in_seconds = 5 timeout_in_seconds = 5
start_time = time.time() start_time = time.time()
self.socketIO.wait(timeout_in_seconds) self.socketIO.wait(timeout_in_seconds)
@ -159,8 +156,17 @@ class Test_XHR_PollingTransport(BaseMixin, TestCase):
def setUp(self): def setUp(self):
super(Test_XHR_PollingTransport, self).setUp() super(Test_XHR_PollingTransport, self).setUp()
self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[ self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[
'xhr-polling']) 'xhr-polling'], verify=False)
self.wait_time_in_seconds = 1 self.assertEqual(self.socketIO.transport_name, 'xhr-polling')
class Test_WebsocketTransport(BaseMixin, TestCase):
def setUp(self):
super(Test_WebsocketTransport, self).setUp()
self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[
'xhr-polling', 'websocket'], verify=False)
self.assertEqual(self.socketIO.transport_name, 'websocket')
class Namespace(LoggingNamespace): class Namespace(LoggingNamespace):

View file

@ -1,6 +1,6 @@
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<script> <script>
var socket = io('http://localhost'); var socket = io('//localhost');
var chat = io('/chat'); var chat = io('/chat');
var news = io('/news'); var news = io('/news');

View file

@ -1,8 +1,19 @@
// DEBUG=* node serve.js // DEBUG=* node serve.js
var app = require('http').createServer(serve).listen(9000); var argv = require('yargs').argv;
if (argv.secure) {
var fs = require('fs');
var path = require('path');
var app = require('https').createServer({
key: fs.readFileSync(path.resolve(__dirname, 'ssl.key')),
cert: fs.readFileSync(path.resolve(__dirname, 'ssl.crt'))
}, serve);
} else {
var app = require('http').createServer(serve);
}
app.listen(9000);
var io = require('socket.io')(app); var io = require('socket.io')(app);
var fs = require('fs');
var PAYLOAD = {'xxx': 'yyy'}; var PAYLOAD = {'xxx': 'yyy'};
io.on('connection', function(socket) { io.on('connection', function(socket) {
@ -54,9 +65,6 @@ io.on('connection', function(socket) {
socket.on('bbb', function(payload, fn) { socket.on('bbb', function(payload, fn) {
if (fn) fn(payload); if (fn) fn(payload);
}); });
socket.on('wait_with_disconnect', function() {
socket.emit('wait_with_disconnect_response');
});
}); });
io.of('/chat').on('connection', function(socket) { io.of('/chat').on('connection', function(socket) {

View file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZTCCAk2gAwIBAgIJAK1HKQ8zF3cCMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxDDAKBgNVBAoMA1hZWjES
MBAGA1UEAwwJbG9jYWxob3N0MB4XDTE1MDQxNTE5NDUwNFoXDTE2MDQxNDE5NDUw
NFowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMQswCQYDVQQHDAJOWTEMMAoG
A1UECgwDWFlaMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDAQUM9+xbiDeJXg+7X6HgXwla2AnGKWbZ11hZZUYbQwHyq
ABDSqRQXVWvzac6b59/trZiJ7cQEH4+c8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2Sb
R/xkSUpq35yTuR5+oHgahDg1gbgXgPhB3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP
8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk6UR3cHCMqyzHulyfhgvkk5FN+EtSR9ex
dIrF6WXmfynhsAa/+bxbsgeBF9MNj3zvckCzxdQStdqOvy0mu40/7i9vwguh9cRo
HDn6lx5EaE+gSGU48UNnKX5iQdqEhprNVDj31MiJAgMBAAGjUDBOMB0GA1UdDgQW
BBRkFsPxYU+e6ZSFwmzoS45qiOzAaDAfBgNVHSMEGDAWgBRkFsPxYU+e6ZSFwmzo
S45qiOzAaDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB4JyOA5bZ3
NbkMvOjpDw+tKzcNiXZIkGfoQ8NC1DbGVhgG7Ps4VjXgUB552YSUV7iwyd3G78OC
+cxEcr+BvxXHXL2Wlxy0c/ZgBjRI5VnGbYQjjI2Iy2qJV+x5IR2oZatv45soZSLq
NFCg2KpOgcSRgs0oDGVBYO0d9m73s/kOySj2NGqVJsaQXqXtLWKnqToaCfl4Vnl+
zcMdUv8ajBZEPRg6oNi2QIvcNT8fS5gd/T4OXBa7pYuC79yOZ1X6bkKsZrcAdNGM
zO/jH6jKFjIBBx1Of+uZTzfAj/eoTu3foPuUQ+Z9NNE2nkE6SLyBSlxE7wD+SfjS
4/J0PNj22Uh3
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAQUM9+xbiDeJX
g+7X6HgXwla2AnGKWbZ11hZZUYbQwHyqABDSqRQXVWvzac6b59/trZiJ7cQEH4+c
8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2SbR/xkSUpq35yTuR5+oHgahDg1gbgXgPhB
3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk
6UR3cHCMqyzHulyfhgvkk5FN+EtSR9exdIrF6WXmfynhsAa/+bxbsgeBF9MNj3zv
ckCzxdQStdqOvy0mu40/7i9vwguh9cRoHDn6lx5EaE+gSGU48UNnKX5iQdqEhprN
VDj31MiJAgMBAAECggEANAFzbxC83+lhkMrfkQgRdFvdmN6QWBxsfvOql/61uUJY
dqQN6O5TwPwad33npcTTjjenS6hFndfrwUjNjLvSgp2aN/FTHVavH3FkkY7uYKEa
VebjHz20I7TZZhxtY1OFKacajV7JrZH1lduY8pccQ/8Is7ub88JvrQ+0zO5oTHnh
KEPYY5r2wLxKrzGm0NavRW9MpiHxz1vUGykvaGq9vR8dVFvZlLC5szYII+BvlII+
78XMnZbJ9ahT7dzfnzPdPPuyP3m4cdJ9c+7Advs0g2F3K/IDL3jZZCRZIaLxHIs0
PeI17teW0OmK4RWrnf6dSf0bww05x5In8GzUYgppAQKBgQD4lJVi3UmAB319CGeP
NE4cZFuMneNsuCkNEJONb8Cfsah2maM0if8tUNoR96JorjWgkUTG4oThGSQgJQw8
fPy6cW4EUhSvWCO+Q4MFFWpTcf0hBiz5O1d06FHVo39o8Ct9dv2bxJqfNtCUUf31
Fz5tvA+wvByOSazUC3AowQZ6FwKBgQDF/ksJbOBd/bu3ss7eJRjE2sXmuhfxrUiu
P5RoOEqHROAifatJk/3hwT6lx2y1g+3kJpZm9V16dNTkcuybL0yJ/VBE3uWuodrj
i9+wcg8XSnRp3BPVKzebKIKDTMdypOeb1f5yhx6cCtChRm1frKQdoXpMQqptM0jq
w3B4bryWXwKBgQCWSv+nLrPpvJ2aoyI56x3u/J59fliquw3W4FbWBOMpqnh4fJu4
gFbQRzoR8u82611xH2O9++brUhANf1jOmaMT9tDVu+rVuSyjNJ5azH/kw96PwPQg
HEjcXjpcOOYnxE4HJZJgQ5ZY/QNPKeOp88vC/RlfedyqCtF7ww6lFU+dMQKBgQC2
M7ut4sne9R8If74rZAwVLBauq1ZZi1O1NsFF33eGX/W7B9bXER+z3vfd61W4/L2x
FWmXOflaNaWsza27aZ2P5tM1bcIEIOKkQBYL9Aq7LkNPH74Ij4rOeEsStVddwy94
k0di8cFTbAhuQbdpMiCdO/qlrzvS3j0d/djEm3NlFQKBgQCpIrHaMcckCFsf2Y6o
zMnbi3859hve94OOJjauQLlw/nRE/+OaDsDN8iJoxnK0seek8ro1ixSBTScpuX8W
G2DBgqs9NrSQLe6FAckkGqVJdluoh5GewNneAcowkkauj2srnb6XtJDhFtTDY141
EPbeqGB9PUY9Ny8VzHkAb1vi6g==
-----END PRIVATE KEY-----

View file

@ -1,12 +1,32 @@
import requests import requests
import six
import socket
import ssl
import sys
import threading
import time import time
import websocket
from six import string_types
from .exceptions import ConnectionError, TimeoutError from .exceptions import ConnectionError, TimeoutError
from .parsers import decode_engineIO_content, encode_engineIO_content from .parsers import (
encode_engineIO_content, decode_engineIO_content,
format_packet_text, parse_packet_text)
from .symmetries import format_query, memoryview, parse_url
if not hasattr(websocket, 'create_connection'):
sys.exit("""\
An incompatible websocket library is conflicting with the one we need.
You can remove the incompatible library and install the correct one
by running the following commands:
yes | pip uninstall websocket websocket-client
pip install -U websocket-client""")
ENGINEIO_PROTOCOL = 3 ENGINEIO_PROTOCOL = 3
TRANSPORTS = 'websocket', 'xhr-polling' TRANSPORTS = 'xhr-polling', 'websocket'
class AbstractTransport(object): class AbstractTransport(object):
@ -20,7 +40,10 @@ class AbstractTransport(object):
def recv_packet(self): def recv_packet(self):
pass pass
def send_packet(self, engineIO_packet_type, engineIO_packet_data): def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
pass
def set_timeout(self, seconds=None):
pass pass
@ -29,56 +52,128 @@ class XHR_PollingTransport(AbstractTransport):
def __init__(self, http_session, is_secure, url, engineIO_session=None): def __init__(self, http_session, is_secure, url, engineIO_session=None):
super(XHR_PollingTransport, self).__init__( super(XHR_PollingTransport, self).__init__(
http_session, is_secure, url, engineIO_session) http_session, is_secure, url, engineIO_session)
self.http_url = '%s://%s/' % ('https' if is_secure else 'http', url) self._params = {
self.params = { 'EIO': ENGINEIO_PROTOCOL, 'transport': 'polling'}
'EIO': ENGINEIO_PROTOCOL,
'transport': 'polling',
}
if engineIO_session: if engineIO_session:
self.request_index = 1 self._request_index = 1
self.kw_get = dict(timeout=engineIO_session.ping_timeout) self._kw_get = dict(
self.kw_post = dict(headers={ timeout=engineIO_session.ping_timeout)
'content-type': 'application/octet-stream', self._kw_post = dict(
}) timeout=engineIO_session.ping_timeout,
self.params['sid'] = engineIO_session.id headers={'content-type': 'application/octet-stream'})
self._params['sid'] = engineIO_session.id
else: else:
self.request_index = 0 self._request_index = 0
self.kw_get = {} self._kw_get = {}
self.kw_post = {} self._kw_post = {}
http_scheme = 'https' if is_secure else 'http'
self._http_url = '%s://%s/' % (http_scheme, url)
self._request_index_lock = threading.Lock()
self._send_packet_lock = threading.Lock()
def recv_packet(self): def recv_packet(self):
params = dict(self.params) params = dict(self._params)
params['t'] = self._get_timestamp() params['t'] = self._get_timestamp()
response = get_response( response = get_response(
self.http_session.get, self.http_session.get,
self.http_url, self._http_url,
params=params, params=params,
**self.kw_get) **self._kw_get)
for engineIO_packet in decode_engineIO_content(response.content): for engineIO_packet in decode_engineIO_content(response.content):
yield engineIO_packet engineIO_packet_type, engineIO_packet_data = engineIO_packet
yield engineIO_packet_type, engineIO_packet_data
def send_packet(self, engineIO_packet_type, engineIO_packet_data): def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
params = dict(self.params) with self._send_packet_lock:
params['t'] = self._get_timestamp() params = dict(self._params)
response = get_response( params['t'] = self._get_timestamp()
self.http_session.post, data = encode_engineIO_content([
self.http_url,
params=params,
data=encode_engineIO_content([
(engineIO_packet_type, engineIO_packet_data), (engineIO_packet_type, engineIO_packet_data),
]), ])
**self.kw_post) response = get_response(
assert response.content == b'ok' self.http_session.post,
self._http_url,
params=params,
data=memoryview(data),
**self._kw_post)
assert response.content == b'ok'
def _get_timestamp(self): def _get_timestamp(self):
timestamp = '%s-%s' % (int(time.time() * 1000), self.request_index) with self._request_index_lock:
self.request_index += 1 timestamp = '%s-%s' % (
int(time.time() * 1000), self._request_index)
self._request_index += 1
return timestamp return timestamp
class WebsocketTransport(AbstractTransport):
def __init__(self, http_session, is_secure, url, engineIO_session=None):
super(WebsocketTransport, self).__init__(
http_session, is_secure, url, engineIO_session)
params = dict(http_session.params, **{
'EIO': ENGINEIO_PROTOCOL, 'transport': 'websocket'})
request = http_session.prepare_request(requests.Request('GET', url))
kw = {'header': ['%s: %s' % x for x in request.headers.items()]}
if engineIO_session:
params['sid'] = engineIO_session.id
kw['timeout'] = self._timeout = engineIO_session.ping_timeout
ws_url = '%s://%s/?%s' % (
'wss' if is_secure else 'ws', url, format_query(params))
http_scheme = 'https' if is_secure else 'http'
if http_scheme in http_session.proxies: # Use the correct proxy
proxy_url_pack = parse_url(http_session.proxies[http_scheme])
kw['http_proxy_host'] = proxy_url_pack.hostname
kw['http_proxy_port'] = proxy_url_pack.port
if proxy_url_pack.username:
kw['http_proxy_auth'] = (
proxy_url_pack.username, proxy_url_pack.password)
if http_session.verify:
if http_session.cert: # Specify certificate path on disk
if isinstance(http_session.cert, string_types):
kw['ca_certs'] = http_session.cert
else:
kw['ca_certs'] = http_session.cert[0]
else: # Do not verify the SSL certificate
kw['sslopt'] = {'cert_reqs': ssl.CERT_NONE}
try:
self._connection = websocket.create_connection(ws_url, **kw)
except Exception as e:
raise ConnectionError(e)
def recv_packet(self):
try:
packet_text = self._connection.recv()
except websocket.WebSocketTimeoutException as e:
raise TimeoutError('recv timed out (%s)' % e)
except websocket.SSLError as e:
raise ConnectionError('recv disconnected by SSL (%s)' % e)
except websocket.WebSocketConnectionClosedException as e:
raise ConnectionError('recv disconnected (%s)' % e)
except socket.error as e:
raise ConnectionError('recv disconnected (%s)' % e)
engineIO_packet_type, engineIO_packet_data = parse_packet_text(
six.b(packet_text))
yield engineIO_packet_type, engineIO_packet_data
def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
packet = format_packet_text(engineIO_packet_type, engineIO_packet_data)
try:
self._connection.send(packet)
except websocket.WebSocketTimeoutException as e:
raise TimeoutError('send timed out (%s)' % e)
except socket.error as e:
raise ConnectionError('send disconnected (%s)' % e)
except websocket.WebSocketConnectionClosedException as e:
raise ConnectionError('send disconnected (%s)' % e)
def set_timeout(self, seconds=None):
self._connection.settimeout(seconds or self._timeout)
def get_response(request, *args, **kw): def get_response(request, *args, **kw):
try: try:
response = request(*args, **kw) response = request(*args, stream=True, **kw)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
raise TimeoutError(e) raise TimeoutError(e)
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
@ -87,7 +182,8 @@ def get_response(request, *args, **kw):
raise ConnectionError('could not negotiate SSL (%s)' % e) raise ConnectionError('could not negotiate SSL (%s)' % e)
status_code = response.status_code status_code = response.status_code
if 200 != status_code: if 200 != status_code:
raise ConnectionError('unexpected status code (%s)' % status_code) raise ConnectionError('unexpected status code (%s %s)' % (
status_code, response.text))
return response return response
@ -98,7 +194,7 @@ def prepare_http_session(kw):
http_session.proxies.update(kw.get('proxies', {})) http_session.proxies.update(kw.get('proxies', {}))
http_session.hooks.update(kw.get('hooks', {})) http_session.hooks.update(kw.get('hooks', {}))
http_session.params.update(kw.get('params', {})) http_session.params.update(kw.get('params', {}))
http_session.verify = kw.get('verify') http_session.verify = kw.get('verify', True)
http_session.cert = kw.get('cert') http_session.cert = kw.get('cert')
http_session.cookies.update(kw.get('cookies', {})) http_session.cookies.update(kw.get('cookies', {}))
return http_session return http_session