├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── debian ├── changelog ├── compat ├── control ├── copyright └── rules ├── setup.cfg ├── setup.py └── socketIO_client ├── __init__.py ├── exceptions.py ├── heartbeats.py ├── logs.py ├── namespaces.py ├── parsers.py ├── symmetries.py ├── tests ├── __init__.py ├── index.html ├── package.json ├── proxy.js ├── serve.js ├── ssl.crt └── ssl.key └── transports.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | # Python 4 | *.egg* 5 | *.py[co] 6 | .cache 7 | .coverage 8 | __pycache__ 9 | build 10 | dist 11 | sdist 12 | # Transient 13 | *.log 14 | # Vim 15 | *.sw[op] 16 | *~ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | before_install: 8 | - sudo apt-get install nodejs; node --version 9 | install: 10 | - pip install -U coverage requests six websocket-client 11 | - npm install -G socket.io 12 | before_script: 13 | - DEBUG=* node socketIO_client/tests/serve.js & 14 | - sleep 1 15 | script: nosetests 16 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.7 2 | --- 3 | - Fixed thread cleanup 4 | - Fixed disconnect detection if defined directly thanks to Andreas Strikos 5 | - Fixed support for unicode payloads 6 | 7 | 0.6 8 | --- 9 | - Upgraded to socket.io protocol 1.x thanks to Sean Arietta and Joe Palmer 10 | - Fixed support for Python 3 11 | - Fixed SSL support 12 | - Added locks to fix concurrency issues with polling transport 13 | - Added SocketIO.off() and SocketIO.once() 14 | 15 | 0.5 16 | --- 17 | - Added support for Python 3 18 | - Added support for jsonp-polling thanks to Bernard Pratz 19 | - Added support for xhr-polling thanks to Francis Bull 20 | - Added support for query params and cookies 21 | - Fixed sending acknowledgments in custom namespaces thanks to Travis Odom 22 | - Rewrote library to use coroutines instead of threads to save memory 23 | 24 | 0.4 25 | --- 26 | - Added support for custom headers and proxies thanks to Rui and Sajal 27 | - Added support for server-side callbacks thanks to Zac Lee 28 | - Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget 29 | 30 | 0.3 31 | --- 32 | - Added support for secure connections 33 | - Added SocketIO.wait() 34 | - Improved exception handling in _RhythmicThread and _ListenerThread 35 | 36 | 0.2 37 | --- 38 | - Added support for callbacks and channels thanks to Paul Kienzle 39 | - Incorporated suggestions from Josh VanderLinden and Ian Fitzpatrick 40 | 41 | 0.1 42 | --- 43 | - Wrapped `code from StackOverflow `_ 44 | - Added exception handling to destructor in case of connection failure 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Roy Hyunjin Han and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include socketIO_client * 2 | include *.html *.js *.rst LICENSE 3 | global-exclude *.pyc 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master 2 | :target: https://travis-ci.org/invisibleroads/socketIO-client 3 | 4 | 5 | socketIO-client 6 | =============== 7 | Here is a `socket.io `_ client library for Python. You can use it to write test code for your socket.io server. 8 | 9 | Please note that this version implements `socket.io protocol 1.x `_, which is not backwards compatible. If you want to communicate using `socket.io protocol 0.9 `_ (which is compatible with `gevent-socketio `_), please use `socketIO-client 0.5.7.2 `_. 10 | 11 | 12 | Installation 13 | ------------ 14 | Install the package in an isolated environment. :: 15 | 16 | VIRTUAL_ENV=$HOME/.virtualenv 17 | 18 | # Prepare isolated environment 19 | virtualenv $VIRTUAL_ENV 20 | 21 | # Activate isolated environment 22 | source $VIRTUAL_ENV/bin/activate 23 | 24 | # Install package 25 | pip install -U socketIO-client 26 | 27 | 28 | Usage 29 | ----- 30 | Activate isolated environment. :: 31 | 32 | VIRTUAL_ENV=$HOME/.virtualenv 33 | source $VIRTUAL_ENV/bin/activate 34 | 35 | Launch your socket.io server. :: 36 | 37 | cd $(python -c "import os, socketIO_client;\ 38 | print(os.path.dirname(socketIO_client.__file__))") 39 | 40 | DEBUG=* node tests/serve.js # Start socket.io server in terminal one 41 | DEBUG=* node tests/proxy.js # Start proxy server in terminal two 42 | nosetests # Run tests in terminal three 43 | 44 | For debugging information, run these commands first. :: 45 | 46 | import logging 47 | logging.getLogger('socketIO-client').setLevel(logging.DEBUG) 48 | logging.basicConfig() 49 | 50 | Emit. :: 51 | 52 | from socketIO_client import SocketIO, LoggingNamespace 53 | 54 | with SocketIO('127.0.0.1', 8000, LoggingNamespace) as socketIO: 55 | socketIO.emit('aaa') 56 | socketIO.wait(seconds=1) 57 | 58 | Emit with callback. :: 59 | 60 | from socketIO_client import SocketIO, LoggingNamespace 61 | 62 | def on_bbb_response(*args): 63 | print('on_bbb_response', args) 64 | 65 | with SocketIO('127.0.0.1', 8000, LoggingNamespace) as socketIO: 66 | socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) 67 | socketIO.wait_for_callbacks(seconds=1) 68 | 69 | Define events. :: 70 | 71 | from socketIO_client import SocketIO, LoggingNamespace 72 | 73 | def on_connect(): 74 | print('connect') 75 | 76 | def on_disconnect(): 77 | print('disconnect') 78 | 79 | def on_reconnect(): 80 | print('reconnect') 81 | 82 | def on_aaa_response(*args): 83 | print('on_aaa_response', args) 84 | 85 | socketIO = SocketIO('127.0.0.1', 8000, LoggingNamespace) 86 | socketIO.on('connect', on_connect) 87 | socketIO.on('disconnect', on_disconnect) 88 | socketIO.on('reconnect', on_reconnect) 89 | 90 | # Listen 91 | socketIO.on('aaa_response', on_aaa_response) 92 | socketIO.emit('aaa') 93 | socketIO.emit('aaa') 94 | socketIO.wait(seconds=1) 95 | 96 | # Stop listening 97 | socketIO.off('aaa_response') 98 | socketIO.emit('aaa') 99 | socketIO.wait(seconds=1) 100 | 101 | # Listen only once 102 | socketIO.once('aaa_response', on_aaa_response) 103 | socketIO.emit('aaa') # Activate aaa_response 104 | socketIO.emit('aaa') # Ignore 105 | socketIO.wait(seconds=1) 106 | 107 | Define events in a namespace. :: 108 | 109 | from socketIO_client import SocketIO, BaseNamespace 110 | 111 | class Namespace(BaseNamespace): 112 | 113 | def on_aaa_response(self, *args): 114 | print('on_aaa_response', args) 115 | self.emit('bbb') 116 | 117 | socketIO = SocketIO('127.0.0.1', 8000, Namespace) 118 | socketIO.emit('aaa') 119 | socketIO.wait(seconds=1) 120 | 121 | Define standard events. :: 122 | 123 | from socketIO_client import SocketIO, BaseNamespace 124 | 125 | class Namespace(BaseNamespace): 126 | 127 | def on_connect(self): 128 | print('[Connected]') 129 | 130 | def on_reconnect(self): 131 | print('[Reconnected]') 132 | 133 | def on_disconnect(self): 134 | print('[Disconnected]') 135 | 136 | socketIO = SocketIO('127.0.0.1', 8000, Namespace) 137 | socketIO.wait(seconds=1) 138 | 139 | Define different namespaces on a single socket. :: 140 | 141 | from socketIO_client import SocketIO, BaseNamespace 142 | 143 | class ChatNamespace(BaseNamespace): 144 | 145 | def on_aaa_response(self, *args): 146 | print('on_aaa_response', args) 147 | 148 | class NewsNamespace(BaseNamespace): 149 | 150 | def on_aaa_response(self, *args): 151 | print('on_aaa_response', args) 152 | 153 | socketIO = SocketIO('127.0.0.1', 8000) 154 | chat_namespace = socketIO.define(ChatNamespace, '/chat') 155 | news_namespace = socketIO.define(NewsNamespace, '/news') 156 | 157 | chat_namespace.emit('aaa') 158 | news_namespace.emit('aaa') 159 | socketIO.wait(seconds=1) 160 | 161 | Connect via SSL (https://github.com/invisibleroads/socketIO-client/issues/54). :: 162 | 163 | from socketIO_client import SocketIO 164 | 165 | # Skip server certificate verification 166 | SocketIO('https://127.0.0.1', verify=False) 167 | # Verify the server certificate 168 | SocketIO('https://127.0.0.1', verify='server.crt') 169 | # Verify the server certificate and encrypt using client certificate 170 | socketIO = SocketIO('https://127.0.0.1', verify='server.crt', cert=( 171 | 'client.crt', 'client.key')) 172 | 173 | Specify params, headers, cookies, proxies thanks to the `requests `_ library. :: 174 | 175 | from socketIO_client import SocketIO 176 | from base64 import b64encode 177 | 178 | SocketIO( 179 | '127.0.0.1', 8000, 180 | params={'q': 'qqq'}, 181 | headers={'Authorization': 'Basic ' + b64encode('username:password')}, 182 | cookies={'a': 'aaa'}, 183 | proxies={'https': 'https://proxy.example.com:8080'}) 184 | 185 | Wait forever. :: 186 | 187 | from socketIO_client import SocketIO 188 | 189 | socketIO = SocketIO('127.0.0.1', 8000) 190 | socketIO.wait() 191 | 192 | Don't wait forever. :: 193 | 194 | from requests.exceptions import ConnectionError 195 | from socketIO_client import SocketIO 196 | 197 | try: 198 | socket = SocketIO('127.0.0.1', 8000, wait_for_connection=False) 199 | socket.wait() 200 | except ConnectionError: 201 | print('The server is down. Try again later.') 202 | 203 | 204 | License 205 | ------- 206 | This software is available under the MIT License. 207 | 208 | 209 | Credits 210 | ------- 211 | - `Guillermo Rauch `_ wrote the `socket.io specification `_. 212 | - `Hiroki Ohtani `_ wrote `websocket-client `_. 213 | - `Roderick Hodgson `_ wrote a `prototype for a Python client to a socket.io server `_. 214 | - `Alexandre Bourget `_ wrote `gevent-socketio `_, which is a socket.io server written in Python. 215 | - `Paul Kienzle `_, `Zac Lee `_, `Josh VanderLinden `_, `Ian Fitzpatrick `_, `Lucas Klein `_, `Rui Chicoria `_, `Travis Odom `_, `Patrick Huber `_, `Brad Campbell `_, `Daniel `_, `Sean Arietta `_, `Sacha Stafyniak `_ submitted code to expand support of the socket.io protocol. 216 | - `Bernard Pratz `_, `Francis Bull `_ wrote prototypes to support xhr-polling and jsonp-polling. 217 | - `Joe Palmer `_ sponsored development. 218 | - `Eric Chen `_, `Denis Zinevich `_, `Thiago Hersan `_, `Nayef Copty `_, `Jörgen Karlsson `_, `Branden Ghena `_, `Tim Landscheidt `_, `Matt Porritt `_, `Matt Dainty `_, `Thomaz de Oliveira dos Reis `_, `Felix König `_, `George Wilson `_, `Andreas Strikos `_, `Alessio Sergi `_ `Claudio Yacarini `_, `Khairi Hafsham `_, `Robbie Clarken `_ suggested ways to make the connection more robust. 219 | - `Merlijn van Deen `_, `Frederic Sureau `_, `Marcus Cobden `_, `Drew Hutchison `_, `wuurrd `_, `Adam Kecer `_, `Alex Monk `_, `Vishal P R `_, `John Vandenberg `_, `Thomas Grainger `_, `Daniel Quinn `_, `Adric Worley `_, `Adam Roses Wight `_, `Jan Včelák `_ proposed changes that make the library more friendly and practical for you! 220 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | python-socketio-client (0.7.0) UNRELEASED; urgency=medium 2 | 3 | * First version with Debian packaging. 4 | 5 | -- Adam Roses Wight Tue, 04 Aug 2015 21:30:11 -0700 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python-socketio-client 2 | Maintainer: Roy Hyunjin Han 3 | Section: python 4 | Priority: optional 5 | Build-Depends: debhelper (>= 9), 6 | dh-python, 7 | python-all, python3-all, 8 | python-coverage, python3-coverage, 9 | python-nose, python3-nose, 10 | python-requests, python3-requests, 11 | python-setuptools, python3-setuptools, 12 | python-six, python3-six, 13 | python-websocket, python3-websocket 14 | Standards-Version: 3.9.6 15 | Homepage: https://github.com/invisibleroads/socketIO-client 16 | 17 | Package: python-socketio-client 18 | Architecture: all 19 | Depends: ${python:Depends}, 20 | ${misc:Depends}, 21 | python-requests, 22 | python-six 23 | Description: A socket.io client library for Python 2 24 | http://pypi.python.org/pypi/socketIO-client 25 | . 26 | This version implements socket.io protocol 1.x 27 | . 28 | Package only includes python2 bindings. 29 | 30 | Package: python3-socketio-client 31 | Architecture: all 32 | Depends: ${python3:Depends}, 33 | ${misc:Depends}, 34 | python3-requests, 35 | python3-six 36 | Description: A socket.io client library for Python 3 37 | http://pypi.python.org/pypi/socketIO-client 38 | . 39 | This version implements socket.io protocol 1.x 40 | . 41 | Package only includes python3 bindings. 42 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: socketIO-client 3 | 4 | Files: * 5 | Copyright: 2016 Roy Hyunjin Han 6 | License: MIT 7 | 8 | License: MIT 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | . 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | . 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_NAME=socketio-client 4 | 5 | %: 6 | dh $@ --with python2,python3 --buildsystem=pybuild 7 | 8 | override_dh_auto_test: 9 | # Skip tests. 10 | # FIXME: Need packages for these nodejs libraries. 11 | # npm install socket.io 12 | # DEBUG=* nodejs socketIO_client/tests/serve.js & 13 | # sleep 1 14 | # PYBUILD_SYSTEM=custom \ 15 | # PYBUILD_TEST_ARGS="nosetests -v -v" \ 16 | # dh_auto_test 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | detailed-errors = TRUE 3 | with-coverage = TRUE 4 | cover-package = socketIO_client 5 | cover-erase = TRUE 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | from os.path import abspath, dirname, join 3 | from setuptools import find_packages, setup 4 | 5 | 6 | HERE = dirname(abspath(__file__)) 7 | LOAD_TEXT = lambda name: io.open(join(HERE, name), encoding='UTF-8').read() 8 | DESCRIPTION = '\n\n'.join(LOAD_TEXT(_) for _ in [ 9 | 'README.rst', 10 | 'CHANGES.rst', 11 | ]) 12 | setup( 13 | name='socketIO-client', 14 | version='0.7.2', 15 | description='A socket.io client library', 16 | long_description=DESCRIPTION, 17 | license='MIT', 18 | classifiers=[ 19 | 'Intended Audience :: Developers', 20 | 'Programming Language :: Python', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Development Status :: 5 - Production/Stable', 23 | ], 24 | keywords='socket.io node.js', 25 | author='Roy Hyunjin Han', 26 | author_email='rhh@crosscompute.com', 27 | url='https://github.com/invisibleroads/socketIO-client', 28 | install_requires=[ 29 | 'requests>=2.7.0', 30 | 'six', 31 | 'websocket-client', 32 | ], 33 | tests_require=[ 34 | 'nose', 35 | 'coverage', 36 | ], 37 | packages=find_packages(), 38 | include_package_data=True, 39 | zip_safe=False) 40 | -------------------------------------------------------------------------------- /socketIO_client/__init__.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | 3 | from .exceptions import ConnectionError, TimeoutError, PacketError 4 | from .heartbeats import HeartbeatThread 5 | from .logs import LoggingMixin 6 | from .namespaces import ( 7 | EngineIONamespace, SocketIONamespace, 8 | LoggingSocketIONamespace, find_callback, make_logging_prefix) 9 | from .parsers import ( 10 | parse_host, parse_engineIO_session, 11 | format_socketIO_packet_data, parse_socketIO_packet_data, 12 | get_namespace_path) 13 | from .symmetries import get_character 14 | from .transports import ( 15 | WebsocketTransport, XHR_PollingTransport, prepare_http_session, TRANSPORTS) 16 | 17 | 18 | __all__ = 'SocketIO', 'SocketIONamespace' 19 | __version__ = '0.7.2' 20 | BaseNamespace = SocketIONamespace 21 | LoggingNamespace = LoggingSocketIONamespace 22 | 23 | 24 | def retry(f): 25 | def wrap(*args, **kw): 26 | self = args[0] 27 | try: 28 | return f(*args, **kw) 29 | except (TimeoutError, ConnectionError): 30 | self._opened = False 31 | return f(*args, **kw) 32 | return wrap 33 | 34 | 35 | class EngineIO(LoggingMixin): 36 | 37 | def __init__( 38 | self, host, port=None, Namespace=EngineIONamespace, 39 | wait_for_connection=True, transports=TRANSPORTS, 40 | resource='engine.io', hurry_interval_in_seconds=1, **kw): 41 | self._is_secure, self._url = parse_host(host, port, resource) 42 | self._wait_for_connection = wait_for_connection 43 | self._client_transports = transports 44 | self._hurry_interval_in_seconds = hurry_interval_in_seconds 45 | self._http_session = prepare_http_session(kw) 46 | 47 | self._log_name = self._url 48 | self._opened = False 49 | self._wants_to_close = False 50 | atexit.register(self._close) 51 | 52 | if Namespace: 53 | self.define(Namespace) 54 | self._transport 55 | 56 | # Connect 57 | 58 | @property 59 | def _transport(self): 60 | if self._opened: 61 | return self._transport_instance 62 | self._engineIO_session = self._get_engineIO_session() 63 | self._negotiate_transport() 64 | self._connect_namespaces() 65 | self._opened = True 66 | self._reset_heartbeat() 67 | return self._transport_instance 68 | 69 | def _get_engineIO_session(self): 70 | warning_screen = self._yield_warning_screen() 71 | for elapsed_time in warning_screen: 72 | transport = XHR_PollingTransport( 73 | self._http_session, self._is_secure, self._url) 74 | try: 75 | engineIO_packet_type, engineIO_packet_data = next( 76 | transport.recv_packet()) 77 | break 78 | except (TimeoutError, ConnectionError) as e: 79 | if not self._wait_for_connection: 80 | raise 81 | warning = Exception( 82 | '[engine.io waiting for connection] %s' % e) 83 | warning_screen.throw(warning) 84 | assert engineIO_packet_type == 0 # engineIO_packet_type == open 85 | return parse_engineIO_session(engineIO_packet_data) 86 | 87 | def _negotiate_transport(self): 88 | self._transport_instance = self._get_transport('xhr-polling') 89 | self.transport_name = 'xhr-polling' 90 | is_ws_client = 'websocket' in self._client_transports 91 | is_ws_server = 'websocket' in self._engineIO_session.transport_upgrades 92 | if is_ws_client and is_ws_server: 93 | try: 94 | transport = self._get_transport('websocket') 95 | transport.send_packet(2, 'probe') 96 | for packet_type, packet_data in transport.recv_packet(): 97 | if packet_type == 3 and packet_data == b'probe': 98 | transport.send_packet(5, '') 99 | self._transport_instance = transport 100 | self.transport_name = 'websocket' 101 | else: 102 | self._warn('unexpected engine.io packet') 103 | except Exception: 104 | pass 105 | self._debug('[engine.io transport selected] %s', self.transport_name) 106 | 107 | def _reset_heartbeat(self): 108 | try: 109 | self._heartbeat_thread.halt() 110 | hurried = self._heartbeat_thread.hurried 111 | except AttributeError: 112 | hurried = False 113 | ping_interval = self._engineIO_session.ping_interval 114 | if self.transport_name.endswith('-polling'): 115 | # Use ping/pong to unblock recv for polling transport 116 | hurry_interval_in_seconds = self._hurry_interval_in_seconds 117 | else: 118 | # Use timeout to unblock recv for websocket transport 119 | hurry_interval_in_seconds = ping_interval 120 | self._heartbeat_thread = HeartbeatThread( 121 | send_heartbeat=self._ping, 122 | relax_interval_in_seconds=ping_interval, 123 | hurry_interval_in_seconds=hurry_interval_in_seconds) 124 | self._heartbeat_thread.start() 125 | if hurried: 126 | self._heartbeat_thread.hurry() 127 | self._debug('[engine.io heartbeat reset]') 128 | 129 | def _connect_namespaces(self): 130 | pass 131 | 132 | def _get_transport(self, transport_name): 133 | SelectedTransport = { 134 | 'xhr-polling': XHR_PollingTransport, 135 | 'websocket': WebsocketTransport, 136 | }[transport_name] 137 | return SelectedTransport( 138 | self._http_session, self._is_secure, self._url, 139 | self._engineIO_session) 140 | 141 | def __enter__(self): 142 | return self 143 | 144 | def __exit__(self, *exception_pack): 145 | self._close() 146 | 147 | def __del__(self): 148 | self._close() 149 | 150 | # Define 151 | 152 | def define(self, Namespace): 153 | self._namespace = namespace = Namespace(self) 154 | return namespace 155 | 156 | def on(self, event, callback): 157 | try: 158 | namespace = self.get_namespace() 159 | except PacketError: 160 | namespace = self.define(EngineIONamespace) 161 | return namespace.on(event, callback) 162 | 163 | def once(self, event, callback): 164 | try: 165 | namespace = self.get_namespace() 166 | except PacketError: 167 | namespace = self.define(EngineIONamespace) 168 | return namespace.once(event, callback) 169 | 170 | def off(self, event): 171 | try: 172 | namespace = self.get_namespace() 173 | except PacketError: 174 | namespace = self.define(EngineIONamespace) 175 | return namespace.off(event) 176 | 177 | def get_namespace(self): 178 | try: 179 | return self._namespace 180 | except AttributeError: 181 | raise PacketError('undefined engine.io namespace') 182 | 183 | # Act 184 | 185 | def send(self, engineIO_packet_data): 186 | self._message(engineIO_packet_data) 187 | 188 | def _open(self): 189 | engineIO_packet_type = 0 190 | self._transport_instance.send_packet(engineIO_packet_type) 191 | 192 | def _close(self): 193 | self._wants_to_close = True 194 | try: 195 | self._heartbeat_thread.halt() 196 | self._heartbeat_thread.join() 197 | except AttributeError: 198 | pass 199 | if not hasattr(self, '_opened') or not self._opened: 200 | return 201 | engineIO_packet_type = 1 202 | try: 203 | self._transport_instance.send_packet(engineIO_packet_type) 204 | except (TimeoutError, ConnectionError): 205 | pass 206 | self._opened = False 207 | 208 | def _ping(self, engineIO_packet_data=''): 209 | engineIO_packet_type = 2 210 | self._transport_instance.send_packet( 211 | engineIO_packet_type, engineIO_packet_data) 212 | 213 | def _pong(self, engineIO_packet_data=''): 214 | engineIO_packet_type = 3 215 | self._transport_instance.send_packet( 216 | engineIO_packet_type, engineIO_packet_data) 217 | 218 | @retry 219 | def _message(self, engineIO_packet_data, with_transport_instance=False): 220 | engineIO_packet_type = 4 221 | if with_transport_instance: 222 | transport = self._transport_instance 223 | else: 224 | transport = self._transport 225 | transport.send_packet(engineIO_packet_type, engineIO_packet_data) 226 | self._debug('[socket.io packet sent] %s', engineIO_packet_data) 227 | 228 | def _upgrade(self): 229 | engineIO_packet_type = 5 230 | self._transport_instance.send_packet(engineIO_packet_type) 231 | 232 | def _noop(self): 233 | engineIO_packet_type = 6 234 | self._transport_instance.send_packet(engineIO_packet_type) 235 | 236 | # React 237 | 238 | def wait(self, seconds=None, **kw): 239 | 'Wait in a loop and react to events as defined in the namespaces' 240 | # Use ping/pong to unblock recv for polling transport 241 | self._heartbeat_thread.hurry() 242 | # Use timeout to unblock recv for websocket transport 243 | self._transport.set_timeout(seconds=1) 244 | # Listen 245 | warning_screen = self._yield_warning_screen(seconds) 246 | for elapsed_time in warning_screen: 247 | if self._should_stop_waiting(**kw): 248 | break 249 | try: 250 | try: 251 | self._process_packets() 252 | except TimeoutError: 253 | pass 254 | except KeyboardInterrupt: 255 | self._close() 256 | raise 257 | except ConnectionError as e: 258 | self._opened = False 259 | try: 260 | warning = Exception('[connection error] %s' % e) 261 | warning_screen.throw(warning) 262 | except StopIteration: 263 | self._warn(warning) 264 | try: 265 | namespace = self.get_namespace() 266 | namespace._find_packet_callback('disconnect')() 267 | except PacketError: 268 | pass 269 | self._heartbeat_thread.relax() 270 | self._transport.set_timeout() 271 | 272 | def _should_stop_waiting(self): 273 | return self._wants_to_close 274 | 275 | def _process_packets(self): 276 | for engineIO_packet in self._transport.recv_packet(): 277 | try: 278 | self._process_packet(engineIO_packet) 279 | except PacketError as e: 280 | self._warn('[packet error] %s', e) 281 | 282 | def _process_packet(self, packet): 283 | engineIO_packet_type, engineIO_packet_data = packet 284 | # Launch callbacks 285 | namespace = self.get_namespace() 286 | try: 287 | delegate = { 288 | 0: self._on_open, 289 | 1: self._on_close, 290 | 2: self._on_ping, 291 | 3: self._on_pong, 292 | 4: self._on_message, 293 | 5: self._on_upgrade, 294 | 6: self._on_noop, 295 | }[engineIO_packet_type] 296 | except KeyError: 297 | raise PacketError( 298 | 'unexpected engine.io packet type (%s)' % engineIO_packet_type) 299 | delegate(engineIO_packet_data, namespace) 300 | if engineIO_packet_type == 4: 301 | return engineIO_packet_data 302 | 303 | def _on_open(self, data, namespace): 304 | namespace._find_packet_callback('open')() 305 | 306 | def _on_close(self, data, namespace): 307 | namespace._find_packet_callback('close')() 308 | 309 | def _on_ping(self, data, namespace): 310 | self._pong(data) 311 | namespace._find_packet_callback('ping')(data) 312 | 313 | def _on_pong(self, data, namespace): 314 | namespace._find_packet_callback('pong')(data) 315 | 316 | def _on_message(self, data, namespace): 317 | namespace._find_packet_callback('message')(data) 318 | 319 | def _on_upgrade(self, data, namespace): 320 | namespace._find_packet_callback('upgrade')() 321 | 322 | def _on_noop(self, data, namespace): 323 | namespace._find_packet_callback('noop')() 324 | 325 | 326 | class SocketIO(EngineIO): 327 | """Create a socket.io client that connects to a socket.io server 328 | at the specified host and port. 329 | 330 | - Define the behavior of the client by specifying a custom Namespace. 331 | - Prefix host with https:// to use SSL. 332 | - Set wait_for_connection=True to block until we have a connection. 333 | - Specify desired transports=['websocket', 'xhr-polling']. 334 | - Pass query params, headers, cookies, proxies as keyword arguments. 335 | 336 | SocketIO( 337 | '127.0.0.1', 8000, 338 | params={'q': 'qqq'}, 339 | headers={'Authorization': 'Basic ' + b64encode('username:password')}, 340 | cookies={'a': 'aaa'}, 341 | proxies={'https': 'https://proxy.example.com:8080'}) 342 | """ 343 | 344 | def __init__( 345 | self, host='127.0.0.1', port=None, Namespace=SocketIONamespace, 346 | wait_for_connection=True, transports=TRANSPORTS, 347 | resource='socket.io', hurry_interval_in_seconds=1, **kw): 348 | self._namespace_by_path = {} 349 | self._callback_by_ack_id = {} 350 | self._ack_id = 0 351 | super(SocketIO, self).__init__( 352 | host, port, Namespace, wait_for_connection, transports, 353 | resource, hurry_interval_in_seconds, **kw) 354 | 355 | # Connect 356 | 357 | @property 358 | def connected(self): 359 | return self._opened 360 | 361 | def _connect_namespaces(self): 362 | for path, namespace in self._namespace_by_path.items(): 363 | namespace._transport = self._transport_instance 364 | if path: 365 | self.connect(path, with_transport_instance=True) 366 | 367 | def __exit__(self, *exception_pack): 368 | self.disconnect() 369 | super(SocketIO, self).__exit__(*exception_pack) 370 | 371 | def __del__(self): 372 | self.disconnect() 373 | super(SocketIO, self).__del__() 374 | 375 | # Define 376 | 377 | def define(self, Namespace, path=''): 378 | self._namespace_by_path[path] = namespace = Namespace(self, path) 379 | if path: 380 | self.connect(path) 381 | self.wait(for_namespace=namespace) 382 | return namespace 383 | 384 | def on(self, event, callback, path=''): 385 | try: 386 | namespace = self.get_namespace(path) 387 | except PacketError: 388 | namespace = self.define(SocketIONamespace, path) 389 | return namespace.on(event, callback) 390 | 391 | def get_namespace(self, path=''): 392 | try: 393 | return self._namespace_by_path[path] 394 | except KeyError: 395 | raise PacketError('undefined socket.io namespace (%s)' % path) 396 | 397 | # Act 398 | 399 | def connect(self, path='', with_transport_instance=False): 400 | if path or not self.connected: 401 | socketIO_packet_type = 0 402 | socketIO_packet_data = format_socketIO_packet_data(path) 403 | self._message( 404 | str(socketIO_packet_type) + socketIO_packet_data, 405 | with_transport_instance) 406 | self._wants_to_close = False 407 | 408 | def disconnect(self, path=''): 409 | if path and self._opened: 410 | socketIO_packet_type = 1 411 | socketIO_packet_data = format_socketIO_packet_data(path) 412 | try: 413 | self._message(str(socketIO_packet_type) + socketIO_packet_data) 414 | except (TimeoutError, ConnectionError): 415 | pass 416 | elif not path: 417 | self._close() 418 | try: 419 | namespace = self._namespace_by_path[path] 420 | namespace._find_packet_callback('disconnect')() 421 | if path: 422 | del self._namespace_by_path[path] 423 | except KeyError: 424 | pass 425 | 426 | def emit(self, event, *args, **kw): 427 | path = kw.get('path', '') 428 | callback, args = find_callback(args, kw) 429 | ack_id = self._set_ack_callback(callback) if callback else None 430 | args = [event] + list(args) 431 | socketIO_packet_type = 2 432 | socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) 433 | self._message(str(socketIO_packet_type) + socketIO_packet_data) 434 | 435 | def send(self, data='', callback=None, **kw): 436 | path = kw.get('path', '') 437 | args = [data] 438 | if callback: 439 | args.append(callback) 440 | self.emit('message', *args, path=path) 441 | 442 | def _ack(self, path, ack_id, *args): 443 | socketIO_packet_type = 3 444 | socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) 445 | self._message(str(socketIO_packet_type) + socketIO_packet_data) 446 | 447 | # React 448 | 449 | def wait_for_callbacks(self, seconds=None): 450 | self.wait(seconds, for_callbacks=True) 451 | 452 | def _should_stop_waiting(self, for_namespace=False, for_callbacks=False): 453 | if for_namespace: 454 | namespace = for_namespace 455 | if getattr(namespace, '_invalid', False): 456 | raise ConnectionError( 457 | 'invalid socket.io namespace (%s)' % namespace.path) 458 | if not getattr(namespace, '_connected', False): 459 | self._debug( 460 | '%s[socket.io waiting for connection]', 461 | make_logging_prefix(namespace.path)) 462 | return False 463 | return True 464 | if for_callbacks and not self._has_ack_callback: 465 | return True 466 | return super(SocketIO, self)._should_stop_waiting() 467 | 468 | def _process_packet(self, packet): 469 | engineIO_packet_data = super(SocketIO, self)._process_packet(packet) 470 | if engineIO_packet_data is None: 471 | return 472 | self._debug('[socket.io packet received] %s', engineIO_packet_data) 473 | socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) 474 | socketIO_packet_data = engineIO_packet_data[1:] 475 | # Launch callbacks 476 | path = get_namespace_path(socketIO_packet_data) 477 | namespace = self.get_namespace(path) 478 | try: 479 | delegate = { 480 | 0: self._on_connect, 481 | 1: self._on_disconnect, 482 | 2: self._on_event, 483 | 3: self._on_ack, 484 | 4: self._on_error, 485 | 5: self._on_binary_event, 486 | 6: self._on_binary_ack, 487 | }[socketIO_packet_type] 488 | except KeyError: 489 | raise PacketError( 490 | 'unexpected socket.io packet type (%s)' % socketIO_packet_type) 491 | delegate(parse_socketIO_packet_data(socketIO_packet_data), namespace) 492 | return socketIO_packet_data 493 | 494 | def _on_connect(self, data_parsed, namespace): 495 | namespace._connected = True 496 | namespace._find_packet_callback('connect')() 497 | self._debug( 498 | '%s[socket.io connected]', make_logging_prefix(namespace.path)) 499 | 500 | def _on_disconnect(self, data_parsed, namespace): 501 | namespace._connected = False 502 | namespace._find_packet_callback('disconnect')() 503 | 504 | def _on_event(self, data_parsed, namespace): 505 | args = data_parsed.args 506 | try: 507 | event = args.pop(0) 508 | except IndexError: 509 | raise PacketError('missing event name') 510 | if data_parsed.ack_id is not None: 511 | args.append(self._prepare_to_send_ack( 512 | data_parsed.path, data_parsed.ack_id)) 513 | namespace._find_packet_callback(event)(*args) 514 | 515 | def _on_ack(self, data_parsed, namespace): 516 | try: 517 | ack_callback = self._get_ack_callback(data_parsed.ack_id) 518 | except KeyError: 519 | return 520 | ack_callback(*data_parsed.args) 521 | 522 | def _on_error(self, data_parsed, namespace): 523 | namespace._find_packet_callback('error')(*data_parsed.args) 524 | 525 | def _on_binary_event(self, data_parsed, namespace): 526 | self._warn('[not implemented] binary event') 527 | 528 | def _on_binary_ack(self, data_parsed, namespace): 529 | self._warn('[not implemented] binary ack') 530 | 531 | def _prepare_to_send_ack(self, path, ack_id): 532 | 'Return function that acknowledges the server' 533 | return lambda *args: self._ack(path, ack_id, *args) 534 | 535 | def _set_ack_callback(self, callback): 536 | self._ack_id += 1 537 | self._callback_by_ack_id[self._ack_id] = callback 538 | return self._ack_id 539 | 540 | def _get_ack_callback(self, ack_id): 541 | return self._callback_by_ack_id.pop(ack_id) 542 | 543 | @property 544 | def _has_ack_callback(self): 545 | return True if self._callback_by_ack_id else False 546 | -------------------------------------------------------------------------------- /socketIO_client/exceptions.py: -------------------------------------------------------------------------------- 1 | class SocketIOError(Exception): 2 | pass 3 | 4 | 5 | class ConnectionError(SocketIOError): 6 | pass 7 | 8 | 9 | class TimeoutError(SocketIOError): 10 | pass 11 | 12 | 13 | class PacketError(SocketIOError): 14 | pass 15 | -------------------------------------------------------------------------------- /socketIO_client/heartbeats.py: -------------------------------------------------------------------------------- 1 | from invisibleroads_macros.log import get_log 2 | from threading import Thread, Event 3 | 4 | from .exceptions import ConnectionError, TimeoutError 5 | 6 | 7 | L = get_log(__name__) 8 | 9 | 10 | class HeartbeatThread(Thread): 11 | 12 | daemon = True 13 | 14 | def __init__( 15 | self, send_heartbeat, 16 | relax_interval_in_seconds, 17 | hurry_interval_in_seconds): 18 | super(HeartbeatThread, self).__init__() 19 | self._send_heartbeat = send_heartbeat 20 | self._relax_interval_in_seconds = relax_interval_in_seconds 21 | self._hurry_interval_in_seconds = hurry_interval_in_seconds 22 | self._adrenaline = Event() 23 | self._rest = Event() 24 | self._halt = Event() 25 | 26 | def run(self): 27 | try: 28 | while not self._halt.is_set(): 29 | try: 30 | self._send_heartbeat() 31 | except TimeoutError: 32 | pass 33 | if self._adrenaline.is_set(): 34 | interval_in_seconds = self._hurry_interval_in_seconds 35 | else: 36 | interval_in_seconds = self._relax_interval_in_seconds 37 | self._rest.wait(interval_in_seconds) 38 | except ConnectionError: 39 | L.debug('[heartbeat connection error]') 40 | 41 | def relax(self): 42 | self._adrenaline.clear() 43 | 44 | def hurry(self): 45 | self._adrenaline.set() 46 | self._rest.set() 47 | self._rest.clear() 48 | 49 | @property 50 | def hurried(self): 51 | return self._adrenaline.is_set() 52 | 53 | def halt(self): 54 | self._rest.set() 55 | self._halt.set() 56 | -------------------------------------------------------------------------------- /socketIO_client/logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from invisibleroads_macros.log import get_log 4 | 5 | 6 | L = get_log('socketIO-client') 7 | 8 | 9 | class LoggingMixin(object): 10 | 11 | def _log(self, level, msg, *attrs): 12 | L.log(level, '%s %s' % (self._log_name, msg), *attrs) 13 | 14 | def _debug(self, msg, *attrs): 15 | self._log(logging.DEBUG, msg, *attrs) 16 | 17 | def _info(self, msg, *attrs): 18 | self._log(logging.INFO, msg, *attrs) 19 | 20 | def _warn(self, msg, *attrs): 21 | self._log(logging.WARNING, msg, *attrs) 22 | 23 | def _yield_warning_screen(self, seconds=None): 24 | last_warning = None 25 | for elapsed_time in _yield_elapsed_time(seconds): 26 | try: 27 | yield elapsed_time 28 | except Exception as warning: 29 | warning = str(warning) 30 | if last_warning != warning: 31 | last_warning = warning 32 | self._warn(warning) 33 | time.sleep(1) 34 | 35 | 36 | def _yield_elapsed_time(seconds=None): 37 | start_time = time.time() 38 | if seconds is None: 39 | while True: 40 | yield _get_elapsed_time(start_time) 41 | while _get_elapsed_time(start_time) < seconds: 42 | yield _get_elapsed_time(start_time) 43 | 44 | 45 | def _get_elapsed_time(start_time): 46 | return time.time() - start_time 47 | -------------------------------------------------------------------------------- /socketIO_client/namespaces.py: -------------------------------------------------------------------------------- 1 | from .logs import LoggingMixin 2 | 3 | 4 | class EngineIONamespace(LoggingMixin): 5 | 'Define engine.io client behavior' 6 | 7 | def __init__(self, io): 8 | self._io = io 9 | self._callback_by_event = {} 10 | self._once_events = set() 11 | self._log_name = io._url 12 | self.initialize() 13 | 14 | def initialize(self): 15 | """Initialize custom variables here. 16 | You can override this method.""" 17 | 18 | def on(self, event, callback): 19 | 'Define a callback to handle an event emitted by the server' 20 | self._callback_by_event[event] = callback 21 | 22 | def once(self, event, callback): 23 | 'Define a callback to handle the first event emitted by the server' 24 | self._once_events.add(event) 25 | self.on(event, callback) 26 | 27 | def off(self, event): 28 | 'Remove an event handler' 29 | try: 30 | self._once_events.remove(event) 31 | except KeyError: 32 | pass 33 | self._callback_by_event.pop(event, None) 34 | 35 | def send(self, data): 36 | 'Send a message' 37 | self._io.send(data) 38 | 39 | def on_open(self): 40 | """Called when client receives open packet from engine.io server. 41 | You can override this method.""" 42 | 43 | def on_close(self): 44 | """Called when client receives close packet from engine.io server. 45 | You can override this method.""" 46 | 47 | def on_ping(self, data): 48 | """Called when client receives ping packet from engine.io server. 49 | You can override this method.""" 50 | 51 | def on_pong(self, data): 52 | """Called when client receives pong packet from engine.io server. 53 | You can override this method.""" 54 | 55 | def on_message(self, data): 56 | """Called when client receives message packet from engine.io server. 57 | You can override this method.""" 58 | 59 | def on_upgrade(self): 60 | """Called when client receives upgrade packet from engine.io server. 61 | You can override this method.""" 62 | 63 | def on_noop(self): 64 | """Called when client receives noop packet from engine.io server. 65 | You can override this method.""" 66 | 67 | def _find_packet_callback(self, event): 68 | # Check callbacks defined by on() 69 | try: 70 | callback = self._callback_by_event[event] 71 | except KeyError: 72 | pass 73 | else: 74 | if event in self._once_events: 75 | self.off(event) 76 | return callback 77 | # Check callbacks defined explicitly 78 | return getattr(self, 'on_' + event) 79 | 80 | 81 | class SocketIONamespace(EngineIONamespace): 82 | 'Define socket.io client behavior' 83 | 84 | def __init__(self, io, path): 85 | self.path = path 86 | super(SocketIONamespace, self).__init__(io) 87 | 88 | def connect(self): 89 | self._io.connect(self.path) 90 | 91 | def disconnect(self): 92 | self._io.disconnect(self.path) 93 | 94 | def emit(self, event, *args, **kw): 95 | self._io.emit(event, path=self.path, *args, **kw) 96 | 97 | def send(self, data='', callback=None): 98 | self._io.send(data, callback) 99 | 100 | def on_connect(self): 101 | """Called when client receives first connect packet from socket.io 102 | server. You can override this method.""" 103 | 104 | def on_reconnect(self): 105 | """Called when client receives subsequent connect packet from 106 | socket.io server. You can override this method.""" 107 | 108 | def on_disconnect(self): 109 | """Called when client receives disconnect packet from socket.io 110 | server. You can override this method.""" 111 | 112 | def on_event(self, event, *args): 113 | """Called if there is no matching event handler. 114 | You can override this method. 115 | There are three ways to define an event handler: 116 | 117 | - Call socketIO.on() 118 | 119 | socketIO = SocketIO('127.0.0.1', 8000) 120 | socketIO.on('my_event', my_function) 121 | 122 | - Call namespace.on() 123 | 124 | namespace = socketIO.get_namespace() 125 | namespace.on('my_event', my_function) 126 | 127 | - Define namespace.on_xxx 128 | 129 | class Namespace(SocketIONamespace): 130 | 131 | def on_my_event(self, *args): 132 | my_function(*args) 133 | 134 | socketIO.define(Namespace)""" 135 | 136 | def on_error(self, data): 137 | """Called when client receives error packet from socket.io server. 138 | You can override this method.""" 139 | if data.lower() == 'invalid namespace': 140 | self._invalid = True 141 | 142 | def _find_packet_callback(self, event): 143 | # Interpret events 144 | if event == 'connect': 145 | if not hasattr(self, '_was_connected'): 146 | self._was_connected = True 147 | else: 148 | event = 'reconnect' 149 | # Check callbacks defined by on() 150 | try: 151 | callback = self._callback_by_event[event] 152 | except KeyError: 153 | pass 154 | else: 155 | if event in self._once_events: 156 | self.off(event) 157 | return callback 158 | # Check callbacks defined explicitly or use on_event() 159 | return getattr( 160 | self, 'on_' + event.replace(' ', '_'), 161 | lambda *args: self.on_event(event, *args)) 162 | 163 | 164 | class LoggingEngineIONamespace(EngineIONamespace): 165 | 166 | def on_open(self): 167 | self._debug('[engine.io open]') 168 | super(LoggingEngineIONamespace, self).on_open() 169 | 170 | def on_close(self): 171 | self._debug('[engine.io close]') 172 | super(LoggingEngineIONamespace, self).on_close() 173 | 174 | def on_ping(self, data): 175 | self._debug('[engine.io ping] %s', data) 176 | super(LoggingEngineIONamespace, self).on_ping(data) 177 | 178 | def on_pong(self, data): 179 | self._debug('[engine.io pong] %s', data) 180 | super(LoggingEngineIONamespace, self).on_pong(data) 181 | 182 | def on_message(self, data): 183 | self._debug('[engine.io message] %s', data) 184 | super(LoggingEngineIONamespace, self).on_message(data) 185 | 186 | def on_upgrade(self): 187 | self._debug('[engine.io upgrade]') 188 | super(LoggingEngineIONamespace, self).on_upgrade() 189 | 190 | def on_noop(self): 191 | self._debug('[engine.io noop]') 192 | super(LoggingEngineIONamespace, self).on_noop() 193 | 194 | def on_event(self, event, *args): 195 | callback, args = find_callback(args) 196 | arguments = [repr(_) for _ in args] 197 | if callback: 198 | arguments.append('callback(*args)') 199 | self._info('[engine.io event] %s(%s)', event, ', '.join(arguments)) 200 | super(LoggingEngineIONamespace, self).on_event(event, *args) 201 | 202 | 203 | class LoggingSocketIONamespace(SocketIONamespace, LoggingEngineIONamespace): 204 | 205 | def on_connect(self): 206 | self._debug( 207 | '%s[socket.io connect]', make_logging_prefix(self.path)) 208 | super(LoggingSocketIONamespace, self).on_connect() 209 | 210 | def on_reconnect(self): 211 | self._debug( 212 | '%s[socket.io reconnect]', make_logging_prefix(self.path)) 213 | super(LoggingSocketIONamespace, self).on_reconnect() 214 | 215 | def on_disconnect(self): 216 | self._debug( 217 | '%s[socket.io disconnect]', make_logging_prefix(self.path)) 218 | super(LoggingSocketIONamespace, self).on_disconnect() 219 | 220 | def on_event(self, event, *args): 221 | callback, args = find_callback(args) 222 | arguments = [repr(_) for _ in args] 223 | if callback: 224 | arguments.append('callback(*args)') 225 | self._info( 226 | '%s[socket.io event] %s(%s)', make_logging_prefix(self.path), 227 | event, ', '.join(arguments)) 228 | super(LoggingSocketIONamespace, self).on_event(event, *args) 229 | 230 | def on_error(self, data): 231 | self._warn( 232 | '%s[socket.io error] %s', make_logging_prefix(self.path), data) 233 | super(LoggingSocketIONamespace, self).on_error(data) 234 | 235 | 236 | def find_callback(args, kw=None): 237 | 'Return callback whether passed as a last argument or as a keyword' 238 | if args and callable(args[-1]): 239 | return args[-1], args[:-1] 240 | try: 241 | return kw['callback'], args 242 | except (KeyError, TypeError): 243 | return None, args 244 | 245 | 246 | def make_logging_prefix(path): 247 | return path + ' ' if path else '' 248 | -------------------------------------------------------------------------------- /socketIO_client/parsers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import six 3 | from collections import namedtuple 4 | from six.moves.urllib.parse import urlparse as parse_url 5 | 6 | from .symmetries import decode_string, encode_string, get_byte, get_character 7 | 8 | 9 | EngineIOSession = namedtuple('EngineIOSession', [ 10 | 'id', 'ping_interval', 'ping_timeout', 'transport_upgrades']) 11 | SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args']) 12 | 13 | 14 | def parse_host(host, port, resource): 15 | if not host.startswith('http'): 16 | host = 'http://' + host 17 | url_pack = parse_url(host) 18 | is_secure = url_pack.scheme == 'https' 19 | port = port or url_pack.port or (443 if is_secure else 80) 20 | url = '%s:%s%s/%s' % (url_pack.hostname, port, url_pack.path, resource) 21 | return is_secure, url 22 | 23 | 24 | def parse_engineIO_session(engineIO_packet_data): 25 | d = json.loads(decode_string(engineIO_packet_data)) 26 | return EngineIOSession( 27 | id=d['sid'], 28 | ping_interval=d['pingInterval'] / float(1000), 29 | ping_timeout=d['pingTimeout'] / float(1000), 30 | transport_upgrades=d['upgrades']) 31 | 32 | 33 | def encode_engineIO_content(engineIO_packets): 34 | content = bytearray() 35 | for packet_type, packet_data in engineIO_packets: 36 | packet_text = format_packet_text(packet_type, packet_data) 37 | content.extend(_make_packet_prefix(packet_text) + packet_text) 38 | return content 39 | 40 | 41 | def decode_engineIO_content(content): 42 | content_index = 0 43 | content_length = len(content) 44 | while content_index < content_length: 45 | try: 46 | content_index, packet_length = _read_packet_length( 47 | content, content_index) 48 | except IndexError: 49 | break 50 | content_index, packet_text = _read_packet_text( 51 | content, content_index, packet_length) 52 | engineIO_packet_type, engineIO_packet_data = parse_packet_text( 53 | packet_text) 54 | yield engineIO_packet_type, engineIO_packet_data 55 | 56 | 57 | def format_socketIO_packet_data(path=None, ack_id=None, args=None): 58 | socketIO_packet_data = json.dumps(args, ensure_ascii=False) if args else '' 59 | if ack_id is not None: 60 | socketIO_packet_data = str(ack_id) + socketIO_packet_data 61 | if path: 62 | socketIO_packet_data = path + ',' + socketIO_packet_data 63 | return socketIO_packet_data 64 | 65 | 66 | def parse_socketIO_packet_data(socketIO_packet_data): 67 | data = decode_string(socketIO_packet_data) 68 | if data.startswith('/'): 69 | try: 70 | path, data = data.split(',', 1) 71 | except ValueError: 72 | path = data 73 | data = '' 74 | else: 75 | path = '' 76 | try: 77 | ack_id_string, data = data.split('[', 1) 78 | data = '[' + data 79 | ack_id = int(ack_id_string) 80 | except (ValueError, IndexError): 81 | ack_id = None 82 | try: 83 | args = json.loads(data) 84 | except ValueError: 85 | args = [] 86 | if isinstance(args, six.string_types): 87 | args = [args] 88 | return SocketIOData(path=path, ack_id=ack_id, args=args) 89 | 90 | 91 | def format_packet_text(packet_type, packet_data): 92 | return encode_string(str(packet_type) + packet_data) 93 | 94 | 95 | def parse_packet_text(packet_text): 96 | packet_type = int(get_character(packet_text, 0)) 97 | packet_data = packet_text[1:] 98 | return packet_type, packet_data 99 | 100 | 101 | def get_namespace_path(socketIO_packet_data): 102 | if not socketIO_packet_data.startswith(b'/'): 103 | return '' 104 | # Loop incrementally in case there is binary data 105 | parts = [] 106 | for i in range(len(socketIO_packet_data)): 107 | character = get_character(socketIO_packet_data, i) 108 | if ',' == character: 109 | break 110 | parts.append(character) 111 | return ''.join(parts) 112 | 113 | 114 | def _make_packet_prefix(packet): 115 | length_string = str(len(packet)) 116 | header_digits = bytearray([0]) 117 | for i in range(len(length_string)): 118 | header_digits.append(ord(length_string[i]) - 48) 119 | header_digits.append(255) 120 | return header_digits 121 | 122 | 123 | def _read_packet_length(content, content_index): 124 | while get_byte(content, content_index) != 0: 125 | content_index += 1 126 | content_index += 1 127 | packet_length_string = '' 128 | byte = get_byte(content, content_index) 129 | while byte != 255: 130 | packet_length_string += str(byte) 131 | content_index += 1 132 | byte = get_byte(content, content_index) 133 | return content_index, int(packet_length_string) 134 | 135 | 136 | def _read_packet_text(content, content_index, packet_length): 137 | while get_byte(content, content_index) == 255: 138 | content_index += 1 139 | packet_text = content[content_index:content_index + packet_length] 140 | return content_index + packet_length, packet_text 141 | -------------------------------------------------------------------------------- /socketIO_client/symmetries.py: -------------------------------------------------------------------------------- 1 | from six import indexbytes 2 | 3 | 4 | try: 5 | from ssl import SSLError 6 | except ImportError: 7 | class SSLError(Exception): 8 | pass 9 | 10 | 11 | try: 12 | memoryview = memoryview 13 | except NameError: 14 | memoryview = buffer 15 | 16 | 17 | def get_byte(x, index): 18 | return indexbytes(x, index) 19 | 20 | 21 | def get_character(x, index): 22 | return chr(get_byte(x, index)) 23 | 24 | 25 | def decode_string(x): 26 | return x.decode('utf-8') 27 | 28 | 29 | def encode_string(x): 30 | return x.encode('utf-8') 31 | -------------------------------------------------------------------------------- /socketIO_client/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | import time 4 | from unittest import TestCase 5 | 6 | from .. import SocketIO, LoggingNamespace, find_callback 7 | from ..exceptions import ConnectionError 8 | 9 | 10 | HOST = '127.0.0.1' 11 | PORT = 9000 12 | DATA = 'xxx' 13 | PAYLOAD = {'xxx': 'yyy'} 14 | UNICODE_PAYLOAD = {u'인삼': u'뿌리'} 15 | BINARY_DATA = bytearray(b'\xff\xff\xff') 16 | BINARY_PAYLOAD = { 17 | 'data': BINARY_DATA, 18 | 'array': [bytearray(b'\xee'), bytearray(b'\xdd')] 19 | } 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | 23 | class BaseMixin(object): 24 | 25 | def setUp(self): 26 | super(BaseMixin, self).setUp() 27 | self.response_count = 0 28 | self.wait_time_in_seconds = 1 29 | 30 | def tearDown(self): 31 | super(BaseMixin, self).tearDown() 32 | self.socketIO.disconnect() 33 | 34 | def test_disconnect(self): 35 | 'Disconnect' 36 | self.socketIO.on('disconnect', self.on_event) 37 | self.assertTrue(self.socketIO.connected) 38 | self.assertEqual(self.response_count, 0) 39 | self.socketIO.disconnect() 40 | self.assertFalse(self.socketIO.connected) 41 | self.assertEqual(self.response_count, 1) 42 | 43 | def test_disconnect_with_namespace(self): 44 | 'Disconnect with namespace' 45 | namespace = self.socketIO.define(Namespace) 46 | self.assertTrue(self.socketIO.connected) 47 | self.assertFalse('disconnect' in namespace.args_by_event) 48 | self.socketIO.disconnect() 49 | self.assertFalse(self.socketIO.connected) 50 | self.assertTrue('disconnect' in namespace.args_by_event) 51 | 52 | def test_reconnect(self): 53 | 'Reconnect' 54 | self.socketIO.on('reconnect', self.on_event) 55 | self.assertEqual(self.response_count, 0) 56 | self.socketIO.connect() 57 | self.assertEqual(self.response_count, 0) 58 | self.socketIO.disconnect() 59 | self.socketIO.connect() 60 | self.socketIO.wait(self.wait_time_in_seconds) 61 | self.assertEqual(self.response_count, 1) 62 | 63 | def test_reconnect_with_namespace(self): 64 | 'Reconnect with namespace' 65 | namespace = self.socketIO.define(Namespace) 66 | self.assertFalse('reconnect' in namespace.args_by_event) 67 | self.socketIO.connect() 68 | self.assertFalse('reconnect' in namespace.args_by_event) 69 | self.socketIO.disconnect() 70 | self.socketIO.connect() 71 | self.socketIO.wait(self.wait_time_in_seconds) 72 | self.assertTrue('reconnect' in namespace.args_by_event) 73 | 74 | def test_emit(self): 75 | 'Emit' 76 | namespace = self.socketIO.define(Namespace) 77 | self.socketIO.emit('emit') 78 | self.socketIO.wait(self.wait_time_in_seconds) 79 | self.assertEqual(namespace.args_by_event, { 80 | 'emit_response': (), 81 | }) 82 | 83 | def test_emit_with_payload(self): 84 | 'Emit with payload' 85 | namespace = self.socketIO.define(Namespace) 86 | self.socketIO.emit('emit_with_payload', PAYLOAD) 87 | self.socketIO.wait(self.wait_time_in_seconds) 88 | self.assertEqual(namespace.args_by_event, { 89 | 'emit_with_payload_response': (PAYLOAD,), 90 | }) 91 | 92 | def test_emit_with_multiple_payloads(self): 93 | 'Emit with multiple payloads' 94 | namespace = self.socketIO.define(Namespace) 95 | self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) 96 | self.socketIO.wait(self.wait_time_in_seconds) 97 | self.assertEqual(namespace.args_by_event, { 98 | 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), 99 | }) 100 | 101 | def test_emit_with_unicode_payload(self): 102 | 'Emit with unicode payload' 103 | namespace = self.socketIO.define(Namespace) 104 | self.socketIO.emit('emit_with_payload', UNICODE_PAYLOAD) 105 | self.socketIO.wait(self.wait_time_in_seconds) 106 | self.assertEqual(namespace.args_by_event, { 107 | 'emit_with_payload_response': (UNICODE_PAYLOAD,), 108 | }) 109 | 110 | """ 111 | def test_emit_with_binary_payload(self): 112 | 'Emit with binary payload' 113 | namespace = self.socketIO.define(Namespace) 114 | self.socketIO.emit('emit_with_payload', BINARY_PAYLOAD) 115 | self.socketIO.wait(self.wait_time_in_seconds) 116 | self.assertEqual(namespace.args_by_event, { 117 | 'emit_with_payload_response': (BINARY_PAYLOAD,), 118 | }) 119 | """ 120 | 121 | def test_emit_with_callback(self): 122 | 'Emit with callback' 123 | self.assertEqual(self.response_count, 0) 124 | self.socketIO.emit('emit_with_callback', self.on_response) 125 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 126 | self.assertEqual(self.response_count, 1) 127 | 128 | def test_emit_with_callback_with_payload(self): 129 | 'Emit with callback with payload' 130 | self.assertEqual(self.response_count, 0) 131 | self.socketIO.emit( 132 | 'emit_with_callback_with_payload', self.on_response) 133 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 134 | self.assertEqual(self.response_count, 1) 135 | 136 | def test_emit_with_callback_with_multiple_payloads(self): 137 | 'Emit with callback with multiple payloads' 138 | self.assertEqual(self.response_count, 0) 139 | self.socketIO.emit( 140 | 'emit_with_callback_with_multiple_payloads', self.on_response) 141 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 142 | self.assertEqual(self.response_count, 1) 143 | 144 | """ 145 | def test_emit_with_callback_with_binary_payload(self): 146 | 'Emit with callback with binary payload' 147 | self.socketIO.emit( 148 | 'emit_with_callback_with_binary_payload', self.on_binary_response) 149 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 150 | self.assertTrue(self.called_on_response) 151 | """ 152 | 153 | def test_emit_with_event(self): 154 | 'Emit to trigger an event' 155 | self.socketIO.on('emit_with_event_response', self.on_response) 156 | self.assertEqual(self.response_count, 0) 157 | self.socketIO.emit('emit_with_event', PAYLOAD) 158 | self.socketIO.wait(self.wait_time_in_seconds) 159 | self.assertEqual(self.response_count, 1) 160 | self.socketIO.emit('emit_with_event', PAYLOAD) 161 | self.socketIO.wait(self.wait_time_in_seconds) 162 | self.assertEqual(self.response_count, 2) 163 | self.socketIO.off('emit_with_event_response') 164 | self.socketIO.emit('emit_with_event', PAYLOAD) 165 | self.socketIO.wait(self.wait_time_in_seconds) 166 | self.assertEqual(self.response_count, 2) 167 | 168 | def test_once(self): 169 | 'Listen for an event only once' 170 | self.socketIO.once('emit_with_event_response', self.on_response) 171 | self.assertEqual(self.response_count, 0) 172 | self.socketIO.emit('emit_with_event', PAYLOAD) 173 | self.socketIO.emit('emit_with_event', PAYLOAD) 174 | self.socketIO.wait(self.wait_time_in_seconds) 175 | self.assertEqual(self.response_count, 1) 176 | 177 | def test_send(self): 178 | 'Send' 179 | namespace = self.socketIO.define(Namespace) 180 | self.socketIO.send() 181 | self.socketIO.wait(self.wait_time_in_seconds) 182 | self.assertEqual(namespace.response, 'message_response') 183 | 184 | def test_send_with_data(self): 185 | 'Send with data' 186 | namespace = self.socketIO.define(Namespace) 187 | self.socketIO.send(DATA) 188 | self.socketIO.wait(self.wait_time_in_seconds) 189 | self.assertEqual(namespace.response, DATA) 190 | 191 | """ 192 | def test_send_with_binary_data(self): 193 | 'Send with binary data' 194 | namespace = self.socketIO.define(Namespace) 195 | self.socketIO.send(BINARY_DATA) 196 | self.socketIO.wait(self.wait_time_in_seconds) 197 | self.assertEqual(namespace.response, BINARY_DATA) 198 | """ 199 | 200 | def test_ack(self): 201 | 'Respond to a server callback request' 202 | namespace = self.socketIO.define(Namespace) 203 | self.socketIO.emit('trigger_server_expects_callback', PAYLOAD) 204 | self.socketIO.wait(self.wait_time_in_seconds) 205 | self.assertEqual(namespace.args_by_event, { 206 | 'server_expects_callback': (PAYLOAD,), 207 | 'server_received_callback': (PAYLOAD,), 208 | }) 209 | 210 | """ 211 | def test_binary_ack(self): 212 | 'Respond to a server callback request with binary data' 213 | namespace = self.socketIO.define(Namespace) 214 | self.socketIO.emit( 215 | 'trigger_server_expects_callback', BINARY_PAYLOAD) 216 | self.socketIO.wait(self.wait_time_in_seconds) 217 | self.assertEqual(namespace.args_by_event, { 218 | 'server_expects_callback': (BINARY_PAYLOAD,), 219 | 'server_received_callback': (BINARY_PAYLOAD,), 220 | }) 221 | """ 222 | 223 | def test_wait_with_disconnect(self): 224 | 'Exit loop when the client wants to disconnect' 225 | self.socketIO.define(Namespace) 226 | self.socketIO.disconnect() 227 | timeout_in_seconds = 5 228 | start_time = time.time() 229 | self.socketIO.wait(timeout_in_seconds) 230 | self.assertTrue(time.time() - start_time < timeout_in_seconds) 231 | 232 | def test_namespace_invalid(self): 233 | 'Connect to a namespace that is not defined on the server' 234 | self.assertRaises( 235 | ConnectionError, self.socketIO.define, Namespace, '/invalid') 236 | 237 | def test_namespace_emit(self): 238 | 'Emit to namespaces' 239 | main_namespace = self.socketIO.define(Namespace) 240 | chat_namespace = self.socketIO.define(Namespace, '/chat') 241 | news_namespace = self.socketIO.define(Namespace, '/news') 242 | news_namespace.emit('emit_with_payload', PAYLOAD) 243 | self.socketIO.wait(self.wait_time_in_seconds) 244 | self.assertEqual(main_namespace.args_by_event, {}) 245 | self.assertEqual(chat_namespace.args_by_event, {}) 246 | self.assertEqual(news_namespace.args_by_event, { 247 | 'emit_with_payload_response': (PAYLOAD,), 248 | }) 249 | 250 | """ 251 | def test_namespace_emit_with_binary_payload(self): 252 | 'Emit to namespaces with binary payload' 253 | main_namespace = self.socketIO.define(Namespace) 254 | chat_namespace = self.socketIO.define(Namespace, '/chat') 255 | news_namespace = self.socketIO.define(Namespace, '/news') 256 | news_namespace.emit('emit_with_payload', BINARY_PAYLOAD) 257 | self.socketIO.wait(self.wait_time_in_seconds) 258 | self.assertEqual(main_namespace.args_by_event, {}) 259 | self.assertEqual(chat_namespace.args_by_event, {}) 260 | self.assertEqual(news_namespace.args_by_event, { 261 | 'emit_with_payload_response': (BINARY_PAYLOAD,), 262 | }) 263 | """ 264 | 265 | def test_namespace_ack(self): 266 | 'Respond to server callback request in namespace' 267 | chat_namespace = self.socketIO.define(Namespace, '/chat') 268 | chat_namespace.emit('trigger_server_expects_callback', PAYLOAD) 269 | self.socketIO.wait(self.wait_time_in_seconds) 270 | self.assertEqual(chat_namespace.args_by_event, { 271 | 'server_expects_callback': (PAYLOAD,), 272 | 'server_received_callback': (PAYLOAD,), 273 | }) 274 | 275 | """ 276 | def test_namespace_ack_with_binary_payload(self): 277 | 'Respond to server callback request in namespace with binary payload' 278 | chat_namespace = self.socketIO.define(Namespace, '/chat') 279 | chat_namespace.emit( 280 | 'trigger_server_expects_callback', BINARY_PAYLOAD) 281 | self.socketIO.wait(self.wait_time_in_seconds) 282 | self.assertEqual(chat_namespace.args_by_event, { 283 | 'server_expects_callback': (BINARY_PAYLOAD,), 284 | 'server_received_callback': (BINARY_PAYLOAD,), 285 | }) 286 | """ 287 | 288 | def on_event(self): 289 | self.response_count += 1 290 | 291 | def on_response(self, *args): 292 | for arg in args: 293 | if isinstance(arg, dict): 294 | self.assertEqual(arg, PAYLOAD) 295 | else: 296 | self.assertEqual(arg, DATA) 297 | self.response_count += 1 298 | 299 | def on_binary_response(self, *args): 300 | for arg in args: 301 | if isinstance(arg, dict): 302 | self.assertEqual(arg, BINARY_PAYLOAD) 303 | else: 304 | self.assertEqual(arg, BINARY_DATA) 305 | self.called_on_response = True 306 | 307 | 308 | class Test_XHR_PollingTransport(BaseMixin, TestCase): 309 | 310 | def setUp(self): 311 | super(Test_XHR_PollingTransport, self).setUp() 312 | self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[ 313 | 'xhr-polling'], verify=False) 314 | self.assertEqual(self.socketIO.transport_name, 'xhr-polling') 315 | 316 | 317 | class Test_WebsocketTransport(BaseMixin, TestCase): 318 | 319 | def setUp(self): 320 | super(Test_WebsocketTransport, self).setUp() 321 | self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[ 322 | 'xhr-polling', 'websocket'], verify=False) 323 | self.assertEqual(self.socketIO.transport_name, 'websocket') 324 | 325 | 326 | class Namespace(LoggingNamespace): 327 | 328 | def initialize(self): 329 | self.args_by_event = {} 330 | self.response = None 331 | 332 | def on_disconnect(self): 333 | self.args_by_event['disconnect'] = () 334 | 335 | def on_reconnect(self): 336 | self.args_by_event['reconnect'] = () 337 | 338 | def on_wait_with_disconnect_response(self): 339 | self.disconnect() 340 | 341 | def on_event(self, event, *args): 342 | callback, args = find_callback(args) 343 | if callback: 344 | callback(*args) 345 | self.args_by_event[event] = args 346 | 347 | def on_message(self, data): 348 | self.response = data 349 | -------------------------------------------------------------------------------- /socketIO_client/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 26 | -------------------------------------------------------------------------------- /socketIO_client/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "http-proxy": ">=1.14.0", 4 | "socket.io": ">=1.4.8" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /socketIO_client/tests/proxy.js: -------------------------------------------------------------------------------- 1 | var proxy = require('http-proxy').createProxyServer({ 2 | target: {host: '127.0.0.1', port: 9000} 3 | }).on('error', function(err, req, res) { 4 | console.log('[ERROR] %s', err); 5 | res.end(); 6 | }); 7 | var server = require('http').createServer(function(req, res) { 8 | console.log('[REQUEST.%s] %s', req.method, req.url); 9 | console.log(req['headers']); 10 | if (req.method == 'POST') { 11 | var body = ''; 12 | req.on('data', function (data) { 13 | body += data; 14 | }); 15 | req.on('end', function () { 16 | print_body('[REQUEST.BODY] ', body); 17 | }); 18 | } 19 | var write = res.write; 20 | res.write = function(data) { 21 | print_body('[RESPONSE.BODY] ', data); 22 | write.call(res, data); 23 | } 24 | proxy.web(req, res); 25 | }); 26 | function print_body(header, body) { 27 | var text = String(body); 28 | console.log(header + text); 29 | if (text.charCodeAt(0) != 0) return; 30 | for (var i = 0; i < text.length; i++) { 31 | var character_code = text.charCodeAt(i); 32 | console.log('body[%s] = %s = %s', i, text[i], character_code); 33 | if (character_code == 65533) break; 34 | } 35 | } 36 | server.listen(8000); 37 | -------------------------------------------------------------------------------- /socketIO_client/tests/serve.js: -------------------------------------------------------------------------------- 1 | // DEBUG=* node serve.js 2 | if (process.argv[2] == 'secure') { 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var app = require('https').createServer({ 6 | key: fs.readFileSync(path.resolve(__dirname, 'ssl.key')), 7 | cert: fs.readFileSync(path.resolve(__dirname, 'ssl.crt')) 8 | }, serve); 9 | } else { 10 | var app = require('http').createServer(serve); 11 | } 12 | app.listen(9000); 13 | 14 | var io = require('socket.io')(app); 15 | var PAYLOAD = {'xxx': 'yyy'}; 16 | var UNICODE_PAYLOAD = {'인삼': '★ 뿌리 ★'}; 17 | 18 | // Travis currently does not support Buffer.from 19 | function getBuffer(array) { 20 | var buffer = new Buffer(array.length); 21 | for (var i = 0; i < array.length; i++) { 22 | buffer[i] = array[i]; 23 | } 24 | return buffer; 25 | } 26 | var BINARY_DATA = getBuffer([255, 255, 255]); 27 | var BINARY_PAYLOAD = { 28 | 'data': BINARY_DATA, 29 | 'array': [getBuffer([238]), getBuffer([221])] 30 | } 31 | 32 | io.on('connection', function(socket) { 33 | socket.on('message', function(data, fn) { 34 | if (fn) { 35 | // Client requests callback 36 | if (data) { 37 | fn(data); 38 | } else { 39 | fn(); 40 | } 41 | } else if (typeof data === 'object') { 42 | // Data has type object or is null 43 | socket.json.send(data ? data : 'message_response'); 44 | } else { 45 | // Data has type string or is '' 46 | socket.send(data ? data : 'message_response'); 47 | } 48 | }); 49 | socket.on('emit', function() { 50 | socket.emit('emit_response'); 51 | }); 52 | socket.on('emit_with_payload', function(payload) { 53 | socket.emit('emit_with_payload_response', payload); 54 | }); 55 | socket.on('emit_with_multiple_payloads', function(payload1, payload2) { 56 | socket.emit('emit_with_multiple_payloads_response', payload1, payload2); 57 | }); 58 | socket.on('emit_with_callback', function(fn) { 59 | fn(); 60 | }); 61 | socket.on('emit_with_callback_with_payload', function(fn) { 62 | fn(PAYLOAD); 63 | }); 64 | socket.on('emit_with_callback_with_multiple_payloads', function(fn) { 65 | fn(PAYLOAD, PAYLOAD); 66 | }); 67 | socket.on('emit_with_callback_with_unicode_payload', function(fn) { 68 | fn(UNICODE_PAYLOAD); 69 | }); 70 | socket.on('emit_with_callback_with_binary_payload', function(fn) { 71 | fn(BINARY_PAYLOAD); 72 | }); 73 | socket.on('emit_with_event', function(payload) { 74 | socket.emit('emit_with_event_response', payload); 75 | }); 76 | socket.on('trigger_server_expects_callback', function(payload) { 77 | socket.emit('server_expects_callback', payload, function(payload) { 78 | socket.emit('server_received_callback', payload); 79 | }); 80 | }); 81 | socket.on('aaa', function() { 82 | socket.emit('aaa_response', PAYLOAD); 83 | }); 84 | socket.on('bbb', function(payload, fn) { 85 | if (fn) fn(payload); 86 | }); 87 | }); 88 | 89 | io.of('/chat').on('connection', function(socket) { 90 | socket.on('emit_with_payload', function(payload) { 91 | socket.emit('emit_with_payload_response', payload); 92 | }); 93 | socket.on('aaa', function() { 94 | socket.emit('aaa_response', 'in chat'); 95 | }); 96 | socket.on('trigger_server_expects_callback', function(payload) { 97 | socket.emit('server_expects_callback', payload, function(payload) { 98 | socket.emit('server_received_callback', payload); 99 | }); 100 | }); 101 | }); 102 | 103 | io.of('/news').on('connection', function(socket) { 104 | socket.on('emit_with_payload', function(payload) { 105 | socket.emit('emit_with_payload_response', payload); 106 | }); 107 | socket.on('aaa', function() { 108 | socket.emit('aaa_response', 'in news'); 109 | }); 110 | }); 111 | 112 | function serve(req, res) { 113 | fs.readFile(__dirname + '/index.html', function(err, data) { 114 | res.writeHead(200); 115 | res.end(data); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /socketIO_client/tests/ssl.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZTCCAk2gAwIBAgIJAK1HKQ8zF3cCMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxDDAKBgNVBAoMA1hZWjES 4 | MBAGA1UEAwwJbG9jYWxob3N0MB4XDTE1MDQxNTE5NDUwNFoXDTE2MDQxNDE5NDUw 5 | NFowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMQswCQYDVQQHDAJOWTEMMAoG 6 | A1UECgwDWFlaMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQDAQUM9+xbiDeJXg+7X6HgXwla2AnGKWbZ11hZZUYbQwHyq 8 | ABDSqRQXVWvzac6b59/trZiJ7cQEH4+c8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2Sb 9 | R/xkSUpq35yTuR5+oHgahDg1gbgXgPhB3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP 10 | 8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk6UR3cHCMqyzHulyfhgvkk5FN+EtSR9ex 11 | dIrF6WXmfynhsAa/+bxbsgeBF9MNj3zvckCzxdQStdqOvy0mu40/7i9vwguh9cRo 12 | HDn6lx5EaE+gSGU48UNnKX5iQdqEhprNVDj31MiJAgMBAAGjUDBOMB0GA1UdDgQW 13 | BBRkFsPxYU+e6ZSFwmzoS45qiOzAaDAfBgNVHSMEGDAWgBRkFsPxYU+e6ZSFwmzo 14 | S45qiOzAaDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB4JyOA5bZ3 15 | NbkMvOjpDw+tKzcNiXZIkGfoQ8NC1DbGVhgG7Ps4VjXgUB552YSUV7iwyd3G78OC 16 | +cxEcr+BvxXHXL2Wlxy0c/ZgBjRI5VnGbYQjjI2Iy2qJV+x5IR2oZatv45soZSLq 17 | NFCg2KpOgcSRgs0oDGVBYO0d9m73s/kOySj2NGqVJsaQXqXtLWKnqToaCfl4Vnl+ 18 | zcMdUv8ajBZEPRg6oNi2QIvcNT8fS5gd/T4OXBa7pYuC79yOZ1X6bkKsZrcAdNGM 19 | zO/jH6jKFjIBBx1Of+uZTzfAj/eoTu3foPuUQ+Z9NNE2nkE6SLyBSlxE7wD+SfjS 20 | 4/J0PNj22Uh3 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /socketIO_client/tests/ssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAQUM9+xbiDeJX 3 | g+7X6HgXwla2AnGKWbZ11hZZUYbQwHyqABDSqRQXVWvzac6b59/trZiJ7cQEH4+c 4 | 8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2SbR/xkSUpq35yTuR5+oHgahDg1gbgXgPhB 5 | 3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk 6 | 6UR3cHCMqyzHulyfhgvkk5FN+EtSR9exdIrF6WXmfynhsAa/+bxbsgeBF9MNj3zv 7 | ckCzxdQStdqOvy0mu40/7i9vwguh9cRoHDn6lx5EaE+gSGU48UNnKX5iQdqEhprN 8 | VDj31MiJAgMBAAECggEANAFzbxC83+lhkMrfkQgRdFvdmN6QWBxsfvOql/61uUJY 9 | dqQN6O5TwPwad33npcTTjjenS6hFndfrwUjNjLvSgp2aN/FTHVavH3FkkY7uYKEa 10 | VebjHz20I7TZZhxtY1OFKacajV7JrZH1lduY8pccQ/8Is7ub88JvrQ+0zO5oTHnh 11 | KEPYY5r2wLxKrzGm0NavRW9MpiHxz1vUGykvaGq9vR8dVFvZlLC5szYII+BvlII+ 12 | 78XMnZbJ9ahT7dzfnzPdPPuyP3m4cdJ9c+7Advs0g2F3K/IDL3jZZCRZIaLxHIs0 13 | PeI17teW0OmK4RWrnf6dSf0bww05x5In8GzUYgppAQKBgQD4lJVi3UmAB319CGeP 14 | NE4cZFuMneNsuCkNEJONb8Cfsah2maM0if8tUNoR96JorjWgkUTG4oThGSQgJQw8 15 | fPy6cW4EUhSvWCO+Q4MFFWpTcf0hBiz5O1d06FHVo39o8Ct9dv2bxJqfNtCUUf31 16 | Fz5tvA+wvByOSazUC3AowQZ6FwKBgQDF/ksJbOBd/bu3ss7eJRjE2sXmuhfxrUiu 17 | P5RoOEqHROAifatJk/3hwT6lx2y1g+3kJpZm9V16dNTkcuybL0yJ/VBE3uWuodrj 18 | i9+wcg8XSnRp3BPVKzebKIKDTMdypOeb1f5yhx6cCtChRm1frKQdoXpMQqptM0jq 19 | w3B4bryWXwKBgQCWSv+nLrPpvJ2aoyI56x3u/J59fliquw3W4FbWBOMpqnh4fJu4 20 | gFbQRzoR8u82611xH2O9++brUhANf1jOmaMT9tDVu+rVuSyjNJ5azH/kw96PwPQg 21 | HEjcXjpcOOYnxE4HJZJgQ5ZY/QNPKeOp88vC/RlfedyqCtF7ww6lFU+dMQKBgQC2 22 | M7ut4sne9R8If74rZAwVLBauq1ZZi1O1NsFF33eGX/W7B9bXER+z3vfd61W4/L2x 23 | FWmXOflaNaWsza27aZ2P5tM1bcIEIOKkQBYL9Aq7LkNPH74Ij4rOeEsStVddwy94 24 | k0di8cFTbAhuQbdpMiCdO/qlrzvS3j0d/djEm3NlFQKBgQCpIrHaMcckCFsf2Y6o 25 | zMnbi3859hve94OOJjauQLlw/nRE/+OaDsDN8iJoxnK0seek8ro1ixSBTScpuX8W 26 | G2DBgqs9NrSQLe6FAckkGqVJdluoh5GewNneAcowkkauj2srnb6XtJDhFtTDY141 27 | EPbeqGB9PUY9Ny8VzHkAb1vi6g== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /socketIO_client/transports.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import six 3 | import ssl 4 | import threading 5 | import time 6 | from six.moves.urllib.parse import urlencode as format_query 7 | from six.moves.urllib.parse import urlparse as parse_url 8 | from socket import error as SocketError 9 | try: 10 | from websocket import ( 11 | WebSocketConnectionClosedException, WebSocketTimeoutException, 12 | create_connection) 13 | except ImportError: 14 | exit("""\ 15 | An incompatible websocket library is conflicting with the one we need. 16 | You can remove the incompatible library and install the correct one 17 | by running the following commands: 18 | 19 | yes | pip uninstall websocket websocket-client 20 | pip install -U websocket-client""") 21 | 22 | from .exceptions import ConnectionError, TimeoutError 23 | from .parsers import ( 24 | encode_engineIO_content, decode_engineIO_content, 25 | format_packet_text, parse_packet_text) 26 | from .symmetries import SSLError, memoryview 27 | 28 | 29 | ENGINEIO_PROTOCOL = 3 30 | TRANSPORTS = 'xhr-polling', 'websocket' 31 | 32 | 33 | class AbstractTransport(object): 34 | 35 | def __init__(self, http_session, is_secure, url, engineIO_session=None): 36 | self.http_session = http_session 37 | self.is_secure = is_secure 38 | self.url = url 39 | self.engineIO_session = engineIO_session 40 | 41 | def recv_packet(self): 42 | pass 43 | 44 | def send_packet(self, engineIO_packet_type, engineIO_packet_data=''): 45 | pass 46 | 47 | def set_timeout(self, seconds=None): 48 | pass 49 | 50 | 51 | class XHR_PollingTransport(AbstractTransport): 52 | 53 | def __init__(self, http_session, is_secure, url, engineIO_session=None): 54 | super(XHR_PollingTransport, self).__init__( 55 | http_session, is_secure, url, engineIO_session) 56 | self._params = { 57 | 'EIO': ENGINEIO_PROTOCOL, 'transport': 'polling'} 58 | if engineIO_session: 59 | self._request_index = 1 60 | self._kw_get = dict( 61 | timeout=engineIO_session.ping_timeout) 62 | self._kw_post = dict( 63 | timeout=engineIO_session.ping_timeout, 64 | headers={'content-type': 'application/octet-stream'}) 65 | self._params['sid'] = engineIO_session.id 66 | else: 67 | self._request_index = 0 68 | self._kw_get = {} 69 | self._kw_post = {} 70 | http_scheme = 'https' if is_secure else 'http' 71 | self._http_url = '%s://%s/' % (http_scheme, url) 72 | self._request_index_lock = threading.Lock() 73 | self._send_packet_lock = threading.Lock() 74 | 75 | def recv_packet(self): 76 | params = dict(self._params) 77 | params['t'] = self._get_timestamp() 78 | response = get_response( 79 | self.http_session.get, 80 | self._http_url, 81 | params=params, 82 | **self._kw_get) 83 | for engineIO_packet in decode_engineIO_content(response.content): 84 | engineIO_packet_type, engineIO_packet_data = engineIO_packet 85 | yield engineIO_packet_type, engineIO_packet_data 86 | 87 | def send_packet(self, engineIO_packet_type, engineIO_packet_data=''): 88 | with self._send_packet_lock: 89 | params = dict(self._params) 90 | params['t'] = self._get_timestamp() 91 | data = encode_engineIO_content([ 92 | (engineIO_packet_type, engineIO_packet_data), 93 | ]) 94 | get_response( 95 | self.http_session.post, 96 | self._http_url, 97 | params=params, 98 | data=memoryview(data), 99 | **self._kw_post) 100 | 101 | def _get_timestamp(self): 102 | with self._request_index_lock: 103 | timestamp = '%s-%s' % ( 104 | int(time.time() * 1000), self._request_index) 105 | self._request_index += 1 106 | return timestamp 107 | 108 | 109 | class WebsocketTransport(AbstractTransport): 110 | 111 | def __init__(self, http_session, is_secure, url, engineIO_session=None): 112 | super(WebsocketTransport, self).__init__( 113 | http_session, is_secure, url, engineIO_session) 114 | params = dict(http_session.params, **{ 115 | 'EIO': ENGINEIO_PROTOCOL, 'transport': 'websocket'}) 116 | request = http_session.prepare_request(requests.Request('GET', url)) 117 | kw = {'header': ['%s: %s' % x for x in request.headers.items()]} 118 | if engineIO_session: 119 | params['sid'] = engineIO_session.id 120 | kw['timeout'] = self._timeout = engineIO_session.ping_timeout 121 | ws_url = '%s://%s/?%s' % ( 122 | 'wss' if is_secure else 'ws', url, format_query(params)) 123 | http_scheme = 'https' if is_secure else 'http' 124 | if http_scheme in http_session.proxies: # Use the correct proxy 125 | proxy_url_pack = parse_url(http_session.proxies[http_scheme]) 126 | kw['http_proxy_host'] = proxy_url_pack.hostname 127 | kw['http_proxy_port'] = proxy_url_pack.port 128 | if proxy_url_pack.username: 129 | kw['http_proxy_auth'] = ( 130 | proxy_url_pack.username, proxy_url_pack.password) 131 | if http_session.verify: 132 | if http_session.cert: # Specify certificate path on disk 133 | if isinstance(http_session.cert, six.string_types): 134 | kw['ca_certs'] = http_session.cert 135 | else: 136 | kw['ca_certs'] = http_session.cert[0] 137 | else: # Do not verify the SSL certificate 138 | kw['sslopt'] = {'cert_reqs': ssl.CERT_NONE} 139 | try: 140 | self._connection = create_connection(ws_url, **kw) 141 | except Exception as e: 142 | raise ConnectionError(e) 143 | 144 | def recv_packet(self): 145 | try: 146 | packet_text = self._connection.recv() 147 | except WebSocketTimeoutException as e: 148 | raise TimeoutError('recv timed out (%s)' % e) 149 | except SSLError as e: 150 | raise ConnectionError('recv disconnected by SSL (%s)' % e) 151 | except WebSocketConnectionClosedException as e: 152 | raise ConnectionError('recv disconnected (%s)' % e) 153 | except SocketError as e: 154 | raise ConnectionError('recv disconnected (%s)' % e) 155 | if not isinstance(packet_text, six.binary_type): 156 | packet_text = packet_text.encode('utf-8') 157 | engineIO_packet_type, engineIO_packet_data = parse_packet_text( 158 | packet_text) 159 | yield engineIO_packet_type, engineIO_packet_data 160 | 161 | def send_packet(self, engineIO_packet_type, engineIO_packet_data=''): 162 | packet = format_packet_text(engineIO_packet_type, engineIO_packet_data) 163 | try: 164 | self._connection.send(packet) 165 | except WebSocketTimeoutException as e: 166 | raise TimeoutError('send timed out (%s)' % e) 167 | except (SocketError, WebSocketConnectionClosedException) as e: 168 | raise ConnectionError('send disconnected (%s)' % e) 169 | 170 | def set_timeout(self, seconds=None): 171 | self._connection.settimeout(seconds or self._timeout) 172 | 173 | 174 | def get_response(request, *args, **kw): 175 | try: 176 | response = request(*args, stream=True, **kw) 177 | except requests.exceptions.Timeout as e: 178 | raise TimeoutError(e) 179 | except requests.exceptions.ConnectionError as e: 180 | raise ConnectionError(e) 181 | except requests.exceptions.SSLError as e: 182 | raise ConnectionError('could not negotiate SSL (%s)' % e) 183 | status_code = response.status_code 184 | if 200 != status_code: 185 | raise ConnectionError('unexpected status code (%s %s)' % ( 186 | status_code, response.text)) 187 | return response 188 | 189 | 190 | def prepare_http_session(kw): 191 | http_session = requests.Session() 192 | http_session.headers.update(kw.get('headers', {})) 193 | http_session.auth = kw.get('auth') 194 | http_session.proxies.update(kw.get('proxies', {})) 195 | http_session.hooks.update(kw.get('hooks', {})) 196 | http_session.params.update(kw.get('params', {})) 197 | http_session.verify = kw.get('verify', True) 198 | http_session.cert = _get_cert(kw) 199 | http_session.cookies.update(kw.get('cookies', {})) 200 | return http_session 201 | 202 | 203 | def _get_cert(kw): 204 | # Reduce (None, None) to None 205 | cert = kw.get('cert') 206 | if hasattr(cert, '__iter__') and cert[0] is None: 207 | cert = None 208 | return cert 209 | --------------------------------------------------------------------------------