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