websocket-extensions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of websocket-extensions might be problematic. Click here for more details.

@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b1c44128d3db51c789832e4c9c8a69f23dd8b477
4
+ data.tar.gz: 5214cf7259a10a9a6d252b4fa6febaf75d8d8727
5
+ SHA512:
6
+ metadata.gz: 0f2189d6fae9bad78b6b5ec275f17435b09345e3113416b0ae2196d262e91ac20e20928a7692d2b448a7a6dac35a4db0091a1214538d50c24d0a25259e2e169c
7
+ data.tar.gz: fdba7d6246380d2088c62837e6ffc84164d188f9d7c0e0a04d00412be766429ed5aca7382c7b8273f1c0f317fdca1d4d6ab5f158f66728d92dc46e80fcbee587
@@ -0,0 +1,337 @@
1
+ # websocket-extensions [![Build status](https://secure.travis-ci.org/faye/websocket-extensions-ruby.svg)](http://travis-ci.org/faye/websocket-extensions-ruby)
2
+
3
+ A minimal framework that supports the implementation of WebSocket extensions in
4
+ a way that's decoupled from the main protocol. This library aims to allow a
5
+ WebSocket extension to be written and used with any protocol library, by
6
+ defining abstract representations of frames and messages that allow modules to
7
+ co-operate.
8
+
9
+ `websocket-extensions` provides a container for registering extension plugins,
10
+ and provides all the functions required to negotiate which extensions to use
11
+ during a session via the `Sec-WebSocket-Extensions` header. By implementing the
12
+ APIs defined in this document, an extension may be used by any WebSocket library
13
+ based on this framework.
14
+
15
+ ## Installation
16
+
17
+ ```
18
+ $ gem install websocket-extensions
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ There are two main audiences for this library: authors implementing the
24
+ WebSocket protocol, and authors implementing extensions. End users of a
25
+ WebSocket library or an extension should be able to use any extension by passing
26
+ it as an argument to their chosen protocol library, without needing to know how
27
+ either of them work, or how the `websocket-extensions` framework operates.
28
+
29
+ The library is designed with the aim that any protocol implementation and any
30
+ extension can be used together, so long as they support the same abstract
31
+ representation of frames and messages.
32
+
33
+ ### Data types
34
+
35
+ The APIs provided by the framework rely on two data types; extensions will
36
+ expect to be given data and to be able to return data in these formats:
37
+
38
+ #### *Frame*
39
+
40
+ *Frame* is a structure representing a single WebSocket frame of any type. Frames
41
+ are simple objects that must have at least the following properties, which
42
+ represent the data encoded in the frame:
43
+
44
+ | property | description |
45
+ | ------------ | ------------------------------------------------------------------ |
46
+ | `final` | `true` if the `FIN` bit is set, `false` otherwise |
47
+ | `rsv1` | `true` if the `RSV1` bit is set, `false` otherwise |
48
+ | `rsv2` | `true` if the `RSV2` bit is set, `false` otherwise |
49
+ | `rsv3` | `true` if the `RSV3` bit is set, `false` otherwise |
50
+ | `opcode` | the numeric opcode (`0`, `1`, `2`, `8`, `9`, or `10`) of the frame |
51
+ | `masked` | `true` if the `MASK` bit is set, `false` otherwise |
52
+ | `masking_key` | a 4-byte string if `masked` is `true`, otherwise `nil` |
53
+ | `payload` | a string containing the (unmasked) application data |
54
+
55
+ #### *Message*
56
+
57
+ A *Message* represents a complete application message, which can be formed from
58
+ text, binary and continuation frames. It has the following properties:
59
+
60
+ | property | description |
61
+ | -------- | ----------------------------------------------------------------- |
62
+ | `rsv1` | `true` if the first frame of the message has the `RSV1` bit set |
63
+ | `rsv2` | `true` if the first frame of the message has the `RSV2` bit set |
64
+ | `rsv3` | `true` if the first frame of the message has the `RSV3` bit set |
65
+ | `opcode` | the numeric opcode (`1` or `2`) of the first frame of the message |
66
+ | `data` | the concatenation of all the frame payloads in the message |
67
+
68
+ ### For driver authors
69
+
70
+ A driver author is someone implementing the WebSocket protocol proper, and who
71
+ wishes end users to be able to use WebSocket extensions with their library.
72
+
73
+ At the start of a WebSocket session, on both the client and the server side,
74
+ they should begin by creating an extension container and adding whichever
75
+ extensions they want to use.
76
+
77
+ ```rb
78
+ require 'websocket/extensions'
79
+ require 'permessage_deflate'
80
+
81
+ exts = WebSocket::Extensions.new
82
+ exts.add(PermessageDeflate)
83
+ ```
84
+
85
+ In the following examples, `exts` refers to this `Extensions` instance.
86
+
87
+ #### Client sessions
88
+
89
+ Clients will use the methods `generate_offer` and `activate(header)`.
90
+
91
+ As part of the handshake process, the client must send a
92
+ `Sec-WebSocket-Extensions` header to advertise that it supports the registered
93
+ extensions. This header should be generated using:
94
+
95
+ ```rb
96
+ request_headers['Sec-WebSocket-Extensions'] = exts.generate_offer
97
+ ```
98
+
99
+ This returns a string, for example `"permessage-deflate;
100
+ client_max_window_bits"`, that represents all the extensions the client is
101
+ offering to use, and their parameters. This string may contain multiple offers
102
+ for the same extension.
103
+
104
+ When the client receives the handshake response from the server, it should pass
105
+ the incoming `Sec-WebSocket-Extensions` header in to `exts` to activate the
106
+ extensions the server has accepted:
107
+
108
+ ```rb
109
+ exts.activate(response_headers['Sec-WebSocket-Extensions'])
110
+ ```
111
+
112
+ If the server has sent any extension responses that the client does not
113
+ recognize, or are in conflict with one another for use of RSV bits, or that use
114
+ invalid parameters for the named extensions, then `exts.activate` will `raise`.
115
+ In this event, the client driver should fail the connection with closing code
116
+ `1010`.
117
+
118
+ #### Server sessions
119
+
120
+ Servers will use the method `generate_response(header)`.
121
+
122
+ A server session needs to generate a `Sec-WebSocket-Extensions` header to send
123
+ in its handshake response:
124
+
125
+ ```rb
126
+ client_offer = request_env['HTTP_SEC_WEBSOCKET_EXTENSIONS']
127
+ ext_response = exts.generate_response(client_offer)
128
+
129
+ response_headers['Sec-WebSocket-Extensions'] = ext_response
130
+ ```
131
+
132
+ Calling `exts.generate_response(header)` activates those extensions the client
133
+ has asked to use, if they are registered, asks each extension for a set of
134
+ response parameters, and returns a string containing the response parameters for
135
+ all accepted extensions.
136
+
137
+ #### In both directions
138
+
139
+ Both clients and servers will use the methods `valid_frame_rsv(frame)`,
140
+ `process_incoming_message(message)` and `process_outgoing_message(message)`.
141
+
142
+ The WebSocket protocol requires that frames do not have any of the `RSV` bits
143
+ set unless there is an extension in use that allows otherwise. When processing
144
+ an incoming frame, sessions should pass a *Frame* object to:
145
+
146
+ ```rb
147
+ exts.valid_frame_rsv(frame)
148
+ ```
149
+
150
+ If this method returns `false`, the session should fail the WebSocket connection
151
+ with closing code `1002`.
152
+
153
+ To pass incoming messages through the extension stack, a session should
154
+ construct a *Message* object according to the above datatype definitions, and
155
+ call:
156
+
157
+ ```rb
158
+ message = exts.process_incoming_message(message)
159
+ ```
160
+
161
+ If any extensions fail to process the message, then this call will `raise` an
162
+ error and the session should fail the WebSocket connection with closing code
163
+ `1010`. Otherwise, `message` should be passed on to the application.
164
+
165
+ To pass outgoing messages through the extension stack, a session should
166
+ construct a *Message* as before, and call:
167
+
168
+ ```rb
169
+ message = exts.process_outgoing_message(message)
170
+ ```
171
+
172
+ If any extensions fail to process the message, then this call will `raise` an
173
+ error and the session should fail the WebSocket connection with closing code
174
+ `1010`. Otherwise, `message` should be converted into frames (with the message's
175
+ `rsv1`, `rsv2`, `rsv3` and `opcode` set on the first frame) and written to the
176
+ transport.
177
+
178
+ At the end of the WebSocket session (either when the protocol is explicitly
179
+ ended or the transport connection disconnects), the driver should call:
180
+
181
+ ```rb
182
+ exts.close
183
+ ```
184
+
185
+ ### For extension authors
186
+
187
+ An extension author is someone implementing an extension that transforms
188
+ WebSocket messages passing between the client and server. They would like to
189
+ implement their extension once and have it work with any protocol library.
190
+
191
+ Extension authors will not install `websocket-extensions` or call it directly.
192
+ Instead, they should implement the following API to allow their extension to
193
+ plug into the `websocket-extensions` framework.
194
+
195
+ An `Extension` is any object that has the following properties:
196
+
197
+ | property | description |
198
+ | -------- | ---------------------------------------------------------------------------- |
199
+ | `name` | a string containing the name of the extension as used in negotiation headers |
200
+ | `type` | a string, must be `"permessage"` |
201
+ | `rsv1` | either `true` if the extension uses the RSV1 bit, `false` otherwise |
202
+ | `rsv2` | either `true` if the extension uses the RSV2 bit, `false` otherwise |
203
+ | `rsv3` | either `true` if the extension uses the RSV3 bit, `false` otherwise |
204
+
205
+ It must also implement the following methods:
206
+
207
+ ```rb
208
+ ext.create_client_session
209
+ ```
210
+
211
+ This returns a *ClientSession*, whose interface is defined below.
212
+
213
+ ```rb
214
+ ext.create_server_session(offers)
215
+ ```
216
+
217
+ This takes an array of offer params and returns a *ServerSession*, whose
218
+ interface is defined below. For example, if the client handshake contains the
219
+ offer header:
220
+
221
+ ```
222
+ Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; server_max_window_bits=8, \
223
+ permessage-deflate; server_max_window_bits=15
224
+ ```
225
+
226
+ then the `permessage-deflate` extension will receive the call:
227
+
228
+ ```rb
229
+ ext.create_server_session([
230
+ {'server_no_context_takeover' => true, 'server_max_window_bits' => 8},
231
+ {'server_max_window_bits' => 15}
232
+ ])
233
+ ```
234
+
235
+ The extension must decide which set of parameters it wants to accept, if any,
236
+ and return a *ServerSession* if it wants to accept the parameters and `nil`
237
+ otherwise.
238
+
239
+ #### *ClientSession*
240
+
241
+ A *ClientSession* is the type returned by `ext.create_client_session`. It must
242
+ implement the following methods, as well as the *Session* API listed below.
243
+
244
+ ```rb
245
+ client_session.generate_offer
246
+ # e.g. -> [
247
+ # {'server_no_context_takeover' => true, 'server_max_window_bits' => 8},
248
+ # {'server_max_window_bits' => 15}
249
+ # ]
250
+ ```
251
+
252
+ This must return a set of parameters to include in the client's
253
+ `Sec-WebSocket-Extensions` offer header. If the session wants to offer multiple
254
+ configurations, it can return an array of sets of parameters as shown above.
255
+
256
+ ```rb
257
+ client_session.activate(params) # -> true
258
+ ```
259
+
260
+ This must take a single set of parameters from the server's handshake response
261
+ and use them to configure the client session. If the client accepts the given
262
+ parameters, then this method must return `true`. If it returns any other value,
263
+ the framework will interpret this as the client rejecting the response, and will
264
+ `raise`.
265
+
266
+ #### *ServerSession*
267
+
268
+ A *ServerSession* is the type returned by `ext.create_server_session(offers)`. It
269
+ must implement the following methods, as well as the *Session* API listed below.
270
+
271
+ ```rb
272
+ server_session.generate_response
273
+ # e.g. -> {'server_max_window_bits' => 8}
274
+ ```
275
+
276
+ This returns the set of parameters the server session wants to send in its
277
+ `Sec-WebSocket-Extensions` response header. Only one set of parameters is
278
+ returned to the client per extension. Server sessions that would confict on
279
+ their use of RSV bits are not activated.
280
+
281
+ #### *Session*
282
+
283
+ The *Session* API must be implemented by both client and server sessions. It
284
+ contains three methods: `process_incoming_message(message)` and
285
+ `process_outgoing_message(message)`.
286
+
287
+ ```rb
288
+ message = session.process_incoming_message(message)
289
+ ```
290
+
291
+ The session must implement this method to take an incoming *Message* as defined
292
+ above, transform it in any way it needs, then return it. If there is an error
293
+ processing the message, this method should `raise` an error.
294
+
295
+ ```rb
296
+ message = session.process_outgoing_message(message)
297
+ ```
298
+
299
+ The session must implement this method to take an outgoing *Message* as defined
300
+ above, transform it in any way it needs, then return it. If there is an error
301
+ processing the message, this method should `raise` an error.
302
+
303
+ ```rb
304
+ session.close
305
+ ```
306
+
307
+ The framework will call this method when the WebSocket session ends, allowing
308
+ the session to release any resources it's using.
309
+
310
+ ## Examples
311
+
312
+ * Consumer: [websocket-driver](https://github.com/faye/websocket-driver-ruby)
313
+ * Provider: [permessage-deflate](https://github.com/faye/permessage-deflate-ruby)
314
+
315
+ ## License
316
+
317
+ (The MIT License)
318
+
319
+ Copyright (c) 2014 James Coglan
320
+
321
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
322
+ this software and associated documentation files (the 'Software'), to deal in
323
+ the Software without restriction, including without limitation the rights to
324
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
325
+ of the Software, and to permit persons to whom the Software is furnished to do
326
+ so, subject to the following conditions:
327
+
328
+ The above copyright notice and this permission notice shall be included in all
329
+ copies or substantial portions of the Software.
330
+
331
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
332
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
333
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
334
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
335
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
336
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
337
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ module WebSocket
2
+ class Extensions
3
+
4
+ autoload :Parser, File.expand_path('../extensions/parser', __FILE__)
5
+
6
+ ExtensionError = Class.new(ArgumentError)
7
+
8
+ MESSAGE_OPCODES = [1, 2]
9
+
10
+ def initialize
11
+ @rsv1 = @rsv2 = @rsv3 = nil
12
+
13
+ @by_name = {}
14
+ @in_order = []
15
+ @sessions = []
16
+ @index = {}
17
+ end
18
+
19
+ def add(ext)
20
+ unless ext.respond_to?(:name) and ext.name.is_a?(String)
21
+ raise TypeError, 'extension.name must be a string'
22
+ end
23
+
24
+ unless ext.respond_to?(:type) and ext.type == 'permessage'
25
+ raise TypeError, 'extension.type must be "permessage"'
26
+ end
27
+
28
+ unless ext.respond_to?(:rsv1) and [true, false].include?(ext.rsv1)
29
+ raise TypeError, 'extension.rsv1 must be true or false'
30
+ end
31
+
32
+ unless ext.respond_to?(:rsv2) and [true, false].include?(ext.rsv2)
33
+ raise TypeError, 'extension.rsv2 must be true or false'
34
+ end
35
+
36
+ unless ext.respond_to?(:rsv3) and [true, false].include?(ext.rsv3)
37
+ raise TypeError, 'extension.rsv3 must be true or false'
38
+ end
39
+
40
+ if @by_name.has_key?(ext.name)
41
+ raise TypeError, %Q{An extension with name "#{ext.name}" is already registered}
42
+ end
43
+
44
+ @by_name[ext.name] = ext
45
+ @in_order.push(ext)
46
+ end
47
+
48
+ def generate_offer
49
+ sessions = []
50
+ offer = []
51
+ index = {}
52
+
53
+ @in_order.each do |ext|
54
+ session = ext.create_client_session
55
+ next unless session
56
+
57
+ record = [ext, session]
58
+ sessions.push(record)
59
+ index[ext.name] = record
60
+
61
+ offers = session.generate_offer
62
+ offers = offers ? [offers].flatten : []
63
+
64
+ offers.each do |off|
65
+ offer.push(Parser.serialize_params(ext.name, off))
66
+ end
67
+ end
68
+
69
+ @sessions = sessions
70
+ @index = index
71
+
72
+ offer.size > 0 ? offer.join(', ') : nil
73
+ end
74
+
75
+ def activate(header)
76
+ responses = Parser.parse_header(header)
77
+ @sessions = []
78
+
79
+ responses.each_offer do |name, params|
80
+ unless record = @index[name]
81
+ raise ExtensionError, %Q{Server sent am extension response for unknown extension "#{name}"}
82
+ end
83
+
84
+ ext, session = *record
85
+
86
+ if reserved = reserved?(ext)
87
+ raise ExtensionError, %Q{Server sent two extension responses that use the RSV#{reserved[0]} } +
88
+ %Q{ bit: "#{reserved[1]}" and "#{ext.name}"}
89
+ end
90
+
91
+ unless session.activate(params) == true
92
+ raise ExtensionError, %Q{Server send unacceptable extension parameters: #{Parser.serialize_params(name, params)}}
93
+ end
94
+
95
+ reserve(ext)
96
+ @sessions.push(record)
97
+ end
98
+ end
99
+
100
+ def generate_response(header)
101
+ offers = Parser.parse_header(header)
102
+ sessions = []
103
+ response = []
104
+
105
+ @in_order.each do |ext|
106
+ offer = offers.by_name(ext.name)
107
+ next if offer.empty? or reserved?(ext)
108
+
109
+ next unless session = ext.create_server_session(offer)
110
+
111
+ reserve(ext)
112
+ sessions.push([ext, session])
113
+ response.push(Parser.serialize_params(ext.name, session.generate_response))
114
+ end
115
+
116
+ @sessions = sessions
117
+ response.size > 0 ? response.join(', ') : nil
118
+ end
119
+
120
+ def valid_frame_rsv(frame)
121
+ allowed = {:rsv1 => false, :rsv2 => false, :rsv3 => false}
122
+
123
+ if MESSAGE_OPCODES.include?(frame.opcode)
124
+ @sessions.each do |ext, session|
125
+ allowed[:rsv1] ||= ext.rsv1
126
+ allowed[:rsv2] ||= ext.rsv2
127
+ allowed[:rsv3] ||= ext.rsv3
128
+ end
129
+ end
130
+
131
+ (allowed[:rsv1] || !frame.rsv1) &&
132
+ (allowed[:rsv2] || !frame.rsv2) &&
133
+ (allowed[:rsv3] || !frame.rsv3)
134
+ end
135
+ alias :valid_frame_rsv? :valid_frame_rsv
136
+
137
+ def process_incoming_message(message)
138
+ @sessions.reverse.inject(message) do |msg, (ext, session)|
139
+ begin
140
+ session.process_incoming_message(msg)
141
+ rescue => error
142
+ raise ExtensionError, [ext.name, error.message].join(': ')
143
+ end
144
+ end
145
+ end
146
+
147
+ def process_outgoing_message(message)
148
+ @sessions.inject(message) do |msg, (ext, session)|
149
+ begin
150
+ session.process_outgoing_message(msg)
151
+ rescue => error
152
+ raise ExtensionError, [ext.name, error.message].join(': ')
153
+ end
154
+ end
155
+ end
156
+
157
+ def close
158
+ @sessions.each do |ext, session|
159
+ session.close rescue nil
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def reserve(ext)
166
+ @rsv1 ||= ext.rsv1 && ext.name
167
+ @rsv2 ||= ext.rsv2 && ext.name
168
+ @rsv3 ||= ext.rsv3 && ext.name
169
+ end
170
+
171
+ def reserved?(ext)
172
+ return [1, @rsv1] if @rsv1 and ext.rsv1
173
+ return [2, @rsv2] if @rsv2 and ext.rsv2
174
+ return [3, @rsv3] if @rsv3 and ext.rsv3
175
+ false
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,109 @@
1
+ module WebSocket
2
+ class Extensions
3
+
4
+ class Parser
5
+ TOKEN = /([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+)/
6
+ NOTOKEN = /([^!#\$%&'\*\+\-\.\^_`\|~0-9a-z])/
7
+ QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"/
8
+ PARAM = %r{#{TOKEN.source}(?:=(?:#{TOKEN.source}|#{QUOTED.source}))?}
9
+ EXT = %r{#{TOKEN.source}(?: *; *#{PARAM.source})*}
10
+ EXT_LIST = %r{^#{EXT.source}(?: *, *#{EXT.source})*$}
11
+ NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/
12
+
13
+ ParseError = Class.new(ArgumentError)
14
+
15
+ def self.parse_header(header)
16
+ offers = Offers.new
17
+ return offers if header == '' or header.nil?
18
+
19
+ unless header =~ EXT_LIST
20
+ raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{header}"
21
+ end
22
+
23
+ scanner = StringScanner.new(header)
24
+ value = scanner.scan(EXT)
25
+
26
+ until value.nil?
27
+ params = value.scan(PARAM)
28
+ name = params.shift.first
29
+ offer = {}
30
+
31
+ params.each do |key, unquoted, quoted|
32
+ if unquoted
33
+ data = unquoted
34
+ elsif quoted
35
+ data = quoted.gsub(/\\/, '')
36
+ else
37
+ data = true
38
+ end
39
+ if data =~ NUMBER
40
+ data = data =~ /\./ ? data.to_f : data.to_i(10)
41
+ end
42
+
43
+ if offer.has_key?(key)
44
+ offer[key] = [*offer[key]] + [data]
45
+ else
46
+ offer[key] = data
47
+ end
48
+ end
49
+
50
+ offers.push(name, offer)
51
+
52
+ scanner.scan(/ *, */)
53
+ value = scanner.scan(EXT)
54
+ end
55
+ offers
56
+ end
57
+
58
+ def self.serialize_params(name, params)
59
+ values = []
60
+
61
+ print = lambda do |key, value|
62
+ case value
63
+ when Array then value.each { |v| print[key, v] }
64
+ when true then values.push(key)
65
+ when Numeric then values.push(key + '=' + value.to_s)
66
+ else
67
+ if value =~ NOTOKEN
68
+ values.push(key + '="' + value.gsub(/"/, '\"') + '"')
69
+ else
70
+ values.push(key + '=' + value)
71
+ end
72
+ end
73
+ end
74
+
75
+ params.keys.sort.each { |key| print[key, params[key]] }
76
+
77
+ ([name] + values).join('; ')
78
+ end
79
+ end
80
+
81
+ class Offers
82
+ def initialize
83
+ @by_name = {}
84
+ @in_order = []
85
+ end
86
+
87
+ def push(name, params)
88
+ @by_name[name] ||= []
89
+ @by_name[name].push(params)
90
+ @in_order.push(:name => name, :params => params)
91
+ end
92
+
93
+ def each_offer(&block)
94
+ @in_order.each do |offer|
95
+ block.call(offer[:name], offer[:params])
96
+ end
97
+ end
98
+
99
+ def by_name(name)
100
+ @by_name[name] || []
101
+ end
102
+
103
+ def to_a
104
+ @in_order.dup
105
+ end
106
+ end
107
+
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: websocket-extensions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Coglan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email: jcoglan@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files:
32
+ - README.md
33
+ files:
34
+ - README.md
35
+ - lib/websocket/extensions.rb
36
+ - lib/websocket/extensions/parser.rb
37
+ homepage: http://github.com/faye/websocket-extensions-ruby
38
+ licenses:
39
+ - MIT
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options:
43
+ - "--main"
44
+ - README.md
45
+ - "--markup"
46
+ - markdown
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.2.2
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Generic extension manager for WebSocket connections
65
+ test_files: []