mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Vendor gevent's socketio and websocket modules
This commit is contained in:
parent
2546cbdbb6
commit
507d2e158d
21
awx/lib/site-packages/geventwebsocket/__init__.py
Normal file
21
awx/lib/site-packages/geventwebsocket/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
VERSION = (0, 9, 3, 'final', 0)
|
||||
|
||||
__all__ = [
|
||||
'WebSocketApplication',
|
||||
'Resource',
|
||||
'WebSocketServer',
|
||||
'WebSocketError',
|
||||
'get_version'
|
||||
]
|
||||
|
||||
|
||||
def get_version(*args, **kwargs):
|
||||
from .utils import get_version
|
||||
return get_version(*args, **kwargs)
|
||||
|
||||
try:
|
||||
from .resource import WebSocketApplication, Resource
|
||||
from .server import WebSocketServer
|
||||
from .exceptions import WebSocketError
|
||||
except ImportError:
|
||||
pass
|
||||
19
awx/lib/site-packages/geventwebsocket/exceptions.py
Normal file
19
awx/lib/site-packages/geventwebsocket/exceptions.py
Normal file
@ -0,0 +1,19 @@
|
||||
from socket import error as socket_error
|
||||
|
||||
|
||||
class WebSocketError(socket_error):
|
||||
"""
|
||||
Base class for all websocket errors.
|
||||
"""
|
||||
|
||||
|
||||
class ProtocolError(WebSocketError):
|
||||
"""
|
||||
Raised if an error occurs when de/encoding the websocket protocol.
|
||||
"""
|
||||
|
||||
|
||||
class FrameTooLargeException(ProtocolError):
|
||||
"""
|
||||
Raised if a frame is received that is too large.
|
||||
"""
|
||||
@ -0,0 +1,6 @@
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
from gunicorn.workers.ggevent import GeventPyWSGIWorker
|
||||
|
||||
|
||||
class GeventWebSocketWorker(GeventPyWSGIWorker):
|
||||
wsgi_handler = WebSocketHandler
|
||||
278
awx/lib/site-packages/geventwebsocket/handler.py
Normal file
278
awx/lib/site-packages/geventwebsocket/handler.py
Normal file
@ -0,0 +1,278 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import warnings
|
||||
|
||||
from gevent.pywsgi import WSGIHandler
|
||||
from .websocket import WebSocket, Stream
|
||||
from .logging import create_logger
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, address, ws):
|
||||
self.address = address
|
||||
self.ws = ws
|
||||
|
||||
|
||||
class WebSocketHandler(WSGIHandler):
|
||||
"""
|
||||
Automatically upgrades the connection to a websocket.
|
||||
|
||||
To prevent the WebSocketHandler to call the underlying WSGI application,
|
||||
but only setup the WebSocket negotiations, do:
|
||||
|
||||
mywebsockethandler.prevent_wsgi_call = True
|
||||
|
||||
before calling run_application(). This is useful if you want to do more
|
||||
things before calling the app, and want to off-load the WebSocket
|
||||
negotiations to this library. Socket.IO needs this for example, to send
|
||||
the 'ack' before yielding the control to your WSGI app.
|
||||
"""
|
||||
|
||||
SUPPORTED_VERSIONS = ('13', '8', '7')
|
||||
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
def run_websocket(self):
|
||||
"""
|
||||
Called when a websocket has been created successfully.
|
||||
"""
|
||||
|
||||
if getattr(self, 'prevent_wsgi_call', False):
|
||||
return
|
||||
|
||||
# In case WebSocketServer is not used
|
||||
if not hasattr(self.server, 'clients'):
|
||||
self.server.clients = {}
|
||||
|
||||
# Since we're now a websocket connection, we don't care what the
|
||||
# application actually responds with for the http response
|
||||
|
||||
try:
|
||||
self.server.clients[self.client_address] = Client(
|
||||
self.client_address, self.websocket)
|
||||
self.application(self.environ, lambda s, h: [])
|
||||
finally:
|
||||
del self.server.clients[self.client_address]
|
||||
if not self.websocket.closed:
|
||||
self.websocket.close()
|
||||
self.environ.update({
|
||||
'wsgi.websocket': None
|
||||
})
|
||||
self.websocket = None
|
||||
|
||||
def run_application(self):
|
||||
if (hasattr(self.server, 'pre_start_hook')
|
||||
and self.server.pre_start_hook):
|
||||
self.logger.debug("Calling pre-start hook")
|
||||
if self.server.pre_start_hook(self):
|
||||
return super(WebSocketHandler, self).run_application()
|
||||
|
||||
self.logger.debug("Initializing WebSocket")
|
||||
self.result = self.upgrade_websocket()
|
||||
|
||||
if hasattr(self, 'websocket'):
|
||||
if self.status and not self.headers_sent:
|
||||
self.write('')
|
||||
|
||||
self.run_websocket()
|
||||
else:
|
||||
if self.status:
|
||||
# A status was set, likely an error so just send the response
|
||||
if not self.result:
|
||||
self.result = []
|
||||
|
||||
self.process_result()
|
||||
return
|
||||
|
||||
# This handler did not handle the request, so defer it to the
|
||||
# underlying application object
|
||||
return super(WebSocketHandler, self).run_application()
|
||||
|
||||
def upgrade_websocket(self):
|
||||
"""
|
||||
Attempt to upgrade the current environ into a websocket enabled
|
||||
connection. If successful, the environ dict with be updated with two
|
||||
new entries, `wsgi.websocket` and `wsgi.websocket_version`.
|
||||
|
||||
:returns: Whether the upgrade was successful.
|
||||
"""
|
||||
|
||||
# Some basic sanity checks first
|
||||
|
||||
self.logger.debug("Validating WebSocket request")
|
||||
|
||||
if self.environ.get('REQUEST_METHOD', '') != 'GET':
|
||||
# This is not a websocket request, so we must not handle it
|
||||
self.logger.debug('Can only upgrade connection if using GET method.')
|
||||
return
|
||||
|
||||
upgrade = self.environ.get('HTTP_UPGRADE', '').lower()
|
||||
|
||||
if upgrade == 'websocket':
|
||||
connection = self.environ.get('HTTP_CONNECTION', '').lower()
|
||||
|
||||
if 'upgrade' not in connection:
|
||||
# This is not a websocket request, so we must not handle it
|
||||
self.logger.warning("Client didn't ask for a connection "
|
||||
"upgrade")
|
||||
return
|
||||
else:
|
||||
# This is not a websocket request, so we must not handle it
|
||||
return
|
||||
|
||||
if self.request_version != 'HTTP/1.1':
|
||||
self.start_response('402 Bad Request', [])
|
||||
self.logger.warning("Bad server protocol in headers")
|
||||
|
||||
return ['Bad protocol version']
|
||||
|
||||
if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'):
|
||||
return self.upgrade_connection()
|
||||
else:
|
||||
self.logger.warning("No protocol defined")
|
||||
self.start_response('426 Upgrade Required', [
|
||||
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))])
|
||||
|
||||
return ['No Websocket protocol version defined']
|
||||
|
||||
def upgrade_connection(self):
|
||||
"""
|
||||
Validate and 'upgrade' the HTTP request to a WebSocket request.
|
||||
|
||||
If an upgrade succeeded then then handler will have `start_response`
|
||||
with a status of `101`, the environ will also be updated with
|
||||
`wsgi.websocket` and `wsgi.websocket_version` keys.
|
||||
|
||||
:param environ: The WSGI environ dict.
|
||||
:param start_response: The callable used to start the response.
|
||||
:param stream: File like object that will be read from/written to by
|
||||
the underlying WebSocket object, if created.
|
||||
:return: The WSGI response iterator is something went awry.
|
||||
"""
|
||||
|
||||
self.logger.debug("Attempting to upgrade connection")
|
||||
|
||||
version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION")
|
||||
|
||||
if version not in self.SUPPORTED_VERSIONS:
|
||||
msg = "Unsupported WebSocket Version: {0}".format(version)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [
|
||||
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))
|
||||
])
|
||||
|
||||
return [msg]
|
||||
|
||||
key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip()
|
||||
|
||||
if not key:
|
||||
# 5.2.1 (3)
|
||||
msg = "Sec-WebSocket-Key header is missing/empty"
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
try:
|
||||
key_len = len(base64.b64decode(key))
|
||||
except TypeError:
|
||||
msg = "Invalid key: {0}".format(key)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
if key_len != 16:
|
||||
# 5.2.1 (3)
|
||||
msg = "Invalid key: {0}".format(key)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
# Check for WebSocket Protocols
|
||||
requested_protocols = self.environ.get(
|
||||
'HTTP_SEC_WEBSOCKET_PROTOCOL', '')
|
||||
protocol = None
|
||||
|
||||
if hasattr(self.application, 'app_protocol'):
|
||||
allowed_protocol = self.application.app_protocol(
|
||||
self.environ['PATH_INFO'])
|
||||
|
||||
if allowed_protocol and allowed_protocol in requested_protocols:
|
||||
protocol = allowed_protocol
|
||||
self.logger.debug("Protocol allowed: {0}".format(protocol))
|
||||
|
||||
self.websocket = WebSocket(self.environ, Stream(self), self)
|
||||
self.environ.update({
|
||||
'wsgi.websocket_version': version,
|
||||
'wsgi.websocket': self.websocket
|
||||
})
|
||||
|
||||
headers = [
|
||||
("Upgrade", "websocket"),
|
||||
("Connection", "Upgrade"),
|
||||
("Sec-WebSocket-Accept", base64.b64encode(
|
||||
hashlib.sha1(key + self.GUID).digest())),
|
||||
]
|
||||
|
||||
if protocol:
|
||||
headers.append(("Sec-WebSocket-Protocol", protocol))
|
||||
|
||||
self.logger.debug("WebSocket request accepted, switching protocols")
|
||||
self.start_response("101 Switching Protocols", headers)
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if not hasattr(self.server, 'logger'):
|
||||
self.server.logger = create_logger(__name__)
|
||||
|
||||
return self.server.logger
|
||||
|
||||
def log_request(self):
|
||||
if '101' not in self.status:
|
||||
self.logger.info(self.format_request())
|
||||
|
||||
@property
|
||||
def active_client(self):
|
||||
return self.server.clients[self.client_address]
|
||||
|
||||
def start_response(self, status, headers, exc_info=None):
|
||||
"""
|
||||
Called when the handler is ready to send a response back to the remote
|
||||
endpoint. A websocket connection may have not been created.
|
||||
"""
|
||||
writer = super(WebSocketHandler, self).start_response(
|
||||
status, headers, exc_info=exc_info)
|
||||
|
||||
self._prepare_response()
|
||||
|
||||
return writer
|
||||
|
||||
def _prepare_response(self):
|
||||
"""
|
||||
Sets up the ``pywsgi.Handler`` to work with a websocket response.
|
||||
|
||||
This is used by other projects that need to support WebSocket
|
||||
connections as part of a larger effort.
|
||||
"""
|
||||
assert not self.headers_sent
|
||||
|
||||
if not self.environ.get('wsgi.websocket'):
|
||||
# a WebSocket connection is not established, do nothing
|
||||
return
|
||||
|
||||
# So that `finalize_headers` doesn't write a Content-Length header
|
||||
self.provided_content_length = False
|
||||
|
||||
# The websocket is now controlling the response
|
||||
self.response_use_chunked = False
|
||||
|
||||
# Once the request is over, the connection must be closed
|
||||
self.close_connection = True
|
||||
|
||||
# Prevents the Date header from being written
|
||||
self.provided_date = True
|
||||
31
awx/lib/site-packages/geventwebsocket/logging.py
Normal file
31
awx/lib/site-packages/geventwebsocket/logging.py
Normal file
@ -0,0 +1,31 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG
|
||||
|
||||
|
||||
def create_logger(name, debug=False, format=None):
|
||||
Logger = getLoggerClass()
|
||||
|
||||
class DebugLogger(Logger):
|
||||
def getEffectiveLevel(x):
|
||||
if x.level == 0 and debug:
|
||||
return DEBUG
|
||||
else:
|
||||
return Logger.getEffectiveLevel(x)
|
||||
|
||||
class DebugHandler(StreamHandler):
|
||||
def emit(x, record):
|
||||
StreamHandler.emit(x, record) if debug else None
|
||||
|
||||
handler = DebugHandler()
|
||||
handler.setLevel(DEBUG)
|
||||
|
||||
if format:
|
||||
handler.setFormatter(Formatter(format))
|
||||
|
||||
logger = getLogger(name)
|
||||
del logger.handlers[:]
|
||||
logger.__class__ = DebugLogger
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
35
awx/lib/site-packages/geventwebsocket/protocols/base.py
Normal file
35
awx/lib/site-packages/geventwebsocket/protocols/base.py
Normal file
@ -0,0 +1,35 @@
|
||||
class BaseProtocol(object):
|
||||
PROTOCOL_NAME = ''
|
||||
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
|
||||
def on_open(self):
|
||||
self.app.on_open()
|
||||
|
||||
def on_message(self, message):
|
||||
self.app.on_message(message)
|
||||
|
||||
def on_close(self, reason=None):
|
||||
self.app.on_close(reason)
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
if self._app:
|
||||
return self._app
|
||||
else:
|
||||
raise Exception("No application coupled")
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
if not hasattr(self.app, 'ws'):
|
||||
return None
|
||||
|
||||
return self.app.ws.handler.server
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
if not hasattr(self.app, 'ws'):
|
||||
return None
|
||||
|
||||
return self.app.ws.handler
|
||||
229
awx/lib/site-packages/geventwebsocket/protocols/wamp.py
Normal file
229
awx/lib/site-packages/geventwebsocket/protocols/wamp.py
Normal file
@ -0,0 +1,229 @@
|
||||
import inspect
|
||||
import random
|
||||
import string
|
||||
import types
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
from ..exceptions import WebSocketError
|
||||
from .base import BaseProtocol
|
||||
|
||||
|
||||
def export_rpc(arg=None):
|
||||
if isinstance(arg, types.FunctionType):
|
||||
arg._rpc = arg.__name__
|
||||
return arg
|
||||
|
||||
|
||||
def serialize(data):
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
class Prefixes(object):
|
||||
def __init__(self):
|
||||
self.prefixes = {}
|
||||
|
||||
def add(self, prefix, uri):
|
||||
self.prefixes[prefix] = uri
|
||||
|
||||
def resolve(self, curie_or_uri):
|
||||
if "http://" in curie_or_uri:
|
||||
return curie_or_uri
|
||||
elif ':' in curie_or_uri:
|
||||
prefix, proc = curie_or_uri.split(':', 1)
|
||||
return self.prefixes[prefix] + proc
|
||||
else:
|
||||
raise Exception(curie_or_uri)
|
||||
|
||||
|
||||
class RemoteProcedures(object):
|
||||
def __init__(self):
|
||||
self.calls = {}
|
||||
|
||||
def register_procedure(self, uri, proc):
|
||||
self.calls[uri] = proc
|
||||
|
||||
def register_object(self, uri, obj):
|
||||
for k in inspect.getmembers(obj, inspect.ismethod):
|
||||
if '_rpc' in k[1].__dict__:
|
||||
proc_uri = uri + k[1]._rpc
|
||||
self.calls[proc_uri] = (obj, k[1])
|
||||
|
||||
def call(self, uri, args):
|
||||
if uri in self.calls:
|
||||
proc = self.calls[uri]
|
||||
|
||||
# Do the correct call whether it's a function or instance method.
|
||||
if isinstance(proc, tuple):
|
||||
if proc[1].__self__ is None:
|
||||
# Create instance of object and call method
|
||||
return proc[1](proc[0](), *args)
|
||||
else:
|
||||
# Call bound method on instance
|
||||
return proc[1](*args)
|
||||
else:
|
||||
return self.calls[uri](*args)
|
||||
else:
|
||||
raise Exception("no such uri '{}'".format(uri))
|
||||
|
||||
|
||||
class Channels(object):
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def create(self, uri, prefix_matching=False):
|
||||
if uri not in self.channels:
|
||||
self.channels[uri] = []
|
||||
|
||||
# TODO: implement prefix matching
|
||||
|
||||
def subscribe(self, uri, client):
|
||||
if uri in self.channels:
|
||||
self.channels[uri].append(client)
|
||||
|
||||
def unsubscribe(self, uri, client):
|
||||
if uri not in self.channels:
|
||||
return
|
||||
|
||||
client_index = self.channels[uri].index(client)
|
||||
self.channels[uri].pop(client_index)
|
||||
|
||||
if len(self.channels[uri]) == 0:
|
||||
del self.channels[uri]
|
||||
|
||||
def publish(self, uri, event, exclude=None, eligible=None):
|
||||
if uri not in self.channels:
|
||||
return
|
||||
|
||||
# TODO: exclude & eligible
|
||||
|
||||
msg = [WampProtocol.MSG_EVENT, uri, event]
|
||||
|
||||
for client in self.channels[uri]:
|
||||
try:
|
||||
client.ws.send(serialize(msg))
|
||||
except WebSocketError:
|
||||
# Seems someone didn't unsubscribe before disconnecting
|
||||
self.channels[uri].remove(client)
|
||||
|
||||
|
||||
class WampProtocol(BaseProtocol):
|
||||
MSG_WELCOME = 0
|
||||
MSG_PREFIX = 1
|
||||
MSG_CALL = 2
|
||||
MSG_CALL_RESULT = 3
|
||||
MSG_CALL_ERROR = 4
|
||||
MSG_SUBSCRIBE = 5
|
||||
MSG_UNSUBSCRIBE = 6
|
||||
MSG_PUBLISH = 7
|
||||
MSG_EVENT = 8
|
||||
|
||||
PROTOCOL_NAME = "wamp"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.procedures = RemoteProcedures()
|
||||
self.prefixes = Prefixes()
|
||||
self.session_id = ''.join(
|
||||
[random.choice(string.digits + string.letters)
|
||||
for i in xrange(16)])
|
||||
|
||||
super(WampProtocol, self).__init__(*args, **kwargs)
|
||||
|
||||
def register_procedure(self, *args, **kwargs):
|
||||
self.procedures.register_procedure(*args, **kwargs)
|
||||
|
||||
def register_object(self, *args, **kwargs):
|
||||
self.procedures.register_object(*args, **kwargs)
|
||||
|
||||
def register_pubsub(self, *args, **kwargs):
|
||||
if not hasattr(self.server, 'channels'):
|
||||
self.server.channels = Channels()
|
||||
|
||||
self.server.channels.create(*args, **kwargs)
|
||||
|
||||
def do_handshake(self):
|
||||
from geventwebsocket import get_version
|
||||
|
||||
welcome = [
|
||||
self.MSG_WELCOME,
|
||||
self.session_id,
|
||||
1,
|
||||
'gevent-websocket/' + get_version()
|
||||
]
|
||||
self.app.ws.send(serialize(welcome))
|
||||
|
||||
def rpc_call(self, data):
|
||||
call_id, curie_or_uri = data[1:3]
|
||||
args = data[3:]
|
||||
|
||||
if not isinstance(call_id, (str, unicode)):
|
||||
raise Exception()
|
||||
if not isinstance(curie_or_uri, (str, unicode)):
|
||||
raise Exception()
|
||||
|
||||
uri = self.prefixes.resolve(curie_or_uri)
|
||||
|
||||
try:
|
||||
result = self.procedures.call(uri, args)
|
||||
result_msg = [self.MSG_CALL_RESULT, call_id, result]
|
||||
except Exception, e:
|
||||
result_msg = [self.MSG_CALL_ERROR,
|
||||
call_id, 'http://TODO#generic',
|
||||
str(type(e)), str(e)]
|
||||
|
||||
self.app.on_message(serialize(result_msg))
|
||||
|
||||
def pubsub_action(self, data):
|
||||
action = data[0]
|
||||
curie_or_uri = data[1]
|
||||
|
||||
if not isinstance(action, int):
|
||||
raise Exception()
|
||||
if not isinstance(curie_or_uri, (str, unicode)):
|
||||
raise Exception()
|
||||
|
||||
uri = self.prefixes.resolve(curie_or_uri)
|
||||
|
||||
if action == self.MSG_SUBSCRIBE and len(data) == 2:
|
||||
self.server.channels.subscribe(data[1], self.handler.active_client)
|
||||
|
||||
elif action == self.MSG_UNSUBSCRIBE and len(data) == 2:
|
||||
self.server.channels.unsubscribe(
|
||||
data[1], self.handler.active_client)
|
||||
|
||||
elif action == self.MSG_PUBLISH and len(data) >= 3:
|
||||
payload = data[2] if len(data) >= 3 else None
|
||||
exclude = data[3] if len(data) >= 4 else None
|
||||
eligible = data[4] if len(data) >= 5 else None
|
||||
|
||||
self.server.channels.publish(uri, payload, exclude, eligible)
|
||||
|
||||
def on_open(self):
|
||||
self.app.on_open()
|
||||
self.do_handshake()
|
||||
|
||||
def on_message(self, message):
|
||||
data = json.loads(message)
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise Exception('incoming data is no list')
|
||||
|
||||
if data[0] == self.MSG_PREFIX and len(data) == 3:
|
||||
prefix, uri = data[1:3]
|
||||
self.prefixes.add(prefix, uri)
|
||||
|
||||
elif data[0] == self.MSG_CALL and len(data) >= 3:
|
||||
return self.rpc_call(data)
|
||||
|
||||
elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE,
|
||||
self.MSG_PUBLISH):
|
||||
return self.pubsub_action(data)
|
||||
else:
|
||||
raise Exception("Unknown call")
|
||||
|
||||
74
awx/lib/site-packages/geventwebsocket/resource.py
Normal file
74
awx/lib/site-packages/geventwebsocket/resource.py
Normal file
@ -0,0 +1,74 @@
|
||||
import re
|
||||
|
||||
from .protocols.base import BaseProtocol
|
||||
from .exceptions import WebSocketError
|
||||
|
||||
|
||||
class WebSocketApplication(object):
|
||||
protocol_class = BaseProtocol
|
||||
|
||||
def __init__(self, ws):
|
||||
self.protocol = self.protocol_class(self)
|
||||
self.ws = ws
|
||||
|
||||
def handle(self):
|
||||
self.protocol.on_open()
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = self.ws.receive()
|
||||
except WebSocketError:
|
||||
self.protocol.on_close()
|
||||
break
|
||||
|
||||
self.protocol.on_message(message)
|
||||
|
||||
def on_open(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def on_close(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def on_message(self, message, *args, **kwargs):
|
||||
self.ws.send(message, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def protocol_name(cls):
|
||||
return cls.protocol_class.PROTOCOL_NAME
|
||||
|
||||
|
||||
class Resource(object):
|
||||
def __init__(self, apps=None):
|
||||
self.apps = apps if apps else []
|
||||
|
||||
def _app_by_path(self, environ_path):
|
||||
# Which app matched the current path?
|
||||
|
||||
for path, app in self.apps.iteritems():
|
||||
if re.match(path, environ_path):
|
||||
return app
|
||||
|
||||
def app_protocol(self, path):
|
||||
app = self._app_by_path(path)
|
||||
|
||||
if hasattr(app, 'protocol_name'):
|
||||
return app.protocol_name()
|
||||
else:
|
||||
return ''
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ = environ
|
||||
current_app = self._app_by_path(environ['PATH_INFO'])
|
||||
|
||||
if current_app is None:
|
||||
raise Exception("No apps defined")
|
||||
|
||||
if 'wsgi.websocket' in environ:
|
||||
ws = environ['wsgi.websocket']
|
||||
current_app = current_app(ws)
|
||||
current_app.ws = ws # TODO: needed?
|
||||
current_app.handle()
|
||||
|
||||
return None
|
||||
else:
|
||||
return current_app(environ, start_response)
|
||||
34
awx/lib/site-packages/geventwebsocket/server.py
Normal file
34
awx/lib/site-packages/geventwebsocket/server.py
Normal file
@ -0,0 +1,34 @@
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
from .handler import WebSocketHandler
|
||||
from .logging import create_logger
|
||||
|
||||
|
||||
class WebSocketServer(WSGIServer):
|
||||
debug_log_format = (
|
||||
'-' * 80 + '\n' +
|
||||
'%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' +
|
||||
'%(message)s\n' +
|
||||
'-' * 80
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.debug = kwargs.pop('debug', False)
|
||||
self.pre_start_hook = kwargs.pop('pre_start_hook', None)
|
||||
self._logger = None
|
||||
self.clients = {}
|
||||
|
||||
kwargs['handler_class'] = WebSocketHandler
|
||||
super(WebSocketServer, self).__init__(*args, **kwargs)
|
||||
|
||||
def handle(self, socket, address):
|
||||
handler = self.handler_class(socket, address, self)
|
||||
handler.handle()
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if not self._logger:
|
||||
self._logger = create_logger(
|
||||
__name__, self.debug, self.debug_log_format)
|
||||
|
||||
return self._logger
|
||||
128
awx/lib/site-packages/geventwebsocket/utf8validator.py
Normal file
128
awx/lib/site-packages/geventwebsocket/utf8validator.py
Normal file
@ -0,0 +1,128 @@
|
||||
###############################################################################
|
||||
##
|
||||
## Copyright 2011-2013 Tavendo GmbH
|
||||
##
|
||||
## Note:
|
||||
##
|
||||
## This code is a Python implementation of the algorithm
|
||||
##
|
||||
## "Flexible and Economical UTF-8 Decoder"
|
||||
##
|
||||
## by Bjoern Hoehrmann
|
||||
##
|
||||
## bjoern@hoehrmann.de
|
||||
## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
##
|
||||
## Licensed under the Apache License, Version 2.0 (the "License");
|
||||
## you may not use this file except in compliance with the License.
|
||||
## You may obtain a copy of the License at
|
||||
##
|
||||
## http://www.apache.org/licenses/LICENSE-2.0
|
||||
##
|
||||
## Unless required by applicable law or agreed to in writing, software
|
||||
## distributed under the License is distributed on an "AS IS" BASIS,
|
||||
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
## See the License for the specific language governing permissions and
|
||||
## limitations under the License.
|
||||
##
|
||||
###############################################################################
|
||||
|
||||
|
||||
## use Cython implementation of UTF8 validator if available
|
||||
##
|
||||
try:
|
||||
from wsaccel.utf8validator import Utf8Validator
|
||||
except:
|
||||
## fallback to pure Python implementation
|
||||
|
||||
class Utf8Validator:
|
||||
"""
|
||||
Incremental UTF-8 validator with constant memory consumption (minimal
|
||||
state).
|
||||
|
||||
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
|
||||
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
|
||||
"""
|
||||
|
||||
## DFA transitions
|
||||
UTF8VALIDATOR_DFA = [
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
|
||||
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
|
||||
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
|
||||
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
|
||||
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
|
||||
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
|
||||
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
|
||||
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
|
||||
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
|
||||
]
|
||||
|
||||
UTF8_ACCEPT = 0
|
||||
UTF8_REJECT = 1
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def decode(self, b):
|
||||
"""
|
||||
Eat one UTF-8 octet, and validate on the fly.
|
||||
|
||||
Returns UTF8_ACCEPT when enough octets have been consumed, in which case
|
||||
self.codepoint contains the decoded Unicode code point.
|
||||
|
||||
Returns UTF8_REJECT when invalid UTF-8 was encountered.
|
||||
|
||||
Returns some other positive integer when more octets need to be eaten.
|
||||
"""
|
||||
type = Utf8Validator.UTF8VALIDATOR_DFA[b]
|
||||
|
||||
if self.state != Utf8Validator.UTF8_ACCEPT:
|
||||
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
|
||||
else:
|
||||
self.codepoint = (0xff >> type) & b
|
||||
|
||||
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type]
|
||||
|
||||
return self.state
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset validator to start new incremental UTF-8 decode/validation.
|
||||
"""
|
||||
self.state = Utf8Validator.UTF8_ACCEPT
|
||||
self.codepoint = 0
|
||||
self.i = 0
|
||||
|
||||
def validate(self, ba):
|
||||
"""
|
||||
Incrementally validate a chunk of bytes provided as string.
|
||||
|
||||
Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex).
|
||||
|
||||
As soon as an octet is encountered which renders the octet sequence
|
||||
invalid, a quad with valid? == False is returned. currentIndex returns
|
||||
the index within the currently consumed chunk, and totalIndex the
|
||||
index within the total consumed sequence that was the point of bail out.
|
||||
When valid? == True, currentIndex will be len(ba) and totalIndex the
|
||||
total amount of consumed bytes.
|
||||
"""
|
||||
|
||||
l = len(ba)
|
||||
|
||||
for i in xrange(l):
|
||||
## optimized version of decode(), since we are not interested in actual code points
|
||||
|
||||
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]]
|
||||
|
||||
if self.state == Utf8Validator.UTF8_REJECT:
|
||||
self.i += i
|
||||
return False, False, i, self.i
|
||||
|
||||
self.i += l
|
||||
|
||||
return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i
|
||||
45
awx/lib/site-packages/geventwebsocket/utils.py
Normal file
45
awx/lib/site-packages/geventwebsocket/utils.py
Normal file
@ -0,0 +1,45 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
"Returns a PEP 386-compliant version number from VERSION."
|
||||
|
||||
if version is None:
|
||||
from geventwebsocket import VERSION as version
|
||||
else:
|
||||
assert len(version) == 5
|
||||
assert version[3] in ('alpha', 'beta', 'rc', 'final')
|
||||
|
||||
# Now build the two parts of the version number:
|
||||
# main = X.Y[.Z]
|
||||
# sub = .devN - for pre-alpha releases
|
||||
# | {a|b|c}N - for alpha, beta and rc releases
|
||||
|
||||
parts = 2 if version[2] == 0 else 3
|
||||
main = '.'.join(str(x) for x in version[:parts])
|
||||
|
||||
sub = ''
|
||||
if version[3] == 'alpha' and version[4] == 0:
|
||||
hg_changeset = get_hg_changeset()
|
||||
if hg_changeset:
|
||||
sub = '.dev{0}'.format(hg_changeset)
|
||||
|
||||
elif version[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return str(main + sub)
|
||||
|
||||
|
||||
def get_hg_changeset():
|
||||
rev, err = subprocess.Popen(
|
||||
'hg id -i',
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
).communicate()
|
||||
|
||||
if err:
|
||||
return None
|
||||
else:
|
||||
return rev.strip().replace('+', '')
|
||||
543
awx/lib/site-packages/geventwebsocket/websocket.py
Normal file
543
awx/lib/site-packages/geventwebsocket/websocket.py
Normal file
@ -0,0 +1,543 @@
|
||||
import struct
|
||||
|
||||
from socket import error
|
||||
|
||||
from .exceptions import ProtocolError
|
||||
from .exceptions import WebSocketError
|
||||
from .exceptions import FrameTooLargeException
|
||||
|
||||
from .utf8validator import Utf8Validator
|
||||
|
||||
|
||||
MSG_SOCKET_DEAD = "Socket is dead"
|
||||
MSG_ALREADY_CLOSED = "Connection is already closed"
|
||||
MSG_CLOSED = "Connection closed"
|
||||
|
||||
|
||||
class WebSocket(object):
|
||||
"""
|
||||
Base class for supporting websocket operations.
|
||||
|
||||
:ivar environ: The http environment referenced by this connection.
|
||||
:ivar closed: Whether this connection is closed/closing.
|
||||
:ivar stream: The underlying file like object that will be read from /
|
||||
written to by this WebSocket object.
|
||||
"""
|
||||
|
||||
__slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed',
|
||||
'stream', 'raw_write', 'raw_read', 'handler')
|
||||
|
||||
OPCODE_CONTINUATION = 0x00
|
||||
OPCODE_TEXT = 0x01
|
||||
OPCODE_BINARY = 0x02
|
||||
OPCODE_CLOSE = 0x08
|
||||
OPCODE_PING = 0x09
|
||||
OPCODE_PONG = 0x0a
|
||||
|
||||
def __init__(self, environ, stream, handler):
|
||||
self.environ = environ
|
||||
self.closed = False
|
||||
|
||||
self.stream = stream
|
||||
|
||||
self.raw_write = stream.write
|
||||
self.raw_read = stream.read
|
||||
|
||||
self.utf8validator = Utf8Validator()
|
||||
self.handler = handler
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.close()
|
||||
except:
|
||||
# close() may fail if __init__ didn't complete
|
||||
pass
|
||||
|
||||
def _decode_bytes(self, bytestring):
|
||||
"""
|
||||
Internal method used to convert the utf-8 encoded bytestring into
|
||||
unicode.
|
||||
|
||||
If the conversion fails, the socket will be closed.
|
||||
"""
|
||||
|
||||
if not bytestring:
|
||||
return u''
|
||||
|
||||
try:
|
||||
return bytestring.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
self.close(1007)
|
||||
|
||||
raise
|
||||
|
||||
def _encode_bytes(self, text):
|
||||
"""
|
||||
:returns: The utf-8 byte string equivalent of `text`.
|
||||
"""
|
||||
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
|
||||
if not isinstance(text, unicode):
|
||||
text = unicode(text or '')
|
||||
|
||||
return text.encode('utf-8')
|
||||
|
||||
def _is_valid_close_code(self, code):
|
||||
"""
|
||||
:returns: Whether the returned close code is a valid hybi return code.
|
||||
"""
|
||||
if code < 1000:
|
||||
return False
|
||||
|
||||
if 1004 <= code <= 1006:
|
||||
return False
|
||||
|
||||
if 1012 <= code <= 1016:
|
||||
return False
|
||||
|
||||
if code == 1100:
|
||||
# not sure about this one but the autobahn fuzzer requires it.
|
||||
return False
|
||||
|
||||
if 2000 <= code <= 2999:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_app(self):
|
||||
if hasattr(self.handler.server.application, 'current_app'):
|
||||
return self.handler.server.application.current_app
|
||||
else:
|
||||
# For backwards compatibility reasons
|
||||
class MockApp():
|
||||
def on_close(self, *args):
|
||||
pass
|
||||
|
||||
return MockApp()
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_ORIGIN')
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION')
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('PATH_INFO')
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
return self.handler.logger
|
||||
|
||||
def handle_close(self, header, payload):
|
||||
"""
|
||||
Called when a close frame has been decoded from the stream.
|
||||
|
||||
:param header: The decoded `Header`.
|
||||
:param payload: The bytestring payload associated with the close frame.
|
||||
"""
|
||||
if not payload:
|
||||
self.close(1000, None)
|
||||
|
||||
return
|
||||
|
||||
if len(payload) < 2:
|
||||
raise ProtocolError('Invalid close frame: {0} {1}'.format(
|
||||
header, payload))
|
||||
|
||||
code = struct.unpack('!H', str(payload[:2]))[0]
|
||||
payload = payload[2:]
|
||||
|
||||
if payload:
|
||||
validator = Utf8Validator()
|
||||
val = validator.validate(payload)
|
||||
|
||||
if not val[0]:
|
||||
raise UnicodeError
|
||||
|
||||
if not self._is_valid_close_code(code):
|
||||
raise ProtocolError('Invalid close code {0}'.format(code))
|
||||
|
||||
self.close(code, payload)
|
||||
|
||||
def handle_ping(self, header, payload):
|
||||
self.send_frame(payload, self.OPCODE_PONG)
|
||||
|
||||
def handle_pong(self, header, payload):
|
||||
pass
|
||||
|
||||
def read_frame(self):
|
||||
"""
|
||||
Block until a full frame has been read from the socket.
|
||||
|
||||
This is an internal method as calling this will not cleanup correctly
|
||||
if an exception is called. Use `receive` instead.
|
||||
|
||||
:return: The header and payload as a tuple.
|
||||
"""
|
||||
|
||||
header = Header.decode_header(self.stream)
|
||||
|
||||
if header.flags:
|
||||
raise ProtocolError
|
||||
|
||||
if not header.length:
|
||||
return header, ''
|
||||
|
||||
try:
|
||||
payload = self.raw_read(header.length)
|
||||
except error:
|
||||
payload = ''
|
||||
except Exception:
|
||||
# TODO log out this exception
|
||||
payload = ''
|
||||
|
||||
if len(payload) != header.length:
|
||||
raise WebSocketError('Unexpected EOF reading frame payload')
|
||||
|
||||
if header.mask:
|
||||
payload = header.unmask_payload(payload)
|
||||
|
||||
return header, payload
|
||||
|
||||
def validate_utf8(self, payload):
|
||||
# Make sure the frames are decodable independently
|
||||
self.utf8validate_last = self.utf8validator.validate(payload)
|
||||
|
||||
if not self.utf8validate_last[0]:
|
||||
raise UnicodeError("Encountered invalid UTF-8 while processing "
|
||||
"text message at payload octet index "
|
||||
"{0:d}".format(self.utf8validate_last[3]))
|
||||
|
||||
def read_message(self):
|
||||
"""
|
||||
Return the next text or binary message from the socket.
|
||||
|
||||
This is an internal method as calling this will not cleanup correctly
|
||||
if an exception is called. Use `receive` instead.
|
||||
"""
|
||||
opcode = None
|
||||
message = ""
|
||||
|
||||
while True:
|
||||
header, payload = self.read_frame()
|
||||
f_opcode = header.opcode
|
||||
|
||||
if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
|
||||
# a new frame
|
||||
if opcode:
|
||||
raise ProtocolError("The opcode in non-fin frame is "
|
||||
"expected to be zero, got "
|
||||
"{0!r}".format(f_opcode))
|
||||
|
||||
# Start reading a new message, reset the validator
|
||||
self.utf8validator.reset()
|
||||
self.utf8validate_last = (True, True, 0, 0)
|
||||
|
||||
opcode = f_opcode
|
||||
|
||||
elif f_opcode == self.OPCODE_CONTINUATION:
|
||||
if not opcode:
|
||||
raise ProtocolError("Unexpected frame with opcode=0")
|
||||
|
||||
elif f_opcode == self.OPCODE_PING:
|
||||
self.handle_ping(header, payload)
|
||||
continue
|
||||
|
||||
elif f_opcode == self.OPCODE_PONG:
|
||||
self.handle_pong(header, payload)
|
||||
continue
|
||||
|
||||
elif f_opcode == self.OPCODE_CLOSE:
|
||||
self.handle_close(header, payload)
|
||||
return
|
||||
|
||||
else:
|
||||
raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode))
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
self.validate_utf8(payload)
|
||||
|
||||
message += payload
|
||||
|
||||
if header.fin:
|
||||
break
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
self.validate_utf8(message)
|
||||
return message
|
||||
else:
|
||||
return bytearray(message)
|
||||
|
||||
def receive(self):
|
||||
"""
|
||||
Read and return a message from the stream. If `None` is returned, then
|
||||
the socket is considered closed/errored.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
raise WebSocketError(MSG_ALREADY_CLOSED)
|
||||
|
||||
try:
|
||||
return self.read_message()
|
||||
except UnicodeError:
|
||||
self.close(1007)
|
||||
except ProtocolError:
|
||||
self.close(1002)
|
||||
except error:
|
||||
self.close()
|
||||
self.current_app.on_close(MSG_CLOSED)
|
||||
|
||||
return None
|
||||
|
||||
def send_frame(self, message, opcode):
|
||||
"""
|
||||
Send a frame over the websocket with message as its payload
|
||||
"""
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
raise WebSocketError(MSG_ALREADY_CLOSED)
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
message = self._encode_bytes(message)
|
||||
elif opcode == self.OPCODE_BINARY:
|
||||
message = str(message)
|
||||
|
||||
header = Header.encode_header(True, opcode, '', len(message), 0)
|
||||
|
||||
try:
|
||||
self.raw_write(header + message)
|
||||
except error:
|
||||
raise WebSocketError(MSG_SOCKET_DEAD)
|
||||
|
||||
def send(self, message, binary=None):
|
||||
"""
|
||||
Send a frame over the websocket with message as its payload
|
||||
"""
|
||||
if binary is None:
|
||||
binary = not isinstance(message, (str, unicode))
|
||||
|
||||
opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT
|
||||
|
||||
try:
|
||||
self.send_frame(message, opcode)
|
||||
except WebSocketError:
|
||||
self.current_app.on_close(MSG_SOCKET_DEAD)
|
||||
raise WebSocketError(MSG_SOCKET_DEAD)
|
||||
|
||||
def close(self, code=1000, message=''):
|
||||
"""
|
||||
Close the websocket and connection, sending the specified code and
|
||||
message. The underlying socket object is _not_ closed, that is the
|
||||
responsibility of the initiator.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
|
||||
try:
|
||||
message = self._encode_bytes(message)
|
||||
|
||||
self.send_frame(
|
||||
struct.pack('!H%ds' % len(message), code, message),
|
||||
opcode=self.OPCODE_CLOSE)
|
||||
except WebSocketError:
|
||||
# Failed to write the closing frame but it's ok because we're
|
||||
# closing the socket anyway.
|
||||
self.logger.debug("Failed to write closing frame -> closing socket")
|
||||
finally:
|
||||
self.logger.debug("Closed WebSocket")
|
||||
self.closed = True
|
||||
|
||||
self.stream = None
|
||||
self.raw_write = None
|
||||
self.raw_read = None
|
||||
|
||||
self.environ = None
|
||||
|
||||
#self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
"""
|
||||
Wraps the handler's socket/rfile attributes and makes it in to a file like
|
||||
object that can be read from/written to by the lower level websocket api.
|
||||
"""
|
||||
|
||||
__slots__ = ('handler', 'read', 'write')
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.read = handler.rfile.read
|
||||
self.write = handler.socket.sendall
|
||||
|
||||
|
||||
class Header(object):
|
||||
__slots__ = ('fin', 'mask', 'opcode', 'flags', 'length')
|
||||
|
||||
FIN_MASK = 0x80
|
||||
OPCODE_MASK = 0x0f
|
||||
MASK_MASK = 0x80
|
||||
LENGTH_MASK = 0x7f
|
||||
|
||||
RSV0_MASK = 0x40
|
||||
RSV1_MASK = 0x20
|
||||
RSV2_MASK = 0x10
|
||||
|
||||
# bitwise mask that will determine the reserved bits for a frame header
|
||||
HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK
|
||||
|
||||
def __init__(self, fin=0, opcode=0, flags=0, length=0):
|
||||
self.mask = ''
|
||||
self.fin = fin
|
||||
self.opcode = opcode
|
||||
self.flags = flags
|
||||
self.length = length
|
||||
|
||||
def mask_payload(self, payload):
|
||||
payload = bytearray(payload)
|
||||
mask = bytearray(self.mask)
|
||||
|
||||
for i in xrange(self.length):
|
||||
payload[i] ^= mask[i % 4]
|
||||
|
||||
return str(payload)
|
||||
|
||||
# it's the same operation
|
||||
unmask_payload = mask_payload
|
||||
|
||||
def __repr__(self):
|
||||
return ("<Header fin={0} opcode={1} length={2} flags={3} at "
|
||||
"0x{4:x}>").format(self.fin, self.opcode, self.length,
|
||||
self.flags, id(self))
|
||||
|
||||
@classmethod
|
||||
def decode_header(cls, stream):
|
||||
"""
|
||||
Decode a WebSocket header.
|
||||
|
||||
:param stream: A file like object that can be 'read' from.
|
||||
:returns: A `Header` instance.
|
||||
"""
|
||||
read = stream.read
|
||||
data = read(2)
|
||||
|
||||
if len(data) != 2:
|
||||
raise WebSocketError("Unexpected EOF while decoding header")
|
||||
|
||||
first_byte, second_byte = struct.unpack('!BB', data)
|
||||
|
||||
header = cls(
|
||||
fin=first_byte & cls.FIN_MASK == cls.FIN_MASK,
|
||||
opcode=first_byte & cls.OPCODE_MASK,
|
||||
flags=first_byte & cls.HEADER_FLAG_MASK,
|
||||
length=second_byte & cls.LENGTH_MASK)
|
||||
|
||||
has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK
|
||||
|
||||
if header.opcode > 0x07:
|
||||
if not header.fin:
|
||||
raise ProtocolError(
|
||||
"Received fragmented control frame: {0!r}".format(data))
|
||||
|
||||
# Control frames MUST have a payload length of 125 bytes or less
|
||||
if header.length > 125:
|
||||
raise FrameTooLargeException(
|
||||
"Control frame cannot be larger than 125 bytes: "
|
||||
"{0!r}".format(data))
|
||||
|
||||
if header.length == 126:
|
||||
# 16 bit length
|
||||
data = read(2)
|
||||
|
||||
if len(data) != 2:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.length = struct.unpack('!H', data)[0]
|
||||
elif header.length == 127:
|
||||
# 64 bit length
|
||||
data = read(8)
|
||||
|
||||
if len(data) != 8:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.length = struct.unpack('!Q', data)[0]
|
||||
|
||||
if has_mask:
|
||||
mask = read(4)
|
||||
|
||||
if len(mask) != 4:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.mask = mask
|
||||
|
||||
return header
|
||||
|
||||
@classmethod
|
||||
def encode_header(cls, fin, opcode, mask, length, flags):
|
||||
"""
|
||||
Encodes a WebSocket header.
|
||||
|
||||
:param fin: Whether this is the final frame for this opcode.
|
||||
:param opcode: The opcode of the payload, see `OPCODE_*`
|
||||
:param mask: Whether the payload is masked.
|
||||
:param length: The length of the frame.
|
||||
:param flags: The RSV* flags.
|
||||
:return: A bytestring encoded header.
|
||||
"""
|
||||
first_byte = opcode
|
||||
second_byte = 0
|
||||
extra = ''
|
||||
|
||||
if fin:
|
||||
first_byte |= cls.FIN_MASK
|
||||
|
||||
if flags & cls.RSV0_MASK:
|
||||
first_byte |= cls.RSV0_MASK
|
||||
|
||||
if flags & cls.RSV1_MASK:
|
||||
first_byte |= cls.RSV1_MASK
|
||||
|
||||
if flags & cls.RSV2_MASK:
|
||||
first_byte |= cls.RSV2_MASK
|
||||
|
||||
# now deal with length complexities
|
||||
if length < 126:
|
||||
second_byte += length
|
||||
elif length <= 0xffff:
|
||||
second_byte += 126
|
||||
extra = struct.pack('!H', length)
|
||||
elif length <= 0xffffffffffffffff:
|
||||
second_byte += 127
|
||||
extra = struct.pack('!Q', length)
|
||||
else:
|
||||
raise FrameTooLargeException
|
||||
|
||||
if mask:
|
||||
second_byte |= cls.MASK_MASK
|
||||
|
||||
extra += mask
|
||||
|
||||
return chr(first_byte) + chr(second_byte) + extra
|
||||
76
awx/lib/site-packages/socketio/__init__.py
Normal file
76
awx/lib/site-packages/socketio/__init__.py
Normal file
@ -0,0 +1,76 @@
|
||||
__version__ = (0, 3, 5)
|
||||
|
||||
import logging
|
||||
import gevent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def socketio_manage(environ, namespaces, request=None, error_handler=None):
|
||||
"""Main SocketIO management function, call from within your Framework of
|
||||
choice's view.
|
||||
|
||||
The ``environ`` variable is the WSGI ``environ``. It is used to extract
|
||||
Socket object from the underlying server (as the 'socketio' key), and will
|
||||
be attached to both the ``Socket`` and ``Namespace`` objects.
|
||||
|
||||
The ``namespaces`` parameter is a dictionary of the namespace string
|
||||
representation as key, and the BaseNamespace namespace class descendant as
|
||||
a value. The empty string ('') namespace is the global namespace. You can
|
||||
use Socket.GLOBAL_NS to be more explicit. So it would look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
namespaces={'': GlobalNamespace,
|
||||
'/chat': ChatNamespace}
|
||||
|
||||
The ``request`` object is not required, but will probably be useful to pass
|
||||
framework-specific things into your Socket and Namespace functions. It will
|
||||
simply be attached to the Socket and Namespace object (accessible through
|
||||
``self.request`` in both cases), and it is not accessed in any case by the
|
||||
``gevent-socketio`` library.
|
||||
|
||||
Pass in an ``error_handler`` if you want to override the default
|
||||
error_handler (which is :func:`socketio.virtsocket.default_error_handler`.
|
||||
The callable you pass in should have the same signature as the default
|
||||
error handler.
|
||||
|
||||
This function will block the current "view" or "controller" in your
|
||||
framework to do the recv/send on the socket, and dispatch incoming messages
|
||||
to your namespaces.
|
||||
|
||||
This is a simple example using Pyramid:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def my_view(request):
|
||||
socketio_manage(request.environ, {'': GlobalNamespace}, request)
|
||||
NOTE: You must understand that this function is going to be called
|
||||
*only once* per socket opening, *even though* you are using a long
|
||||
polling mechanism. The subsequent calls (for long polling) will
|
||||
be hooked directly at the server-level, to interact with the
|
||||
active ``Socket`` instance. This means you will *not* get access
|
||||
to the future ``request`` or ``environ`` objects. This is of
|
||||
particular importance regarding sessions (like Beaker). The
|
||||
session will be opened once at the opening of the Socket, and not
|
||||
closed until the socket is closed. You are responsible for
|
||||
opening and closing the cookie-based session yourself if you want
|
||||
to keep its data in sync with the rest of your GET/POST calls.
|
||||
"""
|
||||
socket = environ['socketio']
|
||||
socket._set_environ(environ)
|
||||
socket._set_namespaces(namespaces)
|
||||
|
||||
if request:
|
||||
socket._set_request(request)
|
||||
|
||||
if error_handler:
|
||||
socket._set_error_handler(error_handler)
|
||||
|
||||
receiver_loop = socket._spawn_receiver_loop()
|
||||
watcher = socket._spawn_watcher()
|
||||
|
||||
gevent.joinall([receiver_loop, watcher])
|
||||
|
||||
# TODO: double check, what happens to the WSGI request here ? it vanishes ?
|
||||
return
|
||||
149
awx/lib/site-packages/socketio/handler.py
Normal file
149
awx/lib/site-packages/socketio/handler.py
Normal file
@ -0,0 +1,149 @@
|
||||
import sys
|
||||
import re
|
||||
import gevent
|
||||
import urlparse
|
||||
|
||||
from gevent.pywsgi import WSGIHandler
|
||||
from socketio import transports
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
|
||||
|
||||
class SocketIOHandler(WSGIHandler):
|
||||
RE_REQUEST_URL = re.compile(r"""
|
||||
^/(?P<resource>[^/]+)
|
||||
/(?P<protocol_version>[^/]+)
|
||||
/(?P<transport_id>[^/]+)
|
||||
/(?P<sessid>[^/]+)/?$
|
||||
""", re.X)
|
||||
RE_HANDSHAKE_URL = re.compile(r"^/(?P<resource>[^/]+)/1/$", re.X)
|
||||
|
||||
handler_types = {
|
||||
'websocket': transports.WebsocketTransport,
|
||||
'flashsocket': transports.FlashSocketTransport,
|
||||
'htmlfile': transports.HTMLFileTransport,
|
||||
'xhr-multipart': transports.XHRMultipartTransport,
|
||||
'xhr-polling': transports.XHRPollingTransport,
|
||||
'jsonp-polling': transports.JSONPolling,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.socketio_connection = False
|
||||
self.allowed_paths = None
|
||||
|
||||
super(SocketIOHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
self.transports = self.handler_types.keys()
|
||||
if self.server.transports:
|
||||
self.transports = self.server.transports
|
||||
if not set(self.transports).issubset(set(self.handler_types)):
|
||||
raise Exception("transports should be elements of: %s" %
|
||||
(self.handler_types.keys()))
|
||||
|
||||
def _do_handshake(self, tokens):
|
||||
if tokens["resource"] != self.server.resource:
|
||||
self.log_error("socket.io URL mismatch")
|
||||
else:
|
||||
socket = self.server.get_socket()
|
||||
data = "%s:15:10:%s" % (socket.sessid, ",".join(self.transports))
|
||||
self.write_smart(data)
|
||||
|
||||
def write_jsonp_result(self, data, wrapper="0"):
|
||||
self.start_response("200 OK", [
|
||||
("Content-Type", "application/javascript"),
|
||||
])
|
||||
self.result = ['io.j[%s]("%s");' % (wrapper, data)]
|
||||
|
||||
def write_plain_result(self, data):
|
||||
self.start_response("200 OK", [
|
||||
("Access-Control-Allow-Origin", self.environ.get('HTTP_ORIGIN', '*')),
|
||||
("Access-Control-Allow-Credentials", "true"),
|
||||
("Access-Control-Allow-Methods", "POST, GET, OPTIONS"),
|
||||
("Access-Control-Max-Age", 3600),
|
||||
("Content-Type", "text/plain"),
|
||||
])
|
||||
self.result = [data]
|
||||
|
||||
def write_smart(self, data):
|
||||
args = urlparse.parse_qs(self.environ.get("QUERY_STRING"))
|
||||
|
||||
if "jsonp" in args:
|
||||
self.write_jsonp_result(data, args["jsonp"][0])
|
||||
else:
|
||||
self.write_plain_result(data)
|
||||
|
||||
self.process_result()
|
||||
|
||||
def handle_one_response(self):
|
||||
path = self.environ.get('PATH_INFO')
|
||||
|
||||
# Kick non-socket.io requests to our superclass
|
||||
if not path.lstrip('/').startswith(self.server.resource):
|
||||
return super(SocketIOHandler, self).handle_one_response()
|
||||
|
||||
self.status = None
|
||||
self.headers_sent = False
|
||||
self.result = None
|
||||
self.response_length = 0
|
||||
self.response_use_chunked = False
|
||||
request_method = self.environ.get("REQUEST_METHOD")
|
||||
request_tokens = self.RE_REQUEST_URL.match(path)
|
||||
|
||||
# Parse request URL and QUERY_STRING and do handshake
|
||||
if request_tokens:
|
||||
request_tokens = request_tokens.groupdict()
|
||||
else:
|
||||
handshake_tokens = self.RE_HANDSHAKE_URL.match(path)
|
||||
|
||||
if handshake_tokens:
|
||||
return self._do_handshake(handshake_tokens.groupdict())
|
||||
else:
|
||||
# This is no socket.io request. Let the WSGI app handle it.
|
||||
return super(SocketIOHandler, self).handle_one_response()
|
||||
|
||||
# Setup the transport and socket
|
||||
transport = self.handler_types.get(request_tokens["transport_id"])
|
||||
sessid = request_tokens["sessid"]
|
||||
socket = self.server.get_socket(sessid)
|
||||
|
||||
# In case this is WebSocket request, switch to the WebSocketHandler
|
||||
# FIXME: fix this ugly class change
|
||||
if issubclass(transport, (transports.WebsocketTransport,
|
||||
transports.FlashSocketTransport)):
|
||||
self.__class__ = WebSocketHandler
|
||||
self.prevent_wsgi_call = True # thank you
|
||||
# TODO: any errors, treat them ??
|
||||
self.handle_one_response()
|
||||
|
||||
# Make the socket object available for WSGI apps
|
||||
self.environ['socketio'] = socket
|
||||
|
||||
# Create a transport and handle the request likewise
|
||||
self.transport = transport(self)
|
||||
|
||||
jobs = self.transport.connect(socket, request_method)
|
||||
# Keep track of those jobs (reading, writing and heartbeat jobs) so
|
||||
# that we can kill them later with Socket.kill()
|
||||
socket.jobs.extend(jobs)
|
||||
|
||||
try:
|
||||
# We'll run the WSGI app if it wasn't already done.
|
||||
if socket.wsgi_app_greenlet is None:
|
||||
# TODO: why don't we spawn a call to handle_one_response here ?
|
||||
# why call directly the WSGI machinery ?
|
||||
start_response = lambda status, headers, exc=None: None
|
||||
socket.wsgi_app_greenlet = gevent.spawn(self.application,
|
||||
self.environ,
|
||||
start_response)
|
||||
except:
|
||||
self.handle_error(*sys.exc_info())
|
||||
|
||||
# TODO DOUBLE-CHECK: do we need to joinall here ?
|
||||
gevent.joinall(jobs)
|
||||
|
||||
def handle_bad_request(self):
|
||||
self.close_connection = True
|
||||
self.start_reponse("400 Bad Request", [
|
||||
('Content-Type', 'text/plain'),
|
||||
('Connection', 'close'),
|
||||
('Content-Length', 0)
|
||||
])
|
||||
73
awx/lib/site-packages/socketio/mixins.py
Normal file
73
awx/lib/site-packages/socketio/mixins.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""
|
||||
These are general-purpose Mixins -- for use with Namespaces -- that are
|
||||
generally useful for most simple projects, e.g. Rooms, Broadcast.
|
||||
|
||||
You'll likely want to create your own Mixins.
|
||||
"""
|
||||
|
||||
|
||||
class RoomsMixin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RoomsMixin, self).__init__(*args, **kwargs)
|
||||
if 'rooms' not in self.session:
|
||||
self.session['rooms'] = set() # a set of simple strings
|
||||
|
||||
def join(self, room):
|
||||
"""Lets a user join a room on a specific Namespace."""
|
||||
self.session['rooms'].add(self._get_room_name(room))
|
||||
|
||||
def leave(self, room):
|
||||
"""Lets a user leave a room on a specific Namespace."""
|
||||
self.session['rooms'].remove(self._get_room_name(room))
|
||||
|
||||
def _get_room_name(self, room):
|
||||
return self.ns_name + '_' + room
|
||||
|
||||
def emit_to_room(self, room, event, *args):
|
||||
"""This is sent to all in the room (in this particular Namespace)"""
|
||||
pkt = dict(type="event",
|
||||
name=event,
|
||||
args=args,
|
||||
endpoint=self.ns_name)
|
||||
room_name = self._get_room_name(room)
|
||||
for sessid, socket in self.socket.server.sockets.iteritems():
|
||||
if 'rooms' not in socket.session:
|
||||
continue
|
||||
if room_name in socket.session['rooms'] and self.socket != socket:
|
||||
socket.send_packet(pkt)
|
||||
|
||||
|
||||
class BroadcastMixin(object):
|
||||
"""Mix in this class with your Namespace to have a broadcast event method.
|
||||
|
||||
Use it like this:
|
||||
class MyNamespace(BaseNamespace, BroadcastMixin):
|
||||
def on_chatmsg(self, event):
|
||||
self.broadcast_event('chatmsg', event)
|
||||
"""
|
||||
def broadcast_event(self, event, *args):
|
||||
"""
|
||||
This is sent to all in the sockets in this particular Namespace,
|
||||
including itself.
|
||||
"""
|
||||
pkt = dict(type="event",
|
||||
name=event,
|
||||
args=args,
|
||||
endpoint=self.ns_name)
|
||||
|
||||
for sessid, socket in self.socket.server.sockets.iteritems():
|
||||
socket.send_packet(pkt)
|
||||
|
||||
def broadcast_event_not_me(self, event, *args):
|
||||
"""
|
||||
This is sent to all in the sockets in this particular Namespace,
|
||||
except itself.
|
||||
"""
|
||||
pkt = dict(type="event",
|
||||
name=event,
|
||||
args=args,
|
||||
endpoint=self.ns_name)
|
||||
|
||||
for sessid, socket in self.socket.server.sockets.iteritems():
|
||||
if socket is not self.socket:
|
||||
socket.send_packet(pkt)
|
||||
468
awx/lib/site-packages/socketio/namespace.py
Normal file
468
awx/lib/site-packages/socketio/namespace.py
Normal file
@ -0,0 +1,468 @@
|
||||
import gevent
|
||||
import re
|
||||
import logging
|
||||
import inspect
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# regex to check the event name contains only alpha numerical characters
|
||||
allowed_event_name_regex = re.compile(r'^[A-Za-z][A-Za-z0-9_ ]*$')
|
||||
|
||||
|
||||
class BaseNamespace(object):
|
||||
"""The **Namespace** is the primary interface a developer will use
|
||||
to create a gevent-socketio-based application.
|
||||
|
||||
You should create your own subclass of this class, optionally using one
|
||||
of the :mod:`socketio.mixins` provided (or your own), and define methods
|
||||
such as:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
def on_my_event(self, my_first_arg, my_second_arg):
|
||||
print "This is the my_first_arg object", my_first_arg
|
||||
print "This is the my_second_arg object", my_second_arg
|
||||
|
||||
def on_my_second_event(self, whatever):
|
||||
print "This holds the first arg that was passed", whatever
|
||||
|
||||
We can also access the full packet directly by making an event handler
|
||||
that accepts a single argument named 'packet':
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
def on_third_event(self, packet):
|
||||
print "The full packet", packet
|
||||
print "See the BaseNamespace::call_method() method for details"
|
||||
"""
|
||||
def __init__(self, environ, ns_name, request=None):
|
||||
self.environ = environ
|
||||
self.socket = environ['socketio']
|
||||
self.session = self.socket.session # easily accessible session
|
||||
self.request = request
|
||||
self.ns_name = ns_name
|
||||
self.allowed_methods = None # be careful, None means all methods
|
||||
# are allowed while an empty list
|
||||
# means totally closed.
|
||||
self.jobs = []
|
||||
|
||||
self.reset_acl()
|
||||
|
||||
# Init the mixins if specified after.
|
||||
super(BaseNamespace, self).__init__()
|
||||
|
||||
def is_method_allowed(self, method_name):
|
||||
"""ACL system: this checks if you have access to that method_name,
|
||||
according to the set ACLs"""
|
||||
if self.allowed_methods is None:
|
||||
return True
|
||||
else:
|
||||
return method_name in self.allowed_methods
|
||||
|
||||
def add_acl_method(self, method_name):
|
||||
"""ACL system: make the method_name accessible to the current socket"""
|
||||
|
||||
if isinstance(self.allowed_methods, set):
|
||||
self.allowed_methods.add(method_name)
|
||||
else:
|
||||
self.allowed_methods = set([method_name])
|
||||
|
||||
def del_acl_method(self, method_name):
|
||||
"""ACL system: ensure the user will not have access to that method."""
|
||||
if self.allowed_methods is None:
|
||||
raise ValueError(
|
||||
"Trying to delete an ACL method, but none were"
|
||||
+ " defined yet! Or: No ACL restrictions yet, why would you"
|
||||
+ " delete one?"
|
||||
)
|
||||
|
||||
self.allowed_methods.remove(method_name)
|
||||
|
||||
def lift_acl_restrictions(self):
|
||||
"""ACL system: This removes restrictions on the Namespace's methods, so
|
||||
that all the ``on_*()`` and ``recv_*()`` can be accessed.
|
||||
"""
|
||||
self.allowed_methods = None
|
||||
|
||||
def get_initial_acl(self):
|
||||
"""ACL system: If you define this function, you must return
|
||||
all the 'event' names that you want your User (the established
|
||||
virtual Socket) to have access to.
|
||||
|
||||
If you do not define this function, the user will have free
|
||||
access to all of the ``on_*()`` and ``recv_*()`` functions,
|
||||
etc.. methods.
|
||||
|
||||
Return something like: ``['on_connect', 'on_public_method']``
|
||||
|
||||
You can later modify this list dynamically (inside
|
||||
``on_connect()`` for example) using:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
self.add_acl_method('on_secure_method')
|
||||
|
||||
``self.request`` is available in here, if you're already ready to
|
||||
do some auth. check.
|
||||
|
||||
The ACLs are checked by the :meth:`process_packet` and/or
|
||||
:meth:`process_event` default implementations, before calling
|
||||
the class's methods.
|
||||
|
||||
**Beware**, returning ``None`` leaves the namespace completely
|
||||
accessible.
|
||||
"""
|
||||
return None
|
||||
|
||||
def reset_acl(self):
|
||||
"""Resets ACL to its initial value (calling
|
||||
:meth:`get_initial_acl`` and applying that again).
|
||||
"""
|
||||
self.allowed_methods = self.get_initial_acl()
|
||||
|
||||
def process_packet(self, packet):
|
||||
"""If you override this, NONE of the functions in this class
|
||||
will be called. It is responsible for dispatching to
|
||||
:meth:`process_event` (which in turn calls ``on_*()`` and
|
||||
``recv_*()`` methods).
|
||||
|
||||
If the packet arrived here, it is because it belongs to this endpoint.
|
||||
|
||||
For each packet arriving, the only possible path of execution, that is,
|
||||
the only methods that *can* be called are the following:
|
||||
|
||||
* recv_connect()
|
||||
* recv_message()
|
||||
* recv_json()
|
||||
* recv_error()
|
||||
* recv_disconnect()
|
||||
* on_*()
|
||||
"""
|
||||
packet_type = packet['type']
|
||||
|
||||
if packet_type == 'event':
|
||||
return self.process_event(packet)
|
||||
elif packet_type == 'message':
|
||||
return self.call_method_with_acl('recv_message', packet,
|
||||
packet['data'])
|
||||
elif packet_type == 'json':
|
||||
return self.call_method_with_acl('recv_json', packet,
|
||||
packet['data'])
|
||||
elif packet_type == 'connect':
|
||||
self.socket.send_packet(packet)
|
||||
return self.call_method_with_acl('recv_connect', packet)
|
||||
elif packet_type == 'error':
|
||||
return self.call_method_with_acl('recv_error', packet)
|
||||
elif packet_type == 'ack':
|
||||
callback = self.socket._pop_ack_callback(packet['ackId'])
|
||||
if not callback:
|
||||
print "ERROR: No such callback for ackId %s" % packet['ackId']
|
||||
return
|
||||
try:
|
||||
return callback(*(packet['args']))
|
||||
except TypeError, e:
|
||||
print "ERROR: Call to callback function failed", packet
|
||||
elif packet_type == 'disconnect':
|
||||
# Force a disconnect on the namespace.
|
||||
return self.call_method_with_acl('recv_disconnect', packet)
|
||||
else:
|
||||
print "Unprocessed packet", packet
|
||||
# TODO: manage the other packet types: disconnect
|
||||
|
||||
def process_event(self, packet):
|
||||
"""This function dispatches ``event`` messages to the correct
|
||||
functions. You should override this method only if you are not
|
||||
satisfied with the automatic dispatching to
|
||||
``on_``-prefixed methods. You could then implement your own dispatch.
|
||||
See the source code for inspiration.
|
||||
|
||||
To process events that have callbacks on the client side, you
|
||||
must define your event with a single parameter: ``packet``.
|
||||
In this case, it will be the full ``packet`` object and you
|
||||
can inspect its ``ack`` and ``id`` keys to define if and how
|
||||
you reply. A correct reply to an event with a callback would
|
||||
look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def on_my_callback(self, packet):
|
||||
if 'ack' in packet':
|
||||
self.emit('go_back', 'param1', id=packet['id'])
|
||||
"""
|
||||
args = packet['args']
|
||||
name = packet['name']
|
||||
if not allowed_event_name_regex.match(name):
|
||||
self.error("unallowed_event_name",
|
||||
"name must only contains alpha numerical characters")
|
||||
return
|
||||
|
||||
method_name = 'on_' + name.replace(' ', '_')
|
||||
# This means the args, passed as a list, will be expanded to
|
||||
# the method arg and if you passed a dict, it will be a dict
|
||||
# as the first parameter.
|
||||
|
||||
return self.call_method_with_acl(method_name, packet, *args)
|
||||
|
||||
def call_method_with_acl(self, method_name, packet, *args):
|
||||
"""You should always use this function to call the methods,
|
||||
as it checks if the user is allowed according to the ACLs.
|
||||
|
||||
If you override :meth:`process_packet` or
|
||||
:meth:`process_event`, you should definitely want to use this
|
||||
instead of ``getattr(self, 'my_method')()``
|
||||
"""
|
||||
if not self.is_method_allowed(method_name):
|
||||
self.error('method_access_denied',
|
||||
'You do not have access to method "%s"' % method_name)
|
||||
return
|
||||
|
||||
return self.call_method(method_name, packet, *args)
|
||||
|
||||
def call_method(self, method_name, packet, *args):
|
||||
"""This function is used to implement the two behaviors on dispatched
|
||||
``on_*()`` and ``recv_*()`` method calls.
|
||||
|
||||
Those are the two behaviors:
|
||||
|
||||
* If there is only one parameter on the dispatched method and
|
||||
it is equal to ``packet``, then pass in the packet as the
|
||||
sole parameter.
|
||||
|
||||
* Otherwise, pass in the arguments as specified by the
|
||||
different ``recv_*()`` methods args specs, or the
|
||||
:meth:`process_event` documentation.
|
||||
"""
|
||||
method = getattr(self, method_name, None)
|
||||
if method is None:
|
||||
self.error('no_such_method',
|
||||
'The method "%s" was not found' % method_name)
|
||||
return
|
||||
|
||||
specs = inspect.getargspec(method)
|
||||
func_args = specs.args
|
||||
if not len(func_args) or func_args[0] != 'self':
|
||||
self.error("invalid_method_args",
|
||||
"The server-side method is invalid, as it doesn't "
|
||||
"have 'self' as its first argument")
|
||||
return
|
||||
if len(func_args) == 2 and func_args[1] == 'packet':
|
||||
return method(packet)
|
||||
else:
|
||||
return method(*args)
|
||||
|
||||
def initialize(self):
|
||||
"""This is called right after ``__init__``, on the initial
|
||||
creation of a namespace so you may handle any setup job you
|
||||
need.
|
||||
|
||||
Namespaces are created only when some packets arrive that ask
|
||||
for the namespace. They are not created altogether when a new
|
||||
:class:`~socketio.virtsocket.Socket` connection is established,
|
||||
so you can have many many namespaces assigned (when calling
|
||||
:func:`~socketio.socketio_manage`) without clogging the
|
||||
memory.
|
||||
|
||||
If you override this method, you probably want to only
|
||||
initialize the variables you're going to use in the events of
|
||||
this namespace, say, with some default values, but not perform
|
||||
any operation that assumes authentication/authorization.
|
||||
"""
|
||||
pass
|
||||
|
||||
def recv_message(self, data):
|
||||
"""This is more of a backwards compatibility hack. This will be
|
||||
called for messages sent with the original send() call on the client
|
||||
side. This is NOT the 'message' event, which you will catch with
|
||||
'on_message()'. The data arriving here is a simple string, with no
|
||||
other info.
|
||||
|
||||
If you want to handle those messages, you should override this method.
|
||||
"""
|
||||
return data
|
||||
|
||||
def recv_json(self, data):
|
||||
"""This is more of a backwards compatibility hack. This will be
|
||||
called for JSON packets sent with the original json() call on the
|
||||
JavaScript side. This is NOT the 'json' event, which you will catch
|
||||
with 'on_json()'. The data arriving here is a python dict, with no
|
||||
event name.
|
||||
|
||||
If you want to handle those messages, you should override this method.
|
||||
"""
|
||||
return data
|
||||
|
||||
def recv_disconnect(self):
|
||||
"""Override this function if you want to do something when you get a
|
||||
*force disconnect* packet.
|
||||
|
||||
By default, this function calls the :meth:`disconnect` clean-up
|
||||
function. You probably want to call it yourself also, and put
|
||||
your clean-up routines in :meth:`disconnect` rather than here,
|
||||
because that :meth:`disconnect` function gets called
|
||||
automatically upon disconnection. This function is a
|
||||
pre-handle for when you get the `disconnect packet`.
|
||||
"""
|
||||
self.disconnect(silent=True)
|
||||
|
||||
def recv_connect(self):
|
||||
"""Called the first time a client connection is open on a
|
||||
Namespace. This *does not* fire on the global namespace.
|
||||
|
||||
This allows you to do boilerplate stuff for
|
||||
the namespace like connecting to rooms, broadcasting events
|
||||
to others, doing authorization work, and tweaking the ACLs to open
|
||||
up the rest of the namespace (if it was closed at the
|
||||
beginning by having :meth:`get_initial_acl` return only
|
||||
['recv_connect'])
|
||||
|
||||
Also see the different :ref:`mixins <mixins_module>` (like
|
||||
`RoomsMixin`, `BroadcastMixin`).
|
||||
"""
|
||||
pass
|
||||
|
||||
def recv_error(self, packet):
|
||||
"""Override this function to handle the errors we get from the client.
|
||||
|
||||
:param packet: the full packet.
|
||||
"""
|
||||
pass
|
||||
|
||||
def error(self, error_name, error_message, msg_id=None, quiet=False):
|
||||
"""Use this to use the configured ``error_handler`` yield an
|
||||
error message to your application.
|
||||
|
||||
:param error_name: is a short string, to associate messages to recovery
|
||||
methods
|
||||
:param error_message: is some human-readable text, describing the error
|
||||
:param msg_id: is used to associate with a request
|
||||
:param quiet: specific to error_handlers. The default doesn't send a
|
||||
message to the user, but shows a debug message on the
|
||||
developer console.
|
||||
"""
|
||||
self.socket.error(error_name, error_message, endpoint=self.ns_name,
|
||||
msg_id=msg_id, quiet=quiet)
|
||||
|
||||
def send(self, message, json=False, callback=None):
|
||||
"""Use send to send a simple string message.
|
||||
|
||||
If ``json`` is True, the message will be encoded as a JSON object
|
||||
on the wire, and decoded on the other side.
|
||||
|
||||
This is mostly for backwards compatibility. ``emit()`` is more fun.
|
||||
|
||||
:param callback: This is a callback function that will be
|
||||
called automatically by the client upon
|
||||
reception. It does not verify that the
|
||||
listener over there was completed with
|
||||
success. It just tells you that the browser
|
||||
got a hold of the packet.
|
||||
:type callback: callable
|
||||
"""
|
||||
pkt = dict(type="message", data=message, endpoint=self.ns_name)
|
||||
if json:
|
||||
pkt['type'] = "json"
|
||||
|
||||
if callback:
|
||||
# By passing ack=True, we use the old behavior of being returned
|
||||
# an 'ack' packet, automatically triggered by the client-side
|
||||
# with no user-code being run. The emit() version of the
|
||||
# callback is more useful I think :) So migrate your code.
|
||||
pkt['ack'] = True
|
||||
pkt['id'] = msgid = self.socket._get_next_msgid()
|
||||
self.socket._save_ack_callback(msgid, callback)
|
||||
|
||||
self.socket.send_packet(pkt)
|
||||
|
||||
def emit(self, event, *args, **kwargs):
|
||||
"""Use this to send a structured event, with a name and arguments, to
|
||||
the client.
|
||||
|
||||
By default, it uses this namespace's endpoint. You can send messages on
|
||||
other endpoints with something like:
|
||||
|
||||
``self.socket['/other_endpoint'].emit()``.
|
||||
|
||||
However, it is possible that the ``'/other_endpoint'`` was not
|
||||
initialized yet, and that would yield a ``KeyError``.
|
||||
|
||||
The only supported ``kwargs`` is ``callback``. All other parameters
|
||||
must be passed positionally.
|
||||
|
||||
:param event: The name of the event to trigger on the other end.
|
||||
:param callback: Pass in the callback keyword argument to define a
|
||||
call-back that will be called when the client acks.
|
||||
|
||||
This callback is slightly different from the one from
|
||||
``send()``, as this callback will receive parameters
|
||||
from the explicit call of the ``ack()`` function
|
||||
passed to the listener on the client side.
|
||||
|
||||
The remote listener will need to explicitly ack (by
|
||||
calling its last argument, a function which is
|
||||
usually called 'ack') with some parameters indicating
|
||||
success or error. The 'ack' packet coming back here
|
||||
will then trigger the callback function with the
|
||||
returned values.
|
||||
:type callback: callable
|
||||
"""
|
||||
callback = kwargs.pop('callback', None)
|
||||
|
||||
if kwargs:
|
||||
raise ValueError(
|
||||
"emit() only supports positional argument, to stay "
|
||||
"compatible with the Socket.IO protocol. You can "
|
||||
"however pass in a dictionary as the first argument")
|
||||
pkt = dict(type="event", name=event, args=args,
|
||||
endpoint=self.ns_name)
|
||||
|
||||
if callback:
|
||||
# By passing 'data', we indicate that we *want* an explicit ack
|
||||
# by the client code, not an automatic as with send().
|
||||
pkt['ack'] = 'data'
|
||||
pkt['id'] = msgid = self.socket._get_next_msgid()
|
||||
self.socket._save_ack_callback(msgid, callback)
|
||||
|
||||
self.socket.send_packet(pkt)
|
||||
|
||||
def spawn(self, fn, *args, **kwargs):
|
||||
"""Spawn a new process, attached to this Namespace.
|
||||
|
||||
It will be monitored by the "watcher" process in the Socket. If the
|
||||
socket disconnects, all these greenlets are going to be killed, after
|
||||
calling BaseNamespace.disconnect()
|
||||
"""
|
||||
# self.log.debug("Spawning sub-Namespace Greenlet: %s" % fn.__name__)
|
||||
new = gevent.spawn(fn, *args, **kwargs)
|
||||
self.jobs.append(new)
|
||||
return new
|
||||
|
||||
def disconnect(self, silent=False):
|
||||
"""Send a 'disconnect' packet, so that the user knows it has been
|
||||
disconnected (booted actually). This will trigger an onDisconnect()
|
||||
call on the client side.
|
||||
|
||||
Over here, we will kill all ``spawn``ed processes and remove the
|
||||
namespace from the Socket object.
|
||||
|
||||
:param silent: do not actually send the packet (if they asked for a
|
||||
disconnect for example), but just kill all jobs spawned
|
||||
by this Namespace, and remove it from the Socket.
|
||||
"""
|
||||
if not silent:
|
||||
packet = {"type": "disconnect",
|
||||
"endpoint": self.ns_name}
|
||||
self.socket.send_packet(packet)
|
||||
self.socket.remove_namespace(self.ns_name)
|
||||
self.kill_local_jobs()
|
||||
|
||||
def kill_local_jobs(self):
|
||||
"""Kills all the jobs spawned with BaseNamespace.spawn() on a namespace
|
||||
object.
|
||||
|
||||
This will be called automatically if the ``watcher`` process detects
|
||||
that the Socket was closed.
|
||||
"""
|
||||
gevent.killall(self.jobs)
|
||||
self.jobs = []
|
||||
204
awx/lib/site-packages/socketio/packet.py
Normal file
204
awx/lib/site-packages/socketio/packet.py
Normal file
@ -0,0 +1,204 @@
|
||||
try:
|
||||
import simplejson as json
|
||||
json_decimal_args = {"use_decimal": True} # pragma: no cover
|
||||
except ImportError:
|
||||
import json
|
||||
import decimal
|
||||
|
||||
class DecimalEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, decimal.Decimal):
|
||||
return float(o)
|
||||
return super(DecimalEncoder, self).default(o)
|
||||
json_decimal_args = {"cls": DecimalEncoder}
|
||||
|
||||
|
||||
MSG_TYPES = {
|
||||
'disconnect': 0,
|
||||
'connect': 1,
|
||||
'heartbeat': 2,
|
||||
'message': 3,
|
||||
'json': 4,
|
||||
'event': 5,
|
||||
'ack': 6,
|
||||
'error': 7,
|
||||
'noop': 8,
|
||||
}
|
||||
|
||||
MSG_VALUES = dict((v, k) for k, v in MSG_TYPES.iteritems())
|
||||
|
||||
ERROR_REASONS = {
|
||||
'transport not supported': 0,
|
||||
'client not handshaken': 1,
|
||||
'unauthorized': 2
|
||||
}
|
||||
|
||||
REASONS_VALUES = dict((v, k) for k, v in ERROR_REASONS.iteritems())
|
||||
|
||||
ERROR_ADVICES = {
|
||||
'reconnect': 0,
|
||||
}
|
||||
|
||||
ADVICES_VALUES = dict((v, k) for k, v in ERROR_ADVICES.iteritems())
|
||||
|
||||
socketio_packet_attributes = ['type', 'name', 'data', 'endpoint', 'args',
|
||||
'ackId', 'reason', 'advice', 'qs', 'id']
|
||||
|
||||
|
||||
def encode(data):
|
||||
"""
|
||||
Encode an attribute dict into a byte string.
|
||||
"""
|
||||
payload = ''
|
||||
msg = str(MSG_TYPES[data['type']])
|
||||
|
||||
if msg in ['0', '1']:
|
||||
# '1::' [path] [query]
|
||||
msg += '::' + data['endpoint']
|
||||
if 'qs' in data and data['qs'] != '':
|
||||
msg += ':' + data['qs']
|
||||
|
||||
elif msg == '2':
|
||||
# heartbeat
|
||||
msg += '::'
|
||||
|
||||
elif msg in ['3', '4', '5']:
|
||||
# '3:' [id ('+')] ':' [endpoint] ':' [data]
|
||||
# '4:' [id ('+')] ':' [endpoint] ':' [json]
|
||||
# '5:' [id ('+')] ':' [endpoint] ':' [json encoded event]
|
||||
# The message id is an incremental integer, required for ACKs.
|
||||
# If the message id is followed by a +, the ACK is not handled by
|
||||
# socket.io, but by the user instead.
|
||||
if msg == '3':
|
||||
payload = data['data']
|
||||
if msg == '4':
|
||||
payload = json.dumps(data['data'], separators=(',', ':'),
|
||||
**json_decimal_args)
|
||||
if msg == '5':
|
||||
d = {}
|
||||
d['name'] = data['name']
|
||||
if 'args' in data and data['args'] != []:
|
||||
d['args'] = data['args']
|
||||
payload = json.dumps(d, separators=(',', ':'), **json_decimal_args)
|
||||
if 'id' in data:
|
||||
msg += ':' + str(data['id'])
|
||||
if data['ack'] == 'data':
|
||||
msg += '+'
|
||||
msg += ':'
|
||||
else:
|
||||
msg += '::'
|
||||
if 'endpoint' not in data:
|
||||
data['endpoint'] = ''
|
||||
if payload != '':
|
||||
msg += data['endpoint'] + ':' + payload
|
||||
else:
|
||||
msg += data['endpoint']
|
||||
|
||||
elif msg == '6':
|
||||
# '6:::' [id] '+' [data]
|
||||
msg += '::' + data.get('endpoint', '') + ':' + str(data['ackId'])
|
||||
if 'args' in data and data['args'] != []:
|
||||
msg += '+' + json.dumps(data['args'], separators=(',', ':'),
|
||||
**json_decimal_args)
|
||||
|
||||
elif msg == '7':
|
||||
# '7::' [endpoint] ':' [reason] '+' [advice]
|
||||
msg += ':::'
|
||||
if 'reason' in data and data['reason'] != '':
|
||||
msg += str(ERROR_REASONS[data['reason']])
|
||||
if 'advice' in data and data['advice'] != '':
|
||||
msg += '+' + str(ERROR_ADVICES[data['advice']])
|
||||
msg += data['endpoint']
|
||||
|
||||
# NoOp, used to close a poll after the polling duration time
|
||||
elif msg == '8':
|
||||
msg += '::'
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def decode(rawstr):
|
||||
"""
|
||||
Decode a rawstr packet arriving from the socket into a dict.
|
||||
"""
|
||||
decoded_msg = {}
|
||||
split_data = rawstr.split(":", 3)
|
||||
msg_type = split_data[0]
|
||||
msg_id = split_data[1]
|
||||
endpoint = split_data[2]
|
||||
|
||||
data = ''
|
||||
|
||||
if msg_id != '':
|
||||
if "+" in msg_id:
|
||||
msg_id = msg_id.split('+')[0]
|
||||
decoded_msg['id'] = int(msg_id)
|
||||
decoded_msg['ack'] = 'data'
|
||||
else:
|
||||
decoded_msg['id'] = int(msg_id)
|
||||
decoded_msg['ack'] = True
|
||||
|
||||
# common to every message
|
||||
msg_type_id = int(msg_type)
|
||||
if msg_type_id in MSG_VALUES:
|
||||
decoded_msg['type'] = MSG_VALUES[int(msg_type)]
|
||||
else:
|
||||
raise Exception("Unknown message type: %s" % msg_type)
|
||||
|
||||
decoded_msg['endpoint'] = endpoint
|
||||
|
||||
if len(split_data) > 3:
|
||||
data = split_data[3]
|
||||
|
||||
if msg_type == "0": # disconnect
|
||||
pass
|
||||
|
||||
elif msg_type == "1": # connect
|
||||
decoded_msg['qs'] = data
|
||||
|
||||
elif msg_type == "2": # heartbeat
|
||||
pass
|
||||
|
||||
elif msg_type == "3": # message
|
||||
decoded_msg['data'] = data
|
||||
|
||||
elif msg_type == "4": # json msg
|
||||
decoded_msg['data'] = json.loads(data)
|
||||
|
||||
elif msg_type == "5": # event
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except ValueError, e:
|
||||
print("Invalid JSON event message", data)
|
||||
decoded_msg['args'] = []
|
||||
else:
|
||||
decoded_msg['name'] = data.pop('name')
|
||||
if 'args' in data:
|
||||
decoded_msg['args'] = data['args']
|
||||
else:
|
||||
decoded_msg['args'] = []
|
||||
|
||||
elif msg_type == "6": # ack
|
||||
if '+' in data:
|
||||
ackId, data = data.split('+')
|
||||
decoded_msg['ackId'] = int(ackId)
|
||||
decoded_msg['args'] = json.loads(data)
|
||||
else:
|
||||
decoded_msg['ackId'] = int(data)
|
||||
decoded_msg['args'] = []
|
||||
|
||||
elif msg_type == "7": # error
|
||||
if '+' in data:
|
||||
reason, advice = data.split('+')
|
||||
decoded_msg['reason'] = REASONS_VALUES[int(reason)]
|
||||
decoded_msg['advice'] = ADVICES_VALUES[int(advice)]
|
||||
else:
|
||||
decoded_msg['advice'] = ''
|
||||
if data != '':
|
||||
decoded_msg['reason'] = REASONS_VALUES[int(data)]
|
||||
else:
|
||||
decoded_msg['reason'] = ''
|
||||
elif msg_type == "8": # noop
|
||||
pass
|
||||
|
||||
return decoded_msg
|
||||
27
awx/lib/site-packages/socketio/policyserver.py
Normal file
27
awx/lib/site-packages/socketio/policyserver.py
Normal file
@ -0,0 +1,27 @@
|
||||
from gevent.server import StreamServer
|
||||
import socket
|
||||
|
||||
__all__ = ['FlashPolicyServer']
|
||||
|
||||
|
||||
class FlashPolicyServer(StreamServer):
|
||||
policyrequest = "<policy-file-request/>"
|
||||
policy = """<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
|
||||
<cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>"""
|
||||
|
||||
def __init__(self, listener=None, backlog=None):
|
||||
if listener is None:
|
||||
listener = ('0.0.0.0', 10843)
|
||||
StreamServer.__init__(self, listener=listener, backlog=backlog)
|
||||
|
||||
def handle(self, sock, address):
|
||||
# send and read functions should not wait longer than three seconds
|
||||
sock.settimeout(3)
|
||||
try:
|
||||
# try to receive at most 128 bytes (`POLICYREQUEST` is shorter)
|
||||
input = sock.recv(128)
|
||||
if input.startswith(FlashPolicyServer.policyrequest):
|
||||
sock.sendall(FlashPolicyServer.policy)
|
||||
except socket.timeout:
|
||||
pass
|
||||
sock.close()
|
||||
83
awx/lib/site-packages/socketio/server.py
Normal file
83
awx/lib/site-packages/socketio/server.py
Normal file
@ -0,0 +1,83 @@
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from socket import error
|
||||
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
from socketio.handler import SocketIOHandler
|
||||
from socketio.policyserver import FlashPolicyServer
|
||||
from socketio.virtsocket import Socket
|
||||
|
||||
__all__ = ['SocketIOServer']
|
||||
|
||||
|
||||
class SocketIOServer(WSGIServer):
|
||||
"""A WSGI Server with a resource that acts like an SocketIO."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
This is just like the standard WSGIServer __init__, except with a
|
||||
few additional ``kwargs``:
|
||||
|
||||
:param resource: The URL which has to be identified as a socket.io request. Defaults to the /socket.io/ URL.
|
||||
:param transports: Optional list of transports to allow. List of
|
||||
strings, each string should be one of
|
||||
handler.SocketIOHandler.handler_types.
|
||||
:param policy_server: Boolean describing whether or not to use the
|
||||
Flash policy server. Default True.
|
||||
:param policy_listener : A tuple containing (host, port) for the
|
||||
policy server. This is optional and used only if policy server
|
||||
is set to true. The default value is 0.0.0.0:843
|
||||
"""
|
||||
self.sockets = {}
|
||||
if 'namespace' in kwargs:
|
||||
print("DEPRECATION WARNING: use resource instead of namespace")
|
||||
self.resource = kwargs.pop('namespace', 'socket.io')
|
||||
else:
|
||||
self.resource = kwargs.pop('resource', 'socket.io')
|
||||
|
||||
self.transports = kwargs.pop('transports', None)
|
||||
|
||||
if kwargs.pop('policy_server', True):
|
||||
policylistener = kwargs.pop('policy_listener', (args[0][0], 10843))
|
||||
self.policy_server = FlashPolicyServer(policylistener)
|
||||
else:
|
||||
self.policy_server = None
|
||||
|
||||
kwargs['handler_class'] = SocketIOHandler
|
||||
super(SocketIOServer, self).__init__(*args, **kwargs)
|
||||
|
||||
def start_accepting(self):
|
||||
if self.policy_server is not None:
|
||||
try:
|
||||
self.policy_server.start()
|
||||
except error, ex:
|
||||
sys.stderr.write(
|
||||
'FAILED to start flash policy server: %s\n' % (ex, ))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.stderr.write('FAILED to start flash policy server.\n\n')
|
||||
super(SocketIOServer, self).start_accepting()
|
||||
|
||||
def kill(self):
|
||||
if self.policy_server is not None:
|
||||
self.policy_server.kill()
|
||||
super(SocketIOServer, self).kill()
|
||||
|
||||
def handle(self, socket, address):
|
||||
handler = self.handler_class(socket, address, self)
|
||||
handler.handle()
|
||||
|
||||
def get_socket(self, sessid=''):
|
||||
"""Return an existing or new client Socket."""
|
||||
|
||||
socket = self.sockets.get(sessid)
|
||||
|
||||
if socket is None:
|
||||
socket = Socket(self)
|
||||
self.sockets[socket.sessid] = socket
|
||||
else:
|
||||
socket.incr_hits()
|
||||
|
||||
return socket
|
||||
57
awx/lib/site-packages/socketio/sgunicorn.py
Normal file
57
awx/lib/site-packages/socketio/sgunicorn.py
Normal file
@ -0,0 +1,57 @@
|
||||
import os
|
||||
|
||||
import gevent
|
||||
from gevent.pool import Pool
|
||||
|
||||
from gunicorn.workers.ggevent import GeventPyWSGIWorker
|
||||
from socketio.server import SocketIOServer
|
||||
from socketio.handler import SocketIOHandler
|
||||
|
||||
|
||||
class GeventSocketIOBaseWorker(GeventPyWSGIWorker):
|
||||
""" The base gunicorn worker class """
|
||||
def run(self):
|
||||
self.socket.setblocking(1)
|
||||
pool = Pool(self.worker_connections)
|
||||
self.server_class.base_env['wsgi.multiprocess'] = \
|
||||
self.cfg.workers > 1
|
||||
server = self.server_class(
|
||||
self.socket, application=self.wsgi,
|
||||
spawn=pool, handler_class=self.wsgi_handler,
|
||||
namespace=self.namespace, policy_server=self.policy_server)
|
||||
server.start()
|
||||
try:
|
||||
while self.alive:
|
||||
self.notify()
|
||||
|
||||
if self.ppid != os.getppid():
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
break
|
||||
|
||||
gevent.sleep(1.0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# try to stop the connections
|
||||
try:
|
||||
self.notify()
|
||||
server.stop(timeout=self.timeout)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class GeventSocketIOWorker(GeventSocketIOBaseWorker):
|
||||
"""
|
||||
Default gunicorn worker utilizing gevent
|
||||
|
||||
Uses the namespace 'socket.io' and defaults to the flash policy server
|
||||
being disabled.
|
||||
"""
|
||||
server_class = SocketIOServer
|
||||
wsgi_handler = SocketIOHandler
|
||||
# We need to define a namespace for the server, it would be nice if this
|
||||
# was a configuration option, will probably end up how this implemented,
|
||||
# for now this is just a proof of concept to make sure this will work
|
||||
namespace = 'socket.io'
|
||||
policy_server = False # Don't run the flash policy server
|
||||
270
awx/lib/site-packages/socketio/transports.py
Normal file
270
awx/lib/site-packages/socketio/transports.py
Normal file
@ -0,0 +1,270 @@
|
||||
import gevent
|
||||
import urllib
|
||||
|
||||
from gevent.queue import Empty
|
||||
|
||||
|
||||
class BaseTransport(object):
|
||||
"""Base class for all transports. Mostly wraps handler class functions."""
|
||||
|
||||
def __init__(self, handler):
|
||||
self.content_type = ("Content-Type", "text/plain; charset=UTF-8")
|
||||
self.headers = [
|
||||
("Access-Control-Allow-Origin", "*"),
|
||||
("Access-Control-Allow-Credentials", "true"),
|
||||
("Access-Control-Allow-Methods", "POST, GET, OPTIONS"),
|
||||
("Access-Control-Max-Age", 3600),
|
||||
]
|
||||
self.headers_list = []
|
||||
self.handler = handler
|
||||
|
||||
def write(self, data=""):
|
||||
if 'Content-Length' not in self.handler.response_headers_list:
|
||||
self.handler.response_headers.append(('Content-Length', len(data)))
|
||||
self.handler.response_headers_list.append('Content-Length')
|
||||
|
||||
self.handler.write(data)
|
||||
|
||||
def start_response(self, status, headers, **kwargs):
|
||||
if "Content-Type" not in [x[0] for x in headers]:
|
||||
headers.append(self.content_type)
|
||||
|
||||
headers.extend(self.headers)
|
||||
#print headers
|
||||
self.handler.start_response(status, headers, **kwargs)
|
||||
|
||||
|
||||
class XHRPollingTransport(BaseTransport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(XHRPollingTransport, self).__init__(*args, **kwargs)
|
||||
|
||||
def options(self):
|
||||
self.start_response("200 OK", ())
|
||||
self.write()
|
||||
return []
|
||||
|
||||
def get(self, socket):
|
||||
socket.heartbeat()
|
||||
|
||||
payload = self.get_messages_payload(socket, timeout=5.0)
|
||||
if not payload:
|
||||
payload = "8::" # NOOP
|
||||
|
||||
self.start_response("200 OK", [])
|
||||
self.write(payload)
|
||||
|
||||
return []
|
||||
|
||||
def _request_body(self):
|
||||
return self.handler.wsgi_input.readline()
|
||||
|
||||
def post(self, socket):
|
||||
for message in self.decode_payload(self._request_body()):
|
||||
socket.put_server_msg(message)
|
||||
|
||||
self.start_response("200 OK", [
|
||||
("Connection", "close"),
|
||||
("Content-Type", "text/plain")
|
||||
])
|
||||
self.write("1")
|
||||
|
||||
return []
|
||||
|
||||
def get_messages_payload(self, socket, timeout=None):
|
||||
"""This will fetch the messages from the Socket's queue, and if
|
||||
there are many messes, pack multiple messages in one payload and return
|
||||
"""
|
||||
try:
|
||||
msgs = socket.get_multiple_client_msgs(timeout=timeout)
|
||||
data = self.encode_payload(msgs)
|
||||
except Empty:
|
||||
data = ""
|
||||
return data
|
||||
|
||||
def encode_payload(self, messages):
|
||||
"""Encode list of messages. Expects messages to be unicode.
|
||||
|
||||
``messages`` - List of raw messages to encode, if necessary
|
||||
|
||||
"""
|
||||
if not messages:
|
||||
return ''
|
||||
|
||||
if len(messages) == 1:
|
||||
return messages[0].encode('utf-8')
|
||||
|
||||
payload = u''.join(u'\ufffd%d\ufffd%s' % (len(p), p)
|
||||
for p in messages)
|
||||
|
||||
return payload.encode('utf-8')
|
||||
|
||||
def decode_payload(self, payload):
|
||||
"""This function can extract multiple messages from one HTTP payload.
|
||||
Some times, the XHR/JSONP/.. transports can pack more than one message
|
||||
on a single packet. They are encoding following the WebSocket
|
||||
semantics, which need to be reproduced here to unwrap the messages.
|
||||
|
||||
The semantics are:
|
||||
|
||||
\ufffd + [length as a string] + \ufffd + [payload as a unicode string]
|
||||
|
||||
This function returns a list of messages, even though there is only
|
||||
one.
|
||||
|
||||
Inspired by socket.io/lib/transports/http.js
|
||||
"""
|
||||
payload = payload.decode('utf-8')
|
||||
if payload[0] == u"\ufffd":
|
||||
#print "MULTIMSG FULL", payload
|
||||
ret = []
|
||||
while len(payload) != 0:
|
||||
len_end = payload.find(u"\ufffd", 1)
|
||||
length = int(payload[1:len_end])
|
||||
msg_start = len_end + 1
|
||||
msg_end = length + msg_start
|
||||
message = payload[msg_start:msg_end]
|
||||
#print "MULTIMSG", length, message
|
||||
ret.append(message)
|
||||
payload = payload[msg_end:]
|
||||
return ret
|
||||
return [payload]
|
||||
|
||||
def connect(self, socket, request_method):
|
||||
if not socket.connection_confirmed:
|
||||
socket.connection_confirmed = True
|
||||
self.start_response("200 OK", [
|
||||
("Connection", "close"),
|
||||
])
|
||||
self.write("1::") # 'connect' packet
|
||||
|
||||
return []
|
||||
elif request_method in ("GET", "POST", "OPTIONS"):
|
||||
return getattr(self, request_method.lower())(socket)
|
||||
else:
|
||||
raise Exception("No support for the method: " + request_method)
|
||||
|
||||
|
||||
class JSONPolling(XHRPollingTransport):
|
||||
def __init__(self, handler):
|
||||
super(JSONPolling, self).__init__(handler)
|
||||
self.content_type = ("Content-Type", "text/javascript; charset=UTF-8")
|
||||
|
||||
def _request_body(self):
|
||||
data = super(JSONPolling, self)._request_body()
|
||||
# resolve %20%3F's, take out wrapping d="...", etc..
|
||||
return urllib.unquote_plus(data)[3:-1] \
|
||||
.replace(r'\"', '"') \
|
||||
.replace(r"\\", "\\")
|
||||
|
||||
def write(self, data):
|
||||
"""Just quote out stuff before sending it out"""
|
||||
# TODO: don't we need to quote this data in here ?
|
||||
super(JSONPolling, self).write("io.j[0]('%s');" % data)
|
||||
|
||||
|
||||
class XHRMultipartTransport(XHRPollingTransport):
|
||||
def __init__(self, handler):
|
||||
super(JSONPolling, self).__init__(handler)
|
||||
self.content_type = (
|
||||
"Content-Type",
|
||||
"multipart/x-mixed-replace;boundary=\"socketio\""
|
||||
)
|
||||
|
||||
def connect(self, socket, request_method):
|
||||
if request_method == "GET":
|
||||
# TODO: double verify this, because we're not sure. look at git revs.
|
||||
heartbeat = socket._spawn_heartbeat()
|
||||
return [heartbeat] + self.get(socket)
|
||||
elif request_method == "POST":
|
||||
return self.post(socket)
|
||||
else:
|
||||
raise Exception("No support for such method: " + request_method)
|
||||
|
||||
def get(self, socket):
|
||||
header = "Content-Type: text/plain; charset=UTF-8\r\n\r\n"
|
||||
|
||||
self.start_response("200 OK", [("Connection", "keep-alive")])
|
||||
self.write_multipart("--socketio\r\n")
|
||||
self.write_multipart(header)
|
||||
self.write_multipart(str(socket.sessid) + "\r\n")
|
||||
self.write_multipart("--socketio\r\n")
|
||||
|
||||
def chunk():
|
||||
while True:
|
||||
payload = self.get_messages_payload(socket)
|
||||
|
||||
if not payload:
|
||||
# That would mean the call to Queue.get() returned Empty,
|
||||
# so it was in fact killed, since we pass no timeout=..
|
||||
socket.kill()
|
||||
break
|
||||
else:
|
||||
try:
|
||||
self.write_multipart(header)
|
||||
self.write_multipart(payload)
|
||||
self.write_multipart("--socketio\r\n")
|
||||
except socket.error:
|
||||
socket.kill()
|
||||
break
|
||||
|
||||
return [gevent.spawn(chunk)]
|
||||
|
||||
|
||||
class WebsocketTransport(BaseTransport):
|
||||
def connect(self, socket, request_method):
|
||||
websocket = self.handler.environ['wsgi.websocket']
|
||||
websocket.send("1::") # 'connect' packet
|
||||
|
||||
def send_into_ws():
|
||||
while True:
|
||||
message = socket.get_client_msg()
|
||||
|
||||
if message is None:
|
||||
socket.kill()
|
||||
break
|
||||
|
||||
websocket.send(message)
|
||||
|
||||
def read_from_ws():
|
||||
while True:
|
||||
message = websocket.receive()
|
||||
|
||||
if not message:
|
||||
socket.kill()
|
||||
break
|
||||
else:
|
||||
if message is not None:
|
||||
socket.put_server_msg(message)
|
||||
|
||||
gr1 = gevent.spawn(send_into_ws)
|
||||
gr2 = gevent.spawn(read_from_ws)
|
||||
heartbeat1, heartbeat2 = socket._spawn_heartbeat()
|
||||
|
||||
return [gr1, gr2, heartbeat1, heartbeat2]
|
||||
|
||||
|
||||
class FlashSocketTransport(WebsocketTransport):
|
||||
pass
|
||||
|
||||
|
||||
class HTMLFileTransport(XHRPollingTransport):
|
||||
"""Not tested at all!"""
|
||||
|
||||
def __init__(self, handler):
|
||||
super(HTMLFileTransport, self).__init__(handler)
|
||||
self.content_type = ("Content-Type", "text/html")
|
||||
|
||||
def write_packed(self, data):
|
||||
self.write("<script>parent.s._('%s', document);</script>" % data)
|
||||
|
||||
def handle_get_response(self, socket):
|
||||
self.start_response("200 OK", [
|
||||
("Connection", "keep-alive"),
|
||||
("Content-Type", "text/html"),
|
||||
("Transfer-Encoding", "chunked"),
|
||||
])
|
||||
self.write("<html><body>" + " " * 244)
|
||||
|
||||
payload = self.get_messages_payload(socket, timeout=5.0)
|
||||
|
||||
self.write_packed(payload)
|
||||
428
awx/lib/site-packages/socketio/virtsocket.py
Normal file
428
awx/lib/site-packages/socketio/virtsocket.py
Normal file
@ -0,0 +1,428 @@
|
||||
"""Virtual Socket implementation, unifies all the Transports into one
|
||||
single interface, and abstracts the work of the long-polling methods.
|
||||
|
||||
This module also has the ``default_error_handler`` implementation.
|
||||
You can define your own so that the error messages are logged or sent
|
||||
in a different way
|
||||
|
||||
:copyright: 2012, Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
|
||||
:moduleauthor: Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
|
||||
|
||||
"""
|
||||
import random
|
||||
import weakref
|
||||
|
||||
import gevent
|
||||
from gevent.queue import Queue
|
||||
from gevent.event import Event
|
||||
|
||||
from socketio import packet
|
||||
|
||||
|
||||
def default_error_handler(socket, error_name, error_message, endpoint,
|
||||
msg_id, quiet):
|
||||
"""This is the default error handler, you can override this when
|
||||
calling :func:`socketio.socketio_manage`.
|
||||
|
||||
It basically sends an event through the socket with the 'error' name.
|
||||
|
||||
See documentation for :meth:`Socket.error`.
|
||||
|
||||
:param quiet: if quiet, this handler will not send a packet to the
|
||||
user, but only log for the server developer.
|
||||
"""
|
||||
pkt = dict(type='event', name='error',
|
||||
args=[error_name, error_message],
|
||||
endpoint=endpoint)
|
||||
if msg_id:
|
||||
pkt['id'] = msg_id
|
||||
|
||||
# Send an error event through the Socket
|
||||
if not quiet:
|
||||
socket.send_packet(pkt)
|
||||
|
||||
# Log that error somewhere for debugging...
|
||||
print "default_error_handler: %s, %s (endpoint=%s, msg_id=%s)" % (
|
||||
error_name, error_message, endpoint, msg_id)
|
||||
|
||||
|
||||
class Socket(object):
|
||||
"""
|
||||
Virtual Socket implementation, checks heartbeats, writes to local queues
|
||||
for message passing, holds the Namespace objects, dispatches de packets
|
||||
to the underlying namespaces.
|
||||
|
||||
This is the abstraction on top of the different transports. It's like
|
||||
if you used a WebSocket only...
|
||||
"""
|
||||
|
||||
STATE_CONNECTING = "CONNECTING"
|
||||
STATE_CONNECTED = "CONNECTED"
|
||||
STATE_DISCONNECTING = "DISCONNECTING"
|
||||
STATE_DISCONNECTED = "DISCONNECTED"
|
||||
|
||||
GLOBAL_NS = ''
|
||||
"""Use this to be explicit when specifying a Global Namespace (an endpoint
|
||||
with no name, not '/chat' or anything."""
|
||||
|
||||
def __init__(self, server, error_handler=None):
|
||||
self.server = weakref.proxy(server)
|
||||
self.sessid = str(random.random())[2:]
|
||||
self.session = {} # the session dict, for general developer usage
|
||||
self.client_queue = Queue() # queue for messages to client
|
||||
self.server_queue = Queue() # queue for messages to server
|
||||
self.hits = 0
|
||||
self.heartbeats = 0
|
||||
self.timeout = Event()
|
||||
self.wsgi_app_greenlet = None
|
||||
self.state = "NEW"
|
||||
self.connection_confirmed = False
|
||||
self.ack_callbacks = {}
|
||||
self.ack_counter = 0
|
||||
self.request = None
|
||||
self.environ = None
|
||||
self.namespaces = {}
|
||||
self.active_ns = {} # Namespace sessions that were instantiated
|
||||
self.jobs = []
|
||||
self.error_handler = default_error_handler
|
||||
if error_handler is not None:
|
||||
self.error_handler = error_handler
|
||||
|
||||
def _set_namespaces(self, namespaces):
|
||||
"""This is a mapping (dict) of the different '/namespaces' to their
|
||||
BaseNamespace object derivative.
|
||||
|
||||
This is called by socketio_manage()."""
|
||||
self.namespaces = namespaces
|
||||
|
||||
def _set_request(self, request):
|
||||
"""Saves the request object for future use by the different Namespaces.
|
||||
|
||||
This is called by socketio_manage().
|
||||
"""
|
||||
self.request = request
|
||||
|
||||
def _set_environ(self, environ):
|
||||
"""Save the WSGI environ, for future use.
|
||||
|
||||
This is called by socketio_manage().
|
||||
"""
|
||||
self.environ = environ
|
||||
|
||||
def _set_error_handler(self, error_handler):
|
||||
"""Changes the default error_handler function to the one specified
|
||||
|
||||
This is called by socketio_manage().
|
||||
"""
|
||||
self.error_handler = error_handler
|
||||
|
||||
def _get_next_msgid(self):
|
||||
"""This retrieves the next value for the 'id' field when sending
|
||||
an 'event' or 'message' or 'json' that asks the remote client
|
||||
to 'ack' back, so that we trigger the local callback.
|
||||
"""
|
||||
self.ack_counter += 1
|
||||
return self.ack_counter
|
||||
|
||||
def _save_ack_callback(self, msgid, callback):
|
||||
"""Keep a reference of the callback on this socket."""
|
||||
if msgid in self.ack_callbacks:
|
||||
return False
|
||||
self.ack_callbacks[msgid] = callback
|
||||
|
||||
def _pop_ack_callback(self, msgid):
|
||||
"""Fetch the callback for a given msgid, if it exists, otherwise,
|
||||
return None"""
|
||||
if msgid not in self.ack_callbacks:
|
||||
return None
|
||||
return self.ack_callbacks.pop(msgid)
|
||||
|
||||
def __str__(self):
|
||||
result = ['sessid=%r' % self.sessid]
|
||||
if self.state == self.STATE_CONNECTED:
|
||||
result.append('connected')
|
||||
if self.client_queue.qsize():
|
||||
result.append('client_queue[%s]' % self.client_queue.qsize())
|
||||
if self.server_queue.qsize():
|
||||
result.append('server_queue[%s]' % self.server_queue.qsize())
|
||||
if self.hits:
|
||||
result.append('hits=%s' % self.hits)
|
||||
if self.heartbeats:
|
||||
result.append('heartbeats=%s' % self.heartbeats)
|
||||
|
||||
return ' '.join(result)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""This will get the nested Namespace using its '/chat' reference.
|
||||
|
||||
Using this, you can go from one Namespace to the other (to emit, add
|
||||
ACLs, etc..) with:
|
||||
|
||||
adminnamespace.socket['/chat'].add_acl_method('kick-ban')
|
||||
|
||||
"""
|
||||
return self.active_ns[key]
|
||||
|
||||
def __hasitem__(self, key):
|
||||
"""Verifies if the namespace is active (was initialized)"""
|
||||
return key in self.active_ns
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Returns whether the state is CONNECTED or not."""
|
||||
return self.state == self.STATE_CONNECTED
|
||||
|
||||
def incr_hits(self):
|
||||
self.hits += 1
|
||||
|
||||
if self.hits == 1:
|
||||
self.state = self.STATE_CONNECTED
|
||||
|
||||
def heartbeat(self):
|
||||
"""This makes the heart beat for another X seconds. Call this when
|
||||
you get a heartbeat packet in.
|
||||
|
||||
This clear the heartbeat disconnect timeout (resets for X seconds).
|
||||
"""
|
||||
self.timeout.set()
|
||||
|
||||
def kill(self):
|
||||
"""This function must/will be called when a socket is to be completely
|
||||
shut down, closed by connection timeout, connection error or explicit
|
||||
disconnection from the client.
|
||||
|
||||
It will call all of the Namespace's
|
||||
:meth:`~socketio.namespace.BaseNamespace.disconnect` methods
|
||||
so that you can shut-down things properly.
|
||||
|
||||
"""
|
||||
# Clear out the callbacks
|
||||
self.ack_callbacks = {}
|
||||
if self.connected:
|
||||
self.state = self.STATE_DISCONNECTING
|
||||
self.server_queue.put_nowait(None)
|
||||
self.client_queue.put_nowait(None)
|
||||
self.disconnect()
|
||||
|
||||
if self.sessid in self.server.sockets:
|
||||
self.server.sockets.pop(self.sessid)
|
||||
|
||||
# gevent.kill(self.wsgi_app_greenlet)
|
||||
else:
|
||||
pass # Fail silently
|
||||
|
||||
def put_server_msg(self, msg):
|
||||
"""Writes to the server's pipe, to end up in in the Namespaces"""
|
||||
self.heartbeat()
|
||||
self.server_queue.put_nowait(msg)
|
||||
|
||||
def put_client_msg(self, msg):
|
||||
"""Writes to the client's pipe, to end up in the browser"""
|
||||
self.heartbeat()
|
||||
self.client_queue.put_nowait(msg)
|
||||
|
||||
def get_client_msg(self, **kwargs):
|
||||
"""Grab a message to send it to the browser"""
|
||||
return self.client_queue.get(**kwargs)
|
||||
|
||||
def get_server_msg(self, **kwargs):
|
||||
"""Grab a message, to process it by the server and dispatch calls
|
||||
"""
|
||||
return self.server_queue.get(**kwargs)
|
||||
|
||||
def get_multiple_client_msgs(self, **kwargs):
|
||||
"""Get multiple messages, in case we're going through the various
|
||||
XHR-polling methods, on which we can pack more than one message if the
|
||||
rate is high, and encode the payload for the HTTP channel."""
|
||||
client_queue = self.client_queue
|
||||
msgs = [client_queue.get(**kwargs)]
|
||||
while client_queue.qsize():
|
||||
msgs.append(client_queue.get())
|
||||
return msgs
|
||||
|
||||
def error(self, error_name, error_message, endpoint=None, msg_id=None,
|
||||
quiet=False):
|
||||
"""Send an error to the user, using the custom or default
|
||||
ErrorHandler configured on the [TODO: Revise this] Socket/Handler
|
||||
object.
|
||||
|
||||
:param error_name: is a simple string, for easy association on
|
||||
the client side
|
||||
|
||||
:param error_message: is a human readable message, the user
|
||||
will eventually see
|
||||
|
||||
:param endpoint: set this if you have a message specific to an
|
||||
end point
|
||||
|
||||
:param msg_id: set this if your error is relative to a
|
||||
specific message
|
||||
|
||||
:param quiet: way to make the error handler quiet. Specific to
|
||||
the handler. The default handler will only log,
|
||||
with quiet.
|
||||
"""
|
||||
handler = self.error_handler
|
||||
return handler(
|
||||
self, error_name, error_message, endpoint, msg_id, quiet)
|
||||
|
||||
# User facing low-level function
|
||||
def disconnect(self, silent=False):
|
||||
"""Calling this method will call the
|
||||
:meth:`~socketio.namespace.BaseNamespace.disconnect` method on
|
||||
all the active Namespaces that were open, killing all their
|
||||
jobs and sending 'disconnect' packets for each of them.
|
||||
|
||||
Normally, the Global namespace (endpoint = '') has special meaning,
|
||||
as it represents the whole connection,
|
||||
|
||||
:param silent: when True, pass on the ``silent`` flag to the Namespace
|
||||
:meth:`~socketio.namespace.BaseNamespace.disconnect`
|
||||
calls.
|
||||
"""
|
||||
for ns_name, ns in list(self.active_ns.iteritems()):
|
||||
ns.recv_disconnect()
|
||||
|
||||
def remove_namespace(self, namespace):
|
||||
"""This removes a Namespace object from the socket.
|
||||
|
||||
This is usually called by
|
||||
:meth:`~socketio.namespace.BaseNamespace.disconnect`.
|
||||
|
||||
"""
|
||||
if namespace in self.active_ns:
|
||||
del self.active_ns[namespace]
|
||||
|
||||
def send_packet(self, pkt):
|
||||
"""Low-level interface to queue a packet on the wire (encoded as wire
|
||||
protocol"""
|
||||
self.put_client_msg(packet.encode(pkt))
|
||||
|
||||
def spawn(self, fn, *args, **kwargs):
|
||||
"""Spawn a new Greenlet, attached to this Socket instance.
|
||||
|
||||
It will be monitored by the "watcher" method
|
||||
"""
|
||||
|
||||
self.debug("Spawning sub-Socket Greenlet: %s" % fn.__name__)
|
||||
job = gevent.spawn(fn, *args, **kwargs)
|
||||
self.jobs.append(job)
|
||||
return job
|
||||
|
||||
def _receiver_loop(self):
|
||||
"""This is the loop that takes messages from the queue for the server
|
||||
to consume, decodes them and dispatches them.
|
||||
"""
|
||||
|
||||
while True:
|
||||
rawdata = self.get_server_msg()
|
||||
|
||||
if not rawdata:
|
||||
continue # or close the connection ?
|
||||
try:
|
||||
pkt = packet.decode(rawdata)
|
||||
except (ValueError, KeyError, Exception), e:
|
||||
self.error('invalid_packet',
|
||||
"There was a decoding error when dealing with packet "
|
||||
"with event: %s... (%s)" % (rawdata[:20], e))
|
||||
continue
|
||||
|
||||
if pkt['type'] == 'heartbeat':
|
||||
# This is already dealth with in put_server_msg() when
|
||||
# any incoming raw data arrives.
|
||||
continue
|
||||
|
||||
if pkt['type'] == 'disconnect' and pkt['endpoint'] == '':
|
||||
# On global namespace, we kill everything.
|
||||
self.kill()
|
||||
continue
|
||||
|
||||
endpoint = pkt['endpoint']
|
||||
|
||||
if endpoint not in self.namespaces:
|
||||
self.error("no_such_namespace",
|
||||
"The endpoint you tried to connect to "
|
||||
"doesn't exist: %s" % endpoint, endpoint=endpoint)
|
||||
continue
|
||||
elif endpoint in self.active_ns:
|
||||
pkt_ns = self.active_ns[endpoint]
|
||||
else:
|
||||
new_ns_class = self.namespaces[endpoint]
|
||||
pkt_ns = new_ns_class(self.environ, endpoint,
|
||||
request=self.request)
|
||||
pkt_ns.initialize() # use this instead of __init__,
|
||||
# for less confusion
|
||||
|
||||
self.active_ns[endpoint] = pkt_ns
|
||||
|
||||
retval = pkt_ns.process_packet(pkt)
|
||||
|
||||
# Has the client requested an 'ack' with the reply parameters ?
|
||||
if pkt.get('ack') == "data" and pkt.get('id'):
|
||||
returning_ack = dict(type='ack', ackId=pkt['id'],
|
||||
args=retval,
|
||||
endpoint=pkt.get('endpoint', ''))
|
||||
self.send_packet(returning_ack)
|
||||
|
||||
# Now, are we still connected ?
|
||||
if not self.connected:
|
||||
self.kill() # ?? what,s the best clean-up when its not a
|
||||
# user-initiated disconnect
|
||||
return
|
||||
|
||||
def _spawn_receiver_loop(self):
|
||||
"""Spawns the reader loop. This is called internall by
|
||||
socketio_manage().
|
||||
"""
|
||||
job = gevent.spawn(self._receiver_loop)
|
||||
self.jobs.append(job)
|
||||
return job
|
||||
|
||||
def _watcher(self):
|
||||
"""Watch if any of the greenlets for a request have died. If so, kill
|
||||
the request and the socket.
|
||||
"""
|
||||
# TODO: add that if any of the request.jobs die, kill them all and exit
|
||||
gevent.sleep(5.0)
|
||||
|
||||
while True:
|
||||
gevent.sleep(1.0)
|
||||
|
||||
if not self.connected:
|
||||
# Killing Socket-level jobs
|
||||
gevent.killall(self.jobs)
|
||||
for ns_name, ns in list(self.active_ns.iteritems()):
|
||||
ns.recv_disconnect()
|
||||
break
|
||||
|
||||
def _spawn_watcher(self):
|
||||
job = gevent.spawn(self._watcher)
|
||||
return job
|
||||
|
||||
def _heartbeat(self):
|
||||
"""Start the heartbeat Greenlet to check connection health."""
|
||||
self.state = self.STATE_CONNECTED
|
||||
|
||||
while self.connected:
|
||||
gevent.sleep(5.0) # FIXME: make this a setting
|
||||
# TODO: this process could use a timeout object like the disconnect
|
||||
# timeout thing, and ONLY send packets when none are sent!
|
||||
# We would do that by calling timeout.set() for a "sending"
|
||||
# timeout. If we're sending 100 messages a second, there is
|
||||
# no need to push some heartbeats in there also.
|
||||
self.put_client_msg("2::") # TODO: make it a heartbeat packet
|
||||
|
||||
def _disconnect_timeout(self):
|
||||
self.timeout.clear()
|
||||
|
||||
if self.timeout.wait(10.0):
|
||||
gevent.spawn(self._disconnect_timeout)
|
||||
else:
|
||||
self.kill()
|
||||
|
||||
def _spawn_heartbeat(self):
|
||||
"""This functions returns a list of jobs"""
|
||||
job_sender = gevent.spawn(self._heartbeat)
|
||||
job_waiter = gevent.spawn(self._disconnect_timeout)
|
||||
self.jobs.extend((job_sender, job_waiter))
|
||||
return job_sender, job_waiter
|
||||
Loading…
x
Reference in New Issue
Block a user