1############################################################################### 2## 3## Copyright 2013 Tavendo GmbH 4## 5## Licensed under the Apache License, Version 2.0 (the "License"); 6## you may not use this file except in compliance with the License. 7## You may obtain a copy of the License at 8## 9## http://www.apache.org/licenses/LICENSE-2.0 10## 11## Unless required by applicable law or agreed to in writing, software 12## distributed under the License is distributed on an "AS IS" BASIS, 13## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14## See the License for the specific language governing permissions and 15## limitations under the License. 16## 17############################################################################### 18 19from __future__ import absolute_import 20 21 22__all__ = ["PerMessageSnappyMixin", 23 "PerMessageSnappyOffer", 24 "PerMessageSnappyOfferAccept", 25 "PerMessageSnappyResponse", 26 "PerMessageSnappyResponseAccept", 27 "PerMessageSnappy"] 28 29 30import snappy 31 32from autobahn.websocket.compress_base import PerMessageCompressOffer, \ 33 PerMessageCompressOfferAccept, \ 34 PerMessageCompressResponse, \ 35 PerMessageCompressResponseAccept, \ 36 PerMessageCompress 37 38 39class PerMessageSnappyMixin: 40 """ 41 Mixin class for this extension. 42 """ 43 44 EXTENSION_NAME = "permessage-snappy" 45 """ 46 Name of this WebSocket extension. 47 """ 48 49 50 51class PerMessageSnappyOffer(PerMessageCompressOffer, PerMessageSnappyMixin): 52 """ 53 Set of extension parameters for `permessage-snappy` WebSocket extension 54 offered by a client to a server. 55 """ 56 57 @classmethod 58 def parse(cls, params): 59 """ 60 Parses a WebSocket extension offer for `permessage-snappy` provided by a client to a server. 61 62 :param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`. 63 :type params: list 64 65 :returns: object -- A new instance of :class:`autobahn.compress.PerMessageSnappyOffer`. 66 """ 67 ## extension parameter defaults 68 ## 69 acceptNoContextTakeover = False 70 requestNoContextTakeover = False 71 72 ## 73 ## verify/parse client ("client-to-server direction") parameters of permessage-snappy offer 74 ## 75 for p in params: 76 77 if len(params[p]) > 1: 78 raise Exception("multiple occurence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME)) 79 80 val = params[p][0] 81 82 if p == 'client_no_context_takeover': 83 if val != True: 84 raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME)) 85 else: 86 acceptNoContextTakeover = True 87 88 elif p == 'server_no_context_takeover': 89 if val != True: 90 raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME)) 91 else: 92 requestNoContextTakeover = True 93 94 else: 95 raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME)) 96 97 offer = cls(acceptNoContextTakeover, 98 requestNoContextTakeover) 99 return offer 100 101 102 def __init__(self, 103 acceptNoContextTakeover = True, 104 requestNoContextTakeover = False): 105 """ 106 Constructor. 107 108 :param acceptNoContextTakeover: Iff true, client accepts "no context takeover" feature. 109 :type acceptNoContextTakeover: bool 110 :param requestNoContextTakeover: Iff true, client request "no context takeover" feature. 111 :type requestNoContextTakeover: bool 112 """ 113 if type(acceptNoContextTakeover) != bool: 114 raise Exception("invalid type %s for acceptNoContextTakeover" % type(acceptNoContextTakeover)) 115 116 self.acceptNoContextTakeover = acceptNoContextTakeover 117 118 if type(requestNoContextTakeover) != bool: 119 raise Exception("invalid type %s for requestNoContextTakeover" % type(requestNoContextTakeover)) 120 121 self.requestNoContextTakeover = requestNoContextTakeover 122 123 124 def getExtensionString(self): 125 """ 126 Returns the WebSocket extension configuration string as sent to the server. 127 128 :returns: str -- PMCE configuration string. 129 """ 130 pmceString = self.EXTENSION_NAME 131 if self.acceptNoContextTakeover: 132 pmceString += "; client_no_context_takeover" 133 if self.requestNoContextTakeover: 134 pmceString += "; server_no_context_takeover" 135 return pmceString 136 137 138 def __json__(self): 139 """ 140 Returns a JSON serializable object representation. 141 142 :returns: object -- JSON serializable represention. 143 """ 144 return {'extension': self.EXTENSION_NAME, 145 'acceptNoContextTakeover': self.acceptNoContextTakeover, 146 'requestNoContextTakeover': self.requestNoContextTakeover} 147 148 149 def __repr__(self): 150 """ 151 Returns Python object representation that can be eval'ed to reconstruct the object. 152 153 :returns: str -- Python string representation. 154 """ 155 return "PerMessageSnappyOffer(acceptNoContextTakeover = %s, requestNoContextTakeover = %s)" % (self.acceptNoContextTakeover, self.requestNoContextTakeover) 156 157 158 159class PerMessageSnappyOfferAccept(PerMessageCompressOfferAccept, PerMessageSnappyMixin): 160 """ 161 Set of parameters with which to accept an `permessage-snappy` offer 162 from a client by a server. 163 """ 164 165 def __init__(self, 166 offer, 167 requestNoContextTakeover = False, 168 noContextTakeover = None): 169 """ 170 Constructor. 171 172 :param offer: The offer being accepted. 173 :type offer: Instance of :class:`autobahn.compress.PerMessageSnappyOffer`. 174 :param requestNoContextTakeover: Iff true, server request "no context takeover" feature. 175 :type requestNoContextTakeover: bool 176 :param noContextTakeover: Override server ("server-to-client direction") context takeover (this must be compatible with offer). 177 :type noContextTakeover: bool 178 """ 179 if not isinstance(offer, PerMessageSnappyOffer): 180 raise Exception("invalid type %s for offer" % type(offer)) 181 182 self.offer = offer 183 184 if type(requestNoContextTakeover) != bool: 185 raise Exception("invalid type %s for requestNoContextTakeover" % type(requestNoContextTakeover)) 186 187 if requestNoContextTakeover and not offer.acceptNoContextTakeover: 188 raise Exception("invalid value %s for requestNoContextTakeover - feature unsupported by client" % requestNoContextTakeover) 189 190 self.requestNoContextTakeover = requestNoContextTakeover 191 192 if noContextTakeover is not None: 193 if type(noContextTakeover) != bool: 194 raise Exception("invalid type %s for noContextTakeover" % type(noContextTakeover)) 195 196 if offer.requestNoContextTakeover and not noContextTakeover: 197 raise Exception("invalid value %s for noContextTakeover - client requested feature" % noContextTakeover) 198 199 self.noContextTakeover = noContextTakeover 200 201 202 def getExtensionString(self): 203 """ 204 Returns the WebSocket extension configuration string as sent to the server. 205 206 :returns: str -- PMCE configuration string. 207 """ 208 pmceString = self.EXTENSION_NAME 209 if self.offer.requestNoContextTakeover: 210 pmceString += "; server_no_context_takeover" 211 if self.requestNoContextTakeover: 212 pmceString += "; client_no_context_takeover" 213 return pmceString 214 215 216 def __json__(self): 217 """ 218 Returns a JSON serializable object representation. 219 220 :returns: object -- JSON serializable represention. 221 """ 222 return {'extension': self.EXTENSION_NAME, 223 'offer': self.offer.__json__(), 224 'requestNoContextTakeover': self.requestNoContextTakeover, 225 'noContextTakeover': self.noContextTakeover} 226 227 228 def __repr__(self): 229 """ 230 Returns Python object representation that can be eval'ed to reconstruct the object. 231 232 :returns: str -- Python string representation. 233 """ 234 return "PerMessageSnappyAccept(offer = %s, requestNoContextTakeover = %s, noContextTakeover = %s)" % (self.offer.__repr__(), self.requestNoContextTakeover, self.noContextTakeover) 235 236 237 238class PerMessageSnappyResponse(PerMessageCompressResponse, PerMessageSnappyMixin): 239 """ 240 Set of parameters for `permessage-snappy` responded by server. 241 """ 242 243 @classmethod 244 def parse(cls, params): 245 """ 246 Parses a WebSocket extension response for `permessage-snappy` provided by a server to a client. 247 248 :param params: Output from :func:`autobahn.websocket.WebSocketProtocol._parseExtensionsHeader`. 249 :type params: list 250 251 :returns: object -- A new instance of :class:`autobahn.compress.PerMessageSnappyResponse`. 252 """ 253 client_no_context_takeover = False 254 server_no_context_takeover = False 255 256 for p in params: 257 258 if len(params[p]) > 1: 259 raise Exception("multiple occurence of extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME)) 260 261 val = params[p][0] 262 263 if p == 'client_no_context_takeover': 264 if val != True: 265 raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME)) 266 else: 267 client_no_context_takeover = True 268 269 elif p == 'server_no_context_takeover': 270 if val != True: 271 raise Exception("illegal extension parameter value '%s' for parameter '%s' of extension '%s'" % (val, p, cls.EXTENSION_NAME)) 272 else: 273 server_no_context_takeover = True 274 275 else: 276 raise Exception("illegal extension parameter '%s' for extension '%s'" % (p, cls.EXTENSION_NAME)) 277 278 response = cls(client_no_context_takeover, 279 server_no_context_takeover) 280 return response 281 282 283 def __init__(self, 284 client_no_context_takeover, 285 server_no_context_takeover): 286 self.client_no_context_takeover = client_no_context_takeover 287 self.server_no_context_takeover = server_no_context_takeover 288 289 290 def __json__(self): 291 """ 292 Returns a JSON serializable object representation. 293 294 :returns: object -- JSON serializable represention. 295 """ 296 return {'extension': self.EXTENSION_NAME, 297 'client_no_context_takeover': self.client_no_context_takeover, 298 'server_no_context_takeover': self.server_no_context_takeover} 299 300 301 def __repr__(self): 302 """ 303 Returns Python object representation that can be eval'ed to reconstruct the object. 304 305 :returns: str -- Python string representation. 306 """ 307 return "PerMessageSnappyResponse(client_no_context_takeover = %s, server_no_context_takeover = %s)" % (self.client_no_context_takeover, self.server_no_context_takeover) 308 309 310 311class PerMessageSnappyResponseAccept(PerMessageCompressResponseAccept, PerMessageSnappyMixin): 312 """ 313 Set of parameters with which to accept an `permessage-snappy` response 314 from a server by a client. 315 """ 316 317 def __init__(self, 318 response, 319 noContextTakeover = None): 320 """ 321 Constructor. 322 323 :param response: The response being accepted. 324 :type response: Instance of :class:`autobahn.compress.PerMessageSnappyResponse`. 325 :param noContextTakeover: Override client ("client-to-server direction") context takeover (this must be compatible with response). 326 :type noContextTakeover: bool 327 """ 328 if not isinstance(response, PerMessageSnappyResponse): 329 raise Exception("invalid type %s for response" % type(response)) 330 331 self.response = response 332 333 if noContextTakeover is not None: 334 if type(noContextTakeover) != bool: 335 raise Exception("invalid type %s for noContextTakeover" % type(noContextTakeover)) 336 337 if response.client_no_context_takeover and not noContextTakeover: 338 raise Exception("invalid value %s for noContextTakeover - server requested feature" % noContextTakeover) 339 340 self.noContextTakeover = noContextTakeover 341 342 343 def __json__(self): 344 """ 345 Returns a JSON serializable object representation. 346 347 :returns: object -- JSON serializable represention. 348 """ 349 return {'extension': self.EXTENSION_NAME, 350 'response': self.response.__json__(), 351 'noContextTakeover': self.noContextTakeover} 352 353 354 def __repr__(self): 355 """ 356 Returns Python object representation that can be eval'ed to reconstruct the object. 357 358 :returns: str -- Python string representation. 359 """ 360 return "PerMessageSnappyResponseAccept(response = %s, noContextTakeover = %s)" % (self.response.__repr__(), self.noContextTakeover) 361 362 363 364class PerMessageSnappy(PerMessageCompress, PerMessageSnappyMixin): 365 """ 366 `permessage-snappy` WebSocket extension processor. 367 """ 368 369 @classmethod 370 def createFromResponseAccept(cls, isServer, accept): 371 pmce = cls(isServer, 372 accept.response.server_no_context_takeover, 373 accept.noContextTakeover if accept.noContextTakeover is not None else accept.response.client_no_context_takeover) 374 return pmce 375 376 377 @classmethod 378 def createFromOfferAccept(cls, isServer, accept): 379 pmce = cls(isServer, 380 accept.noContextTakeover if accept.noContextTakeover is not None else accept.offer.requestNoContextTakeover, 381 accept.requestNoContextTakeover) 382 return pmce 383 384 385 def __init__(self, 386 isServer, 387 server_no_context_takeover, 388 client_no_context_takeover): 389 self._isServer = isServer 390 self.server_no_context_takeover = server_no_context_takeover 391 self.client_no_context_takeover = client_no_context_takeover 392 393 self._compressor = None 394 self._decompressor = None 395 396 397 def __json__(self): 398 return {'extension': self.EXTENSION_NAME, 399 'server_no_context_takeover': self.server_no_context_takeover, 400 'client_no_context_takeover': self.client_no_context_takeover} 401 402 403 def __repr__(self): 404 return "PerMessageSnappy(isServer = %s, server_no_context_takeover = %s, client_no_context_takeover = %s)" % (self._isServer, self.server_no_context_takeover, self.client_no_context_takeover) 405 406 407 def startCompressMessage(self): 408 if self._isServer: 409 if self._compressor is None or self.server_no_context_takeover: 410 self._compressor = snappy.StreamCompressor() 411 else: 412 if self._compressor is None or self.client_no_context_takeover: 413 self._compressor = snappy.StreamCompressor() 414 415 416 def compressMessageData(self, data): 417 return self._compressor.add_chunk(data) 418 419 420 def endCompressMessage(self): 421 return "" 422 423 424 def startDecompressMessage(self): 425 if self._isServer: 426 if self._decompressor is None or self.client_no_context_takeover: 427 self._decompressor = snappy.StreamDecompressor() 428 else: 429 if self._decompressor is None or self.server_no_context_takeover: 430 self._decompressor = snappy.StreamDecompressor() 431 432 433 def decompressMessageData(self, data): 434 return self._decompressor.decompress(data) 435 436 437 def endDecompressMessage(self): 438 pass 439