--- /dev/null
+###############################################################################\r
+##\r
+## Copyright 2011,2012 Tavendo GmbH\r
+##\r
+## Licensed under the Apache License, Version 2.0 (the "License");\r
+## you may not use this file except in compliance with the License.\r
+## You may obtain a copy of the License at\r
+##\r
+## http://www.apache.org/licenses/LICENSE-2.0\r
+##\r
+## Unless required by applicable law or agreed to in writing, software\r
+## distributed under the License is distributed on an "AS IS" BASIS,\r
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+## See the License for the specific language governing permissions and\r
+## limitations under the License.\r
+##\r
+###############################################################################\r
+\r
+import json\r
+import random\r
+import inspect, types\r
+import traceback\r
+\r
+import hashlib, hmac, binascii\r
+\r
+from twisted.python import log\r
+from twisted.internet import reactor\r
+from twisted.internet.defer import Deferred, maybeDeferred\r
+\r
+import autobahn\r
+\r
+from websocket import WebSocketProtocol, HttpException\r
+from websocket import WebSocketClientProtocol, WebSocketClientFactory\r
+from websocket import WebSocketServerFactory, WebSocketServerProtocol\r
+\r
+from httpstatus import HTTP_STATUS_CODE_BAD_REQUEST\r
+from prefixmap import PrefixMap\r
+from util import utcstr, utcnow, parseutc, newid\r
+\r
+\r
+def exportRpc(arg = None):\r
+ """\r
+ Decorator for RPC'ed callables.\r
+ """\r
+ ## decorator without argument\r
+ if type(arg) is types.FunctionType:\r
+ arg._autobahn_rpc_id = arg.__name__\r
+ return arg\r
+ ## decorator with argument\r
+ else:\r
+ def inner(f):\r
+ f._autobahn_rpc_id = arg\r
+ return f\r
+ return inner\r
+\r
+def exportSub(arg, prefixMatch = False):\r
+ """\r
+ Decorator for subscription handlers.\r
+ """\r
+ def inner(f):\r
+ f._autobahn_sub_id = arg\r
+ f._autobahn_sub_prefix_match = prefixMatch\r
+ return f\r
+ return inner\r
+\r
+def exportPub(arg, prefixMatch = False):\r
+ """\r
+ Decorator for publication handlers.\r
+ """\r
+ def inner(f):\r
+ f._autobahn_pub_id = arg\r
+ f._autobahn_pub_prefix_match = prefixMatch\r
+ return f\r
+ return inner\r
+\r
+\r
+class WampProtocol:\r
+ """\r
+ WAMP protocol base class. Mixin for WampServerProtocol and WampClientProtocol.\r
+ """\r
+\r
+ WAMP_PROTOCOL_VERSION = 1\r
+ """\r
+ WAMP version this server speaks. Versions are numbered consecutively\r
+ (integers, no gaps).\r
+ """\r
+\r
+ MESSAGE_TYPEID_WELCOME = 0\r
+ """\r
+ Server-to-client welcome message containing session ID.\r
+ """\r
+\r
+ MESSAGE_TYPEID_PREFIX = 1\r
+ """\r
+ Client-to-server message establishing a URI prefix to be used in CURIEs.\r
+ """\r
+\r
+ MESSAGE_TYPEID_CALL = 2\r
+ """\r
+ Client-to-server message initiating an RPC.\r
+ """\r
+\r
+ MESSAGE_TYPEID_CALL_RESULT = 3\r
+ """\r
+ Server-to-client message returning the result of a successful RPC.\r
+ """\r
+\r
+ MESSAGE_TYPEID_CALL_ERROR = 4\r
+ """\r
+ Server-to-client message returning the error of a failed RPC.\r
+ """\r
+\r
+ MESSAGE_TYPEID_SUBSCRIBE = 5\r
+ """\r
+ Client-to-server message subscribing to a topic.\r
+ """\r
+\r
+ MESSAGE_TYPEID_UNSUBSCRIBE = 6\r
+ """\r
+ Client-to-server message unsubscribing from a topic.\r
+ """\r
+\r
+ MESSAGE_TYPEID_PUBLISH = 7\r
+ """\r
+ Client-to-server message publishing an event to a topic.\r
+ """\r
+\r
+ MESSAGE_TYPEID_EVENT = 8\r
+ """\r
+ Server-to-client message providing the event of a (subscribed) topic.\r
+ """\r
+\r
+\r
+ ERROR_URI_BASE = "http://autobahn.tavendo.de/error#"\r
+\r
+ ERROR_URI_GENERIC = ERROR_URI_BASE + "generic"\r
+ ERROR_DESC_GENERIC = "generic error"\r
+\r
+ ERROR_URI_INTERNAL = ERROR_URI_BASE + "internal"\r
+ ERROR_DESC_INTERNAL = "internal error"\r
+\r
+\r
+ def connectionMade(self):\r
+ self.debugWamp = self.factory.debugWamp\r
+ self.debugApp = self.factory.debugApp\r
+ self.prefixes = PrefixMap()\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ pass\r
+\r
+\r
+ def _protocolError(self, reason):\r
+ if self.debugWamp:\r
+ log.msg("Closing Wamp session on protocol violation : %s" % reason)\r
+\r
+ ## FIXME: subprotocols are probably not supposed to close with CLOSE_STATUS_CODE_PROTOCOL_ERROR\r
+ ##\r
+ self.protocolViolation("Wamp RPC/PubSub protocol violation ('%s')" % reason)\r
+\r
+\r
+ def shrink(self, uri, passthrough = False):\r
+ """\r
+ Shrink given URI to CURIE according to current prefix mapping.\r
+ If no appropriate prefix mapping is available, return original URI.\r
+\r
+ :param uri: URI to shrink.\r
+ :type uri: str\r
+\r
+ :returns str -- CURIE or original URI.\r
+ """\r
+ return self.prefixes.shrink(uri)\r
+\r
+\r
+ def resolve(self, curieOrUri, passthrough = False):\r
+ """\r
+ Resolve given CURIE/URI according to current prefix mapping or return\r
+ None if cannot be resolved.\r
+\r
+ :param curieOrUri: CURIE or URI.\r
+ :type curieOrUri: str\r
+\r
+ :returns: str -- Full URI for CURIE or None.\r
+ """\r
+ return self.prefixes.resolve(curieOrUri)\r
+\r
+\r
+ def resolveOrPass(self, curieOrUri):\r
+ """\r
+ Resolve given CURIE/URI according to current prefix mapping or return\r
+ string verbatim if cannot be resolved.\r
+\r
+ :param curieOrUri: CURIE or URI.\r
+ :type curieOrUri: str\r
+\r
+ :returns: str -- Full URI for CURIE or original string.\r
+ """\r
+ return self.prefixes.resolveOrPass(curieOrUri)\r
+\r
+\r
+\r
+class WampFactory:\r
+ """\r
+ WAMP factory base class. Mixin for WampServerFactory and WampClientFactory.\r
+ """\r
+\r
+ pass\r
+\r
+\r
+\r
+class WampServerProtocol(WebSocketServerProtocol, WampProtocol):\r
+ """\r
+ Server factory for Wamp RPC/PubSub.\r
+ """\r
+\r
+ SUBSCRIBE = 1\r
+ PUBLISH = 2\r
+\r
+ def onSessionOpen(self):\r
+ """\r
+ Callback fired when WAMP session was fully established.\r
+ """\r
+ pass\r
+\r
+\r
+ def onOpen(self):\r
+ """\r
+ Default implementation for WAMP connection opened sends\r
+ Welcome message containing session ID.\r
+ """\r
+ self.session_id = newid()\r
+ msg = [WampProtocol.MESSAGE_TYPEID_WELCOME,\r
+ self.session_id,\r
+ WampProtocol.WAMP_PROTOCOL_VERSION,\r
+ "Autobahn/%s" % autobahn.version]\r
+ o = json.dumps(msg)\r
+ self.sendMessage(o)\r
+ self.factory._addSession(self, self.session_id)\r
+ self.onSessionOpen()\r
+\r
+\r
+ def onConnect(self, connectionRequest):\r
+ """\r
+ Default implementation for WAMP connection acceptance:\r
+ check if client announced WAMP subprotocol, and only accept connection\r
+ if client did so.\r
+ """\r
+ for p in connectionRequest.protocols:\r
+ if p in self.factory.protocols:\r
+ return p\r
+ raise HttpException(HTTP_STATUS_CODE_BAD_REQUEST[0], "this server only speaks WAMP")\r
+\r
+\r
+ def connectionMade(self):\r
+ WebSocketServerProtocol.connectionMade(self)\r
+ WampProtocol.connectionMade(self)\r
+\r
+ ## RPCs registered in this session (a URI map of (object, procedure)\r
+ ## pairs for object methods or (None, procedure) for free standing procedures)\r
+ self.procs = {}\r
+\r
+ ## Publication handlers registered in this session (a URI map of (object, pubHandler) pairs\r
+ ## pairs for object methods (handlers) or (None, None) for topic without handler)\r
+ self.pubHandlers = {}\r
+\r
+ ## Subscription handlers registered in this session (a URI map of (object, subHandler) pairs\r
+ ## pairs for object methods (handlers) or (None, None) for topic without handler)\r
+ self.subHandlers = {}\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ self.factory._unsubscribeClient(self)\r
+ self.factory._removeSession(self)\r
+\r
+ WampProtocol.connectionLost(self, reason)\r
+ WebSocketServerProtocol.connectionLost(self, reason)\r
+\r
+\r
+ def sendMessage(self, payload):\r
+ if self.debugWamp:\r
+ log.msg("TX WAMP: %s" % str(payload))\r
+ WebSocketServerProtocol.sendMessage(self, payload)\r
+\r
+\r
+ def _getPubHandler(self, topicUri):\r
+ ## Longest matching prefix based resolution of (full) topic URI to\r
+ ## publication handler.\r
+ ## Returns a 5-tuple (consumedUriPart, unconsumedUriPart, handlerObj, handlerProc, prefixMatch)\r
+ ##\r
+ for i in xrange(len(topicUri), -1, -1):\r
+ tt = topicUri[:i]\r
+ if self.pubHandlers.has_key(tt):\r
+ h = self.pubHandlers[tt]\r
+ return (tt, topicUri[i:], h[0], h[1], h[2])\r
+ return None\r
+\r
+\r
+ def _getSubHandler(self, topicUri):\r
+ ## Longest matching prefix based resolution of (full) topic URI to\r
+ ## subscription handler.\r
+ ## Returns a 5-tuple (consumedUriPart, unconsumedUriPart, handlerObj, handlerProc, prefixMatch)\r
+ ##\r
+ for i in xrange(len(topicUri), -1, -1):\r
+ tt = topicUri[:i]\r
+ if self.subHandlers.has_key(tt):\r
+ h = self.subHandlers[tt]\r
+ return (tt, topicUri[i:], h[0], h[1], h[2])\r
+ return None\r
+\r
+\r
+ def registerForPubSub(self, topicUri, prefixMatch = False, pubsub = PUBLISH | SUBSCRIBE):\r
+ """\r
+ Register a topic URI as publish/subscribe channel in this session.\r
+\r
+ :param topicUri: Topic URI to be established as publish/subscribe channel.\r
+ :type topicUri: str\r
+ :param prefixMatch: Allow to match this topic URI by prefix.\r
+ :type prefixMatch: bool\r
+ :param pubsub: Allow publication and/or subscription.\r
+ :type pubsub: WampServerProtocol.PUB, WampServerProtocol.SUB, WampServerProtocol.PUB | WampServerProtocol.SUB\r
+ """\r
+ if pubsub & WampServerProtocol.PUBLISH:\r
+ self.pubHandlers[topicUri] = (None, None, prefixMatch)\r
+ if self.debugWamp:\r
+ log.msg("registered topic %s for publication (match by prefix = %s)" % (topicUri, prefixMatch))\r
+ if pubsub & WampServerProtocol.SUBSCRIBE:\r
+ self.subHandlers[topicUri] = (None, None, prefixMatch)\r
+ if self.debugWamp:\r
+ log.msg("registered topic %s for subscription (match by prefix = %s)" % (topicUri, prefixMatch))\r
+\r
+\r
+ def registerHandlerForPubSub(self, obj, baseUri = ""):\r
+ """\r
+ Register a handler object for PubSub. A handler object has methods\r
+ which are decorated using @exportPub and @exportSub.\r
+\r
+ :param obj: The object to be registered (in this WebSockets session) for PubSub.\r
+ :type obj: Object with methods decorated using @exportPub and @exportSub.\r
+ :param baseUri: Optional base URI which is prepended to topic names for export.\r
+ :type baseUri: String.\r
+ """\r
+ for k in inspect.getmembers(obj.__class__, inspect.ismethod):\r
+ if k[1].__dict__.has_key("_autobahn_pub_id"):\r
+ uri = baseUri + k[1].__dict__["_autobahn_pub_id"]\r
+ prefixMatch = k[1].__dict__["_autobahn_pub_prefix_match"]\r
+ proc = k[1]\r
+ self.registerHandlerForPub(uri, obj, proc, prefixMatch)\r
+ elif k[1].__dict__.has_key("_autobahn_sub_id"):\r
+ uri = baseUri + k[1].__dict__["_autobahn_sub_id"]\r
+ prefixMatch = k[1].__dict__["_autobahn_sub_prefix_match"]\r
+ proc = k[1]\r
+ self.registerHandlerForSub(uri, obj, proc, prefixMatch)\r
+\r
+\r
+ def registerHandlerForSub(self, uri, obj, proc, prefixMatch = False):\r
+ """\r
+ Register a method of an object as subscription handler.\r
+\r
+ :param uri: Topic URI to register subscription handler for.\r
+ :type uri: str\r
+ :param obj: The object on which to register a method as subscription handler.\r
+ :type obj: object\r
+ :param proc: Unbound object method to register as subscription handler.\r
+ :type proc: unbound method\r
+ :param prefixMatch: Allow to match this topic URI by prefix.\r
+ :type prefixMatch: bool\r
+ """\r
+ self.subHandlers[uri] = (obj, proc, prefixMatch)\r
+ if not self.pubHandlers.has_key(uri):\r
+ self.pubHandlers[uri] = (None, None, False)\r
+ if self.debugWamp:\r
+ log.msg("registered subscription handler for topic %s" % uri)\r
+\r
+\r
+ def registerHandlerForPub(self, uri, obj, proc, prefixMatch = False):\r
+ """\r
+ Register a method of an object as publication handler.\r
+\r
+ :param uri: Topic URI to register publication handler for.\r
+ :type uri: str\r
+ :param obj: The object on which to register a method as publication handler.\r
+ :type obj: object\r
+ :param proc: Unbound object method to register as publication handler.\r
+ :type proc: unbound method\r
+ :param prefixMatch: Allow to match this topic URI by prefix.\r
+ :type prefixMatch: bool\r
+ """\r
+ self.pubHandlers[uri] = (obj, proc, prefixMatch)\r
+ if not self.subHandlers.has_key(uri):\r
+ self.subHandlers[uri] = (None, None, False)\r
+ if self.debugWamp:\r
+ log.msg("registered publication handler for topic %s" % uri)\r
+\r
+\r
+ def registerForRpc(self, obj, baseUri = "", methods = None):\r
+ """\r
+ Register an service object for RPC. A service object has methods\r
+ which are decorated using @exportRpc.\r
+\r
+ :param obj: The object to be registered (in this WebSockets session) for RPC.\r
+ :type obj: Object with methods decorated using @exportRpc.\r
+ :param baseUri: Optional base URI which is prepended to method names for export.\r
+ :type baseUri: String.\r
+ :param methods: If not None, a list of unbound class methods corresponding to obj\r
+ which should be registered. This can be used to register only a subset\r
+ of the methods decorated with @exportRpc.\r
+ :type methods: List of unbound class methods.\r
+ """\r
+ for k in inspect.getmembers(obj.__class__, inspect.ismethod):\r
+ if k[1].__dict__.has_key("_autobahn_rpc_id"):\r
+ if methods is None or k[1] in methods:\r
+ uri = baseUri + k[1].__dict__["_autobahn_rpc_id"]\r
+ proc = k[1]\r
+ self.registerMethodForRpc(uri, obj, proc)\r
+\r
+\r
+ def registerMethodForRpc(self, uri, obj, proc):\r
+ """\r
+ Register a method of an object for RPC.\r
+\r
+ :param uri: URI to register RPC method under.\r
+ :type uri: str\r
+ :param obj: The object on which to register a method for RPC.\r
+ :type obj: object\r
+ :param proc: Unbound object method to register RPC for.\r
+ :type proc: unbound method\r
+ """\r
+ self.procs[uri] = (obj, proc)\r
+ if self.debugWamp:\r
+ log.msg("registered remote procedure on %s" % uri)\r
+\r
+\r
+ def registerProcedureForRpc(self, uri, proc):\r
+ """\r
+ Register a (free standing) function/procedure for RPC.\r
+\r
+ :param uri: URI to register RPC function/procedure under.\r
+ :type uri: str\r
+ :param proc: Free-standing function/procedure.\r
+ :type proc: function/procedure\r
+ """\r
+ self.procs[uri] = (None, proc)\r
+ if self.debugWamp:\r
+ log.msg("registered remote procedure on %s" % uri)\r
+\r
+\r
+ def dispatch(self, topicUri, event, exclude = [], eligible = None):\r
+ """\r
+ Dispatch an event for a topic to all clients subscribed to\r
+ and authorized for that topic.\r
+\r
+ Optionally, exclude list of clients and/or only consider clients\r
+ from explicit eligibles. In other words, the event is delivered\r
+ to the set\r
+\r
+ (subscribers - excluded) & eligible\r
+\r
+ :param topicUri: URI of topic to publish event to.\r
+ :type topicUri: str\r
+ :param event: Event to dispatch.\r
+ :type event: obj\r
+ :param exclude: Optional list of clients (WampServerProtocol instances) to exclude.\r
+ :type exclude: list of obj\r
+ :param eligible: Optional list of clients (WampServerProtocol instances) eligible at all (or None for all).\r
+ :type eligible: list of obj\r
+ """\r
+ self.factory.dispatch(topicUri, event, exclude, eligible)\r
+\r
+\r
+ def _callProcedure(self, uri, arg = None):\r
+ """\r
+ INTERNAL METHOD! Actually performs the call of a procedure invoked via RPC.\r
+ """\r
+ if self.procs.has_key(uri):\r
+ m = self.procs[uri]\r
+ if arg:\r
+ ## method/function called with args\r
+ args = tuple(arg)\r
+ if m[0]:\r
+ ## call object method\r
+ return m[1](m[0], *args)\r
+ else:\r
+ ## call free-standing function/procedure\r
+ return m[1](*args)\r
+ else:\r
+ ## method/function called without args\r
+ if m[0]:\r
+ ## call object method\r
+ return m[1](m[0])\r
+ else:\r
+ ## call free-standing function/procedure\r
+ return m[1]()\r
+ else:\r
+ raise Exception("no procedure %s" % uri)\r
+\r
+\r
+ def _sendCallResult(self, result, callid):\r
+ """\r
+ INTERNAL METHOD! Marshal and send a RPC success result.\r
+ """\r
+ msg = [WampProtocol.MESSAGE_TYPEID_CALL_RESULT, callid, result]\r
+ try:\r
+ rmsg = json.dumps(msg)\r
+ except:\r
+ raise Exception("call result not JSON serializable")\r
+ else:\r
+ self.sendMessage(rmsg)\r
+\r
+\r
+ def _sendCallError(self, error, callid):\r
+ """\r
+ INTERNAL METHOD! Marshal and send a RPC error result.\r
+ """\r
+ try:\r
+\r
+ eargs = error.value.args\r
+ leargs = len(eargs)\r
+ traceb = error.getTraceback()\r
+\r
+ if leargs == 0:\r
+ erroruri = WampProtocol.ERROR_URI_GENERIC\r
+ errordesc = WampProtocol.ERROR_DESC_GENERIC\r
+ errordetails = None\r
+\r
+ elif leargs == 1:\r
+ if type(eargs[0]) not in [str, unicode]:\r
+ raise Exception("invalid type %s for errorDesc" % type(eargs[0]))\r
+ erroruri = WampProtocol.ERROR_URI_GENERIC\r
+ errordesc = eargs[0]\r
+ errordetails = None\r
+\r
+ elif leargs in [2, 3]:\r
+ if type(eargs[0]) not in [str, unicode]:\r
+ raise Exception("invalid type %s for errorUri" % type(eargs[0]))\r
+ erroruri = eargs[0]\r
+ if type(eargs[1]) not in [str, unicode]:\r
+ raise Exception("invalid type %s for errorDesc" % type(eargs[1]))\r
+ errordesc = eargs[1]\r
+ if leargs > 2:\r
+ errordetails = eargs[2] # this must be JSON serializable .. if not, we get exception later in sendMessage\r
+ else:\r
+ errordetails = None\r
+\r
+ else:\r
+ raise Exception("invalid args length %d for exception" % leargs)\r
+\r
+ if errordetails is not None:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_CALL_ERROR, callid, self.prefixes.shrink(erroruri), errordesc, errordetails]\r
+ else:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_CALL_ERROR, callid, self.prefixes.shrink(erroruri), errordesc]\r
+\r
+ try:\r
+ rmsg = json.dumps(msg)\r
+ except Exception, e:\r
+ raise Exception("invalid object for errorDetails - not JSON serializable (%s)" % str(e))\r
+\r
+ if self.debugApp:\r
+ log.msg("application error")\r
+ log.msg(traceb)\r
+ log.msg(msg)\r
+\r
+ except Exception, e:\r
+\r
+ if self.debugWamp:\r
+ log.err(str(e))\r
+ log.err(error.getTraceback())\r
+\r
+ msg = [WampProtocol.MESSAGE_TYPEID_CALL_ERROR, callid, self.prefixes.shrink(WampProtocol.ERROR_URI_INTERNAL), WampProtocol.ERROR_DESC_INTERNAL]\r
+ rmsg = json.dumps(msg)\r
+\r
+ finally:\r
+\r
+ self.sendMessage(rmsg)\r
+\r
+\r
+ def onMessage(self, msg, binary):\r
+ """\r
+ INTERNAL METHOD! Handle WAMP messages received from WAMP client.\r
+ """\r
+\r
+ if self.debugWamp:\r
+ log.msg("RX WAMP: %s" % str(msg))\r
+\r
+ if not binary:\r
+ try:\r
+ obj = json.loads(msg)\r
+ if type(obj) == list:\r
+\r
+ ## Call Message\r
+ ##\r
+ if obj[0] == WampProtocol.MESSAGE_TYPEID_CALL:\r
+ callid = obj[1]\r
+ procuri = self.prefixes.resolveOrPass(obj[2])\r
+ arg = obj[3:]\r
+ d = maybeDeferred(self._callProcedure, procuri, arg)\r
+ d.addCallback(self._sendCallResult, callid)\r
+ d.addErrback(self._sendCallError, callid)\r
+\r
+ ## Subscribe Message\r
+ ##\r
+ elif obj[0] == WampProtocol.MESSAGE_TYPEID_SUBSCRIBE:\r
+ topicUri = self.prefixes.resolveOrPass(obj[1])\r
+ h = self._getSubHandler(topicUri)\r
+ if h:\r
+ ## either exact match or prefix match allowed\r
+ if h[1] == "" or h[4]:\r
+\r
+ ## direct topic\r
+ if h[2] is None and h[3] is None:\r
+ self.factory._subscribeClient(self, topicUri)\r
+\r
+ ## topic handled by subscription handler\r
+ else:\r
+ try:\r
+ ## handler is object method\r
+ if h[2]:\r
+ a = h[3](h[2], str(h[0]), str(h[1]))\r
+\r
+ ## handler is free standing procedure\r
+ else:\r
+ a = h[3](str(h[0]), str(h[1]))\r
+\r
+ ## only subscribe client if handler did return True\r
+ if a:\r
+ self.factory._subscribeClient(self, topicUri)\r
+ except:\r
+ if self.debugWamp:\r
+ log.msg("execption during topic subscription handler")\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("topic %s matches only by prefix and prefix match disallowed" % topicUri)\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("no topic / subscription handler registered for %s" % topicUri)\r
+\r
+ ## Unsubscribe Message\r
+ ##\r
+ elif obj[0] == WampProtocol.MESSAGE_TYPEID_UNSUBSCRIBE:\r
+ topicUri = self.prefixes.resolveOrPass(obj[1])\r
+ self.factory._unsubscribeClient(self, topicUri)\r
+\r
+ ## Publish Message\r
+ ##\r
+ elif obj[0] == WampProtocol.MESSAGE_TYPEID_PUBLISH:\r
+ topicUri = self.prefixes.resolveOrPass(obj[1])\r
+ h = self._getPubHandler(topicUri)\r
+ if h:\r
+ ## either exact match or prefix match allowed\r
+ if h[1] == "" or h[4]:\r
+\r
+ ## Event\r
+ ##\r
+ event = obj[2]\r
+\r
+ ## Exclude Sessions List\r
+ ##\r
+ exclude = [self] # exclude publisher by default\r
+ if len(obj) >= 4:\r
+ if type(obj[3]) == bool:\r
+ if not obj[3]:\r
+ exclude = []\r
+ elif type(obj[3]) == list:\r
+ ## map session IDs to protos\r
+ exclude = self.factory.sessionIdsToProtos(obj[3])\r
+ else:\r
+ ## FIXME: invalid type\r
+ pass\r
+\r
+ ## Eligible Sessions List\r
+ ##\r
+ eligible = None # all sessions are eligible by default\r
+ if len(obj) >= 5:\r
+ if type(obj[4]) == list:\r
+ ## map session IDs to protos\r
+ eligible = self.factory.sessionIdsToProtos(obj[4])\r
+ else:\r
+ ## FIXME: invalid type\r
+ pass\r
+\r
+ ## direct topic\r
+ if h[2] is None and h[3] is None:\r
+ self.factory.dispatch(topicUri, event, exclude, eligible)\r
+\r
+ ## topic handled by publication handler\r
+ else:\r
+ try:\r
+ ## handler is object method\r
+ if h[2]:\r
+ e = h[3](h[2], str(h[0]), str(h[1]), event)\r
+\r
+ ## handler is free standing procedure\r
+ else:\r
+ e = h[3](str(h[0]), str(h[1]), event)\r
+\r
+ ## only dispatch event if handler did return event\r
+ if e:\r
+ self.factory.dispatch(topicUri, e, exclude, eligible)\r
+ except:\r
+ if self.debugWamp:\r
+ log.msg("execption during topic publication handler")\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("topic %s matches only by prefix and prefix match disallowed" % topicUri)\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("no topic / publication handler registered for %s" % topicUri)\r
+\r
+ ## Define prefix to be used in CURIEs\r
+ ##\r
+ elif obj[0] == WampProtocol.MESSAGE_TYPEID_PREFIX:\r
+ prefix = obj[1]\r
+ uri = obj[2]\r
+ self.prefixes.set(prefix, uri)\r
+\r
+ else:\r
+ log.msg("unknown message type")\r
+ else:\r
+ log.msg("msg not a list")\r
+ except Exception, e:\r
+ traceback.print_exc()\r
+ else:\r
+ log.msg("binary message")\r
+\r
+\r
+\r
+class WampServerFactory(WebSocketServerFactory, WampFactory):\r
+ """\r
+ Server factory for Wamp RPC/PubSub.\r
+ """\r
+\r
+ protocol = WampServerProtocol\r
+ """\r
+ Twisted protocol used by default for WAMP servers.\r
+ """\r
+\r
+ def __init__(self, url, debug = False, debugCodePaths = False, debugWamp = False, debugApp = False):\r
+ WebSocketServerFactory.__init__(self, url, protocols = ["wamp"], debug = debug, debugCodePaths = debugCodePaths)\r
+ self.debugWamp = debugWamp\r
+ self.debugApp = debugApp\r
+\r
+\r
+ def _subscribeClient(self, proto, topicUri):\r
+ """\r
+ INTERNAL METHOD! Called from proto to subscribe client for topic.\r
+ """\r
+\r
+ if self.debugWamp:\r
+ log.msg("subscribed peer %s for topic %s" % (proto.peerstr, topicUri))\r
+\r
+ if not self.subscriptions.has_key(topicUri):\r
+ self.subscriptions[topicUri] = set()\r
+ self.subscriptions[topicUri].add(proto)\r
+\r
+\r
+ def _unsubscribeClient(self, proto, topicUri = None):\r
+ """\r
+ INTERNAL METHOD! Called from proto to unsubscribe client from topic.\r
+ """\r
+\r
+ if topicUri:\r
+ if self.subscriptions.has_key(topicUri):\r
+ self.subscriptions[topicUri].discard(proto)\r
+ if self.debugWamp:\r
+ log.msg("unsubscribed peer %s from topic %s" % (proto.peerstr, topicUri))\r
+ else:\r
+ for t in self.subscriptions:\r
+ self.subscriptions[t].discard(proto)\r
+ if self.debugWamp:\r
+ log.msg("unsubscribed peer %s from all topics" % (proto.peerstr))\r
+\r
+\r
+ def dispatch(self, topicUri, event, exclude = [], eligible = None):\r
+ """\r
+ Dispatch an event to all peers subscribed to the event topic.\r
+\r
+ :param topicUri: Topic to publish event to.\r
+ :type topicUri: str\r
+ :param event: Event to publish (must be JSON serializable).\r
+ :type event: obj\r
+ :param exclude: List of WampServerProtocol instances to exclude from receivers.\r
+ :type exclude: List of obj\r
+ :param eligible: List of WampServerProtocol instances eligible as receivers (or None for all).\r
+ :type eligible: List of obj\r
+\r
+ :returns twisted.internet.defer.Deferred -- Will be fired when event was\r
+ dispatched to all subscribers. The return value provided to the deferred\r
+ is a pair (delivered, requested), where delivered = number of actual\r
+ receivers, and requested = number of (subscribers - excluded) & eligible.\r
+ """\r
+ if self.debugWamp:\r
+ log.msg("publish event %s for topicUri %s" % (str(event), topicUri))\r
+\r
+ d = Deferred()\r
+\r
+ if self.subscriptions.has_key(topicUri) and len(self.subscriptions[topicUri]) > 0:\r
+\r
+ ## FIXME: this might break ordering of event delivery from a\r
+ ## receiver perspective. We might need to have send queues\r
+ ## per receiver OR do recvs = deque(sorted(..))\r
+\r
+ ## However, see http://twistedmatrix.com/trac/ticket/1396\r
+\r
+ if eligible is not None:\r
+ subscrbs = set(eligible) & self.subscriptions[topicUri]\r
+ else:\r
+ subscrbs = self.subscriptions[topicUri]\r
+\r
+ if len(exclude) > 0:\r
+ recvs = subscrbs - set(exclude)\r
+ else:\r
+ recvs = subscrbs\r
+\r
+ l = len(recvs)\r
+ if l > 0:\r
+\r
+ o = [WampProtocol.MESSAGE_TYPEID_EVENT, topicUri, event]\r
+ try:\r
+ msg = json.dumps(o)\r
+ if self.debugWamp:\r
+ log.msg("serialized event msg: " + str(msg))\r
+ except:\r
+ raise Exception("invalid type for event (not JSON serializable)")\r
+\r
+ preparedMsg = self.prepareMessage(msg)\r
+ self._sendEvents(preparedMsg, recvs.copy(), 0, l, d)\r
+ else:\r
+ d.callback((0, 0))\r
+\r
+ return d\r
+\r
+\r
+ def _sendEvents(self, preparedMsg, recvs, delivered, requested, d):\r
+ """\r
+ INTERNAL METHOD! Delivers events to receivers in chunks and\r
+ reenters the reactor in-between, so that other stuff can run.\r
+ """\r
+ ## deliver a batch of events\r
+ done = False\r
+ for i in xrange(0, 256):\r
+ try:\r
+ proto = recvs.pop()\r
+ if proto.state == WebSocketProtocol.STATE_OPEN:\r
+ try:\r
+ proto.sendPreparedMessage(preparedMsg)\r
+ except:\r
+ pass\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("delivered event to peer %s" % proto.peerstr)\r
+ delivered += 1\r
+ except KeyError:\r
+ # all receivers done\r
+ done = True\r
+ break\r
+\r
+ if not done:\r
+ ## if there are receivers left, redo\r
+ reactor.callLater(0, self._sendEvents, preparedMsg, recvs, delivered, requested, d)\r
+ else:\r
+ ## else fire final result\r
+ d.callback((delivered, requested))\r
+\r
+\r
+ def _addSession(self, proto, session_id):\r
+ """\r
+ INTERNAL METHOD! Add proto for session ID.\r
+ """\r
+ if not self.protoToSessions.has_key(proto):\r
+ self.protoToSessions[proto] = session_id\r
+ else:\r
+ raise Exception("logic error - dublicate _addSession for protoToSessions")\r
+ if not self.sessionsToProto.has_key(session_id):\r
+ self.sessionsToProto[session_id] = proto\r
+ else:\r
+ raise Exception("logic error - dublicate _addSession for sessionsToProto")\r
+\r
+\r
+ def _removeSession(self, proto):\r
+ """\r
+ INTERNAL METHOD! Remove session by proto.\r
+ """\r
+ if self.protoToSessions.has_key(proto):\r
+ session_id = self.protoToSessions[proto]\r
+ del self.protoToSessions[proto]\r
+ if self.sessionsToProto.has_key(session_id):\r
+ del self.sessionsToProto[session_id]\r
+\r
+\r
+ def sessionIdsToProtos(self, sessionIds):\r
+ """\r
+ Map session IDs to connected client protocol instances.\r
+\r
+ :param sessionIds: List of session IDs to be mapped.\r
+ :type sessionIds: list of str\r
+\r
+ :returns list of WampServerProtocol instances -- List of protocol instances corresponding to the session IDs.\r
+ """\r
+ protos = []\r
+ for s in sessionIds:\r
+ if self.sessionsToProto.has_key(s):\r
+ protos.append(self.sessionsToProto[s])\r
+ return protos\r
+\r
+\r
+ def protosToSessionIds(self, protos):\r
+ """\r
+ Map connected client protocol instances to session IDs.\r
+\r
+ :param protos: List of instances of WampServerProtocol to be mapped.\r
+ :type protos: list of WampServerProtocol\r
+\r
+ :returns list of str -- List of session IDs corresponding to the protos.\r
+ """\r
+ sessionIds = []\r
+ for p in protos:\r
+ if self.protoToSessions.has_key(p):\r
+ sessionIds.append(self.protoToSessions[p])\r
+ return sessionIds\r
+\r
+\r
+ def startFactory(self):\r
+ """\r
+ Called by Twisted when the factory starts up. When overriding, make\r
+ sure to call the base method.\r
+ """\r
+ if self.debugWamp:\r
+ log.msg("WampServerFactory starting")\r
+ self.subscriptions = {}\r
+ self.protoToSessions = {}\r
+ self.sessionsToProto = {}\r
+\r
+\r
+ def stopFactory(self):\r
+ """\r
+ Called by Twisted when the factory shuts down. When overriding, make\r
+ sure to call the base method.\r
+ """\r
+ if self.debugWamp:\r
+ log.msg("WampServerFactory stopped")\r
+\r
+\r
+\r
+class WampClientProtocol(WebSocketClientProtocol, WampProtocol):\r
+ """\r
+ Twisted client protocol for WAMP.\r
+ """\r
+\r
+ def onSessionOpen(self):\r
+ """\r
+ Callback fired when WAMP session was fully established. Override\r
+ in derived class.\r
+ """\r
+ pass\r
+\r
+\r
+ def onOpen(self):\r
+ ## do nothing here .. onSessionOpen is only fired when welcome\r
+ ## message was received (and thus session ID set)\r
+ pass\r
+\r
+\r
+ def onConnect(self, connectionResponse):\r
+ if connectionResponse.protocol not in self.factory.protocols:\r
+ raise Exception("server does not speak WAMP")\r
+\r
+\r
+ def connectionMade(self):\r
+ WebSocketClientProtocol.connectionMade(self)\r
+ WampProtocol.connectionMade(self)\r
+\r
+ self.calls = {}\r
+ self.subscriptions = {}\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ WampProtocol.connectionLost(self, reason)\r
+ WebSocketClientProtocol.connectionLost(self, reason)\r
+\r
+\r
+ def sendMessage(self, payload):\r
+ if self.debugWamp:\r
+ log.msg("TX WAMP: %s" % str(payload))\r
+ WebSocketClientProtocol.sendMessage(self, payload)\r
+\r
+\r
+ def onMessage(self, msg, binary):\r
+ """Internal method to handle WAMP messages received from WAMP server."""\r
+\r
+ ## WAMP is text message only\r
+ ##\r
+ if binary:\r
+ self._protocolError("binary WebSocket message received")\r
+ return\r
+\r
+ if self.debugWamp:\r
+ log.msg("RX WAMP: %s" % str(msg))\r
+\r
+ ## WAMP is proper JSON payload\r
+ ##\r
+ try:\r
+ obj = json.loads(msg)\r
+ except:\r
+ self._protocolError("WAMP message payload not valid JSON")\r
+ return\r
+\r
+ ## Every WAMP message is a list\r
+ ##\r
+ if type(obj) != list:\r
+ self._protocolError("WAMP message payload not a list")\r
+ return\r
+\r
+ ## Every WAMP message starts with an integer for message type\r
+ ##\r
+ if len(obj) < 1:\r
+ self._protocolError("WAMP message without message type")\r
+ return\r
+ if type(obj[0]) != int:\r
+ self._protocolError("WAMP message type not an integer")\r
+ return\r
+\r
+ ## WAMP message type\r
+ ##\r
+ msgtype = obj[0]\r
+\r
+ ## Valid WAMP message types received by WAMP clients\r
+ ##\r
+ if msgtype not in [WampProtocol.MESSAGE_TYPEID_WELCOME,\r
+ WampProtocol.MESSAGE_TYPEID_CALL_RESULT,\r
+ WampProtocol.MESSAGE_TYPEID_CALL_ERROR,\r
+ WampProtocol.MESSAGE_TYPEID_EVENT]:\r
+ self._protocolError("invalid WAMP message type %d" % msgtype)\r
+ return\r
+\r
+ ## WAMP CALL_RESULT / CALL_ERROR\r
+ ##\r
+ if msgtype in [WampProtocol.MESSAGE_TYPEID_CALL_RESULT, WampProtocol.MESSAGE_TYPEID_CALL_ERROR]:\r
+\r
+ ## Call ID\r
+ ##\r
+ if len(obj) < 2:\r
+ self._protocolError("WAMP CALL_RESULT/CALL_ERROR message without <callid>")\r
+ return\r
+ if type(obj[1]) not in [unicode, str]:\r
+ self._protocolError("WAMP CALL_RESULT/CALL_ERROR message with invalid type %s for <callid>" % type(obj[1]))\r
+ return\r
+ callid = str(obj[1])\r
+\r
+ ## Pop and process Call Deferred\r
+ ##\r
+ d = self.calls.pop(callid, None)\r
+ if d:\r
+ ## WAMP CALL_RESULT\r
+ ##\r
+ if msgtype == WampProtocol.MESSAGE_TYPEID_CALL_RESULT:\r
+ ## Call Result\r
+ ##\r
+ if len(obj) != 3:\r
+ self._protocolError("WAMP CALL_RESULT message with invalid length %d" % len(obj))\r
+ return\r
+ result = obj[2]\r
+\r
+ ## Fire Call Success Deferred\r
+ ##\r
+ d.callback(result)\r
+\r
+ ## WAMP CALL_ERROR\r
+ ##\r
+ elif msgtype == WampProtocol.MESSAGE_TYPEID_CALL_ERROR:\r
+ if len(obj) not in [4, 5]:\r
+ self._protocolError("call error message invalid length %d" % len(obj))\r
+ return\r
+\r
+ ## Error URI\r
+ ##\r
+ if type(obj[2]) not in [unicode, str]:\r
+ self._protocolError("invalid type %s for errorUri in call error message" % str(type(obj[2])))\r
+ return\r
+ erroruri = str(obj[2])\r
+\r
+ ## Error Description\r
+ ##\r
+ if type(obj[3]) not in [unicode, str]:\r
+ self._protocolError("invalid type %s for errorDesc in call error message" % str(type(obj[3])))\r
+ return\r
+ errordesc = str(obj[3])\r
+\r
+ ## Error Details\r
+ ##\r
+ if len(obj) > 4:\r
+ errordetails = obj[4]\r
+ else:\r
+ errordetails = None\r
+\r
+ ## Fire Call Error Deferred\r
+ ##\r
+ e = Exception()\r
+ e.args = (erroruri, errordesc, errordetails)\r
+ d.errback(e)\r
+ else:\r
+ raise Exception("logic error")\r
+ else:\r
+ if self.debugWamp:\r
+ log.msg("callid not found for received call result/error message")\r
+\r
+ ## WAMP EVENT\r
+ ##\r
+ elif msgtype == WampProtocol.MESSAGE_TYPEID_EVENT:\r
+ ## Topic\r
+ ##\r
+ if len(obj) != 3:\r
+ self._protocolError("WAMP EVENT message invalid length %d" % len(obj))\r
+ return\r
+ if type(obj[1]) not in [unicode, str]:\r
+ self._protocolError("invalid type for <topic> in WAMP EVENT message")\r
+ return\r
+ unresolvedTopicUri = str(obj[1])\r
+ topicUri = self.prefixes.resolveOrPass(unresolvedTopicUri)\r
+\r
+ ## Fire PubSub Handler\r
+ ##\r
+ if self.subscriptions.has_key(topicUri):\r
+ event = obj[2]\r
+ self.subscriptions[topicUri](topicUri, event)\r
+ else:\r
+ ## event received for non-subscribed topic (could be because we\r
+ ## just unsubscribed, and server already sent out event for\r
+ ## previous subscription)\r
+ pass\r
+\r
+ ## WAMP WELCOME\r
+ ##\r
+ elif msgtype == WampProtocol.MESSAGE_TYPEID_WELCOME:\r
+ ## Session ID\r
+ ##\r
+ if len(obj) < 2:\r
+ self._protocolError("WAMP WELCOME message invalid length %d" % len(obj))\r
+ return\r
+ if type(obj[1]) not in [unicode, str]:\r
+ self._protocolError("invalid type for <sessionid> in WAMP WELCOME message")\r
+ return\r
+ self.session_id = str(obj[1])\r
+\r
+ ## WAMP Protocol Version\r
+ ##\r
+ if len(obj) > 2:\r
+ if type(obj[2]) not in [int]:\r
+ self._protocolError("invalid type for <version> in WAMP WELCOME message")\r
+ return\r
+ else:\r
+ self.session_protocol_version = obj[2]\r
+ else:\r
+ self.session_protocol_version = None\r
+\r
+ ## Server Ident\r
+ ##\r
+ if len(obj) > 3:\r
+ if type(obj[3]) not in [unicode, str]:\r
+ self._protocolError("invalid type for <server> in WAMP WELCOME message")\r
+ return\r
+ else:\r
+ self.session_server = obj[3]\r
+ else:\r
+ self.session_server = None\r
+\r
+ self.onSessionOpen()\r
+\r
+ else:\r
+ raise Exception("logic error")\r
+\r
+\r
+ def call(self, *args):\r
+ """\r
+ Perform a remote-procedure call (RPC). The first argument is the procedure\r
+ URI (mandatory). Subsequent positional arguments can be provided (must be\r
+ JSON serializable). The return value is a Twisted Deferred.\r
+ """\r
+\r
+ if len(args) < 1:\r
+ raise Exception("missing procedure URI")\r
+\r
+ if type(args[0]) not in [unicode, str]:\r
+ raise Exception("invalid type for procedure URI")\r
+\r
+ procuri = args[0]\r
+ while True:\r
+ callid = newid()\r
+ if not self.calls.has_key(callid):\r
+ break\r
+ d = Deferred()\r
+ self.calls[callid] = d\r
+ msg = [WampProtocol.MESSAGE_TYPEID_CALL, callid, procuri]\r
+ msg.extend(args[1:])\r
+\r
+ try:\r
+ o = json.dumps(msg)\r
+ except:\r
+ raise Exception("call argument(s) not JSON serializable")\r
+\r
+ self.sendMessage(o)\r
+ return d\r
+\r
+\r
+ def prefix(self, prefix, uri):\r
+ """\r
+ Establishes a prefix to be used in CURIEs instead of URIs having that\r
+ prefix for both client-to-server and server-to-client messages.\r
+\r
+ :param prefix: Prefix to be used in CURIEs.\r
+ :type prefix: str\r
+ :param uri: URI that this prefix will resolve to.\r
+ :type uri: str\r
+ """\r
+\r
+ if type(prefix) != str:\r
+ raise Exception("invalid type for prefix")\r
+\r
+ if type(uri) not in [unicode, str]:\r
+ raise Exception("invalid type for URI")\r
+\r
+ if self.prefixes.get(prefix):\r
+ raise Exception("prefix already defined")\r
+\r
+ self.prefixes.set(prefix, uri)\r
+\r
+ msg = [WampProtocol.MESSAGE_TYPEID_PREFIX, prefix, uri]\r
+\r
+ self.sendMessage(json.dumps(msg))\r
+\r
+\r
+ def publish(self, topicUri, event, excludeMe = None, exclude = None, eligible = None):\r
+ """\r
+ Publish an event under a topic URI. The latter may be abbreviated using a\r
+ CURIE which has been previously defined using prefix(). The event must\r
+ be JSON serializable.\r
+\r
+ :param topicUri: The topic URI or CURIE.\r
+ :type topicUri: str\r
+ :param event: Event to be published (must be JSON serializable) or None.\r
+ :type event: value\r
+ :param excludeMe: When True, don't deliver the published event to myself (when I'm subscribed).\r
+ :type excludeMe: bool\r
+ :param exclude: Optional list of session IDs to exclude from receivers.\r
+ :type exclude: list of str\r
+ :param eligible: Optional list of session IDs to that are eligible as receivers.\r
+ :type eligible: list of str\r
+ """\r
+\r
+ if type(topicUri) not in [unicode, str]:\r
+ raise Exception("invalid type for parameter 'topicUri' - must be string (was %s)" % type(topicUri))\r
+\r
+ if excludeMe is not None:\r
+ if type(excludeMe) != bool:\r
+ raise Exception("invalid type for parameter 'excludeMe' - must be bool (was %s)" % type(excludeMe))\r
+\r
+ if exclude is not None:\r
+ if type(exclude) != list:\r
+ raise Exception("invalid type for parameter 'exclude' - must be list (was %s)" % type(exclude))\r
+\r
+ if eligible is not None:\r
+ if type(eligible) != list:\r
+ raise Exception("invalid type for parameter 'eligible' - must be list (was %s)" % type(eligible))\r
+\r
+ if exclude is not None or eligible is not None:\r
+ if exclude is None:\r
+ if excludeMe is not None:\r
+ if excludeMe:\r
+ exclude = [self.session_id]\r
+ else:\r
+ exclude = []\r
+ else:\r
+ exclude = [self.session_id]\r
+ if eligible is not None:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_PUBLISH, topicUri, event, exclude, eligible]\r
+ else:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_PUBLISH, topicUri, event, exclude]\r
+ else:\r
+ if excludeMe:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_PUBLISH, topicUri, event]\r
+ else:\r
+ msg = [WampProtocol.MESSAGE_TYPEID_PUBLISH, topicUri, event, excludeMe]\r
+\r
+ try:\r
+ o = json.dumps(msg)\r
+ except:\r
+ raise Exception("invalid type for parameter 'event' - not JSON serializable")\r
+\r
+ self.sendMessage(o)\r
+\r
+\r
+ def subscribe(self, topicUri, handler):\r
+ """\r
+ Subscribe to topic. When already subscribed, will overwrite the handler.\r
+\r
+ :param topicUri: URI or CURIE of topic to subscribe to.\r
+ :type topicUri: str\r
+ :param handler: Event handler to be invoked upon receiving events for topic.\r
+ :type handler: Python callable, will be called as in <callable>(eventUri, event).\r
+ """\r
+ if type(topicUri) not in [unicode, str]:\r
+ raise Exception("invalid type for parameter 'topicUri' - must be string (was %s)" % type(topicUri))\r
+\r
+ if type(handler) not in [types.FunctionType, types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType]:\r
+ raise Exception("invalid type for parameter 'handler' - must be a callable (was %s)" % type(handler))\r
+\r
+ turi = self.prefixes.resolveOrPass(topicUri)\r
+ if not self.subscriptions.has_key(turi):\r
+ msg = [WampProtocol.MESSAGE_TYPEID_SUBSCRIBE, topicUri]\r
+ o = json.dumps(msg)\r
+ self.sendMessage(o)\r
+ self.subscriptions[turi] = handler\r
+\r
+\r
+ def unsubscribe(self, topicUri):\r
+ """\r
+ Unsubscribe from topic. Will do nothing when currently not subscribed to the topic.\r
+\r
+ :param topicUri: URI or CURIE of topic to unsubscribe from.\r
+ :type topicUri: str\r
+ """\r
+ if type(topicUri) not in [unicode, str]:\r
+ raise Exception("invalid type for parameter 'topicUri' - must be string (was %s)" % type(topicUri))\r
+\r
+ turi = self.prefixes.resolveOrPass(topicUri)\r
+ if self.subscriptions.has_key(turi):\r
+ msg = [WampProtocol.MESSAGE_TYPEID_UNSUBSCRIBE, topicUri]\r
+ o = json.dumps(msg)\r
+ self.sendMessage(o)\r
+ del self.subscriptions[turi]\r
+\r
+\r
+\r
+class WampClientFactory(WebSocketClientFactory, WampFactory):\r
+ """\r
+ Twisted client factory for WAMP.\r
+ """\r
+\r
+ protocol = WampClientProtocol\r
+\r
+ def __init__(self, url, debug = False, debugCodePaths = False, debugWamp = False, debugApp = False):\r
+ WebSocketClientFactory.__init__(self, url, protocols = ["wamp"], debug = debug, debugCodePaths = debugCodePaths)\r
+ self.debugWamp = debugWamp\r
+ self.debugApp = debugApp\r
+\r
+\r
+ def startFactory(self):\r
+ """\r
+ Called by Twisted when the factory starts up. When overriding, make\r
+ sure to call the base method.\r
+ """\r
+ if self.debugWamp:\r
+ log.msg("WebSocketClientFactory starting")\r
+\r
+\r
+ def stopFactory(self):\r
+ """\r
+ Called by Twisted when the factory shuts down. When overriding, make\r
+ sure to call the base method.\r
+ """\r
+ if self.debugWamp:\r
+ log.msg("WebSocketClientFactory stopped")\r
+\r
+\r
+\r
+class WampCraProtocol:\r
+ """\r
+ Base class for WAMP Challenge-Response Authentication protocols (client and server).\r
+\r
+ WAMP-CRA is a cryptographically strong challenge response authentication\r
+ protocol based on HMAC-SHA256.\r
+\r
+ The protocol performs in-band authentication of WAMP clients to WAMP servers.\r
+\r
+ WAMP-CRA does not introduce any new WAMP protocol level message types, but\r
+ implements the authentication handshake via standard WAMP RPCs with well-known\r
+ procedure URIs and signatures.\r
+ """\r
+\r
+ URI_WAMP_BASE = "http://api.wamp.ws/"\r
+ """\r
+ WAMP base URI for WAMP predefined things.\r
+ """\r
+\r
+ URI_WAMP_ERROR = URI_WAMP_BASE + "error#"\r
+ """\r
+ Prefix for WAMP errors.\r
+ """\r
+\r
+ URI_WAMP_RPC = URI_WAMP_BASE + "procedure#"\r
+ """\r
+ Prefix for WAMP predefined RPCs.\r
+ """\r
+\r
+ URI_WAMP_EVENT = URI_WAMP_BASE + "event#"\r
+ """\r
+ Prefix for WAMP predefined PubSub events.\r
+ """\r
+\r
+\r
+class WampCraClientProtocol(WampClientProtocol, WampCraProtocol):\r
+ """\r
+ Simple, authenticated WAMP client protocol.\r
+\r
+ The client can perform WAMP-Challenge-Response-Authentication ("WAMP-CRA") to authenticate\r
+ itself to a WAMP server. The server needs to implement WAMP-CRA also of course.\r
+ """\r
+\r
+ def authSignature(self, authChallenge, authSecret = None):\r
+ """\r
+ Compute the authentication signature from an authentication challenge and a secret.\r
+\r
+ :param authChallenge: The authentication challenge.\r
+ :type authChallenge: str\r
+ :param authSecret: The authentication secret.\r
+ :type authSecret: str\r
+ :returns str -- The authentication signature.\r
+ """\r
+ if authSecret is None:\r
+ authSecret = ""\r
+ h = hmac.new(authSecret, authChallenge, hashlib.sha256)\r
+ sig = binascii.b2a_base64(h.digest()).strip()\r
+ return sig\r
+\r
+ def authenticate(self, onAuthSuccess, onAuthError = None, authKey = None, authExtra = None, authSecret = None):\r
+ """\r
+ Authenticate the WAMP session to server. Upon authentication success or failure, the appropriate callback will fire.\r
+\r
+ :param onAuthSuccess: Callback for authentication success.\r
+ :type onAuthSuccess: A callable.\r
+ :param onAuthError: Callback for authentication failure.\r
+ :type onAuthError: A callable.\r
+ :param authKey: The key of the authentication credentials, something like a user or application name.\r
+ :type authKey: str\r
+ :param authExtra: Any extra authentication information.\r
+ :type authExtra: dict\r
+ :param authSecret: The secret of the authentication credentials, something like the user password or application secret key.\r
+ """\r
+\r
+ def _onAuthError(e):\r
+ erroruri, errodesc, errordetails = e.value.args\r
+ if onAuthError is not None:\r
+ onAuthError(erroruri, errodesc, errordetails)\r
+\r
+ def _onAuthChallenge(challenge):\r
+ if authKey is not None:\r
+ sig = self.authSignature(challenge, authSecret)\r
+ else:\r
+ sig = None\r
+ d = self.call(WampCraProtocol.URI_WAMP_RPC + "auth", sig)\r
+ d.addCallbacks(onAuthSuccess, _onAuthError)\r
+\r
+ d = self.call(WampCraProtocol.URI_WAMP_RPC + "authreq", authKey, authExtra)\r
+ d.addCallbacks(_onAuthChallenge, _onAuthError)\r
+\r
+\r
+\r
+class WampCraServerProtocol(WampServerProtocol, WampCraProtocol):\r
+ """\r
+ Simple, authenticating WAMP server protocol.\r
+\r
+ The server lets clients perform WAMP-Challenge-Response-Authentication ("WAMP-CRA")\r
+ to authenticate. The clients need to implement WAMP-CRA also of course.\r
+\r
+ To implement an authenticating server, override:\r
+\r
+ * getAuthSecret\r
+ * getAuthPermissions\r
+ * onAuthenticated\r
+\r
+ in your class deriving from this class.\r
+ """\r
+\r
+ ## global client auth options\r
+ ##\r
+ clientAuthTimeout = 0\r
+ clientAuthAllowAnonymous = True\r
+\r
+\r
+ def getAuthPermissions(self, authKey, authExtra):\r
+ """\r
+ Get the permissions the session is granted when the authentication succeeds\r
+ for the given key / extra information.\r
+\r
+ Override in derived class to implement your authentication.\r
+\r
+ :param authKey: The authentication key.\r
+ :type authKey: str\r
+ :param authExtra: Authentication extra information.\r
+ :type authExtra: dict\r
+ :returns str -- The authentication secret for the key or None when the key does not exist.\r
+ """\r
+ return []\r
+\r
+\r
+ def getAuthSecret(self, authKey):\r
+ """\r
+ Get the authentication secret for an authentication key, i.e. the\r
+ user password for the user name. Return None when the authentication\r
+ key does not exist.\r
+\r
+ Override in derived class to implement your authentication.\r
+\r
+ :param authKey: The authentication key.\r
+ :type authKey: str\r
+ :returns str -- The authentication secret for the key or None when the key does not exist.\r
+ """\r
+ return None\r
+\r
+\r
+ def onAuthTimeout(self):\r
+ """\r
+ Fired when the client does not authenticate itself in time. The default implementation\r
+ will simply fail the connection.\r
+\r
+ May be overridden in derived class.\r
+ """\r
+ if not self.clientAuthenticated:\r
+ log.msg("failing connection upon client authentication timeout [%s secs]" % self.clientAuthTimeout)\r
+ self.failConnection()\r
+\r
+\r
+ def onAuthenticated(self, permissions):\r
+ """\r
+ Fired when client authentication was successful.\r
+\r
+ Override in derived class and register PubSub topics and/or RPC endpoints.\r
+\r
+ :param permissions: The permissions granted to the now authenticated client.\r
+ :type permissions: list\r
+ """\r
+ pass\r
+\r
+\r
+ def registerForPubSubFromPermissions(self, permissions):\r
+ """\r
+ Register topics for PubSub from auth permissions.\r
+\r
+ :param permissions: The permissions granted to the now authenticated client.\r
+ :type permissions: list\r
+ """\r
+ for p in permissions['pubsub']:\r
+ ## register topics for the clients\r
+ ##\r
+ pubsub = (WampServerProtocol.PUBLISH if p['pub'] else 0) | \\r
+ (WampServerProtocol.SUBSCRIBE if p['sub'] else 0)\r
+ topic = p['uri']\r
+ if self.pubHandlers.has_key(topic) or self.subHandlers.has_key(topic):\r
+ ## FIXME: handle dups!\r
+ log.msg("DUPLICATE TOPIC PERMISSION !!! " + topic)\r
+ self.registerForPubSub(topic, p['prefix'], pubsub)\r
+\r
+\r
+ def onSessionOpen(self):\r
+ """\r
+ Called when WAMP session has been established, but not yet authenticated. The default\r
+ implementation will prepare the session allowing the client to authenticate itself.\r
+ """\r
+\r
+ self.registerForRpc(self, WampCraProtocol.URI_WAMP_RPC, [WampCraServerProtocol.authRequest,\r
+ WampCraServerProtocol.auth])\r
+\r
+ ## reset authentication state\r
+ ##\r
+ self.clientAuthenticated = False\r
+ self.clientPendingAuth = None\r
+\r
+ ## client authentication timeout\r
+ ##\r
+ if self.clientAuthTimeout > 0:\r
+ self.clientAuthTimeoutCall = reactor.callLater(self.clientAuthTimeout, self.onAuthTimeout)\r
+ else:\r
+ self.clientAuthTimeoutCall = None\r
+\r
+\r
+ def authSignature(self, authChallenge, authKey = None):\r
+ """\r
+ Compute the authentication signature from an authentication challenge and for an authentication key.\r
+\r
+ :param authChallenge: The authentication challenge.\r
+ :type authChallenge: str\r
+ :param authKey: The authentication key for which to compute the signature.\r
+ :type authKey: str\r
+ :returns str -- The authentication signature.\r
+ """\r
+ if authKey is None:\r
+ secret = ""\r
+ else:\r
+ secret = self.getAuthSecret(authKey)\r
+ h = hmac.new(secret, authChallenge, hashlib.sha256)\r
+ sig = binascii.b2a_base64(h.digest()).strip()\r
+ return sig\r
+\r
+\r
+ @exportRpc("authreq")\r
+ def authRequest(self, appkey = None, extra = None):\r
+ """\r
+ RPC for clients to initiate the authentication handshake.\r
+\r
+ :param appkey: Authentication key, such as user name or application name.\r
+ :type appkey: str\r
+ :param extra: Authentication extra information.\r
+ :type extra: dict\r
+ :returns str -- Authentication challenge. The client will need to create an authentication signature from this.\r
+ """\r
+\r
+ ## check authentication state\r
+ ##\r
+ if self.clientAuthenticated:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "already-authenticated"), "already authenticated")\r
+ if self.clientPendingAuth is not None:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "authentication-already-requested"), "authentication request already issues - authentication pending")\r
+\r
+ ## check appkey\r
+ ##\r
+ if appkey is None and not self.clientAuthAllowAnonymous:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "anyonymous-auth-forbidden"), "authentication as anonymous forbidden")\r
+\r
+ if type(appkey) not in [str, unicode, types.NoneType]:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "invalid-argument"), "application key must be a string (was %s)" % str(type(appkey)))\r
+ if appkey is not None and self.getAuthSecret(appkey) is None:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "no-such-appkey"), "application key '%s' does not exist." % appkey)\r
+\r
+ ## check extra\r
+ ##\r
+ if extra:\r
+ if type(extra) != dict:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "invalid-argument"), "extra not a dictionary (was %s)." % str(type(extra)))\r
+ else:\r
+ extra = {}\r
+ for k in extra:\r
+ if type(extra[k]) not in [str, unicode, int, long, float, bool, types.NoneType]:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "invalid-argument"), "attribute '%s' in extra not a primitive type (was %s)" % (k, str(type(extra[k]))))\r
+\r
+ ## each authentication request gets a unique authid, which can only be used (later) once!\r
+ ##\r
+ authid = newid()\r
+\r
+ ## create authentication challenge\r
+ ##\r
+ info = {}\r
+ info['authid'] = authid\r
+ info['appkey'] = appkey\r
+ info['timestamp'] = utcnow()\r
+ info['sessionid'] = self.session_id\r
+ info['extra'] = extra\r
+\r
+ try:\r
+ pp = self.getAuthPermissions(appkey, extra)\r
+ if pp is None:\r
+ pp = {'pubsub': [], 'rpc': []}\r
+ info['permissions'] = pp\r
+ except Exception, e:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "auth-permissions-error"), str(e))\r
+\r
+ if appkey:\r
+ ## authenticated\r
+ ##\r
+ infoser = json.dumps(info)\r
+ sig = self.authSignature(infoser, appkey)\r
+\r
+ self.clientPendingAuth = (info, sig)\r
+ return infoser\r
+ else:\r
+ ## anonymous\r
+ ##\r
+ self.clientPendingAuth = (info, None)\r
+ return None\r
+\r
+\r
+ @exportRpc("auth")\r
+ def auth(self, signature = None):\r
+ """\r
+ RPC for clients to actually authenticate after requesting authentication and computing\r
+ a signature from the authentication challenge.\r
+\r
+ :param signature: Authenticatin signature computed by the client.\r
+ :type signature: str\r
+ :returns list -- A list of permissions the client is granted when authentication was successful.\r
+ """\r
+\r
+ ## check authentication state\r
+ ##\r
+ if self.clientAuthenticated:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "already-authenticated"), "already authenticated")\r
+ if self.clientPendingAuth is None:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "no-authentication-requested"), "no authentication previously requested")\r
+\r
+ ## check signature\r
+ ##\r
+ if type(signature) not in [str, unicode, types.NoneType]:\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "invalid-argument"), "signature must be a string or None (was %s)" % str(type(signature)))\r
+ if self.clientPendingAuth[1] != signature:\r
+ ## delete pending authentication, so that no retries are possible. authid is only valid for 1 try!!\r
+ ## FIXME: drop the connection?\r
+ self.clientPendingAuth = None\r
+ raise Exception(self.shrink(WampCraProtocol.URI_WAMP_ERROR + "invalid-signature"), "signature for authentication request is invalid")\r
+\r
+ ## at this point, the client has successfully authenticated!\r
+\r
+ ## get the permissions we determined earlier\r
+ ##\r
+ perms = self.clientPendingAuth[0]['permissions']\r
+\r
+ ## delete auth request and mark client as authenticated\r
+ ##\r
+ self.clientAppkey = self.clientPendingAuth[0]['appkey']\r
+ self.clientAuthenticated = True\r
+ self.clientPendingAuth = None\r
+ if self.clientAuthTimeoutCall is not None:\r
+ self.clientAuthTimeoutCall.cancel()\r
+ self.clientAuthTimeoutCall = None\r
+\r
+ ## fire authentication callback\r
+ ##\r
+ self.onAuthenticated(self.clientAppkey, perms)\r
+\r
+ ## return permissions to client\r
+ ##\r
+ return perms\r
+\r
--- /dev/null
+###############################################################################\r
+##\r
+## Copyright 2011,2012 Tavendo GmbH\r
+##\r
+## Licensed under the Apache License, Version 2.0 (the "License");\r
+## you may not use this file except in compliance with the License.\r
+## You may obtain a copy of the License at\r
+##\r
+## http://www.apache.org/licenses/LICENSE-2.0\r
+##\r
+## Unless required by applicable law or agreed to in writing, software\r
+## distributed under the License is distributed on an "AS IS" BASIS,\r
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+## See the License for the specific language governing permissions and\r
+## limitations under the License.\r
+##\r
+###############################################################################\r
+\r
+## The Python urlparse module currently does not contain the ws/wss\r
+## schemes, so we add those dynamically (which is a hack of course).\r
+##\r
+import urlparse\r
+wsschemes = ["ws", "wss"]\r
+urlparse.uses_relative.extend(wsschemes)\r
+urlparse.uses_netloc.extend(wsschemes)\r
+urlparse.uses_params.extend(wsschemes)\r
+urlparse.uses_query.extend(wsschemes)\r
+urlparse.uses_fragment.extend(wsschemes)\r
+\r
+from twisted.internet import reactor, protocol\r
+from twisted.python import log\r
+import urllib\r
+import binascii\r
+import hashlib\r
+import base64\r
+import struct\r
+import random\r
+import os\r
+from array import array\r
+from collections import deque\r
+from utf8validator import Utf8Validator\r
+from xormasker import XorMaskerNull, XorMaskerSimple, XorMaskerShifted1\r
+from httpstatus import *\r
+import autobahn # need autobahn.version\r
+\r
+\r
+def createWsUrl(hostname, port = None, isSecure = False, path = None, params = None):\r
+ """\r
+ Create a WbeSocket URL from components.\r
+\r
+ :param hostname: WebSocket server hostname.\r
+ :type hostname: str\r
+ :param port: WebSocket service port or None (to select default ports 80/443 depending on isSecure).\r
+ :type port: int\r
+ :param isSecure: Set True for secure WebSockets ("wss" scheme).\r
+ :type isSecure: bool\r
+ :param path: Path component of addressed resource (will be properly URL escaped).\r
+ :type path: str\r
+ :param params: A dictionary of key-values to construct the query component of the addressed resource (will be properly URL escaped).\r
+ :type params: dict\r
+\r
+ :returns str -- Constructed WebSocket URL.\r
+ """\r
+ if port is not None:\r
+ netloc = "%s:%d" % (hostname, port)\r
+ else:\r
+ if isSecure:\r
+ netloc = "%s:443" % hostname\r
+ else:\r
+ netloc = "%s:80" % hostname\r
+ if isSecure:\r
+ scheme = "wss"\r
+ else:\r
+ scheme = "ws"\r
+ if path is not None:\r
+ ppath = urllib.quote(path)\r
+ else:\r
+ ppath = "/"\r
+ if params is not None:\r
+ query = urllib.urlencode(params)\r
+ else:\r
+ query = None\r
+ return urlparse.urlunparse((scheme, netloc, ppath, None, query, None))\r
+\r
+\r
+def parseWsUrl(url):\r
+ """\r
+ Parses as WebSocket URL into it's components and returns a tuple (isSecure, host, port, resource, path, params).\r
+\r
+ isSecure is a flag which is True for wss URLs.\r
+ host is the hostname or IP from the URL.\r
+ port is the port from the URL or standard port derived from scheme (ws = 80, wss = 443).\r
+ resource is the /resource name/ from the URL, the /path/ together with the (optional) /query/ component.\r
+ path is the /path/ component properly unescaped.\r
+ params is the /query) component properly unescaped and returned as dictionary.\r
+\r
+ :param url: A valid WebSocket URL, i.e. ws://localhost:9000/myresource?param1=23¶m2=666\r
+ :type url: str\r
+\r
+ :returns: tuple -- A tuple (isSecure, host, port, resource, path, params)\r
+ """\r
+ parsed = urlparse.urlparse(url)\r
+ if parsed.scheme not in ["ws", "wss"]:\r
+ raise Exception("invalid WebSocket scheme '%s'" % parsed.scheme)\r
+ if parsed.port is None or parsed.port == "":\r
+ if parsed.scheme == "ws":\r
+ port = 80\r
+ else:\r
+ port = 443\r
+ else:\r
+ port = int(parsed.port)\r
+ if parsed.fragment is not None and parsed.fragment != "":\r
+ raise Exception("invalid WebSocket URL: non-empty fragment '%s" % parsed.fragment)\r
+ if parsed.path is not None and parsed.path != "":\r
+ ppath = parsed.path\r
+ path = urllib.unquote(ppath)\r
+ else:\r
+ ppath = "/"\r
+ path = ppath\r
+ if parsed.query is not None and parsed.query != "":\r
+ resource = ppath + "?" + parsed.query\r
+ params = urlparse.parse_qs(parsed.query)\r
+ else:\r
+ resource = ppath\r
+ params = {}\r
+ return (parsed.scheme == "wss", parsed.hostname, port, resource, path, params)\r
+\r
+\r
+def connectWS(factory, contextFactory = None, timeout = 30, bindAddress = None):\r
+ """\r
+ Establish WebSockets connection to a server. The connection parameters like target\r
+ host, port, resource and others are provided via the factory.\r
+\r
+ :param factory: The WebSockets protocol factory to be used for creating client protocol instances.\r
+ :type factory: An :class:`autobahn.websocket.WebSocketClientFactory` instance.\r
+ :param contextFactory: SSL context factory, required for secure WebSockets connections ("wss").\r
+ :type contextFactory: A twisted.internet.ssl.ClientContextFactory instance.\r
+ :param timeout: Number of seconds to wait before assuming the connection has failed.\r
+ :type timeout: int\r
+ :param bindAddress: A (host, port) tuple of local address to bind to, or None.\r
+ :type bindAddress: tuple\r
+\r
+ :returns: obj -- An object which provides twisted.interface.IConnector.\r
+ """\r
+ if factory.isSecure:\r
+ if contextFactory is None:\r
+ # create default client SSL context factory when none given\r
+ from twisted.internet import ssl\r
+ contextFactory = ssl.ClientContextFactory()\r
+ conn = reactor.connectSSL(factory.host, factory.port, factory, contextFactory, timeout, bindAddress)\r
+ else:\r
+ conn = reactor.connectTCP(factory.host, factory.port, factory, timeout, bindAddress)\r
+ return conn\r
+\r
+\r
+def listenWS(factory, contextFactory = None, backlog = 50, interface = ''):\r
+ """\r
+ Listen for incoming WebSocket connections from clients. The connection parameters like\r
+ listening port and others are provided via the factory.\r
+\r
+ :param factory: The WebSockets protocol factory to be used for creating server protocol instances.\r
+ :type factory: An :class:`autobahn.websocket.WebSocketServerFactory` instance.\r
+ :param contextFactory: SSL context factory, required for secure WebSockets connections ("wss").\r
+ :type contextFactory: A twisted.internet.ssl.ContextFactory.\r
+ :param backlog: Size of the listen queue.\r
+ :type backlog: int\r
+ :param interface: The interface (derived from hostname given) to bind to, defaults to '' (all).\r
+ :type interface: str\r
+\r
+ :returns: obj -- An object that provides twisted.interface.IListeningPort.\r
+ """\r
+ if factory.isSecure:\r
+ if contextFactory is None:\r
+ raise Exception("Secure WebSocket listen requested, but no SSL context factory given")\r
+ listener = reactor.listenSSL(factory.port, factory, contextFactory, backlog, interface)\r
+ else:\r
+ listener = reactor.listenTCP(factory.port, factory, backlog, interface)\r
+ return listener\r
+\r
+\r
+class FrameHeader:\r
+ """\r
+ Thin-wrapper for storing WebSockets frame metadata.\r
+\r
+ FOR INTERNAL USE ONLY!\r
+ """\r
+\r
+ def __init__(self, opcode, fin, rsv, length, mask):\r
+ """\r
+ Constructor.\r
+\r
+ :param opcode: Frame opcode (0-15).\r
+ :type opcode: int\r
+ :param fin: Frame FIN flag.\r
+ :type fin: bool\r
+ :param rsv: Frame reserved flags (0-7).\r
+ :type rsv: int\r
+ :param length: Frame payload length.\r
+ :type length: int\r
+ :param mask: Frame mask (binary string) or None.\r
+ :type mask: str\r
+ """\r
+ self.opcode = opcode\r
+ self.fin = fin\r
+ self.rsv = rsv\r
+ self.length = length\r
+ self.mask = mask\r
+\r
+\r
+class HttpException():\r
+ """\r
+ Throw an instance of this class to deny a WebSockets connection\r
+ during handshake in :meth:`autobahn.websocket.WebSocketServerProtocol.onConnect`.\r
+ You can find definitions of HTTP status codes in module :mod:`autobahn.httpstatus`.\r
+ """\r
+\r
+ def __init__(self, code, reason):\r
+ """\r
+ Constructor.\r
+\r
+ :param code: HTTP error code.\r
+ :type code: int\r
+ :param reason: HTTP error reason.\r
+ :type reason: str\r
+ """\r
+ self.code = code\r
+ self.reason = reason\r
+\r
+\r
+class ConnectionRequest():\r
+ """\r
+ Thin-wrapper for WebSockets connection request information\r
+ provided in :meth:`autobahn.websocket.WebSocketServerProtocol.onConnect` when a WebSockets\r
+ client establishes a connection to a WebSockets server.\r
+ """\r
+ def __init__(self, peer, peerstr, headers, host, path, params, version, origin, protocols, extensions):\r
+ """\r
+ Constructor.\r
+\r
+ :param peer: IP address/port of the connecting client.\r
+ :type peer: object\r
+ :param peerstr: IP address/port of the connecting client as string.\r
+ :type peerstr: str\r
+ :param headers: HTTP headers from opening handshake request.\r
+ :type headers: dict\r
+ :param host: Host from opening handshake HTTP header.\r
+ :type host: str\r
+ :param path: Path from requested HTTP resource URI. For example, a resource URI of "/myservice?foo=23&foo=66&bar=2" will be parsed to "/myservice".\r
+ :type path: str\r
+ :param params: Query parameters (if any) from requested HTTP resource URI. For example, a resource URI of "/myservice?foo=23&foo=66&bar=2" will be parsed to {'foo': ['23', '66'], 'bar': ['2']}.\r
+ :type params: dict of arrays of strings\r
+ :param version: The WebSockets protocol version the client announced (and will be spoken, when connection is accepted).\r
+ :type version: int\r
+ :param origin: The WebSockets origin header or None. Note that this only a reliable source of information for browser clients!\r
+ :type origin: str\r
+ :param protocols: The WebSockets (sub)protocols the client announced. You must select and return one of those (or None) in :meth:`autobahn.websocket.WebSocketServerProtocol.onConnect`.\r
+ :type protocols: array of strings\r
+ :param extensions: The WebSockets extensions the client requested and the server accepted (and thus will be spoken, when WS connection is established).\r
+ :type extensions: array of strings\r
+ """\r
+ self.peer = peer\r
+ self.peerstr = peerstr\r
+ self.headers = headers\r
+ self.host = host\r
+ self.path = path\r
+ self.params = params\r
+ self.version = version\r
+ self.origin = origin\r
+ self.protocols = protocols\r
+ self.extensions = extensions\r
+\r
+\r
+class ConnectionResponse():\r
+ """\r
+ Thin-wrapper for WebSockets connection response information\r
+ provided in :meth:`autobahn.websocket.WebSocketClientProtocol.onConnect` when a WebSockets\r
+ client has established a connection to a WebSockets server.\r
+ """\r
+ def __init__(self, peer, peerstr, headers, version, protocol, extensions):\r
+ """\r
+ Constructor.\r
+\r
+ :param peer: IP address/port of the connected server.\r
+ :type peer: object\r
+ :param peerstr: IP address/port of the connected server as string.\r
+ :type peerstr: str\r
+ :param headers: HTTP headers from opening handshake response.\r
+ :type headers: dict\r
+ :param version: The WebSockets protocol version that is spoken.\r
+ :type version: int\r
+ :param protocol: The WebSockets (sub)protocol in use.\r
+ :type protocol: str\r
+ :param extensions: The WebSockets extensions in use.\r
+ :type extensions: array of strings\r
+ """\r
+ self.peer = peer\r
+ self.peerstr = peerstr\r
+ self.headers = headers\r
+ self.version = version\r
+ self.protocol = protocol\r
+ self.extensions = extensions\r
+\r
+\r
+def parseHttpHeader(data):\r
+ """\r
+ Parses the beginning of a HTTP request header (the data up to the \n\n line) into a pair\r
+ of status line and HTTP headers dictionary.\r
+ Header keys are normalized to all-lower-case.\r
+\r
+ FOR INTERNAL USE ONLY!\r
+\r
+ :param data: The HTTP header data up to the \n\n line.\r
+ :type data: str\r
+ """\r
+ raw = data.splitlines()\r
+ http_status_line = raw[0].strip()\r
+ http_headers = {}\r
+ http_headers_cnt = {}\r
+ for h in raw[1:]:\r
+ i = h.find(":")\r
+ if i > 0:\r
+ ## HTTP header keys are case-insensitive\r
+ key = h[:i].strip().lower()\r
+\r
+ ## not sure if UTF-8 is allowed for HTTP header values..\r
+ value = h[i+1:].strip().decode("utf-8")\r
+\r
+ ## handle HTTP headers split across multiple lines\r
+ if http_headers.has_key(key):\r
+ http_headers[key] += ", %s" % value\r
+ http_headers_cnt[key] += 1\r
+ else:\r
+ http_headers[key] = value\r
+ http_headers_cnt[key] = 1\r
+ else:\r
+ # skip bad HTTP header\r
+ pass\r
+ return (http_status_line, http_headers, http_headers_cnt)\r
+\r
+\r
+class WebSocketProtocol(protocol.Protocol):\r
+ """\r
+ A Twisted Protocol class for WebSockets. This class is used by both WebSocket\r
+ client and server protocol version. It is unusable standalone, for example\r
+ the WebSockets initial handshake is implemented in derived class differently\r
+ for clients and servers.\r
+ """\r
+\r
+ SUPPORTED_SPEC_VERSIONS = [0, 10, 11, 12, 13, 14, 15, 16, 17, 18]\r
+ """\r
+ WebSockets protocol spec (draft) versions supported by this implementation.\r
+ Use of version 18 indicates RFC6455. Use of versions < 18 indicate actual\r
+ draft spec versions (Hybi-Drafts). Use of version 0 indicates Hixie-76.\r
+ """\r
+\r
+ SUPPORTED_PROTOCOL_VERSIONS = [0, 8, 13]\r
+ """\r
+ WebSocket protocol versions supported by this implementation. For Hixie-76,\r
+ there is no protocol version announced in HTTP header, and we just use the\r
+ draft version (0) in this case.\r
+ """\r
+\r
+ SPEC_TO_PROTOCOL_VERSION = {0: 0, 10: 8, 11: 8, 12: 8, 13: 13, 14: 13, 15: 13, 16: 13, 17: 13, 18: 13}\r
+ """\r
+ Mapping from protocol spec (draft) version to protocol version. For Hixie-76,\r
+ there is no protocol version announced in HTTP header, and we just use the\r
+ pseudo protocol version 0 in this case.\r
+ """\r
+\r
+ PROTOCOL_TO_SPEC_VERSION = {0: 0, 8: 12, 13: 18}\r
+ """\r
+ Mapping from protocol version to the latest protocol spec (draft) version\r
+ using that protocol version. For Hixie-76, there is no protocol version\r
+ announced in HTTP header, and we just use the draft version (0) in this case.\r
+ """\r
+\r
+ DEFAULT_SPEC_VERSION = 10\r
+ """\r
+ Default WebSockets protocol spec version this implementation speaks.\r
+ We use Hybi-10, since this is what is currently targeted by widely distributed\r
+ browsers (namely Firefox 8 and the like).\r
+ """\r
+\r
+ DEFAULT_ALLOW_HIXIE76 = False\r
+ """\r
+ By default, this implementation will not allow to speak the obsoleted\r
+ Hixie-76 protocol version. That protocol version has security issues, but\r
+ is still spoken by some clients. Enable at your own risk! Enabling can be\r
+ done by using setProtocolOptions() on the factories for clients and servers.\r
+ """\r
+\r
+ WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"\r
+ """\r
+ Protocol defined magic used during WebSocket handshake (used in Hybi-drafts\r
+ and final RFC6455.\r
+ """\r
+\r
+ QUEUED_WRITE_DELAY = 0.00001\r
+ """For synched/chopped writes, this is the reactor reentry delay in seconds."""\r
+\r
+ PAYLOAD_LEN_XOR_BREAKEVEN = 128\r
+ """Tuning parameter which chooses XORer used for masking/unmasking based on\r
+ payload length."""\r
+\r
+ MESSAGE_TYPE_TEXT = 1\r
+ """WebSockets text message type (UTF-8 payload)."""\r
+\r
+ MESSAGE_TYPE_BINARY = 2\r
+ """WebSockets binary message type (arbitrary binary payload)."""\r
+\r
+ ## WebSockets protocol state:\r
+ ## STATE_CONNECTING => STATE_OPEN => STATE_CLOSING => STATE_CLOSED\r
+ ##\r
+ STATE_CLOSED = 0\r
+ STATE_CONNECTING = 1\r
+ STATE_CLOSING = 2\r
+ STATE_OPEN = 3\r
+\r
+ ## Streaming Send State\r
+ SEND_STATE_GROUND = 0\r
+ SEND_STATE_MESSAGE_BEGIN = 1\r
+ SEND_STATE_INSIDE_MESSAGE = 2\r
+ SEND_STATE_INSIDE_MESSAGE_FRAME = 3\r
+\r
+ ## WebSockets protocol close codes\r
+ ##\r
+ CLOSE_STATUS_CODE_NORMAL = 1000\r
+ """Normal close of connection."""\r
+\r
+ CLOSE_STATUS_CODE_GOING_AWAY = 1001\r
+ """Going away."""\r
+\r
+ CLOSE_STATUS_CODE_PROTOCOL_ERROR = 1002\r
+ """Protocol error."""\r
+\r
+ CLOSE_STATUS_CODE_UNSUPPORTED_DATA = 1003\r
+ """Unsupported data."""\r
+\r
+ CLOSE_STATUS_CODE_RESERVED1 = 1004\r
+ """RESERVED"""\r
+\r
+ CLOSE_STATUS_CODE_NULL = 1005 # MUST NOT be set in close frame!\r
+ """No status received. (MUST NOT be used as status code when sending a close)."""\r
+\r
+ CLOSE_STATUS_CODE_ABNORMAL_CLOSE = 1006 # MUST NOT be set in close frame!\r
+ """Abnormal close of connection. (MUST NOT be used as status code when sending a close)."""\r
+\r
+ CLOSE_STATUS_CODE_INVALID_PAYLOAD = 1007\r
+ """Invalid frame payload data."""\r
+\r
+ CLOSE_STATUS_CODE_POLICY_VIOLATION = 1008\r
+ """Policy violation."""\r
+\r
+ CLOSE_STATUS_CODE_MESSAGE_TOO_BIG = 1009\r
+ """Message too big."""\r
+\r
+ CLOSE_STATUS_CODE_MANDATORY_EXTENSION = 1010\r
+ """Mandatory extension."""\r
+\r
+ CLOSE_STATUS_CODE_INTERNAL_ERROR = 1011\r
+ """The peer encountered an unexpected condition or internal error."""\r
+\r
+ CLOSE_STATUS_CODE_TLS_HANDSHAKE_FAILED = 1015 # MUST NOT be set in close frame!\r
+ """TLS handshake failed, i.e. server certificate could not be verified. (MUST NOT be used as status code when sending a close)."""\r
+\r
+ CLOSE_STATUS_CODES_ALLOWED = [CLOSE_STATUS_CODE_NORMAL,\r
+ CLOSE_STATUS_CODE_GOING_AWAY,\r
+ CLOSE_STATUS_CODE_PROTOCOL_ERROR,\r
+ CLOSE_STATUS_CODE_UNSUPPORTED_DATA,\r
+ CLOSE_STATUS_CODE_INVALID_PAYLOAD,\r
+ CLOSE_STATUS_CODE_POLICY_VIOLATION,\r
+ CLOSE_STATUS_CODE_MESSAGE_TOO_BIG,\r
+ CLOSE_STATUS_CODE_MANDATORY_EXTENSION,\r
+ CLOSE_STATUS_CODE_INTERNAL_ERROR]\r
+ """Status codes allowed to send in close."""\r
+\r
+\r
+ def onOpen(self):\r
+ """\r
+ Callback when initial WebSockets handshake was completed. Now you may send messages.\r
+ Default implementation does nothing. Override in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.debugCodePaths:\r
+ log.msg("WebSocketProtocol.onOpen")\r
+\r
+\r
+ def onMessageBegin(self, opcode):\r
+ """\r
+ Callback when receiving a new message has begun. Default implementation will\r
+ prepare to buffer message frames. Override in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ :param opcode: Opcode of message.\r
+ :type opcode: int\r
+ """\r
+ self.message_opcode = opcode\r
+ self.message_data = []\r
+ self.message_data_total_length = 0\r
+\r
+\r
+ def onMessageFrameBegin(self, length, reserved):\r
+ """\r
+ Callback when receiving a new message frame has begun. Default implementation will\r
+ prepare to buffer message frame data. Override in derived class.\r
+\r
+ Modes: Hybi\r
+\r
+ :param length: Payload length of message frame which is to be received.\r
+ :type length: int\r
+ :param reserved: Reserved bits set in frame (an integer from 0 to 7).\r
+ :type reserved: int\r
+ """\r
+ self.frame_length = length\r
+ self.frame_reserved = reserved\r
+ self.frame_data = []\r
+ self.message_data_total_length += length\r
+ if not self.failedByMe:\r
+ if self.maxMessagePayloadSize > 0 and self.message_data_total_length > self.maxMessagePayloadSize:\r
+ self.wasMaxMessagePayloadSizeExceeded = True\r
+ self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_MESSAGE_TOO_BIG, "message exceeds payload limit of %d octets" % self.maxMessagePayloadSize)\r
+ elif self.maxFramePayloadSize > 0 and length > self.maxFramePayloadSize:\r
+ self.wasMaxFramePayloadSizeExceeded = True\r
+ self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_POLICY_VIOLATION, "frame exceeds payload limit of %d octets" % self.maxFramePayloadSize)\r
+\r
+\r
+ def onMessageFrameData(self, payload):\r
+ """\r
+ Callback when receiving data witin message frame. Default implementation will\r
+ buffer data for frame. Override in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, this method is slightly misnamed for historic reasons.\r
+\r
+ :param payload: Partial payload for message frame.\r
+ :type payload: str\r
+ """\r
+ if not self.failedByMe:\r
+ if self.websocket_version == 0:\r
+ self.message_data_total_length += len(payload)\r
+ if self.maxMessagePayloadSize > 0 and self.message_data_total_length > self.maxMessagePayloadSize:\r
+ self.wasMaxMessagePayloadSizeExceeded = True\r
+ self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_MESSAGE_TOO_BIG, "message exceeds payload limit of %d octets" % self.maxMessagePayloadSize)\r
+ self.message_data.append(payload)\r
+ else:\r
+ self.frame_data.append(payload)\r
+\r
+\r
+ def onMessageFrameEnd(self):\r
+ """\r
+ Callback when a message frame has been completely received. Default implementation\r
+ will flatten the buffered frame data and callback onMessageFrame. Override\r
+ in derived class.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if not self.failedByMe:\r
+ self.onMessageFrame(self.frame_data, self.frame_reserved)\r
+\r
+ self.frame_data = None\r
+\r
+\r
+ def onMessageFrame(self, payload, reserved):\r
+ """\r
+ Callback fired when complete message frame has been received. Default implementation\r
+ will buffer frame for message. Override in derived class.\r
+\r
+ Modes: Hybi\r
+\r
+ :param payload: Message frame payload.\r
+ :type payload: list of str\r
+ :param reserved: Reserved bits set in frame (an integer from 0 to 7).\r
+ :type reserved: int\r
+ """\r
+ if not self.failedByMe:\r
+ self.message_data.extend(payload)\r
+\r
+\r
+ def onMessageEnd(self):\r
+ """\r
+ Callback when a message has been completely received. Default implementation\r
+ will flatten the buffered frames and callback onMessage. Override\r
+ in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if not self.failedByMe:\r
+ payload = ''.join(self.message_data)\r
+ self.onMessage(payload, self.message_opcode == WebSocketProtocol.MESSAGE_TYPE_BINARY)\r
+\r
+ self.message_data = None\r
+\r
+\r
+ def onMessage(self, payload, binary):\r
+ """\r
+ Callback when a complete message was received. Default implementation does nothing.\r
+ Override in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ :param payload: Message payload (UTF-8 encoded text string or binary string). Can also be an empty string, when message contained no payload.\r
+ :type payload: str\r
+ :param binary: If True, payload is binary, otherwise text.\r
+ :type binary: bool\r
+ """\r
+ if self.debug:\r
+ log.msg("WebSocketProtocol.onMessage")\r
+\r
+\r
+ def onPing(self, payload):\r
+ """\r
+ Callback when Ping was received. Default implementation responds\r
+ with a Pong. Override in derived class.\r
+\r
+ Modes: Hybi\r
+\r
+ :param payload: Payload of Ping, when there was any. Can be arbitrary, up to 125 octets.\r
+ :type payload: str\r
+ """\r
+ if self.debug:\r
+ log.msg("WebSocketProtocol.onPing")\r
+ if self.state == WebSocketProtocol.STATE_OPEN:\r
+ self.sendPong(payload)\r
+\r
+\r
+ def onPong(self, payload):\r
+ """\r
+ Callback when Pong was received. Default implementation does nothing.\r
+ Override in derived class.\r
+\r
+ Modes: Hybi\r
+\r
+ :param payload: Payload of Pong, when there was any. Can be arbitrary, up to 125 octets.\r
+ """\r
+ if self.debug:\r
+ log.msg("WebSocketProtocol.onPong")\r
+\r
+\r
+ def onClose(self, wasClean, code, reason):\r
+ """\r
+ Callback when the connection has been closed. Override in derived class.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ :param wasClean: True, iff the connection was closed cleanly.\r
+ :type wasClean: bool\r
+ :param code: None or close status code (sent by peer), if there was one (:class:`WebSocketProtocol`.CLOSE_STATUS_CODE_*).\r
+ :type code: int\r
+ :param reason: None or close reason (sent by peer) (when present, a status code MUST have been also be present).\r
+ :type reason: str\r
+ """\r
+ if self.debugCodePaths:\r
+ s = "WebSocketProtocol.onClose:\n"\r
+ s += "wasClean=%s\n" % wasClean\r
+ s += "code=%s\n" % code\r
+ s += "reason=%s\n" % reason\r
+ s += "self.closedByMe=%s\n" % self.closedByMe\r
+ s += "self.failedByMe=%s\n" % self.failedByMe\r
+ s += "self.droppedByMe=%s\n" % self.droppedByMe\r
+ s += "self.wasClean=%s\n" % self.wasClean\r
+ s += "self.wasNotCleanReason=%s\n" % self.wasNotCleanReason\r
+ s += "self.localCloseCode=%s\n" % self.localCloseCode\r
+ s += "self.localCloseReason=%s\n" % self.localCloseReason\r
+ s += "self.remoteCloseCode=%s\n" % self.remoteCloseCode\r
+ s += "self.remoteCloseReason=%s\n" % self.remoteCloseReason\r
+ log.msg(s)\r
+\r
+\r
+ def onCloseFrame(self, code, reasonRaw):\r
+ """\r
+ Callback when a Close frame was received. The default implementation answers by\r
+ sending a Close when no Close was sent before. Otherwise it drops\r
+ the TCP connection either immediately (when we are a server) or after a timeout\r
+ (when we are a client and expect the server to drop the TCP).\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, this method is slightly misnamed for historic reasons.\r
+ - For Hixie mode, code and reasonRaw are silently ignored.\r
+\r
+ :param code: None or close status code, if there was one (:class:`WebSocketProtocol`.CLOSE_STATUS_CODE_*).\r
+ :type code: int\r
+ :param reason: None or close reason (when present, a status code MUST have been also be present).\r
+ :type reason: str\r
+ """\r
+ if self.debugCodePaths:\r
+ log.msg("WebSocketProtocol.onCloseFrame")\r
+\r
+ self.remoteCloseCode = code\r
+ self.remoteCloseReason = reasonRaw\r
+\r
+ ## reserved close codes: 0-999, 1004, 1005, 1006, 1011-2999, >= 5000\r
+ ##\r
+ if code is not None and (code < 1000 or (code >= 1000 and code <= 2999 and code not in WebSocketProtocol.CLOSE_STATUS_CODES_ALLOWED) or code >= 5000):\r
+ if self.protocolViolation("invalid close code %d" % code):\r
+ return True\r
+\r
+ ## closing reason\r
+ ##\r
+ if reasonRaw is not None:\r
+ ## we use our own UTF-8 validator to get consistent and fully conformant\r
+ ## UTF-8 validation behavior\r
+ u = Utf8Validator()\r
+ val = u.validate(reasonRaw)\r
+ if not val[0]:\r
+ if self.invalidPayload("invalid close reason (non-UTF-8 payload)"):\r
+ return True\r
+\r
+ if self.state == WebSocketProtocol.STATE_CLOSING:\r
+ ## We already initiated the closing handshake, so this\r
+ ## is the peer's reply to our close frame.\r
+\r
+ ## cancel any closing HS timer if present\r
+ ##\r
+ if self.closeHandshakeTimeoutCall is not None:\r
+ if self.debugCodePaths:\r
+ log.msg("closeHandshakeTimeoutCall.cancel")\r
+ self.closeHandshakeTimeoutCall.cancel()\r
+ self.closeHandshakeTimeoutCall = None\r
+\r
+ self.wasClean = True\r
+\r
+ if self.isServer:\r
+ ## When we are a server, we immediately drop the TCP.\r
+ self.dropConnection(abort = True)\r
+ else:\r
+ ## When we are a client, the server should drop the TCP\r
+ ## If that doesn't happen, we do. And that will set wasClean = False.\r
+ if self.serverConnectionDropTimeout > 0:\r
+ self.serverConnectionDropTimeoutCall = reactor.callLater(self.serverConnectionDropTimeout, self.onServerConnectionDropTimeout)\r
+\r
+ elif self.state == WebSocketProtocol.STATE_OPEN:\r
+ ## The peer initiates a closing handshake, so we reply\r
+ ## by sending close frame.\r
+\r
+ self.wasClean = True\r
+\r
+ if self.websocket_version == 0:\r
+ self.sendCloseFrame(isReply = True)\r
+ else:\r
+ ## Either reply with same code/reason, or code == NORMAL/reason=None\r
+ if self.echoCloseCodeReason:\r
+ self.sendCloseFrame(code = code, reasonUtf8 = reason.encode("UTF-8"), isReply = True)\r
+ else:\r
+ self.sendCloseFrame(code = WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL, isReply = True)\r
+\r
+ if self.isServer:\r
+ ## When we are a server, we immediately drop the TCP.\r
+ self.dropConnection(abort = False)\r
+ else:\r
+ ## When we are a client, we expect the server to drop the TCP,\r
+ ## and when the server fails to do so, a timeout in sendCloseFrame()\r
+ ## will set wasClean = False back again.\r
+ pass\r
+\r
+ else:\r
+ ## STATE_CONNECTING, STATE_CLOSED\r
+ raise Exception("logic error")\r
+\r
+\r
+ def onServerConnectionDropTimeout(self):\r
+ """\r
+ We (a client) expected the peer (a server) to drop the connection,\r
+ but it didn't (in time self.serverConnectionDropTimeout).\r
+ So we drop the connection, but set self.wasClean = False.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ self.serverConnectionDropTimeoutCall = None\r
+ if self.state != WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("onServerConnectionDropTimeout")\r
+ self.wasClean = False\r
+ self.wasNotCleanReason = "server did not drop TCP connection (in time)"\r
+ self.wasServerConnectionDropTimeout = True\r
+ self.dropConnection(abort = True)\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping onServerConnectionDropTimeout since connection is already closed")\r
+\r
+\r
+ def onOpenHandshakeTimeout(self):\r
+ """\r
+ We expected the peer to complete the opening handshake with to us.\r
+ It didn't do so (in time self.openHandshakeTimeout).\r
+ So we drop the connection, but set self.wasClean = False.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ self.openHandshakeTimeoutCall = None\r
+ if self.state == WebSocketProtocol.STATE_CONNECTING:\r
+ if self.debugCodePaths:\r
+ log.msg("onOpenHandshakeTimeout fired")\r
+ self.wasClean = False\r
+ self.wasNotCleanReason = "peer did not finish (in time) the opening handshake"\r
+ self.wasOpenHandshakeTimeout = True\r
+ self.dropConnection(abort = True)\r
+ elif self.state == WebSocketProtocol.STATE_OPEN:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping onOpenHandshakeTimeout since WebSocket connection is open (opening handshake already finished)")\r
+ elif self.state == WebSocketProtocol.STATE_CLOSING:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping onOpenHandshakeTimeout since WebSocket connection is closing")\r
+ elif self.state == WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping onOpenHandshakeTimeout since WebSocket connection already closed")\r
+ else:\r
+ # should not arrive here\r
+ raise Exception("logic error")\r
+\r
+\r
+ def onCloseHandshakeTimeout(self):\r
+ """\r
+ We expected the peer to respond to us initiating a close handshake. It didn't\r
+ respond (in time self.closeHandshakeTimeout) with a close response frame though.\r
+ So we drop the connection, but set self.wasClean = False.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ self.closeHandshakeTimeoutCall = None\r
+ if self.state != WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("onCloseHandshakeTimeout fired")\r
+ self.wasClean = False\r
+ self.wasNotCleanReason = "peer did not respond (in time) in closing handshake"\r
+ self.wasCloseHandshakeTimeout = True\r
+ self.dropConnection(abort = True)\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping onCloseHandshakeTimeout since connection is already closed")\r
+\r
+\r
+ def dropConnection(self, abort = False):\r
+ """\r
+ Drop the underlying TCP connection. For abort parameter, see:\r
+\r
+ * http://twistedmatrix.com/documents/current/core/howto/servers.html#auto2\r
+ * https://github.com/tavendo/AutobahnPython/issues/96\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("dropping connection")\r
+ self.droppedByMe = True\r
+ self.state = WebSocketProtocol.STATE_CLOSED\r
+\r
+ if abort:\r
+ self.transport.abortConnection()\r
+ else:\r
+ self.transport.loseConnection()\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping dropConnection since connection is already closed")\r
+\r
+\r
+ def failConnection(self, code = CLOSE_STATUS_CODE_GOING_AWAY, reason = "Going Away"):\r
+ """\r
+ Fails the WebSockets connection.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, the code and reason are silently ignored.\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("Failing connection : %s - %s" % (code, reason))\r
+ self.failedByMe = True\r
+ if self.failByDrop:\r
+ ## brutally drop the TCP connection\r
+ self.wasClean = False\r
+ self.wasNotCleanReason = "I failed the WebSocket connection by dropping the TCP connection"\r
+ self.dropConnection(abort = True)\r
+ else:\r
+ ## perform WebSockets closing handshake\r
+ if self.state != WebSocketProtocol.STATE_CLOSING:\r
+ self.sendCloseFrame(code = code, reasonUtf8 = reason.encode("UTF-8"), isReply = False)\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping failConnection since connection is already closing")\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipping failConnection since connection is already closed")\r
+\r
+\r
+ def protocolViolation(self, reason):\r
+ """\r
+ Fired when a WebSockets protocol violation/error occurs.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, reason is silently ignored.\r
+\r
+ :param reason: Protocol violation that was encountered (human readable).\r
+ :type reason: str\r
+\r
+ :returns: bool -- True, when any further processing should be discontinued.\r
+ """\r
+ if self.debugCodePaths:\r
+ log.msg("Protocol violation : %s" % reason)\r
+ self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, reason)\r
+ if self.failByDrop:\r
+ return True\r
+ else:\r
+ ## if we don't immediately drop the TCP, we need to skip the invalid frame\r
+ ## to continue to later receive the closing handshake reply\r
+ return False\r
+\r
+\r
+ def invalidPayload(self, reason):\r
+ """\r
+ Fired when invalid payload is encountered. Currently, this only happens\r
+ for text message when payload is invalid UTF-8 or close frames with\r
+ close reason that is invalid UTF-8.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, reason is silently ignored.\r
+\r
+ :param reason: What was invalid for the payload (human readable).\r
+ :type reason: str\r
+\r
+ :returns: bool -- True, when any further processing should be discontinued.\r
+ """\r
+ if self.debugCodePaths:\r
+ log.msg("Invalid payload : %s" % reason)\r
+ self.failConnection(WebSocketProtocol.CLOSE_STATUS_CODE_INVALID_PAYLOAD, reason)\r
+ if self.failByDrop:\r
+ return True\r
+ else:\r
+ ## if we don't immediately drop the TCP, we need to skip the invalid frame\r
+ ## to continue to later receive the closing handshake reply\r
+ return False\r
+\r
+\r
+ def connectionMade(self):\r
+ """\r
+ This is called by Twisted framework when a new TCP connection has been established\r
+ and handed over to a Protocol instance (an instance of this class).\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+\r
+ ## copy default options from factory (so we are not affected by changed on those)\r
+ ##\r
+\r
+ self.debug = self.factory.debug\r
+ self.debugCodePaths = self.factory.debugCodePaths\r
+\r
+ self.logOctets = self.factory.logOctets\r
+ self.logFrames = self.factory.logFrames\r
+\r
+ self.allowHixie76 = self.factory.allowHixie76\r
+ self.utf8validateIncoming = self.factory.utf8validateIncoming\r
+ self.applyMask = self.factory.applyMask\r
+ self.maxFramePayloadSize = self.factory.maxFramePayloadSize\r
+ self.maxMessagePayloadSize = self.factory.maxMessagePayloadSize\r
+ self.autoFragmentSize = self.factory.autoFragmentSize\r
+ self.failByDrop = self.factory.failByDrop\r
+ self.echoCloseCodeReason = self.factory.echoCloseCodeReason\r
+ self.openHandshakeTimeout = self.factory.openHandshakeTimeout\r
+ self.closeHandshakeTimeout = self.factory.closeHandshakeTimeout\r
+ self.tcpNoDelay = self.factory.tcpNoDelay\r
+\r
+ if self.isServer:\r
+ self.versions = self.factory.versions\r
+ self.webStatus = self.factory.webStatus\r
+ self.requireMaskedClientFrames = self.factory.requireMaskedClientFrames\r
+ self.maskServerFrames = self.factory.maskServerFrames\r
+ else:\r
+ self.version = self.factory.version\r
+ self.acceptMaskedServerFrames = self.factory.acceptMaskedServerFrames\r
+ self.maskClientFrames = self.factory.maskClientFrames\r
+ self.serverConnectionDropTimeout = self.factory.serverConnectionDropTimeout\r
+\r
+ ## Set "Nagle"\r
+ self.transport.setTcpNoDelay(self.tcpNoDelay)\r
+\r
+ ## the peer we are connected to\r
+ self.peer = self.transport.getPeer()\r
+ self.peerstr = "%s:%d" % (self.peer.host, self.peer.port)\r
+\r
+ ## initial state\r
+ self.state = WebSocketProtocol.STATE_CONNECTING\r
+ self.send_state = WebSocketProtocol.SEND_STATE_GROUND\r
+ self.data = ""\r
+\r
+ ## for chopped/synched sends, we need to queue to maintain\r
+ ## ordering when recalling the reactor to actually "force"\r
+ ## the octets to wire (see test/trickling in the repo)\r
+ self.send_queue = deque()\r
+ self.triggered = False\r
+\r
+ ## incremental UTF8 validator\r
+ self.utf8validator = Utf8Validator()\r
+\r
+ ## track when frame/message payload sizes (incoming) were exceeded\r
+ self.wasMaxFramePayloadSizeExceeded = False\r
+ self.wasMaxMessagePayloadSizeExceeded = False\r
+\r
+ ## the following vars are related to connection close handling/tracking\r
+\r
+ # True, iff I have initiated closing HS (that is, did send close first)\r
+ self.closedByMe = False\r
+\r
+ # True, iff I have failed the WS connection (i.e. due to protocol error)\r
+ # Failing can be either by initiating close HS or brutal drop (this is\r
+ # controlled by failByDrop option)\r
+ self.failedByMe = False\r
+\r
+ # True, iff I dropped the TCP connection (called transport.loseConnection())\r
+ self.droppedByMe = False\r
+\r
+ # True, iff full WebSockets closing handshake was performed (close frame sent\r
+ # and received) _and_ the server dropped the TCP (which is its responsibility)\r
+ self.wasClean = False\r
+\r
+ # When self.wasClean = False, the reason (what happened)\r
+ self.wasNotCleanReason = None\r
+\r
+ # When we are a client, and we expected the server to drop the TCP, but that\r
+ # didn't happen in time, this gets True\r
+ self.wasServerConnectionDropTimeout = False\r
+\r
+ # When the initial WebSocket opening handshake times out, this gets True\r
+ self.wasOpenHandshakeTimeout = False\r
+\r
+ # When we initiated a closing handshake, but the peer did not respond in\r
+ # time, this gets True\r
+ self.wasCloseHandshakeTimeout = False\r
+\r
+ # The close code I sent in close frame (if any)\r
+ self.localCloseCode = None\r
+\r
+ # The close reason I sent in close frame (if any)\r
+ self.localCloseReason = None\r
+\r
+ # The close code the peer sent me in close frame (if any)\r
+ self.remoteCloseCode = None\r
+\r
+ # The close reason the peer sent me in close frame (if any)\r
+ self.remoteCloseReason = None\r
+\r
+ # timers, which might get set up later, and remembered here to get canceled\r
+ # when appropriate\r
+ if not self.isServer:\r
+ self.serverConnectionDropTimeoutCall = None\r
+ self.openHandshakeTimeoutCall = None\r
+ self.closeHandshakeTimeoutCall = None\r
+\r
+ # set opening handshake timeout handler\r
+ if self.openHandshakeTimeout > 0:\r
+ self.openHandshakeTimeoutCall = reactor.callLater(self.openHandshakeTimeout, self.onOpenHandshakeTimeout)\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ """\r
+ This is called by Twisted framework when a TCP connection was lost.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ ## cancel any server connection drop timer if present\r
+ ##\r
+ if not self.isServer and self.serverConnectionDropTimeoutCall is not None:\r
+ if self.debugCodePaths:\r
+ log.msg("serverConnectionDropTimeoutCall.cancel")\r
+ self.serverConnectionDropTimeoutCall.cancel()\r
+ self.serverConnectionDropTimeoutCall = None\r
+\r
+ self.state = WebSocketProtocol.STATE_CLOSED\r
+ if not self.wasClean:\r
+ if not self.droppedByMe and self.wasNotCleanReason is None:\r
+ self.wasNotCleanReason = "peer dropped the TCP connection without previous WebSocket closing handshake"\r
+ self.onClose(self.wasClean, WebSocketProtocol.CLOSE_STATUS_CODE_ABNORMAL_CLOSE, "connection was closed uncleanly (%s)" % self.wasNotCleanReason)\r
+ else:\r
+ self.onClose(self.wasClean, self.remoteCloseCode, self.remoteCloseReason)\r
+\r
+\r
+ def logRxOctets(self, data):\r
+ """\r
+ Hook fired right after raw octets have been received, but only when self.logOctets == True.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ log.msg("RX Octets from %s : octets = %s" % (self.peerstr, binascii.b2a_hex(data)))\r
+\r
+\r
+ def logTxOctets(self, data, sync):\r
+ """\r
+ Hook fired right after raw octets have been sent, but only when self.logOctets == True.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ log.msg("TX Octets to %s : sync = %s, octets = %s" % (self.peerstr, sync, binascii.b2a_hex(data)))\r
+\r
+\r
+ def logRxFrame(self, frameHeader, payload):\r
+ """\r
+ Hook fired right after WebSocket frame has been received and decoded, but only when self.logFrames == True.\r
+\r
+ Modes: Hybi\r
+ """\r
+ data = ''.join(payload)\r
+ info = (self.peerstr,\r
+ frameHeader.fin,\r
+ frameHeader.rsv,\r
+ frameHeader.opcode,\r
+ binascii.b2a_hex(frameHeader.mask) if frameHeader.mask else "-",\r
+ frameHeader.length,\r
+ data if frameHeader.opcode == 1 else binascii.b2a_hex(data))\r
+\r
+ log.msg("RX Frame from %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, payload = %s" % info)\r
+\r
+\r
+ def logTxFrame(self, frameHeader, payload, repeatLength, chopsize, sync):\r
+ """\r
+ Hook fired right after WebSocket frame has been encoded and sent, but only when self.logFrames == True.\r
+\r
+ Modes: Hybi\r
+ """\r
+ info = (self.peerstr,\r
+ frameHeader.fin,\r
+ frameHeader.rsv,\r
+ frameHeader.opcode,\r
+ binascii.b2a_hex(frameHeader.mask) if frameHeader.mask else "-",\r
+ frameHeader.length,\r
+ repeatLength,\r
+ chopsize,\r
+ sync,\r
+ payload if frameHeader.opcode == 1 else binascii.b2a_hex(payload))\r
+\r
+ log.msg("TX Frame to %s : fin = %s, rsv = %s, opcode = %s, mask = %s, length = %s, repeat_length = %s, chopsize = %s, sync = %s, payload = %s" % info)\r
+\r
+\r
+ def dataReceived(self, data):\r
+ """\r
+ This is called by Twisted framework upon receiving data on TCP connection.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.logOctets:\r
+ self.logRxOctets(data)\r
+ self.data += data\r
+ self.consumeData()\r
+\r
+\r
+ def consumeData(self):\r
+ """\r
+ Consume buffered (incoming) data.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+\r
+ ## WebSocket is open (handshake was completed) or close was sent\r
+ ##\r
+ if self.state == WebSocketProtocol.STATE_OPEN or self.state == WebSocketProtocol.STATE_CLOSING:\r
+\r
+ ## process until no more buffered data left or WS was closed\r
+ ##\r
+ while self.processData() and self.state != WebSocketProtocol.STATE_CLOSED:\r
+ pass\r
+\r
+ ## WebSocket needs handshake\r
+ ##\r
+ elif self.state == WebSocketProtocol.STATE_CONNECTING:\r
+\r
+ ## the implementation of processHandshake() in derived\r
+ ## class needs to perform client or server handshake\r
+ ## from other party here ..\r
+ ##\r
+ self.processHandshake()\r
+\r
+ ## we failed the connection .. don't process any more data!\r
+ ##\r
+ elif self.state == WebSocketProtocol.STATE_CLOSED:\r
+\r
+ ## ignore any data received after WS was closed\r
+ ##\r
+ if self.debugCodePaths:\r
+ log.msg("received data in STATE_CLOSED")\r
+\r
+ ## should not arrive here (invalid state)\r
+ ##\r
+ else:\r
+ raise Exception("invalid state")\r
+\r
+\r
+ def processHandshake(self):\r
+ """\r
+ Process WebSockets handshake.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ raise Exception("must implement handshake (client or server) in derived class")\r
+\r
+\r
+ def registerProducer(self, producer, streaming):\r
+ """\r
+ Register a Twisted producer with this protocol.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ :param producer: A Twisted push or pull producer.\r
+ :type producer: object\r
+ :param streaming: Producer type.\r
+ :type streaming: bool\r
+ """\r
+ self.transport.registerProducer(producer, streaming)\r
+\r
+\r
+ def _trigger(self):\r
+ """\r
+ Trigger sending stuff from send queue (which is only used for chopped/synched writes).\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if not self.triggered:\r
+ self.triggered = True\r
+ self._send()\r
+\r
+\r
+ def _send(self):\r
+ """\r
+ Send out stuff from send queue. For details how this works, see test/trickling\r
+ in the repo.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if len(self.send_queue) > 0:\r
+ e = self.send_queue.popleft()\r
+ if self.state != WebSocketProtocol.STATE_CLOSED:\r
+ self.transport.write(e[0])\r
+ if self.logOctets:\r
+ self.logTxOctets(e[0], e[1])\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("skipped delayed write, since connection is closed")\r
+ # we need to reenter the reactor to make the latter\r
+ # reenter the OS network stack, so that octets\r
+ # can get on the wire. Note: this is a "heuristic",\r
+ # since there is no (easy) way to really force out\r
+ # octets from the OS network stack to wire.\r
+ reactor.callLater(WebSocketProtocol.QUEUED_WRITE_DELAY, self._send)\r
+ else:\r
+ self.triggered = False\r
+\r
+\r
+ def sendData(self, data, sync = False, chopsize = None):\r
+ """\r
+ Wrapper for self.transport.write which allows to give a chopsize.\r
+ When asked to chop up writing to TCP stream, we write only chopsize octets\r
+ and then give up control to select() in underlying reactor so that bytes\r
+ get onto wire immediately. Note that this is different from and unrelated\r
+ to WebSockets data message fragmentation. Note that this is also different\r
+ from the TcpNoDelay option which can be set on the socket.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if chopsize and chopsize > 0:\r
+ i = 0\r
+ n = len(data)\r
+ done = False\r
+ while not done:\r
+ j = i + chopsize\r
+ if j >= n:\r
+ done = True\r
+ j = n\r
+ self.send_queue.append((data[i:j], True))\r
+ i += chopsize\r
+ self._trigger()\r
+ else:\r
+ if sync or len(self.send_queue) > 0:\r
+ self.send_queue.append((data, sync))\r
+ self._trigger()\r
+ else:\r
+ self.transport.write(data)\r
+ if self.logOctets:\r
+ self.logTxOctets(data, False)\r
+\r
+\r
+ def sendPreparedMessage(self, preparedMsg):\r
+ """\r
+ Send a message that was previously prepared with\r
+ WebSocketFactory.prepareMessage().\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.websocket_version == 0:\r
+ self.sendData(preparedMsg.payloadHixie)\r
+ else:\r
+ self.sendData(preparedMsg.payloadHybi)\r
+\r
+\r
+ def processData(self):\r
+ """\r
+ After WebSockets handshake has been completed, this procedure will do all\r
+ subsequent processing of incoming bytes.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.websocket_version == 0:\r
+ return self.processDataHixie76()\r
+ else:\r
+ return self.processDataHybi()\r
+\r
+\r
+ def processDataHixie76(self):\r
+ """\r
+ Hixie-76 incoming data processing.\r
+\r
+ Modes: Hixie\r
+ """\r
+ buffered_len = len(self.data)\r
+\r
+ ## outside a message, that is we are awaiting data which starts a new message\r
+ ##\r
+ if not self.inside_message:\r
+ if buffered_len >= 2:\r
+\r
+ ## new message\r
+ ##\r
+ if self.data[0] == '\x00':\r
+\r
+ self.inside_message = True\r
+\r
+ if self.utf8validateIncoming:\r
+ self.utf8validator.reset()\r
+ self.utf8validateIncomingCurrentMessage = True\r
+ self.utf8validateLast = (True, True, 0, 0)\r
+ else:\r
+ self.utf8validateIncomingCurrentMessage = False\r
+\r
+ self.data = self.data[1:]\r
+ self.onMessageBegin(1)\r
+\r
+ ## Hixie close from peer received\r
+ ##\r
+ elif self.data[0] == '\xff' and self.data[1] == '\x00':\r
+ self.onCloseFrame()\r
+ self.data = self.data[2:]\r
+ # stop receiving/processing after having received close!\r
+ return False\r
+\r
+ ## malformed data\r
+ ##\r
+ else:\r
+ if self.protocolViolation("malformed data received"):\r
+ return False\r
+ else:\r
+ ## need more data\r
+ return False\r
+\r
+ end_index = self.data.find('\xff')\r
+ if end_index > 0:\r
+ payload = self.data[:end_index]\r
+ self.data = self.data[end_index + 1:]\r
+ else:\r
+ payload = self.data\r
+ self.data = ''\r
+\r
+ ## incrementally validate UTF-8 payload\r
+ ##\r
+ if self.utf8validateIncomingCurrentMessage:\r
+ self.utf8validateLast = self.utf8validator.validate(payload)\r
+ if not self.utf8validateLast[0]:\r
+ if self.invalidPayload("encountered invalid UTF-8 while processing text message at payload octet index %d" % self.utf8validateLast[3]):\r
+ return False\r
+\r
+ self.onMessageFrameData(payload)\r
+\r
+ if end_index > 0:\r
+ self.inside_message = False\r
+ self.onMessageEnd()\r
+\r
+ return len(self.data) > 0\r
+\r
+\r
+ def processDataHybi(self):\r
+ """\r
+ RFC6455/Hybi-Drafts incoming data processing.\r
+\r
+ Modes: Hybi\r
+ """\r
+ buffered_len = len(self.data)\r
+\r
+ ## outside a frame, that is we are awaiting data which starts a new frame\r
+ ##\r
+ if self.current_frame is None:\r
+\r
+ ## need minimum of 2 octets to for new frame\r
+ ##\r
+ if buffered_len >= 2:\r
+\r
+ ## FIN, RSV, OPCODE\r
+ ##\r
+ b = ord(self.data[0])\r
+ frame_fin = (b & 0x80) != 0\r
+ frame_rsv = (b & 0x70) >> 4\r
+ frame_opcode = b & 0x0f\r
+\r
+ ## MASK, PAYLOAD LEN 1\r
+ ##\r
+ b = ord(self.data[1])\r
+ frame_masked = (b & 0x80) != 0\r
+ frame_payload_len1 = b & 0x7f\r
+\r
+ ## MUST be 0 when no extension defining\r
+ ## the semantics of RSV has been negotiated\r
+ ##\r
+ if frame_rsv != 0:\r
+ if self.protocolViolation("RSV != 0 and no extension negotiated"):\r
+ return False\r
+\r
+ ## all client-to-server frames MUST be masked\r
+ ##\r
+ if self.isServer and self.requireMaskedClientFrames and not frame_masked:\r
+ if self.protocolViolation("unmasked client-to-server frame"):\r
+ return False\r
+\r
+ ## all server-to-client frames MUST NOT be masked\r
+ ##\r
+ if not self.isServer and not self.acceptMaskedServerFrames and frame_masked:\r
+ if self.protocolViolation("masked server-to-client frame"):\r
+ return False\r
+\r
+ ## check frame\r
+ ##\r
+ if frame_opcode > 7: # control frame (have MSB in opcode set)\r
+\r
+ ## control frames MUST NOT be fragmented\r
+ ##\r
+ if not frame_fin:\r
+ if self.protocolViolation("fragmented control frame"):\r
+ return False\r
+\r
+ ## control frames MUST have payload 125 octets or less\r
+ ##\r
+ if frame_payload_len1 > 125:\r
+ if self.protocolViolation("control frame with payload length > 125 octets"):\r
+ return False\r
+\r
+ ## check for reserved control frame opcodes\r
+ ##\r
+ if frame_opcode not in [8, 9, 10]:\r
+ if self.protocolViolation("control frame using reserved opcode %d" % frame_opcode):\r
+ return False\r
+\r
+ ## close frame : if there is a body, the first two bytes of the body MUST be a 2-byte\r
+ ## unsigned integer (in network byte order) representing a status code\r
+ ##\r
+ if frame_opcode == 8 and frame_payload_len1 == 1:\r
+ if self.protocolViolation("received close control frame with payload len 1"):\r
+ return False\r
+\r
+ else: # data frame\r
+\r
+ ## check for reserved data frame opcodes\r
+ ##\r
+ if frame_opcode not in [0, 1, 2]:\r
+ if self.protocolViolation("data frame using reserved opcode %d" % frame_opcode):\r
+ return False\r
+\r
+ ## check opcode vs message fragmentation state 1/2\r
+ ##\r
+ if not self.inside_message and frame_opcode == 0:\r
+ if self.protocolViolation("received continuation data frame outside fragmented message"):\r
+ return False\r
+\r
+ ## check opcode vs message fragmentation state 2/2\r
+ ##\r
+ if self.inside_message and frame_opcode != 0:\r
+ if self.protocolViolation("received non-continuation data frame while inside fragmented message"):\r
+ return False\r
+\r
+ ## compute complete header length\r
+ ##\r
+ if frame_masked:\r
+ mask_len = 4\r
+ else:\r
+ mask_len = 0\r
+\r
+ if frame_payload_len1 < 126:\r
+ frame_header_len = 2 + mask_len\r
+ elif frame_payload_len1 == 126:\r
+ frame_header_len = 2 + 2 + mask_len\r
+ elif frame_payload_len1 == 127:\r
+ frame_header_len = 2 + 8 + mask_len\r
+ else:\r
+ raise Exception("logic error")\r
+\r
+ ## only proceed when we have enough data buffered for complete\r
+ ## frame header (which includes extended payload len + mask)\r
+ ##\r
+ if buffered_len >= frame_header_len:\r
+\r
+ ## minimum frame header length (already consumed)\r
+ ##\r
+ i = 2\r
+\r
+ ## extract extended payload length\r
+ ##\r
+ if frame_payload_len1 == 126:\r
+ frame_payload_len = struct.unpack("!H", self.data[i:i+2])[0]\r
+ if frame_payload_len < 126:\r
+ if self.protocolViolation("invalid data frame length (not using minimal length encoding)"):\r
+ return False\r
+ i += 2\r
+ elif frame_payload_len1 == 127:\r
+ frame_payload_len = struct.unpack("!Q", self.data[i:i+8])[0]\r
+ if frame_payload_len > 0x7FFFFFFFFFFFFFFF: # 2**63\r
+ if self.protocolViolation("invalid data frame length (>2^63)"):\r
+ return False\r
+ if frame_payload_len < 65536:\r
+ if self.protocolViolation("invalid data frame length (not using minimal length encoding)"):\r
+ return False\r
+ i += 8\r
+ else:\r
+ frame_payload_len = frame_payload_len1\r
+\r
+ ## when payload is masked, extract frame mask\r
+ ##\r
+ frame_mask = None\r
+ if frame_masked:\r
+ frame_mask = self.data[i:i+4]\r
+ i += 4\r
+\r
+ if frame_masked and frame_payload_len > 0 and self.applyMask:\r
+ if frame_payload_len < WebSocketProtocol.PAYLOAD_LEN_XOR_BREAKEVEN:\r
+ self.current_frame_masker = XorMaskerSimple(frame_mask)\r
+ else:\r
+ self.current_frame_masker = XorMaskerShifted1(frame_mask)\r
+ else:\r
+ self.current_frame_masker = XorMaskerNull()\r
+\r
+\r
+ ## remember rest (payload of current frame after header and everything thereafter)\r
+ ##\r
+ self.data = self.data[i:]\r
+\r
+ ## ok, got complete frame header\r
+ ##\r
+ self.current_frame = FrameHeader(frame_opcode,\r
+ frame_fin,\r
+ frame_rsv,\r
+ frame_payload_len,\r
+ frame_mask)\r
+\r
+ ## process begin on new frame\r
+ ##\r
+ self.onFrameBegin()\r
+\r
+ ## reprocess when frame has no payload or and buffered data left\r
+ ##\r
+ return frame_payload_len == 0 or len(self.data) > 0\r
+\r
+ else:\r
+ return False # need more data\r
+ else:\r
+ return False # need more data\r
+\r
+ ## inside a started frame\r
+ ##\r
+ else:\r
+\r
+ ## cut out rest of frame payload\r
+ ##\r
+ rest = self.current_frame.length - self.current_frame_masker.pointer()\r
+ if buffered_len >= rest:\r
+ data = self.data[:rest]\r
+ length = rest\r
+ self.data = self.data[rest:]\r
+ else:\r
+ data = self.data\r
+ length = buffered_len\r
+ self.data = ""\r
+\r
+ if length > 0:\r
+ ## unmask payload\r
+ ##\r
+ payload = self.current_frame_masker.process(data)\r
+\r
+ ## process frame data\r
+ ##\r
+ fr = self.onFrameData(payload)\r
+ if fr == False:\r
+ return False\r
+\r
+ ## fire frame end handler when frame payload is complete\r
+ ##\r
+ if self.current_frame_masker.pointer() == self.current_frame.length:\r
+ fr = self.onFrameEnd()\r
+ if fr == False:\r
+ return False\r
+\r
+ ## reprocess when no error occurred and buffered data left\r
+ ##\r
+ return len(self.data) > 0\r
+\r
+\r
+ def onFrameBegin(self):\r
+ """\r
+ Begin of receive new frame.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if self.current_frame.opcode > 7:\r
+ self.control_frame_data = []\r
+ else:\r
+ ## new message started\r
+ ##\r
+ if not self.inside_message:\r
+\r
+ self.inside_message = True\r
+\r
+ if self.current_frame.opcode == WebSocketProtocol.MESSAGE_TYPE_TEXT and self.utf8validateIncoming:\r
+ self.utf8validator.reset()\r
+ self.utf8validateIncomingCurrentMessage = True\r
+ self.utf8validateLast = (True, True, 0, 0)\r
+ else:\r
+ self.utf8validateIncomingCurrentMessage = False\r
+\r
+ self.onMessageBegin(self.current_frame.opcode)\r
+\r
+ self.onMessageFrameBegin(self.current_frame.length, self.current_frame.rsv)\r
+\r
+\r
+ def onFrameData(self, payload):\r
+ """\r
+ New data received within frame.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if self.current_frame.opcode > 7:\r
+ self.control_frame_data.append(payload)\r
+ else:\r
+ ## incrementally validate UTF-8 payload\r
+ ##\r
+ if self.utf8validateIncomingCurrentMessage:\r
+ self.utf8validateLast = self.utf8validator.validate(payload)\r
+ if not self.utf8validateLast[0]:\r
+ if self.invalidPayload("encountered invalid UTF-8 while processing text message at payload octet index %d" % self.utf8validateLast[3]):\r
+ return False\r
+\r
+ self.onMessageFrameData(payload)\r
+\r
+\r
+ def onFrameEnd(self):\r
+ """\r
+ End of frame received.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if self.current_frame.opcode > 7:\r
+ if self.logFrames:\r
+ self.logRxFrame(self.current_frame, self.control_frame_data)\r
+ self.processControlFrame()\r
+ else:\r
+ if self.logFrames:\r
+ self.logRxFrame(self.current_frame, self.frame_data)\r
+ self.onMessageFrameEnd()\r
+ if self.current_frame.fin:\r
+ if self.utf8validateIncomingCurrentMessage:\r
+ if not self.utf8validateLast[1]:\r
+ if self.invalidPayload("UTF-8 text message payload ended within Unicode code point at payload octet index %d" % self.utf8validateLast[3]):\r
+ return False\r
+ self.onMessageEnd()\r
+ self.inside_message = False\r
+ self.current_frame = None\r
+\r
+\r
+ def processControlFrame(self):\r
+ """\r
+ Process a completely received control frame.\r
+\r
+ Modes: Hybi\r
+ """\r
+\r
+ payload = ''.join(self.control_frame_data)\r
+ self.control_frame_data = None\r
+\r
+ ## CLOSE frame\r
+ ##\r
+ if self.current_frame.opcode == 8:\r
+\r
+ code = None\r
+ reasonRaw = None\r
+ ll = len(payload)\r
+ if ll > 1:\r
+ code = struct.unpack("!H", payload[0:2])[0]\r
+ if ll > 2:\r
+ reasonRaw = payload[2:]\r
+\r
+ if self.onCloseFrame(code, reasonRaw):\r
+ return False\r
+\r
+ ## PING frame\r
+ ##\r
+ elif self.current_frame.opcode == 9:\r
+ self.onPing(payload)\r
+\r
+ ## PONG frame\r
+ ##\r
+ elif self.current_frame.opcode == 10:\r
+ self.onPong(payload)\r
+\r
+ else:\r
+ ## we might arrive here, when protocolViolation\r
+ ## wants us to continue anyway\r
+ pass\r
+\r
+ return True\r
+\r
+\r
+ def sendFrame(self, opcode, payload = "", fin = True, rsv = 0, mask = None, payload_len = None, chopsize = None, sync = False):\r
+ """\r
+ Send out frame. Normally only used internally via sendMessage(), sendPing(), sendPong() and sendClose().\r
+\r
+ This method deliberately allows to send invalid frames (that is frames invalid\r
+ per-se, or frames invalid because of protocol state). Other than in fuzzing servers,\r
+ calling methods will ensure that no invalid frames are sent.\r
+\r
+ In addition, this method supports explicit specification of payload length.\r
+ When payload_len is given, it will always write that many octets to the stream.\r
+ It'll wrap within payload, resending parts of that when more octets were requested\r
+ The use case is again for fuzzing server which want to sent increasing amounts\r
+ of payload data to peers without having to construct potentially large messges\r
+ themselfes.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+\r
+ if payload_len is not None:\r
+ if len(payload) < 1:\r
+ raise Exception("cannot construct repeated payload with length %d from payload of length %d" % (payload_len, len(payload)))\r
+ l = payload_len\r
+ pl = ''.join([payload for k in range(payload_len / len(payload))]) + payload[:payload_len % len(payload)]\r
+ else:\r
+ l = len(payload)\r
+ pl = payload\r
+\r
+ ## first byte\r
+ ##\r
+ b0 = 0\r
+ if fin:\r
+ b0 |= (1 << 7)\r
+ b0 |= (rsv % 8) << 4\r
+ b0 |= opcode % 128\r
+\r
+ ## second byte, payload len bytes and mask\r
+ ##\r
+ b1 = 0\r
+ if mask or (not self.isServer and self.maskClientFrames) or (self.isServer and self.maskServerFrames):\r
+ b1 |= 1 << 7\r
+ if not mask:\r
+ mask = struct.pack("!I", random.getrandbits(32))\r
+ mv = mask\r
+ else:\r
+ mv = ""\r
+\r
+ ## mask frame payload\r
+ ##\r
+ if l > 0 and self.applyMask:\r
+ if l < WebSocketProtocol.PAYLOAD_LEN_XOR_BREAKEVEN:\r
+ masker = XorMaskerSimple(mask)\r
+ else:\r
+ masker = XorMaskerShifted1(mask)\r
+ plm = masker.process(pl)\r
+ else:\r
+ plm = pl\r
+\r
+ else:\r
+ mv = ""\r
+ plm = pl\r
+\r
+ el = ""\r
+ if l <= 125:\r
+ b1 |= l\r
+ elif l <= 0xFFFF:\r
+ b1 |= 126\r
+ el = struct.pack("!H", l)\r
+ elif l <= 0x7FFFFFFFFFFFFFFF:\r
+ b1 |= 127\r
+ el = struct.pack("!Q", l)\r
+ else:\r
+ raise Exception("invalid payload length")\r
+\r
+ raw = ''.join([chr(b0), chr(b1), el, mv, plm])\r
+\r
+ if self.logFrames:\r
+ frameHeader = FrameHeader(opcode, fin, rsv, l, mask)\r
+ self.logTxFrame(frameHeader, payload, payload_len, chopsize, sync)\r
+\r
+ ## send frame octets\r
+ ##\r
+ self.sendData(raw, sync, chopsize)\r
+\r
+\r
+ def sendPing(self, payload = None):\r
+ """\r
+ Send out Ping to peer. A peer is expected to Pong back the payload a soon\r
+ as "practical". When more than 1 Ping is outstanding at a peer, the peer may\r
+ elect to respond only to the last Ping.\r
+\r
+ Modes: Hybi\r
+\r
+ :param payload: An optional, arbitrary payload of length < 126 octets.\r
+ :type payload: str\r
+ """\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ if payload:\r
+ l = len(payload)\r
+ if l > 125:\r
+ raise Exception("invalid payload for PING (payload length must be <= 125, was %d)" % l)\r
+ self.sendFrame(opcode = 9, payload = payload)\r
+ else:\r
+ self.sendFrame(opcode = 9)\r
+\r
+\r
+ def sendPong(self, payload = None):\r
+ """\r
+ Send out Pong to peer. A Pong frame MAY be sent unsolicited.\r
+ This serves as a unidirectional heartbeat. A response to an unsolicited pong is "not expected".\r
+\r
+ Modes: Hybi\r
+\r
+ :param payload: An optional, arbitrary payload of length < 126 octets.\r
+ :type payload: str\r
+ """\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ if payload:\r
+ l = len(payload)\r
+ if l > 125:\r
+ raise Exception("invalid payload for PONG (payload length must be <= 125, was %d)" % l)\r
+ self.sendFrame(opcode = 10, payload = payload)\r
+ else:\r
+ self.sendFrame(opcode = 10)\r
+\r
+\r
+ def sendCloseFrame(self, code = None, reasonUtf8 = None, isReply = False):\r
+ """\r
+ Send a close frame and update protocol state. Note, that this is\r
+ an internal method which deliberately allows not send close\r
+ frame with invalid payload.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, this method is slightly misnamed for historic reasons.\r
+ - For Hixie mode, code and reasonUtf8 will be silently ignored.\r
+ """\r
+ if self.state == WebSocketProtocol.STATE_CLOSING:\r
+ if self.debugCodePaths:\r
+ log.msg("ignoring sendCloseFrame since connection is closing")\r
+\r
+ elif self.state == WebSocketProtocol.STATE_CLOSED:\r
+ if self.debugCodePaths:\r
+ log.msg("ignoring sendCloseFrame since connection already closed")\r
+\r
+ elif self.state == WebSocketProtocol.STATE_CONNECTING:\r
+ raise Exception("cannot close a connection not yet connected")\r
+\r
+ elif self.state == WebSocketProtocol.STATE_OPEN:\r
+\r
+ if self.websocket_version == 0:\r
+ self.sendData("\xff\x00")\r
+ else:\r
+ ## construct Hybi close frame payload and send frame\r
+ payload = ""\r
+ if code is not None:\r
+ payload += struct.pack("!H", code)\r
+ if reasonUtf8 is not None:\r
+ payload += reasonUtf8\r
+ self.sendFrame(opcode = 8, payload = payload)\r
+\r
+ ## update state\r
+ self.state = WebSocketProtocol.STATE_CLOSING\r
+ self.closedByMe = not isReply\r
+\r
+ ## remember payload of close frame we sent\r
+ self.localCloseCode = code\r
+ self.localCloseReason = reasonUtf8\r
+\r
+ ## drop connection when timeout on receiving close handshake reply\r
+ if self.closedByMe and self.closeHandshakeTimeout > 0:\r
+ self.closeHandshakeTimeoutCall = reactor.callLater(self.closeHandshakeTimeout, self.onCloseHandshakeTimeout)\r
+\r
+ else:\r
+ raise Exception("logic error")\r
+\r
+\r
+ def sendClose(self, code = None, reason = None):\r
+ """\r
+ Starts a closing handshake.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, code and reason will be silently ignored.\r
+\r
+ :param code: An optional close status code (:class:`WebSocketProtocol`.CLOSE_STATUS_CODE_NORMAL or 3000-4999).\r
+ :type code: int\r
+ :param reason: An optional close reason (a string that when present, a status code MUST also be present).\r
+ :type reason: str\r
+ """\r
+ if code is not None:\r
+ if type(code) != int:\r
+ raise Exception("invalid type %s for close code" % type(code))\r
+ if code != 1000 and not (code >= 3000 and code <= 4999):\r
+ raise Exception("invalid close code %d" % code)\r
+ if reason is not None:\r
+ if code is None:\r
+ raise Exception("close reason without close code")\r
+ if type(reason) not in [str, unicode]:\r
+ raise Exception("invalid type %s for close reason" % type(reason))\r
+ reasonUtf8 = reason.encode("UTF-8")\r
+ if len(reasonUtf8) + 2 > 125:\r
+ raise Exception("close reason too long (%d)" % len(reasonUtf8))\r
+ else:\r
+ reasonUtf8 = None\r
+ self.sendCloseFrame(code = code, reasonUtf8 = reasonUtf8, isReply = False)\r
+\r
+\r
+ def beginMessage(self, opcode = MESSAGE_TYPE_TEXT):\r
+ """\r
+ Begin sending new message.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ :param opcode: Message type, normally either WebSocketProtocol.MESSAGE_TYPE_TEXT (default) or\r
+ WebSocketProtocol.MESSAGE_TYPE_BINARY (only Hybi mode).\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+\r
+ ## check if sending state is valid for this method\r
+ ##\r
+ if self.send_state != WebSocketProtocol.SEND_STATE_GROUND:\r
+ raise Exception("WebSocketProtocol.beginMessage invalid in current sending state")\r
+\r
+ if self.websocket_version == 0:\r
+ if opcode != 1:\r
+ raise Exception("cannot send non-text message in Hixie mode")\r
+\r
+ self.sendData('\x00')\r
+ self.send_state = WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE\r
+ else:\r
+ if opcode not in [1, 2]:\r
+ raise Exception("use of reserved opcode %d" % opcode)\r
+\r
+ ## remember opcode for later (when sending first frame)\r
+ ##\r
+ self.send_message_opcode = opcode\r
+ self.send_state = WebSocketProtocol.SEND_STATE_MESSAGE_BEGIN\r
+\r
+\r
+\r
+ def beginMessageFrame(self, length, reserved = 0, mask = None):\r
+ """\r
+ Begin sending new message frame.\r
+\r
+ Modes: Hybi\r
+\r
+ :param length: Length of frame which is started. Must be >= 0 and <= 2^63.\r
+ :type length: int\r
+ :param reserved: Reserved bits for frame (an integer from 0 to 7). Note that reserved != 0 is only legal when an extension has been negoiated which defines semantics.\r
+ :type reserved: int\r
+ :param mask: Optional frame mask. When given, this is used. When None and the peer is a client, a mask will be internally generated. For servers None is default.\r
+ :type mask: str\r
+ """\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ ## check if sending state is valid for this method\r
+ ##\r
+ if self.send_state not in [WebSocketProtocol.SEND_STATE_MESSAGE_BEGIN, WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE]:\r
+ raise Exception("WebSocketProtocol.beginMessageFrame invalid in current sending state")\r
+\r
+ if (not type(length) in [int, long]) or length < 0 or length > 0x7FFFFFFFFFFFFFFF: # 2**63\r
+ raise Exception("invalid value for message frame length")\r
+\r
+ if type(reserved) is not int or reserved < 0 or reserved > 7:\r
+ raise Exception("invalid value for reserved bits")\r
+\r
+ self.send_message_frame_length = length\r
+\r
+ if mask:\r
+ ## explicit mask given\r
+ ##\r
+ assert type(mask) == str\r
+ assert len(mask) == 4\r
+ self.send_message_frame_mask = mask\r
+\r
+ elif (not self.isServer and self.maskClientFrames) or (self.isServer and self.maskServerFrames):\r
+ ## automatic mask:\r
+ ## - client-to-server masking (if not deactivated)\r
+ ## - server-to-client masking (if activated)\r
+ ##\r
+ self.send_message_frame_mask = struct.pack("!I", random.getrandbits(32))\r
+\r
+ else:\r
+ ## no mask\r
+ ##\r
+ self.send_message_frame_mask = None\r
+\r
+ ## payload masker\r
+ ##\r
+ if self.send_message_frame_mask and length > 0 and self.applyMask:\r
+ if length < WebSocketProtocol.PAYLOAD_LEN_XOR_BREAKEVEN:\r
+ self.send_message_frame_masker = XorMaskerSimple(self.send_message_frame_mask)\r
+ else:\r
+ self.send_message_frame_masker = XorMaskerShifted1(self.send_message_frame_mask)\r
+ else:\r
+ self.send_message_frame_masker = XorMaskerNull()\r
+\r
+ ## first byte\r
+ ##\r
+ b0 = (reserved % 8) << 4 # FIN = false .. since with streaming, we don't know when message ends\r
+\r
+ if self.send_state == WebSocketProtocol.SEND_STATE_MESSAGE_BEGIN:\r
+ self.send_state = WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE\r
+ b0 |= self.send_message_opcode % 128\r
+ else:\r
+ pass # message continuation frame\r
+\r
+ ## second byte, payload len bytes and mask\r
+ ##\r
+ b1 = 0\r
+ if self.send_message_frame_mask:\r
+ b1 |= 1 << 7\r
+ mv = self.send_message_frame_mask\r
+ else:\r
+ mv = ""\r
+\r
+ el = ""\r
+ if length <= 125:\r
+ b1 |= length\r
+ elif length <= 0xFFFF:\r
+ b1 |= 126\r
+ el = struct.pack("!H", length)\r
+ elif length <= 0x7FFFFFFFFFFFFFFF:\r
+ b1 |= 127\r
+ el = struct.pack("!Q", length)\r
+ else:\r
+ raise Exception("invalid payload length")\r
+\r
+ ## write message frame header\r
+ ##\r
+ header = ''.join([chr(b0), chr(b1), el, mv])\r
+ self.sendData(header)\r
+\r
+ ## now we are inside message frame ..\r
+ ##\r
+ self.send_state = WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE_FRAME\r
+\r
+\r
+ def sendMessageFrameData(self, payload, sync = False):\r
+ """\r
+ Send out data when within message frame (message was begun, frame was begun).\r
+ Note that the frame is automatically ended when enough data has been sent\r
+ that is, there is no endMessageFrame, since you have begun the frame specifying\r
+ the frame length, which implicitly defined the frame end. This is different from\r
+ messages, which you begin and end, since a message can contain an unlimited number\r
+ of frames.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Notes:\r
+ - For Hixie mode, this method is slightly misnamed for historic reasons.\r
+\r
+ :param payload: Data to send.\r
+\r
+ :returns: int -- Hybi mode: when frame still incomplete, returns outstanding octets, when frame complete, returns <= 0, when < 0, the amount of unconsumed data in payload argument. Hixie mode: returns None.\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+\r
+ if self.websocket_version == 0:\r
+ ## Hixie Mode\r
+ ##\r
+ if self.send_state != WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE:\r
+ raise Exception("WebSocketProtocol.sendMessageFrameData invalid in current sending state")\r
+ self.sendData(payload, sync = sync)\r
+ return None\r
+\r
+ else:\r
+ ## Hybi Mode\r
+ ##\r
+ if self.send_state != WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE_FRAME:\r
+ raise Exception("WebSocketProtocol.sendMessageFrameData invalid in current sending state")\r
+\r
+ rl = len(payload)\r
+ if self.send_message_frame_masker.pointer() + rl > self.send_message_frame_length:\r
+ l = self.send_message_frame_length - self.send_message_frame_masker.pointer()\r
+ rest = -(rl - l)\r
+ pl = payload[:l]\r
+ else:\r
+ l = rl\r
+ rest = self.send_message_frame_length - self.send_message_frame_masker.pointer() - l\r
+ pl = payload\r
+\r
+ ## mask frame payload\r
+ ##\r
+ plm = self.send_message_frame_masker.process(pl)\r
+\r
+ ## send frame payload\r
+ ##\r
+ self.sendData(plm, sync = sync)\r
+\r
+ ## if we are done with frame, move back into "inside message" state\r
+ ##\r
+ if self.send_message_frame_masker.pointer() >= self.send_message_frame_length:\r
+ self.send_state = WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE\r
+\r
+ ## when =0 : frame was completed exactly\r
+ ## when >0 : frame is still uncomplete and that much amount is still left to complete the frame\r
+ ## when <0 : frame was completed and there was this much unconsumed data in payload argument\r
+ ##\r
+ return rest\r
+\r
+\r
+ def endMessage(self):\r
+ """\r
+ End a previously begun message. No more frames may be sent (for that message). You have to\r
+ begin a new message before sending again.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ ## check if sending state is valid for this method\r
+ ##\r
+ if self.send_state != WebSocketProtocol.SEND_STATE_INSIDE_MESSAGE:\r
+ raise Exception("WebSocketProtocol.endMessage invalid in current sending state [%d]" % self.send_state)\r
+\r
+ if self.websocket_version == 0:\r
+ self.sendData('\x00')\r
+ else:\r
+ self.sendFrame(opcode = 0, fin = True)\r
+\r
+ self.send_state = WebSocketProtocol.SEND_STATE_GROUND\r
+\r
+\r
+ def sendMessageFrame(self, payload, reserved = 0, mask = None, sync = False):\r
+ """\r
+ When a message has begun, send a complete message frame in one go.\r
+\r
+ Modes: Hybi\r
+ """\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ if self.websocket_version == 0:\r
+ raise Exception("function not supported in Hixie-76 mode")\r
+ self.beginMessageFrame(len(payload), reserved, mask)\r
+ self.sendMessageFrameData(payload, sync)\r
+\r
+\r
+ def sendMessage(self, payload, binary = False, payload_frag_size = None, sync = False):\r
+ """\r
+ Send out a message in one go.\r
+\r
+ You can send text or binary message, and optionally specifiy a payload fragment size.\r
+ When the latter is given, the payload will be split up into frames with\r
+ payload <= the payload_frag_size given.\r
+\r
+ Modes: Hybi, Hixie\r
+ """\r
+ if self.state != WebSocketProtocol.STATE_OPEN:\r
+ return\r
+ if self.websocket_version == 0:\r
+ if binary:\r
+ raise Exception("cannot send binary message in Hixie76 mode")\r
+ if payload_frag_size:\r
+ raise Exception("cannot fragment messages in Hixie76 mode")\r
+ self.sendMessageHixie76(payload, sync)\r
+ else:\r
+ self.sendMessageHybi(payload, binary, payload_frag_size, sync)\r
+\r
+\r
+ def sendMessageHixie76(self, payload, sync = False):\r
+ """\r
+ Hixie76-Variant of sendMessage().\r
+\r
+ Modes: Hixie\r
+ """\r
+ self.sendData('\x00' + payload + '\xff', sync = sync)\r
+\r
+\r
+ def sendMessageHybi(self, payload, binary = False, payload_frag_size = None, sync = False):\r
+ """\r
+ Hybi-Variant of sendMessage().\r
+\r
+ Modes: Hybi\r
+ """\r
+ ## (initial) frame opcode\r
+ ##\r
+ if binary:\r
+ opcode = 2\r
+ else:\r
+ opcode = 1\r
+\r
+ ## explicit payload_frag_size arguments overrides autoFragmentSize setting\r
+ ##\r
+ if payload_frag_size is not None:\r
+ pfs = payload_frag_size\r
+ else:\r
+ if self.autoFragmentSize > 0:\r
+ pfs = self.autoFragmentSize\r
+ else:\r
+ pfs = None\r
+\r
+ ## send unfragmented\r
+ ##\r
+ if pfs is None or len(payload) <= pfs:\r
+ self.sendFrame(opcode = opcode, payload = payload, sync = sync)\r
+\r
+ ## send data message in fragments\r
+ ##\r
+ else:\r
+ if pfs < 1:\r
+ raise Exception("payload fragment size must be at least 1 (was %d)" % pfs)\r
+ n = len(payload)\r
+ i = 0\r
+ done = False\r
+ first = True\r
+ while not done:\r
+ j = i + pfs\r
+ if j > n:\r
+ done = True\r
+ j = n\r
+ if first:\r
+ self.sendFrame(opcode = opcode, payload = payload[i:j], fin = done, sync = sync)\r
+ first = False\r
+ else:\r
+ self.sendFrame(opcode = 0, payload = payload[i:j], fin = done, sync = sync)\r
+ i += pfs\r
+\r
+\r
+\r
+class PreparedMessage:\r
+ """\r
+ Encapsulates a prepared message to be sent later once or multiple\r
+ times. This is used for optimizing Broadcast/PubSub.\r
+\r
+ The message serialization formats currently created internally are:\r
+ * Hybi\r
+ * Hixie\r
+\r
+ The construction of different formats is needed, since we support\r
+ mixed clients (speaking different protocol versions).\r
+\r
+ It will also be the place to add a 3rd format, when we support\r
+ the deflate extension, since then, the clients will be mixed\r
+ between Hybi-Deflate-Unsupported, Hybi-Deflate-Supported and Hixie.\r
+ """\r
+\r
+ def __init__(self, payload, binary, masked):\r
+ self.initHixie(payload, binary)\r
+ self.initHybi(payload, binary, masked)\r
+\r
+\r
+ def initHixie(self, payload, binary):\r
+ if binary:\r
+ # silently filter out .. probably do something else:\r
+ # base64?\r
+ # dunno\r
+ self.payloadHixie = ''\r
+ else:\r
+ self.payloadHixie = '\x00' + payload + '\xff'\r
+\r
+\r
+ def initHybi(self, payload, binary, masked):\r
+ l = len(payload)\r
+\r
+ ## first byte\r
+ ##\r
+ b0 = ((1 << 7) | 2) if binary else ((1 << 7) | 1)\r
+\r
+ ## second byte, payload len bytes and mask\r
+ ##\r
+ if masked:\r
+ b1 = 1 << 7\r
+ mask = struct.pack("!I", random.getrandbits(32))\r
+ if l == 0:\r
+ plm = payload\r
+ elif l < WebSocketProtocol.PAYLOAD_LEN_XOR_BREAKEVEN:\r
+ plm = XorMaskerSimple(mask).process(payload)\r
+ else:\r
+ plm = XorMaskerShifted1(mask).process(payload)\r
+ else:\r
+ b1 = 0\r
+ mask = ""\r
+ plm = payload\r
+\r
+ ## payload extended length\r
+ ##\r
+ el = ""\r
+ if l <= 125:\r
+ b1 |= l\r
+ elif l <= 0xFFFF:\r
+ b1 |= 126\r
+ el = struct.pack("!H", l)\r
+ elif l <= 0x7FFFFFFFFFFFFFFF:\r
+ b1 |= 127\r
+ el = struct.pack("!Q", l)\r
+ else:\r
+ raise Exception("invalid payload length")\r
+\r
+ ## raw WS message (single frame)\r
+ ##\r
+ self.payloadHybi = ''.join([chr(b0), chr(b1), el, mask, plm])\r
+\r
+\r
+\r
+class WebSocketFactory:\r
+ """\r
+ Mixin for WebSocketClientFactory and WebSocketServerFactory.\r
+ """\r
+\r
+ def prepareMessage(self, payload, binary = False, masked = None):\r
+ """\r
+ Prepare a WebSocket message. This can be later used on multiple\r
+ instances of WebSocketProtocol using sendPreparedMessage().\r
+\r
+ By doing so, you can avoid the (small) overhead of framing the\r
+ _same_ payload into WS messages when that payload is to be sent\r
+ out on multiple connections.\r
+\r
+ Modes: Hybi, Hixie\r
+\r
+ Caveats:\r
+\r
+ 1) Only use when you know what you are doing. I.e. calling\r
+ sendPreparedMessage() on the _same_ protocol instance multiples\r
+ times with the same prepared message might break the spec.\r
+ Since i.e. the frame mask will be the same!\r
+\r
+ 2) Treat the object returned as opaque. It may change!\r
+ """\r
+ if masked is None:\r
+ masked = not self.isServer\r
+\r
+ return PreparedMessage(payload, binary, masked)\r
+\r
+\r
+\r
+class WebSocketServerProtocol(WebSocketProtocol):\r
+ """\r
+ A Twisted protocol for WebSockets servers.\r
+ """\r
+\r
+ def onConnect(self, connectionRequest):\r
+ """\r
+ Callback fired during WebSocket opening handshake when new WebSocket client\r
+ connection is about to be established.\r
+\r
+ Throw HttpException when you don't want to accept the WebSocket\r
+ connection request. For example, throw a\r
+ HttpException(httpstatus.HTTP_STATUS_CODE_UNAUTHORIZED[0], "You are not authorized for this!").\r
+\r
+ When you want to accept the connection, return the accepted protocol\r
+ from list of WebSockets (sub)protocols provided by client or None to\r
+ speak no specific one or when the client list was empty.\r
+\r
+ :param connectionRequest: WebSocket connection request information.\r
+ :type connectionRequest: instance of :class:`autobahn.websocket.ConnectionRequest`\r
+ """\r
+ return None\r
+\r
+\r
+ def connectionMade(self):\r
+ """\r
+ Called by Twisted when new TCP connection from client was accepted. Default\r
+ implementation will prepare for initial WebSocket opening handshake.\r
+ When overriding in derived class, make sure to call this base class\r
+ implementation _before_ your code.\r
+ """\r
+ self.isServer = True\r
+ WebSocketProtocol.connectionMade(self)\r
+ self.factory.countConnections += 1\r
+ if self.debug:\r
+ log.msg("connection accepted from peer %s" % self.peerstr)\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ """\r
+ Called by Twisted when established TCP connection from client was lost. Default\r
+ implementation will tear down all state properly.\r
+ When overriding in derived class, make sure to call this base class\r
+ implementation _after_ your code.\r
+ """\r
+ WebSocketProtocol.connectionLost(self, reason)\r
+ self.factory.countConnections -= 1\r
+ if self.debug:\r
+ log.msg("connection from %s lost" % self.peerstr)\r
+\r
+\r
+ def parseHixie76Key(self, key):\r
+ return int(filter(lambda x: x.isdigit(), key)) / key.count(" ")\r
+\r
+\r
+ def processHandshake(self):\r
+ """\r
+ Process WebSockets opening handshake request from client.\r
+ """\r
+ ## only proceed when we have fully received the HTTP request line and all headers\r
+ ##\r
+ end_of_header = self.data.find("\x0d\x0a\x0d\x0a")\r
+ if end_of_header >= 0:\r
+\r
+ self.http_request_data = self.data[:end_of_header + 4]\r
+ if self.debug:\r
+ log.msg("received HTTP request:\n\n%s\n\n" % self.http_request_data)\r
+\r
+ ## extract HTTP status line and headers\r
+ ##\r
+ (self.http_status_line, self.http_headers, http_headers_cnt) = parseHttpHeader(self.http_request_data)\r
+\r
+ ## validate WebSocket opening handshake client request\r
+ ##\r
+ if self.debug:\r
+ log.msg("received HTTP status line in opening handshake : %s" % str(self.http_status_line))\r
+ log.msg("received HTTP headers in opening handshake : %s" % str(self.http_headers))\r
+\r
+ ## HTTP Request line : METHOD, VERSION\r
+ ##\r
+ rl = self.http_status_line.split()\r
+ if len(rl) != 3:\r
+ return self.failHandshake("Bad HTTP request status line '%s'" % self.http_status_line)\r
+ if rl[0].strip() != "GET":\r
+ return self.failHandshake("HTTP method '%s' not allowed" % rl[0], HTTP_STATUS_CODE_METHOD_NOT_ALLOWED[0])\r
+ vs = rl[2].strip().split("/")\r
+ if len(vs) != 2 or vs[0] != "HTTP" or vs[1] not in ["1.1"]:\r
+ return self.failHandshake("Unsupported HTTP version '%s'" % rl[2], HTTP_STATUS_CODE_UNSUPPORTED_HTTP_VERSION[0])\r
+\r
+ ## HTTP Request line : REQUEST-URI\r
+ ##\r
+ self.http_request_uri = rl[1].strip()\r
+ try:\r
+ (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(self.http_request_uri)\r
+\r
+ ## FIXME: check that if absolute resource URI is given,\r
+ ## the scheme/netloc matches the server\r
+ if scheme != "" or netloc != "":\r
+ pass\r
+\r
+ ## Fragment identifiers are meaningless in the context of WebSocket\r
+ ## URIs, and MUST NOT be used on these URIs.\r
+ if fragment != "":\r
+ return self.failHandshake("HTTP requested resource contains a fragment identifier '%s'" % fragment)\r
+\r
+ ## resource path and query parameters .. this will get forwarded\r
+ ## to onConnect()\r
+ self.http_request_path = path\r
+ self.http_request_params = urlparse.parse_qs(query)\r
+ except:\r
+ return self.failHandshake("Bad HTTP request resource - could not parse '%s'" % rl[1].strip())\r
+\r
+ ## Host\r
+ ##\r
+ if not self.http_headers.has_key("host"):\r
+ return self.failHandshake("HTTP Host header missing in opening handshake request")\r
+ if http_headers_cnt["host"] > 1:\r
+ return self.failHandshake("HTTP Host header appears more than once in opening handshake request")\r
+ self.http_request_host = self.http_headers["host"].strip()\r
+ if self.http_request_host.find(":") >= 0:\r
+ (h, p) = self.http_request_host.split(":")\r
+ try:\r
+ port = int(str(p.strip()))\r
+ except:\r
+ return self.failHandshake("invalid port '%s' in HTTP Host header '%s'" % (str(p.strip()), str(self.http_request_host)))\r
+ if port != self.factory.port:\r
+ return self.failHandshake("port %d in HTTP Host header '%s' does not match server listening port %s" % (port, str(self.http_request_host), self.factory.port))\r
+ self.http_request_host = h\r
+ else:\r
+ if not ((self.factory.isSecure and self.factory.port == 443) or (not self.factory.isSecure and self.factory.port == 80)):\r
+ return self.failHandshake("missing port in HTTP Host header '%s' and server runs on non-standard port %d (wss = %s)" % (str(self.http_request_host), self.factory.port, self.factory.isSecure))\r
+\r
+ ## Upgrade\r
+ ##\r
+ if not self.http_headers.has_key("upgrade"):\r
+ ## When no WS upgrade, render HTML server status page\r
+ ##\r
+ if self.webStatus:\r
+ self.sendServerStatus()\r
+ self.dropConnection(abort = False)\r
+ return\r
+ else:\r
+ return self.failHandshake("HTTP Upgrade header missing", HTTP_STATUS_CODE_UPGRADE_REQUIRED[0])\r
+ upgradeWebSocket = False\r
+ for u in self.http_headers["upgrade"].split(","):\r
+ if u.strip().lower() == "websocket":\r
+ upgradeWebSocket = True\r
+ break\r
+ if not upgradeWebSocket:\r
+ return self.failHandshake("HTTP Upgrade headers do not include 'websocket' value (case-insensitive) : %s" % self.http_headers["upgrade"])\r
+\r
+ ## Connection\r
+ ##\r
+ if not self.http_headers.has_key("connection"):\r
+ return self.failHandshake("HTTP Connection header missing")\r
+ connectionUpgrade = False\r
+ for c in self.http_headers["connection"].split(","):\r
+ if c.strip().lower() == "upgrade":\r
+ connectionUpgrade = True\r
+ break\r
+ if not connectionUpgrade:\r
+ return self.failHandshake("HTTP Connection headers do not include 'upgrade' value (case-insensitive) : %s" % self.http_headers["connection"])\r
+\r
+ ## Sec-WebSocket-Version PLUS determine mode: Hybi or Hixie\r
+ ##\r
+ if not self.http_headers.has_key("sec-websocket-version"):\r
+ if self.debugCodePaths:\r
+ log.msg("Hixie76 protocol detected")\r
+ if self.allowHixie76:\r
+ version = 0\r
+ else:\r
+ return self.failHandshake("WebSocket connection denied - Hixie76 protocol mode disabled.")\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg("Hybi protocol detected")\r
+ if http_headers_cnt["sec-websocket-version"] > 1:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Version header appears more than once in opening handshake request")\r
+ try:\r
+ version = int(self.http_headers["sec-websocket-version"])\r
+ except:\r
+ return self.failHandshake("could not parse HTTP Sec-WebSocket-Version header '%s' in opening handshake request" % self.http_headers["sec-websocket-version"])\r
+\r
+ if version not in self.versions:\r
+\r
+ ## respond with list of supported versions (descending order)\r
+ ##\r
+ sv = sorted(self.versions)\r
+ sv.reverse()\r
+ svs = ','.join([str(x) for x in sv])\r
+ return self.failHandshake("WebSocket version %d not supported (supported versions: %s)" % (version, svs),\r
+ HTTP_STATUS_CODE_BAD_REQUEST[0],\r
+ [("Sec-WebSocket-Version", svs)])\r
+ else:\r
+ ## store the protocol version we are supposed to talk\r
+ self.websocket_version = version\r
+\r
+ ## Sec-WebSocket-Protocol\r
+ ##\r
+ if self.http_headers.has_key("sec-websocket-protocol"):\r
+ protocols = [str(x.strip()) for x in self.http_headers["sec-websocket-protocol"].split(",")]\r
+ # check for duplicates in protocol header\r
+ pp = {}\r
+ for p in protocols:\r
+ if pp.has_key(p):\r
+ return self.failHandshake("duplicate protocol '%s' specified in HTTP Sec-WebSocket-Protocol header" % p)\r
+ else:\r
+ pp[p] = 1\r
+ # ok, no duplicates, save list in order the client sent it\r
+ self.websocket_protocols = protocols\r
+ else:\r
+ self.websocket_protocols = []\r
+\r
+ ## Origin / Sec-WebSocket-Origin\r
+ ## http://tools.ietf.org/html/draft-ietf-websec-origin-02\r
+ ##\r
+ if self.websocket_version < 13 and self.websocket_version != 0:\r
+ # Hybi, but only < Hybi-13\r
+ websocket_origin_header_key = 'sec-websocket-origin'\r
+ else:\r
+ # RFC6455, >= Hybi-13 and Hixie\r
+ websocket_origin_header_key = "origin"\r
+\r
+ self.websocket_origin = None\r
+ if self.http_headers.has_key(websocket_origin_header_key):\r
+ if http_headers_cnt[websocket_origin_header_key] > 1:\r
+ return self.failHandshake("HTTP Origin header appears more than once in opening handshake request")\r
+ self.websocket_origin = self.http_headers[websocket_origin_header_key].strip()\r
+ else:\r
+ # non-browser clients are allowed to omit this header\r
+ pass\r
+\r
+ ## Sec-WebSocket-Extensions\r
+ ##\r
+ ## extensions requested by client\r
+ self.websocket_extensions = []\r
+ ## extensions selected by server\r
+ self.websocket_extensions_in_use = []\r
+\r
+ if self.http_headers.has_key("sec-websocket-extensions"):\r
+ if self.websocket_version == 0:\r
+ return self.failHandshake("Sec-WebSocket-Extensions header specified for Hixie-76")\r
+ extensions = [x.strip() for x in self.http_headers["sec-websocket-extensions"].split(',')]\r
+ if len(extensions) > 0:\r
+ self.websocket_extensions = extensions\r
+ if self.debug:\r
+ log.msg("client requested extensions we don't support (%s)" % str(extensions))\r
+\r
+ ## Sec-WebSocket-Key (Hybi) or Sec-WebSocket-Key1/Sec-WebSocket-Key2 (Hixie-76)\r
+ ##\r
+ if self.websocket_version == 0:\r
+ for kk in ['Sec-WebSocket-Key1', 'Sec-WebSocket-Key2']:\r
+ k = kk.lower()\r
+ if not self.http_headers.has_key(k):\r
+ return self.failHandshake("HTTP %s header missing" % kk)\r
+ if http_headers_cnt[k] > 1:\r
+ return self.failHandshake("HTTP %s header appears more than once in opening handshake request" % kk)\r
+ try:\r
+ key1 = self.parseHixie76Key(self.http_headers["sec-websocket-key1"].strip())\r
+ key2 = self.parseHixie76Key(self.http_headers["sec-websocket-key2"].strip())\r
+ except:\r
+ return self.failHandshake("could not parse Sec-WebSocket-Key1/2")\r
+ else:\r
+ if not self.http_headers.has_key("sec-websocket-key"):\r
+ return self.failHandshake("HTTP Sec-WebSocket-Key header missing")\r
+ if http_headers_cnt["sec-websocket-key"] > 1:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Key header appears more than once in opening handshake request")\r
+ key = self.http_headers["sec-websocket-key"].strip()\r
+ if len(key) != 24: # 16 bytes => (ceil(128/24)*24)/6 == 24\r
+ return self.failHandshake("bad Sec-WebSocket-Key (length must be 24 ASCII chars) '%s'" % key)\r
+ if key[-2:] != "==": # 24 - ceil(128/6) == 2\r
+ return self.failHandshake("bad Sec-WebSocket-Key (invalid base64 encoding) '%s'" % key)\r
+ for c in key[:-2]:\r
+ if c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/":\r
+ return self.failHandshake("bad character '%s' in Sec-WebSocket-Key (invalid base64 encoding) '%s'" (c, key))\r
+\r
+ ## For Hixie-76, we need 8 octets of HTTP request body to complete HS!\r
+ ##\r
+ if self.websocket_version == 0:\r
+ if len(self.data) < end_of_header + 4 + 8:\r
+ return\r
+ else:\r
+ key3 = self.data[end_of_header + 4:end_of_header + 4 + 8]\r
+\r
+ ## Ok, got complete HS input, remember rest (if any)\r
+ ##\r
+ if self.websocket_version == 0:\r
+ self.data = self.data[end_of_header + 4 + 8:]\r
+ else:\r
+ self.data = self.data[end_of_header + 4:]\r
+\r
+ ## WebSocket handshake validated => produce opening handshake response\r
+\r
+ ## Now fire onConnect() on derived class, to give that class a chance to accept or deny\r
+ ## the connection. onConnect() may throw, in which case the connection is denied, or it\r
+ ## may return a protocol from the protocols provided by client or None.\r
+ ##\r
+ try:\r
+ connectionRequest = ConnectionRequest(self.peer,\r
+ self.peerstr,\r
+ self.http_headers,\r
+ self.http_request_host,\r
+ self.http_request_path,\r
+ self.http_request_params,\r
+ self.websocket_version,\r
+ self.websocket_origin,\r
+ self.websocket_protocols,\r
+ self.websocket_extensions)\r
+\r
+ ## onConnect() will return the selected subprotocol or None\r
+ ## or raise an HttpException\r
+ ##\r
+ protocol = self.onConnect(connectionRequest)\r
+\r
+ if protocol is not None and not (protocol in self.websocket_protocols):\r
+ raise Exception("protocol accepted must be from the list client sent or None")\r
+\r
+ self.websocket_protocol_in_use = protocol\r
+\r
+ except HttpException, e:\r
+ return self.failHandshake(e.reason, e.code)\r
+ #return self.sendHttpRequestFailure(e.code, e.reason)\r
+\r
+ except Exception, e:\r
+ log.msg("Exception raised in onConnect() - %s" % str(e))\r
+ return self.failHandshake("Internal Server Error", HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR[0])\r
+\r
+\r
+ ## build response to complete WebSocket handshake\r
+ ##\r
+ response = "HTTP/1.1 %d Switching Protocols\x0d\x0a" % HTTP_STATUS_CODE_SWITCHING_PROTOCOLS[0]\r
+\r
+ if self.factory.server is not None and self.factory.server != "":\r
+ response += "Server: %s\x0d\x0a" % self.factory.server.encode("utf-8")\r
+\r
+ response += "Upgrade: WebSocket\x0d\x0a"\r
+ response += "Connection: Upgrade\x0d\x0a"\r
+\r
+ if self.websocket_protocol_in_use is not None:\r
+ response += "Sec-WebSocket-Protocol: %s\x0d\x0a" % str(self.websocket_protocol_in_use)\r
+\r
+ if self.websocket_version == 0:\r
+\r
+ if self.websocket_origin:\r
+ ## browser client provide the header, and expect it to be echo'ed\r
+ response += "Sec-WebSocket-Origin: %s\x0d\x0a" % str(self.websocket_origin)\r
+\r
+ if self.debugCodePaths:\r
+ log.msg('factory isSecure = %s port = %s' % (self.factory.isSecure, self.factory.port))\r
+\r
+ if (self.factory.isSecure and self.factory.port != 443) or ((not self.factory.isSecure) and self.factory.port != 80):\r
+ if self.debugCodePaths:\r
+ log.msg('factory running on non-default port')\r
+ response_port = ':' + str(self.factory.port)\r
+ else:\r
+ if self.debugCodePaths:\r
+ log.msg('factory running on default port')\r
+ response_port = ''\r
+\r
+ ## FIXME: check this! But see below ..\r
+ if False:\r
+ response_host = str(self.factory.host)\r
+ response_path = str(self.factory.path)\r
+ else:\r
+ response_host = str(self.http_request_host)\r
+ response_path = str(self.http_request_uri)\r
+\r
+ location = "%s://%s%s%s" % ('wss' if self.factory.isSecure else 'ws', response_host, response_port, response_path)\r
+\r
+ # Safari is very picky about this one\r
+ response += "Sec-WebSocket-Location: %s\x0d\x0a" % location\r
+\r
+ ## end of HTTP response headers\r
+ response += "\x0d\x0a"\r
+\r
+ ## compute accept body\r
+ ##\r
+ accept_val = struct.pack(">II", key1, key2) + key3\r
+ accept = hashlib.md5(accept_val).digest()\r
+ response_body = str(accept)\r
+ else:\r
+ ## compute Sec-WebSocket-Accept\r
+ ##\r
+ sha1 = hashlib.sha1()\r
+ sha1.update(key + WebSocketProtocol.WS_MAGIC)\r
+ sec_websocket_accept = base64.b64encode(sha1.digest())\r
+\r
+ response += "Sec-WebSocket-Accept: %s\x0d\x0a" % sec_websocket_accept\r
+\r
+ if len(self.websocket_extensions_in_use) > 0:\r
+ response += "Sec-WebSocket-Extensions: %s\x0d\x0a" % ','.join(self.websocket_extensions_in_use)\r
+\r
+ ## end of HTTP response headers\r
+ response += "\x0d\x0a"\r
+ response_body = ''\r
+\r
+ if self.debug:\r
+ log.msg("sending HTTP response:\n\n%s%s\n\n" % (response, binascii.b2a_hex(response_body)))\r
+\r
+ ## save and send out opening HS data\r
+ ##\r
+ self.http_response_data = response + response_body\r
+ self.sendData(self.http_response_data)\r
+\r
+ ## opening handshake completed, move WebSockets connection into OPEN state\r
+ ##\r
+ self.state = WebSocketProtocol.STATE_OPEN\r
+\r
+ ## cancel any opening HS timer if present\r
+ ##\r
+ if self.openHandshakeTimeoutCall is not None:\r
+ if self.debugCodePaths:\r
+ log.msg("openHandshakeTimeoutCall.cancel")\r
+ self.openHandshakeTimeoutCall.cancel()\r
+ self.openHandshakeTimeoutCall = None\r
+\r
+ ## init state\r
+ ##\r
+ self.inside_message = False\r
+ if self.websocket_version != 0:\r
+ self.current_frame = None\r
+\r
+ ## fire handler on derived class\r
+ ##\r
+ self.onOpen()\r
+\r
+ ## process rest, if any\r
+ ##\r
+ if len(self.data) > 0:\r
+ self.consumeData()\r
+\r
+\r
+ def failHandshake(self, reason, code = HTTP_STATUS_CODE_BAD_REQUEST[0], responseHeaders = []):\r
+ """\r
+ During opening handshake the client request was invalid, we send a HTTP\r
+ error response and then drop the connection.\r
+ """\r
+ if self.debug:\r
+ log.msg("failing WebSockets opening handshake ('%s')" % reason)\r
+ self.sendHttpErrorResponse(code, reason, responseHeaders)\r
+ self.dropConnection(abort = False)\r
+\r
+\r
+ def sendHttpErrorResponse(self, code, reason, responseHeaders = []):\r
+ """\r
+ Send out HTTP error response.\r
+ """\r
+ response = "HTTP/1.1 %d %s\x0d\x0a" % (code, reason.encode("utf-8"))\r
+ for h in responseHeaders:\r
+ response += "%s: %s\x0d\x0a" % (h[0], h[1].encode("utf-8"))\r
+ response += "\x0d\x0a"\r
+ self.sendData(response)\r
+\r
+\r
+ def sendHtml(self, html):\r
+ raw = html.encode("utf-8")\r
+ response = "HTTP/1.1 %d %s\x0d\x0a" % (HTTP_STATUS_CODE_OK[0], HTTP_STATUS_CODE_OK[1])\r
+ if self.factory.server is not None and self.factory.server != "":\r
+ response += "Server: %s\x0d\x0a" % self.factory.server.encode("utf-8")\r
+ response += "Content-Type: text/html; charset=UTF-8\x0d\x0a"\r
+ response += "Content-Length: %d\x0d\x0a" % len(raw)\r
+ response += "\x0d\x0a"\r
+ response += raw\r
+ self.sendData(response)\r
+\r
+\r
+ def sendServerStatus(self):\r
+ """\r
+ Used to send out server status/version upon receiving a HTTP/GET without\r
+ upgrade to WebSocket header (and option serverStatus is True).\r
+ """\r
+ html = """\r
+<!DOCTYPE html>\r
+<html>\r
+ <body>\r
+ <h1>Autobahn WebSockets %s</h1>\r
+ <p>\r
+ I am not Web server, but a WebSocket endpoint.\r
+ You can talk to me using the WebSocket <a href="http://tools.ietf.org/html/rfc6455">protocol</a>.\r
+ </p>\r
+ <p>\r
+ For more information, please visit <a href="http://autobahn.ws">my homepage</a>.\r
+ </p>\r
+ </body>\r
+</html>\r
+""" % str(autobahn.version)\r
+ self.sendHtml(html)\r
+\r
+\r
+class WebSocketServerFactory(protocol.ServerFactory, WebSocketFactory):\r
+ """\r
+ A Twisted factory for WebSockets server protocols.\r
+ """\r
+\r
+ protocol = WebSocketServerProtocol\r
+ """\r
+ The protocol to be spoken. Must be derived from :class:`autobahn.websocket.WebSocketServerProtocol`.\r
+ """\r
+\r
+\r
+ def __init__(self,\r
+\r
+ ## WebSockect session parameters\r
+ url = None,\r
+ protocols = [],\r
+ server = "AutobahnPython/%s" % autobahn.version,\r
+\r
+ ## debugging\r
+ debug = False,\r
+ debugCodePaths = False):\r
+ """\r
+ Create instance of WebSocket server factory.\r
+\r
+ Note that you MUST set URL either here or using setSessionParameters() _before_ the factory is started.\r
+\r
+ :param url: WebSocket listening URL - ("ws:" | "wss:") "//" host [ ":" port ] path [ "?" query ].\r
+ :type url: str\r
+ :param protocols: List of subprotocols the server supports. The subprotocol used is the first from the list of subprotocols announced by the client that is contained in this list.\r
+ :type protocols: list of strings\r
+ :param server: Server as announced in HTTP response header during opening handshake or None (default: "AutobahnWebSockets/x.x.x").\r
+ :type server: str\r
+ :param debug: Debug mode (default: False).\r
+ :type debug: bool\r
+ :param debugCodePaths: Debug code paths mode (default: False).\r
+ :type debugCodePaths: bool\r
+ """\r
+ self.debug = debug\r
+ self.debugCodePaths = debugCodePaths\r
+\r
+ self.logOctets = debug\r
+ self.logFrames = debug\r
+\r
+ self.isServer = True\r
+\r
+ ## seed RNG which is used for WS frame masks generation\r
+ random.seed()\r
+\r
+ ## default WS session parameters\r
+ ##\r
+ self.setSessionParameters(url, protocols, server)\r
+\r
+ ## default WebSocket protocol options\r
+ ##\r
+ self.resetProtocolOptions()\r
+\r
+ ## number of currently connected clients\r
+ ##\r
+ self.countConnections = 0\r
+\r
+\r
+ def setSessionParameters(self, url = None, protocols = [], server = None):\r
+ """\r
+ Set WebSocket session parameters.\r
+\r
+ :param url: WebSocket listening URL - ("ws:" | "wss:") "//" host [ ":" port ].\r
+ :type url: str\r
+ :param protocols: List of subprotocols the server supports. The subprotocol used is the first from the list of subprotocols announced by the client that is contained in this list.\r
+ :type protocols: list of strings\r
+ :param server: Server as announced in HTTP response header during opening handshake.\r
+ :type server: str\r
+ """\r
+ if url is not None:\r
+ ## parse WebSocket URI into components\r
+ (isSecure, host, port, resource, path, params) = parseWsUrl(url)\r
+ if path != "/":\r
+ raise Exception("path specified for server WebSocket URL")\r
+ if len(params) > 0:\r
+ raise Exception("query parameters specified for server WebSocket URL")\r
+ self.url = url\r
+ self.isSecure = isSecure\r
+ self.host = host\r
+ self.port = port\r
+ else:\r
+ self.url = None\r
+ self.isSecure = None\r
+ self.host = None\r
+ self.port = None\r
+\r
+ self.protocols = protocols\r
+ self.server = server\r
+\r
+\r
+ def resetProtocolOptions(self):\r
+ """\r
+ Reset all WebSocket protocol options to defaults.\r
+ """\r
+ self.versions = WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS\r
+ self.allowHixie76 = WebSocketProtocol.DEFAULT_ALLOW_HIXIE76\r
+ self.webStatus = True\r
+ self.utf8validateIncoming = True\r
+ self.requireMaskedClientFrames = True\r
+ self.maskServerFrames = False\r
+ self.applyMask = True\r
+ self.maxFramePayloadSize = 0\r
+ self.maxMessagePayloadSize = 0\r
+ self.autoFragmentSize = 0\r
+ self.failByDrop = True\r
+ self.echoCloseCodeReason = False\r
+ self.openHandshakeTimeout = 5\r
+ self.closeHandshakeTimeout = 1\r
+ self.tcpNoDelay = True\r
+\r
+\r
+ def setProtocolOptions(self,\r
+ versions = None,\r
+ allowHixie76 = None,\r
+ webStatus = None,\r
+ utf8validateIncoming = None,\r
+ maskServerFrames = None,\r
+ requireMaskedClientFrames = None,\r
+ applyMask = None,\r
+ maxFramePayloadSize = None,\r
+ maxMessagePayloadSize = None,\r
+ autoFragmentSize = None,\r
+ failByDrop = None,\r
+ echoCloseCodeReason = None,\r
+ openHandshakeTimeout = None,\r
+ closeHandshakeTimeout = None,\r
+ tcpNoDelay = None):\r
+ """\r
+ Set WebSocket protocol options used as defaults for new protocol instances.\r
+\r
+ :param versions: The WebSockets protocol versions accepted by the server (default: WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS).\r
+ :type versions: list of ints\r
+ :param allowHixie76: Allow to speak Hixie76 protocol version.\r
+ :type allowHixie76: bool\r
+ :param webStatus: Return server status/version on HTTP/GET without WebSocket upgrade header (default: True).\r
+ :type webStatus: bool\r
+ :param utf8validateIncoming: Validate incoming UTF-8 in text message payloads (default: True).\r
+ :type utf8validateIncoming: bool\r
+ :param maskServerFrames: Mask server-to-client frames (default: False).\r
+ :type maskServerFrames: bool\r
+ :param requireMaskedClientFrames: Require client-to-server frames to be masked (default: True).\r
+ :type requireMaskedClientFrames: bool\r
+ :param applyMask: Actually apply mask to payload when mask it present. Applies for outgoing and incoming frames (default: True).\r
+ :type applyMask: bool\r
+ :param maxFramePayloadSize: Maximum frame payload size that will be accepted when receiving or 0 for unlimited (default: 0).\r
+ :type maxFramePayloadSize: int\r
+ :param maxMessagePayloadSize: Maximum message payload size (after reassembly of fragmented messages) that will be accepted when receiving or 0 for unlimited (default: 0).\r
+ :type maxMessagePayloadSize: int\r
+ :param autoFragmentSize: Automatic fragmentation of outgoing data messages (when using the message-based API) into frames with payload length <= this size or 0 for no auto-fragmentation (default: 0).\r
+ :type autoFragmentSize: int\r
+ :param failByDrop: Fail connections by dropping the TCP connection without performaing closing handshake (default: True).\r
+ :type failbyDrop: bool\r
+ :param echoCloseCodeReason: Iff true, when receiving a close, echo back close code/reason. Otherwise reply with code == NORMAL, reason = "" (default: False).\r
+ :type echoCloseCodeReason: bool\r
+ :param openHandshakeTimeout: Opening WebSocket handshake timeout, timeout in seconds or 0 to deactivate (default: 0).\r
+ :type openHandshakeTimeout: float\r
+ :param closeHandshakeTimeout: When we expect to receive a closing handshake reply, timeout in seconds (default: 1).\r
+ :type closeHandshakeTimeout: float\r
+ :param tcpNoDelay: TCP NODELAY ("Nagle") socket option (default: True).\r
+ :type tcpNoDelay: bool\r
+ """\r
+ if allowHixie76 is not None and allowHixie76 != self.allowHixie76:\r
+ self.allowHixie76 = allowHixie76\r
+\r
+ if versions is not None:\r
+ for v in versions:\r
+ if v not in WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS:\r
+ raise Exception("invalid WebSockets protocol version %s (allowed values: %s)" % (v, str(WebSocketProtocol.SUPPORTED_PROTOCOL_VERSIONS)))\r
+ if v == 0 and not self.allowHixie76:\r
+ raise Exception("use of Hixie-76 requires allowHixie76 == True")\r
+ if set(versions) != set(self.versions):\r
+ self.versions = versions\r
+\r
+ if webStatus is not None and webStatus != self.webStatus:\r
+ self.webStatus = webStatus\r
+\r
+ if utf8validateIncoming is not None and utf8validateIncoming != self.utf8validateIncoming:\r
+ self.utf8validateIncoming = utf8validateIncoming\r
+\r
+ if requireMaskedClientFrames is not None and requireMaskedClientFrames != self.requireMaskedClientFrames:\r
+ self.requireMaskedClientFrames = requireMaskedClientFrames\r
+\r
+ if maskServerFrames is not None and maskServerFrames != self.maskServerFrames:\r
+ self.maskServerFrames = maskServerFrames\r
+\r
+ if applyMask is not None and applyMask != self.applyMask:\r
+ self.applyMask = applyMask\r
+\r
+ if maxFramePayloadSize is not None and maxFramePayloadSize != self.maxFramePayloadSize:\r
+ self.maxFramePayloadSize = maxFramePayloadSize\r
+\r
+ if maxMessagePayloadSize is not None and maxMessagePayloadSize != self.maxMessagePayloadSize:\r
+ self.maxMessagePayloadSize = maxMessagePayloadSize\r
+\r
+ if autoFragmentSize is not None and autoFragmentSize != self.autoFragmentSize:\r
+ self.autoFragmentSize = autoFragmentSize\r
+\r
+ if failByDrop is not None and failByDrop != self.failByDrop:\r
+ self.failByDrop = failByDrop\r
+\r
+ if echoCloseCodeReason is not None and echoCloseCodeReason != self.echoCloseCodeReason:\r
+ self.echoCloseCodeReason = echoCloseCodeReason\r
+\r
+ if openHandshakeTimeout is not None and openHandshakeTimeout != self.openHandshakeTimeout:\r
+ self.openHandshakeTimeout = openHandshakeTimeout\r
+\r
+ if closeHandshakeTimeout is not None and closeHandshakeTimeout != self.closeHandshakeTimeout:\r
+ self.closeHandshakeTimeout = closeHandshakeTimeout\r
+\r
+ if tcpNoDelay is not None and tcpNoDelay != self.tcpNoDelay:\r
+ self.tcpNoDelay = tcpNoDelay\r
+\r
+\r
+ def getConnectionCount(self):\r
+ """\r
+ Get number of currently connected clients.\r
+\r
+ :returns: int -- Number of currently connected clients.\r
+ """\r
+ return self.countConnections\r
+\r
+\r
+ def startFactory(self):\r
+ """\r
+ Called by Twisted before starting to listen on port for incoming connections.\r
+ Default implementation does nothing. Override in derived class when appropriate.\r
+ """\r
+ pass\r
+\r
+\r
+ def stopFactory(self):\r
+ """\r
+ Called by Twisted before stopping to listen on port for incoming connections.\r
+ Default implementation does nothing. Override in derived class when appropriate.\r
+ """\r
+ pass\r
+\r
+\r
+class WebSocketClientProtocol(WebSocketProtocol):\r
+ """\r
+ Client protocol for WebSockets.\r
+ """\r
+\r
+ def onConnect(self, connectionResponse):\r
+ """\r
+ Callback fired directly after WebSocket opening handshake when new WebSocket server\r
+ connection was established.\r
+\r
+ :param connectionResponse: WebSocket connection response information.\r
+ :type connectionResponse: instance of :class:`autobahn.websocket.ConnectionResponse`\r
+ """\r
+ pass\r
+\r
+\r
+ def connectionMade(self):\r
+ """\r
+ Called by Twisted when new TCP connection to server was established. Default\r
+ implementation will start the initial WebSocket opening handshake.\r
+ When overriding in derived class, make sure to call this base class\r
+ implementation _before_ your code.\r
+ """\r
+ self.isServer = False\r
+ WebSocketProtocol.connectionMade(self)\r
+ if self.debug:\r
+ log.msg("connection to %s established" % self.peerstr)\r
+ self.startHandshake()\r
+\r
+\r
+ def connectionLost(self, reason):\r
+ """\r
+ Called by Twisted when established TCP connection to server was lost. Default\r
+ implementation will tear down all state properly.\r
+ When overriding in derived class, make sure to call this base class\r
+ implementation _after_ your code.\r
+ """\r
+ WebSocketProtocol.connectionLost(self, reason)\r
+ if self.debug:\r
+ log.msg("connection to %s lost" % self.peerstr)\r
+\r
+\r
+ def createHixieKey(self):\r
+ """\r
+ Supposed to implement the crack smoker algorithm below. Well, crack\r
+ probably wasn't the stuff they smoked - dog poo?\r
+\r
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#page-21\r
+ Items 16 - 22\r
+ """\r
+ spaces1 = random.randint(1, 12)\r
+ max1 = int(4294967295L / spaces1)\r
+ number1 = random.randint(0, max1)\r
+ product1 = number1 * spaces1\r
+ key1 = str(product1)\r
+ rchars = filter(lambda x: (x >= 0x21 and x <= 0x2f) or (x >= 0x3a and x <= 0x7e), range(0,127))\r
+ for i in xrange(random.randint(1, 12)):\r
+ p = random.randint(0, len(key1) - 1)\r
+ key1 = key1[:p] + chr(random.choice(rchars)) + key1[p:]\r
+ for i in xrange(spaces1):\r
+ p = random.randint(1, len(key1) - 2)\r
+ key1 = key1[:p] + ' ' + key1[p:]\r
+ return (key1, number1)\r
+\r
+\r
+ def startHandshake(self):\r
+ """\r
+ Start WebSockets opening handshake.\r
+ """\r
+\r
+ ## construct WS opening handshake HTTP header\r
+ ##\r
+ request = "GET %s HTTP/1.1\x0d\x0a" % self.factory.resource.encode("utf-8")\r
+\r
+ if self.factory.useragent is not None and self.factory.useragent != "":\r
+ request += "User-Agent: %s\x0d\x0a" % self.factory.useragent.encode("utf-8")\r
+\r
+ request += "Host: %s:%d\x0d\x0a" % (self.factory.host.encode("utf-8"), self.factory.port)\r
+ request += "Upgrade: WebSocket\x0d\x0a"\r
+ request += "Connection: Upgrade\x0d\x0a"\r
+\r
+ ## handshake random key\r
+ ##\r
+ if self.version == 0:\r
+ (self.websocket_key1, number1) = self.createHixieKey()\r
+ (self.websocket_key2, number2) = self.createHixieKey()\r
+ self.websocket_key3 = os.urandom(8)\r
+ accept_val = struct.pack(">II", number1, number2) + self.websocket_key3\r
+ self.websocket_expected_challenge_response = hashlib.md5(accept_val).digest()\r
+\r
+ request += "Sec-WebSocket-Key1: %s\x0d\x0a" % self.websocket_key1\r
+ request += "Sec-WebSocket-Key2: %s\x0d\x0a" % self.websocket_key2\r
+ else:\r
+ self.websocket_key = base64.b64encode(os.urandom(16))\r
+ request += "Sec-WebSocket-Key: %s\x0d\x0a" % self.websocket_key\r
+\r
+ ## optional origin announced\r
+ ##\r
+ if self.factory.origin:\r
+ if self.version > 10 or self.version == 0:\r
+ request += "Origin: %d\x0d\x0a" % self.factory.origin.encode("utf-8")\r
+ else:\r
+ request += "Sec-WebSocket-Origin: %d\x0d\x0a" % self.factory.origin.encode("utf-8")\r
+\r
+ ## optional list of WS subprotocols announced\r
+ ##\r
+ if len(self.factory.protocols) > 0:\r
+ request += "Sec-WebSocket-Protocol: %s\x0d\x0a" % ','.join(self.factory.protocols)\r
+\r
+ ## set WS protocol version depending on WS spec version\r
+ ##\r
+ if self.version != 0:\r
+ request += "Sec-WebSocket-Version: %d\x0d\x0a" % WebSocketProtocol.SPEC_TO_PROTOCOL_VERSION[self.version]\r
+\r
+ request += "\x0d\x0a"\r
+\r
+ if self.version == 0:\r
+ request += self.websocket_key3\r
+\r
+ self.http_request_data = request\r
+\r
+ if self.debug:\r
+ log.msg(self.http_request_data)\r
+\r
+ self.sendData(self.http_request_data)\r
+\r
+\r
+ def processHandshake(self):\r
+ """\r
+ Process WebSockets opening handshake response from server.\r
+ """\r
+ ## only proceed when we have fully received the HTTP request line and all headers\r
+ ##\r
+ end_of_header = self.data.find("\x0d\x0a\x0d\x0a")\r
+ if end_of_header >= 0:\r
+\r
+ self.http_response_data = self.data[:end_of_header + 4]\r
+ if self.debug:\r
+ log.msg("received HTTP response:\n\n%s\n\n" % self.http_response_data)\r
+\r
+ ## extract HTTP status line and headers\r
+ ##\r
+ (self.http_status_line, self.http_headers, http_headers_cnt) = parseHttpHeader(self.http_response_data)\r
+\r
+ ## validate WebSocket opening handshake server response\r
+ ##\r
+ if self.debug:\r
+ log.msg("received HTTP status line in opening handshake : %s" % str(self.http_status_line))\r
+ log.msg("received HTTP headers in opening handshake : %s" % str(self.http_headers))\r
+\r
+ ## Response Line\r
+ ##\r
+ sl = self.http_status_line.split()\r
+ if len(sl) < 2:\r
+ return self.failHandshake("Bad HTTP response status line '%s'" % self.http_status_line)\r
+\r
+ ## HTTP version\r
+ ##\r
+ http_version = sl[0].strip()\r
+ if http_version != "HTTP/1.1":\r
+ return self.failHandshake("Unsupported HTTP version ('%s')" % http_version)\r
+\r
+ ## HTTP status code\r
+ ##\r
+ try:\r
+ status_code = int(sl[1].strip())\r
+ except:\r
+ return self.failHandshake("Bad HTTP status code ('%s')" % sl[1].strip())\r
+ if status_code != HTTP_STATUS_CODE_SWITCHING_PROTOCOLS[0]:\r
+\r
+ ## FIXME: handle redirects\r
+ ## FIXME: handle authentication required\r
+\r
+ if len(sl) > 2:\r
+ reason = " - %s" % sl[2].strip()\r
+ else:\r
+ reason = ""\r
+ return self.failHandshake("WebSockets connection upgrade failed (%d%s)" % (status_code, reason))\r
+\r
+ ## Upgrade\r
+ ##\r
+ if not self.http_headers.has_key("upgrade"):\r
+ return self.failHandshake("HTTP Upgrade header missing")\r
+ if self.http_headers["upgrade"].strip().lower() != "websocket":\r
+ return self.failHandshake("HTTP Upgrade header different from 'websocket' (case-insensitive) : %s" % self.http_headers["upgrade"])\r
+\r
+ ## Connection\r
+ ##\r
+ if not self.http_headers.has_key("connection"):\r
+ return self.failHandshake("HTTP Connection header missing")\r
+ connectionUpgrade = False\r
+ for c in self.http_headers["connection"].split(","):\r
+ if c.strip().lower() == "upgrade":\r
+ connectionUpgrade = True\r
+ break\r
+ if not connectionUpgrade:\r
+ return self.failHandshake("HTTP Connection header does not include 'upgrade' value (case-insensitive) : %s" % self.http_headers["connection"])\r
+\r
+ ## compute Sec-WebSocket-Accept\r
+ ##\r
+ if self.version != 0:\r
+ if not self.http_headers.has_key("sec-websocket-accept"):\r
+ return self.failHandshake("HTTP Sec-WebSocket-Accept header missing in opening handshake reply")\r
+ else:\r
+ if http_headers_cnt["sec-websocket-accept"] > 1:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Accept header appears more than once in opening handshake reply")\r
+ sec_websocket_accept_got = self.http_headers["sec-websocket-accept"].strip()\r
+\r
+ sha1 = hashlib.sha1()\r
+ sha1.update(self.websocket_key + WebSocketProtocol.WS_MAGIC)\r
+ sec_websocket_accept = base64.b64encode(sha1.digest())\r
+\r
+ if sec_websocket_accept_got != sec_websocket_accept:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Accept bogus value : expected %s / got %s" % (sec_websocket_accept, sec_websocket_accept_got))\r
+\r
+ ## handle "extensions in use" - if any\r
+ ##\r
+ self.websocket_extensions_in_use = []\r
+ if self.version != 0:\r
+ if self.http_headers.has_key("sec-websocket-extensions"):\r
+ if http_headers_cnt["sec-websocket-extensions"] > 1:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Extensions header appears more than once in opening handshake reply")\r
+ exts = self.http_headers["sec-websocket-extensions"].strip()\r
+ ##\r
+ ## we don't support any extension, but if we did, we needed\r
+ ## to set self.websocket_extensions_in_use here, and don't fail the handshake\r
+ ##\r
+ return self.failHandshake("server wants to use extensions (%s), but no extensions implemented" % exts)\r
+\r
+ ## handle "subprotocol in use" - if any\r
+ ##\r
+ self.websocket_protocol_in_use = None\r
+ if self.http_headers.has_key("sec-websocket-protocol"):\r
+ if http_headers_cnt["sec-websocket-protocol"] > 1:\r
+ return self.failHandshake("HTTP Sec-WebSocket-Protocol header appears more than once in opening handshake reply")\r
+ sp = str(self.http_headers["sec-websocket-protocol"].strip())\r
+ if sp != "":\r
+ if sp not in self.factory.protocols:\r
+ return self.failHandshake("subprotocol selected by server (%s) not in subprotocol list requested by client (%s)" % (sp, str(self.factory.protocols)))\r
+ else:\r
+ ## ok, subprotocol in use\r
+ ##\r
+ self.websocket_protocol_in_use = sp\r
+\r
+\r
+ ## For Hixie-76, we need 16 octets of HTTP request body to complete HS!\r
+ ##\r
+ if self.version == 0:\r
+ if len(self.data) < end_of_header + 4 + 16:\r
+ return\r
+ else:\r
+ challenge_response = self.data[end_of_header + 4:end_of_header + 4 + 16]\r
+ if challenge_response != self.websocket_expected_challenge_response:\r
+ return self.failHandshake("invalid challenge response received from server (Hixie-76)")\r
+\r
+ ## Ok, got complete HS input, remember rest (if any)\r
+ ##\r
+ if self.version == 0:\r
+ self.data = self.data[end_of_header + 4 + 16:]\r
+ else:\r
+ self.data = self.data[end_of_header + 4:]\r
+\r
+ ## opening handshake completed, move WebSockets connection into OPEN state\r
+ ##\r
+ self.state = WebSocketProtocol.STATE_OPEN\r
+ self.inside_message = False\r
+ if self.version != 0:\r
+ self.current_frame = None\r
+ self.websocket_version = self.version\r
+\r
+ ## we handle this symmetrical to server-side .. that is, give the\r
+ ## client a chance to bail out .. i.e. on no subprotocol selected\r
+ ## by server\r
+ try:\r
+ connectionResponse = ConnectionResponse(self.peer,\r
+ self.peerstr,\r
+ self.http_headers,\r
+ None, # FIXME\r
+ self.websocket_protocol_in_use,\r
+ self.websocket_extensions_in_use)\r
+\r
+ self.onConnect(connectionResponse)\r
+\r
+ except Exception, e:\r
+ ## immediately close the WS connection\r
+ ##\r
+ self.failConnection(1000, str(e))\r
+ else:\r
+ ## fire handler on derived class\r
+ ##\r
+ self.onOpen()\r
+\r
+ ## process rest, if any\r
+ ##\r
+ if len(self.data) > 0:\r
+ self.consumeData()\r
+\r
+\r
+ def failHandshake(self, reason):\r
+ """\r
+ During opening handshake the server response is invalid and we drop the\r
+ connection.\r
+ """\r
+ if self.debug:\r
+ log.msg("failing WebSockets opening handshake ('%s')" % reason)\r
+ self.dropConnection(abort = True)\r
+\r
+\r
+class WebSocketClientFactory(protocol.ClientFactory, WebSocketFactory):\r
+ """\r
+ A Twisted factory for WebSockets client protocols.\r
+ """\r
+\r
+ protocol = WebSocketClientProtocol\r
+ """\r
+ The protocol to be spoken. Must be derived from :class:`autobahn.websocket.WebSocketClientProtocol`.\r
+ """\r
+\r
+\r
+ def __init__(self,\r
+\r
+ ## WebSockect session parameters\r
+ url = None,\r
+ origin = None,\r
+ protocols = [],\r
+ useragent = "AutobahnPython/%s" % autobahn.version,\r
+\r
+ ## debugging\r
+ debug = False,\r
+ debugCodePaths = False):\r
+ """\r
+ Create instance of WebSocket client factory.\r
+\r
+ Note that you MUST set URL either here or using setSessionParameters() _before_ the factory is started.\r
+\r
+ :param url: WebSocket URL to connect to - ("ws:" | "wss:") "//" host [ ":" port ] path [ "?" query ].\r
+ :type url: str\r
+ :param origin: The origin to be sent in WebSockets opening handshake or None (default: None).\r
+ :type origin: str\r
+ :param protocols: List of subprotocols the client should announce in WebSockets opening handshake (default: []).\r
+ :type protocols: list of strings\r
+ :param useragent: User agent as announced in HTTP request header or None (default: "AutobahnWebSockets/x.x.x").\r
+ :type useragent: str\r
+ :param debug: Debug mode (default: False).\r
+ :type debug: bool\r
+ :param debugCodePaths: Debug code paths mode (default: False).\r
+ :type debugCodePaths: bool\r
+ """\r
+ self.debug = debug\r
+ self.debugCodePaths = debugCodePaths\r
+\r
+ self.logOctets = debug\r
+ self.logFrames = debug\r
+\r
+ self.isServer = False\r
+\r
+ ## seed RNG which is used for WS opening handshake key and WS frame masks generation\r
+ random.seed()\r
+\r
+ ## default WS session parameters\r
+ ##\r
+ self.setSessionParameters(url, origin, protocols, useragent)\r
+\r
+ ## default WebSocket protocol options\r
+ ##\r
+ self.resetProtocolOptions()\r
+\r
+\r
+ def setSessionParameters(self, url = None, origin = None, protocols = [], useragent = None):\r
+ """\r
+ Set WebSocket session parameters.\r
+\r
+ :param url: WebSocket URL to connect to - ("ws:" | "wss:") "//" host [ ":" port ] path [ "?" query ].\r
+ :type url: str\r
+ :param origin: The origin to be sent in opening handshake.\r
+ :type origin: str\r
+ :param protocols: List of WebSocket subprotocols the client should announce in opening handshake.\r
+ :type protocols: list of strings\r
+ :param useragent: User agent as announced in HTTP request header during opening handshake.\r
+ :type useragent: str\r
+ """\r
+ if url is not None:\r
+ ## parse WebSocket URI into components\r
+ (isSecure, host, port, resource, path, params) = parseWsUrl(url)\r
+ self.url = url\r
+ self.isSecure = isSecure\r
+ self.host = host\r
+ self.port = port\r
+ self.resource = resource\r
+ self.path = path\r
+ self.params = params\r
+ else:\r
+ self.url = None\r
+ self.isSecure = None\r
+ self.host = None\r
+ self.port = None\r
+ self.resource = None\r
+ self.path = None\r
+ self.params = None\r
+\r
+ self.origin = origin\r
+ self.protocols = protocols\r
+ self.useragent = useragent\r
+\r
+\r
+ def resetProtocolOptions(self):\r
+ """\r
+ Reset all WebSocket protocol options to defaults.\r
+ """\r
+ self.version = WebSocketProtocol.DEFAULT_SPEC_VERSION\r
+ self.allowHixie76 = WebSocketProtocol.DEFAULT_ALLOW_HIXIE76\r
+ self.utf8validateIncoming = True\r
+ self.acceptMaskedServerFrames = False\r
+ self.maskClientFrames = True\r
+ self.applyMask = True\r
+ self.maxFramePayloadSize = 0\r
+ self.maxMessagePayloadSize = 0\r
+ self.autoFragmentSize = 0\r
+ self.failByDrop = True\r
+ self.echoCloseCodeReason = False\r
+ self.serverConnectionDropTimeout = 1\r
+ self.openHandshakeTimeout = 5\r
+ self.closeHandshakeTimeout = 1\r
+ self.tcpNoDelay = True\r
+\r
+\r
+ def setProtocolOptions(self,\r
+ version = None,\r
+ allowHixie76 = None,\r
+ utf8validateIncoming = None,\r
+ acceptMaskedServerFrames = None,\r
+ maskClientFrames = None,\r
+ applyMask = None,\r
+ maxFramePayloadSize = None,\r
+ maxMessagePayloadSize = None,\r
+ autoFragmentSize = None,\r
+ failByDrop = None,\r
+ echoCloseCodeReason = None,\r
+ serverConnectionDropTimeout = None,\r
+ openHandshakeTimeout = None,\r
+ closeHandshakeTimeout = None,\r
+ tcpNoDelay = None):\r
+ """\r
+ Set WebSocket protocol options used as defaults for _new_ protocol instances.\r
+\r
+ :param version: The WebSockets protocol spec (draft) version to be used (default: WebSocketProtocol.DEFAULT_SPEC_VERSION).\r
+ :type version: int\r
+ :param allowHixie76: Allow to speak Hixie76 protocol version.\r
+ :type allowHixie76: bool\r
+ :param utf8validateIncoming: Validate incoming UTF-8 in text message payloads (default: True).\r
+ :type utf8validateIncoming: bool\r
+ :param acceptMaskedServerFrames: Accept masked server-to-client frames (default: False).\r
+ :type acceptMaskedServerFrames: bool\r
+ :param maskClientFrames: Mask client-to-server frames (default: True).\r
+ :type maskClientFrames: bool\r
+ :param applyMask: Actually apply mask to payload when mask it present. Applies for outgoing and incoming frames (default: True).\r
+ :type applyMask: bool\r
+ :param maxFramePayloadSize: Maximum frame payload size that will be accepted when receiving or 0 for unlimited (default: 0).\r
+ :type maxFramePayloadSize: int\r
+ :param maxMessagePayloadSize: Maximum message payload size (after reassembly of fragmented messages) that will be accepted when receiving or 0 for unlimited (default: 0).\r
+ :type maxMessagePayloadSize: int\r
+ :param autoFragmentSize: Automatic fragmentation of outgoing data messages (when using the message-based API) into frames with payload length <= this size or 0 for no auto-fragmentation (default: 0).\r
+ :type autoFragmentSize: int\r
+ :param failByDrop: Fail connections by dropping the TCP connection without performing closing handshake (default: True).\r
+ :type failbyDrop: bool\r
+ :param echoCloseCodeReason: Iff true, when receiving a close, echo back close code/reason. Otherwise reply with code == NORMAL, reason = "" (default: False).\r
+ :type echoCloseCodeReason: bool\r
+ :param serverConnectionDropTimeout: When the client expects the server to drop the TCP, timeout in seconds (default: 1).\r
+ :type serverConnectionDropTimeout: float\r
+ :param openHandshakeTimeout: Opening WebSocket handshake timeout, timeout in seconds or 0 to deactivate (default: 0).\r
+ :type openHandshakeTimeout: float\r
+ :param closeHandshakeTimeout: When we expect to receive a closing handshake reply, timeout in seconds (default: 1).\r
+ :type closeHandshakeTimeout: float\r
+ :param tcpNoDelay: TCP NODELAY ("Nagle") socket option (default: True).\r
+ :type tcpNoDelay: bool\r
+ """\r
+ if allowHixie76 is not None and allowHixie76 != self.allowHixie76:\r
+ self.allowHixie76 = allowHixie76\r
+\r
+ if version is not None:\r
+ if version not in WebSocketProtocol.SUPPORTED_SPEC_VERSIONS:\r
+ raise Exception("invalid WebSockets draft version %s (allowed values: %s)" % (version, str(WebSocketProtocol.SUPPORTED_SPEC_VERSIONS)))\r
+ if version == 0 and not self.allowHixie76:\r
+ raise Exception("use of Hixie-76 requires allowHixie76 == True")\r
+ if version != self.version:\r
+ self.version = version\r
+\r
+ if utf8validateIncoming is not None and utf8validateIncoming != self.utf8validateIncoming:\r
+ self.utf8validateIncoming = utf8validateIncoming\r
+\r
+ if acceptMaskedServerFrames is not None and acceptMaskedServerFrames != self.acceptMaskedServerFrames:\r
+ self.acceptMaskedServerFrames = acceptMaskedServerFrames\r
+\r
+ if maskClientFrames is not None and maskClientFrames != self.maskClientFrames:\r
+ self.maskClientFrames = maskClientFrames\r
+\r
+ if applyMask is not None and applyMask != self.applyMask:\r
+ self.applyMask = applyMask\r
+\r
+ if maxFramePayloadSize is not None and maxFramePayloadSize != self.maxFramePayloadSize:\r
+ self.maxFramePayloadSize = maxFramePayloadSize\r
+\r
+ if maxMessagePayloadSize is not None and maxMessagePayloadSize != self.maxMessagePayloadSize:\r
+ self.maxMessagePayloadSize = maxMessagePayloadSize\r
+\r
+ if autoFragmentSize is not None and autoFragmentSize != self.autoFragmentSize:\r
+ self.autoFragmentSize = autoFragmentSize\r
+\r
+ if failByDrop is not None and failByDrop != self.failByDrop:\r
+ self.failByDrop = failByDrop\r
+\r
+ if echoCloseCodeReason is not None and echoCloseCodeReason != self.echoCloseCodeReason:\r
+ self.echoCloseCodeReason = echoCloseCodeReason\r
+\r
+ if serverConnectionDropTimeout is not None and serverConnectionDropTimeout != self.serverConnectionDropTimeout:\r
+ self.serverConnectionDropTimeout = serverConnectionDropTimeout\r
+\r
+ if openHandshakeTimeout is not None and openHandshakeTimeout != self.openHandshakeTimeout:\r
+ self.openHandshakeTimeout = openHandshakeTimeout\r
+\r
+ if closeHandshakeTimeout is not None and closeHandshakeTimeout != self.closeHandshakeTimeout:\r
+ self.closeHandshakeTimeout = closeHandshakeTimeout\r
+\r
+ if tcpNoDelay is not None and tcpNoDelay != self.tcpNoDelay:\r
+ self.tcpNoDelay = tcpNoDelay\r
+\r
+\r
+ def clientConnectionFailed(self, connector, reason):\r
+ """\r
+ Called by Twisted when the connection to server has failed. Default implementation\r
+ does nothing. Override in derived class when appropriate.\r
+ """\r
+ pass\r
+\r
+\r
+ def clientConnectionLost(self, connector, reason):\r
+ """\r
+ Called by Twisted when the connection to server was lost. Default implementation\r
+ does nothing. Override in derived class when appropriate.\r
+ """\r
+ pass\r