mongrel2 0.15.1 → 0.16.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.
@@ -58,7 +58,7 @@ class Mongrel2::Request
58
58
  ###
59
59
  ### class MyFramework::JSONRequest < Mongrel2::JSONRequest
60
60
  ### register_request_type self, 'JSON'
61
- ###
61
+ ###
62
62
  ### # Override #initialize to do any stuff specific to your
63
63
  ### # request type, but you'll likely want to super() to
64
64
  ### # Mongrel2::JSONRequest.
@@ -69,9 +69,10 @@ class Mongrel2::Request
69
69
  ###
70
70
  ### end # class MyFramework::JSONRequest
71
71
  ###
72
- ### If you wish one of your subclasses to be used instead of Mongrel2::Request
72
+ ### If you wish one of your subclasses to be used instead of Mongrel2::Request
73
73
  ### for the default request class, register it with a METHOD of :__default.
74
74
  def self::register_request_type( subclass, *req_methods )
75
+ Mongrel2.log.debug "Registering %p for %p requests" % [ subclass, req_methods ]
75
76
  req_methods.each do |methname|
76
77
  if methname == :__default
77
78
  # Clear cached lookups
@@ -103,7 +104,7 @@ class Mongrel2::Request
103
104
  ### I N S T A N C E M E T H O D S
104
105
  #################################################################
105
106
 
106
- ### Create a new Request object with the given +sender_id+, +conn_id+, +path+, +headers+,
107
+ ### Create a new Request object with the given +sender_id+, +conn_id+, +path+, +headers+,
107
108
  ### and +body+. The optional +nil+ is for the raw request content, which can be useful
108
109
  ### later for debugging.
109
110
  def initialize( sender_id, conn_id, path, headers, body='', raw=nil )
@@ -67,11 +67,27 @@ module Mongrel2
67
67
  end
68
68
 
69
69
 
70
+ ### Return the default testing headers hash for the receiving class.
71
+ def self::default_headers
72
+ return const_get( :DEFAULT_TESTING_HEADERS )
73
+ end
74
+
75
+
76
+ ### Return the default configuration for the receiving factory class.
77
+ def self::default_factory_config
78
+ return const_get( :DEFAULT_FACTORY_CONFIG )
79
+ end
80
+
81
+
82
+ #############################################################
83
+ ### I N S T A N C E M E T H O D S
84
+ #############################################################
85
+
70
86
  ### Create a new RequestFactory with the given +config+, which will be merged with
71
87
  ### DEFAULT_FACTORY_CONFIG.
72
88
  def initialize( config={} )
73
- config[:headers] = DEFAULT_TESTING_HEADERS.merge( config[:headers] ) if config[:headers]
74
- config = DEFAULT_FACTORY_CONFIG.merge( config )
89
+ config[:headers] = self.class.default_headers.merge( config[:headers] ) if config[:headers]
90
+ config = self.class.default_factory_config.merge( config )
75
91
 
76
92
  @sender_id = config[:sender_id]
77
93
  @host = config[:host]
@@ -198,5 +214,188 @@ module Mongrel2
198
214
 
199
215
  end # RequestFactory
200
216
 
217
+
218
+ ### A factory for generating WebSocket request objects for testing.
219
+ class WebSocketFrameFactory < Mongrel2::RequestFactory
220
+ include Mongrel2::Constants
221
+
222
+ # The default host
223
+ DEFAULT_TESTING_HOST = 'localhost'
224
+ DEFAULT_TESTING_PORT = '8113'
225
+ DEFAULT_TESTING_ROUTE = '/ws'
226
+
227
+ # The default WebSocket opcode
228
+ DEFAULT_OPCODE = :text
229
+
230
+ # Default headers
231
+ DEFAULT_TESTING_HEADERS = {
232
+ 'METHOD' => 'WEBSOCKET',
233
+ 'PATTERN' => '/ws',
234
+ 'URI' => '/ws',
235
+ 'VERSION' => 'HTTP/1.1',
236
+ 'PATH' => '/ws',
237
+ 'upgrade' => 'websocket',
238
+ 'host' => DEFAULT_TESTING_HOST,
239
+ 'sec-websocket-key' => 'rBP9u8uxVvIYrH/8bNOPwQ==',
240
+ 'sec-websocket-version' => '13',
241
+ 'connection' => 'Upgrade',
242
+ 'origin' => "http://#{DEFAULT_TESTING_HOST}",
243
+ 'FLAGS' => '0x89', # FIN + PING
244
+ 'x-forwarded-for' => '127.0.0.1'
245
+ }
246
+
247
+ # The defaults used by the websocket request factory
248
+ DEFAULT_FACTORY_CONFIG = {
249
+ :sender_id => DEFAULT_TEST_UUID,
250
+ :conn_id => DEFAULT_CONN_ID,
251
+ :host => DEFAULT_TESTING_HOST,
252
+ :port => DEFAULT_TESTING_PORT,
253
+ :route => DEFAULT_TESTING_ROUTE,
254
+ :headers => DEFAULT_TESTING_HEADERS,
255
+ }
256
+
257
+ # Freeze all testing constants
258
+ constants.each do |cname|
259
+ const_get(cname).freeze
260
+ end
261
+
262
+
263
+ ### Create a new factory using the specified +config+.
264
+ def initialize( config={} )
265
+ config[:headers] = DEFAULT_TESTING_HEADERS.merge( config[:headers] ) if config[:headers]
266
+ config = DEFAULT_FACTORY_CONFIG.merge( config )
267
+
268
+ @sender_id = config[:sender_id]
269
+ @host = config[:host]
270
+ @port = config[:port]
271
+ @route = config[:route]
272
+ @headers = Mongrel2::Table.new( config[:headers] )
273
+
274
+ @conn_id = 0
275
+ end
276
+
277
+ ######
278
+ public
279
+ ######
280
+
281
+ ### Create a new request with the specified +uri+, +data+, and +flags+.
282
+ def create( uri, data, *flags )
283
+ raise "Request doesn't route through %p" % [ self.route ] unless
284
+ uri.start_with?( self.route )
285
+
286
+ headers = if flags.last.is_a?( Hash ) then flags.pop else {} end
287
+ flagheader = make_flags_header( flags )
288
+ headers = self.make_merged_headers( uri, flagheader, headers )
289
+ rclass = Mongrel2::Request.subclass_for_method( :WEBSOCKET )
290
+
291
+ return rclass.new( self.sender_id, self.conn_id.to_s, self.route, headers, data )
292
+ end
293
+
294
+
295
+ ### Create a continuation frame.
296
+ def continuation( uri, payload='', *flags )
297
+ flags << :continuation
298
+ return self.create( uri, payload, flags )
299
+ end
300
+
301
+
302
+ ### Create a text frame.
303
+ def text( uri, payload='', *flags )
304
+ flags << :text
305
+ return self.create( uri, payload, flags )
306
+ end
307
+
308
+
309
+ ### Create a binary frame.
310
+ def binary( uri, payload='', *flags )
311
+ flags << :binary
312
+ return self.create( uri, payload, flags )
313
+ end
314
+
315
+
316
+ ### Create a close frame.
317
+ def close( uri, payload='', *flags )
318
+ flags << :close << :fin
319
+ return self.create( uri, payload, flags )
320
+ end
321
+
322
+
323
+ ### Create a ping frame.
324
+ def ping( uri, payload='', *flags )
325
+ flags << :ping << :fin
326
+ return self.create( uri, payload, flags )
327
+ end
328
+
329
+
330
+ ### Create a pong frame.
331
+ def pong( uri, payload='', *flags )
332
+ flags << :pong << :fin
333
+ return self.create( uri, payload, flags )
334
+ end
335
+
336
+
337
+
338
+ #########
339
+ protected
340
+ #########
341
+
342
+ ### Merge the factory's headers with +userheaders+, and then merge in the
343
+ ### special headers that Mongrel2 adds that are based on the +uri+ and other
344
+ ### server attributes.
345
+ def make_merged_headers( uri, flags, userheaders )
346
+ headers = self.headers.merge( userheaders )
347
+ uri = URI( uri )
348
+
349
+ # Add mongrel headers
350
+ headers.uri = uri.to_s
351
+ headers.path = uri.path
352
+ headers.host = "%s:%d" % [ self.host, self.port ]
353
+ headers.query = uri.query if uri.query
354
+ headers.pattern = self.route
355
+ headers.origin = "http://#{headers.host}"
356
+ headers.flags = "0x%02x" % [ flags ]
357
+
358
+ return headers
359
+ end
360
+
361
+
362
+ #######
363
+ private
364
+ #######
365
+
366
+ ### Make a flags value out of flag Symbols that correspond to the flag
367
+ ### bits and opcodes: [ :fin, :rsv1, :rsv2, :rsv3, :continuation,
368
+ ### :text, :binary, :close, :ping, :pong ]. If the flags contain
369
+ ### Integers instead, they are ORed with the result.
370
+ def make_flags_header( *flag_symbols )
371
+ flag_symbols.flatten!
372
+ flag_symbols.compact!
373
+
374
+ Mongrel2.log.debug "Making a flags header for symbols: %p" % [ flag_symbols ]
375
+
376
+ return flag_symbols.inject( 0x00 ) do |flags, flag|
377
+ case flag
378
+ when :fin
379
+ flags | WebSocket::FIN_FLAG
380
+ when :rsv1
381
+ flags | WebSocket::RSV1_FLAG
382
+ when :rsv2
383
+ flags | WebSocket::RSV2_FLAG
384
+ when :rsv3
385
+ flags | WebSocket::RSV3_FLAG
386
+ when :continuation, :text, :binary, :close, :ping, :pong
387
+ # Opcodes clear any other opcodes present
388
+ flags ^= ( flags & WebSocket::OPCODE_BITMASK )
389
+ flags | WebSocket::OPCODE[ flag ]
390
+ when Integer
391
+ flags | flag
392
+ else
393
+ raise ArgumentError, "Don't know what the %p flag is." % [ flag ]
394
+ end
395
+ end
396
+ end
397
+
398
+ end # class WebSocketFrameFactory
399
+
201
400
  end # module Mongrel2
202
401
 
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'mongrel2/request' unless defined?( Mongrel2::Request )
4
+ require 'mongrel2/mixins'
5
+ require 'mongrel2/constants'
6
+
7
+
8
+ # The Mongrel2 WebSocket namespace module. Contains constants and classes for
9
+ # building WebSocket services.
10
+ #
11
+ # class WebSocketEchoServer
12
+ #
13
+ # def handle_websocket( frame )
14
+ #
15
+ # # Close connections that send invalid frames
16
+ # if !frame.valid?
17
+ # res = frame.response( :close )
18
+ # res.set_close_status( WebSocket::CLOSE_PROTOCOL_ERROR )
19
+ # return res
20
+ # end
21
+ #
22
+ # # Do something with the frame
23
+ # ...
24
+ # end
25
+ # end
26
+ module Mongrel2::WebSocket
27
+
28
+ # WebSocket-related header and status constants
29
+ module Constants
30
+ # WebSocket frame header
31
+ # 0 1 2 3
32
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
33
+ # +-+-+-+-+-------+-+-------------+-------------------------------+
34
+ # |F|R|R|R| opcode|M| Payload len | Extended payload length |
35
+ # |I|S|S|S| (4) |A| (7) | (16/64) |
36
+ # |N|V|V|V| |S| | (if payload len==126/127) |
37
+ # | |1|2|3| |K| | |
38
+ # +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
39
+ # | Extended payload length continued, if payload len == 127 |
40
+ # + - - - - - - - - - - - - - - - +-------------------------------+
41
+ # | |Masking-key, if MASK set to 1 |
42
+ # +-------------------------------+-------------------------------+
43
+ # | Masking-key (continued) | Payload Data |
44
+ # +-------------------------------- - - - - - - - - - - - - - - - +
45
+ # : Payload Data continued ... :
46
+ # + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
47
+ # | Payload Data continued ... |
48
+ # +---------------------------------------------------------------+
49
+
50
+ # Masks of the bits of the FLAGS header that corresponds to the FIN and RSV1-3 flags
51
+ FIN_FLAG = 0b10000000
52
+ RSV1_FLAG = 0b01000000
53
+ RSV2_FLAG = 0b00100000
54
+ RSV3_FLAG = 0b00010000
55
+
56
+ # Mask for checking for one or more of the RSV[1-3] flags
57
+ RSV_FLAG_MASK = 0b01110000
58
+
59
+ # Mask for picking the opcode out of the flags header
60
+ OPCODE_BITMASK = 0b00001111
61
+
62
+ # Mask for testing to see if the frame is a control frame
63
+ OPCODE_CONTROL_MASK = 0b00001000
64
+
65
+ # %x0 denotes a continuation frame
66
+ # %x1 denotes a text frame
67
+ # %x2 denotes a binary frame
68
+ # %x3-7 are reserved for further non-control frames
69
+ # %x8 denotes a connection close
70
+ # %x9 denotes a ping
71
+ # %xA denotes a pong
72
+ # %xB-F are reserved for further control frames
73
+
74
+ # Opcodes from the flags header
75
+ OPCODE_NAME = Hash.new do |codes,bit|
76
+ raise RangeError, "invalid opcode %d!" % [ bit ] unless bit.between?( 0x0, 0xf )
77
+ codes[ bit ] = :reserved
78
+ end
79
+ OPCODE_NAME[ 0x0 ] = :continuation
80
+ OPCODE_NAME[ 0x1 ] = :text
81
+ OPCODE_NAME[ 0x2 ] = :binary
82
+ OPCODE_NAME[ 0x8 ] = :close
83
+ OPCODE_NAME[ 0x9 ] = :ping
84
+ OPCODE_NAME[ 0xA ] = :pong
85
+
86
+ # Opcode bits keyed by name
87
+ OPCODE = OPCODE_NAME.invert
88
+
89
+ # Closing status codes (http://tools.ietf.org/html/rfc6455#section-7.4.1)
90
+
91
+ # 1000 indicates a normal closure, meaning that the purpose for
92
+ # which the connection was established has been fulfilled.
93
+ CLOSE_NORMAL = 1000
94
+
95
+ # 1001 indicates that an endpoint is "going away", such as a server
96
+ # going down or a browser having navigated away from a page.
97
+ CLOSE_GOING_AWAY = 1001
98
+
99
+ # 1002 indicates that an endpoint is terminating the connection due
100
+ # to a protocol error.
101
+ CLOSE_PROTOCOL_ERROR = 1002
102
+
103
+ # 1003 indicates that an endpoint is terminating the connection
104
+ # because it has received a type of data it cannot accept (e.g., an
105
+ # endpoint that understands only text data MAY send this if it
106
+ # receives a binary message).
107
+ CLOSE_BAD_DATA_TYPE = 1003
108
+
109
+ # Reserved. The specific meaning might be defined in the future.
110
+ CLOSE_RESERVED = 1004
111
+
112
+ # 1005 is a reserved value and MUST NOT be set as a status code in a
113
+ # Close control frame by an endpoint. It is designated for use in
114
+ # applications expecting a status code to indicate that no status
115
+ # code was actually present.
116
+ CLOSE_MISSING_STATUS = 1005
117
+
118
+ # 1006 is a reserved value and MUST NOT be set as a status code in a
119
+ # Close control frame by an endpoint. It is designated for use in
120
+ # applications expecting a status code to indicate that the
121
+ # connection was closed abnormally, e.g., without sending or
122
+ # receiving a Close control frame.
123
+ CLOSE_ABNORMAL_STATUS = 1006
124
+
125
+ # 1007 indicates that an endpoint is terminating the connection
126
+ # because it has received data within a message that was not
127
+ # consistent with the type of the message (e.g., non-UTF-8 [RFC3629]
128
+ # data within a text message).
129
+ CLOSE_BAD_DATA = 1007
130
+
131
+ # 1008 indicates that an endpoint is terminating the connection
132
+ # because it has received a message that violates its policy. This
133
+ # is a generic status code that can be returned when there is no
134
+ # other more suitable status code (e.g., 1003 or 1009) or if there
135
+ # is a need to hide specific details about the policy.
136
+ CLOSE_POLICY_VIOLATION = 1008
137
+
138
+ # 1009 indicates that an endpoint is terminating the connection
139
+ # because it has received a message that is too big for it to
140
+ # process.
141
+ CLOSE_MESSAGE_TOO_LARGE = 1009
142
+
143
+ # 1010 indicates that an endpoint (client) is terminating the
144
+ # connection because it has expected the server to negotiate one or
145
+ # more extension, but the server didn't return them in the response
146
+ # message of the WebSocket handshake. The list of extensions that
147
+ # are needed SHOULD appear in the /reason/ part of the Close frame.
148
+ # Note that this status code is not used by the server, because it
149
+ # can fail the WebSocket handshake instead.
150
+ CLOSE_MISSING_EXTENSION = 1010
151
+
152
+ # 1011 indicates that a server is terminating the connection because
153
+ # it encountered an unexpected condition that prevented it from
154
+ # fulfilling the request.
155
+ CLOSE_EXCEPTION = 1011
156
+
157
+ # 1015 is a reserved value and MUST NOT be set as a status code in a
158
+ # Close control frame by an endpoint. It is designated for use in
159
+ # applications expecting a status code to indicate that the
160
+ # connection was closed due to a failure to perform a TLS handshake
161
+ # (e.g., the server certificate can't be verified).
162
+ CLOSE_TLS_ERROR = 1015
163
+
164
+ # Human-readable messages for each closing status code.
165
+ CLOSING_STATUS_DESC = {
166
+ CLOSE_NORMAL => 'Session closed normally.',
167
+ CLOSE_GOING_AWAY => 'Endpoint going away.',
168
+ CLOSE_PROTOCOL_ERROR => 'Protocol error.',
169
+ CLOSE_BAD_DATA_TYPE => 'Unhandled data type.',
170
+ CLOSE_RESERVED => 'Reserved for future use.',
171
+ CLOSE_MISSING_STATUS => 'No status code was present.',
172
+ CLOSE_ABNORMAL_STATUS => 'Abnormal close.',
173
+ CLOSE_BAD_DATA => 'Bad or malformed data.',
174
+ CLOSE_POLICY_VIOLATION => 'Policy violation.',
175
+ CLOSE_MESSAGE_TOO_LARGE => 'Message too large for endpoint.',
176
+ CLOSE_MISSING_EXTENSION => 'Missing extension.',
177
+ CLOSE_EXCEPTION => 'Unexpected condition/exception.',
178
+ CLOSE_TLS_ERROR => 'TLS handshake failure.',
179
+ }
180
+
181
+ end # module WebSocket
182
+ include Constants
183
+
184
+ # Base exception class for WebSocket-related errors
185
+ class Error < ::RuntimeError; end
186
+
187
+ # Exception raised when a frame is malformed, doesn't parse, or is otherwise invalid.
188
+ class FrameError < Mongrel2::WebSocket::Error; end
189
+
190
+
191
+ # WebSocket frame class; this is used for both requests and responses in
192
+ # WebSocket services.
193
+ class Frame < Mongrel2::Request
194
+ include Mongrel2::WebSocket::Constants
195
+
196
+ # The default frame header flags: FIN + CLOSE
197
+ DEFAULT_FLAGS = FIN_FLAG | OPCODE[:close]
198
+
199
+
200
+ # Set this class as the one that will handle WEBSOCKET requests
201
+ register_request_type( self, :WEBSOCKET )
202
+
203
+
204
+ ### Override the type of response returned by this request type. Since
205
+ ### WebSocket connections are symmetrical, responses are just new
206
+ ### WebSocketFrames with the same Mongrel2 sender and connection IDs.
207
+ def self::response_class
208
+ return self
209
+ end
210
+
211
+
212
+ ### Create a response frame from the given request +frame+.
213
+ def self::from_request( frame )
214
+ Mongrel2.log.debug "Creating a %p response to request %p" % [ self, frame ]
215
+ response = new( frame.sender_id, frame.conn_id, frame.path )
216
+ response.request_frame = frame
217
+
218
+ return response
219
+ end
220
+
221
+
222
+ ### Define accessors for the flag of the specified +name+ and +bit+.
223
+ def self::attr_flag( name, bitmask )
224
+ define_method( "#{name}?" ) do
225
+ (self.flags & bitmask).nonzero?
226
+ end
227
+ define_method( "#{name}=" ) do |newvalue|
228
+ if newvalue
229
+ self.flags |= bitmask
230
+ else
231
+ self.flags ^= ( self.flags & bitmask )
232
+ end
233
+ end
234
+ end
235
+
236
+
237
+
238
+ #################################################################
239
+ ### I N S T A N C E M E T H O D S
240
+ #################################################################
241
+
242
+ ### Override the constructor to add Integer flags extracted from the FLAGS header.
243
+ def initialize( sender_id, conn_id, path, headers={}, payload='', raw=nil )
244
+ payload.force_encoding( Encoding::UTF_8 ) if
245
+ payload.encoding == Encoding::ASCII_8BIT
246
+
247
+ super
248
+
249
+ @flags = Integer( self.headers.flags || DEFAULT_FLAGS )
250
+ @request_frame = nil
251
+ @errors = []
252
+ end
253
+
254
+
255
+ ######
256
+ public
257
+ ######
258
+
259
+ # The payload data
260
+ attr_accessor :body
261
+ alias_method :payload, :body
262
+ alias_method :payload=, :body=
263
+
264
+
265
+ # The frame's header flags as an Integer
266
+ attr_accessor :flags
267
+
268
+ # The frame that this one is a response to
269
+ attr_accessor :request_frame
270
+
271
+ # The Array of validation errors
272
+ attr_reader :errors
273
+
274
+
275
+ ### Returns +true+ if the request's FIN flag is set. This flag indicates that
276
+ ### this is the final fragment in a message. The first fragment MAY also be
277
+ ### the final fragment.
278
+ attr_flag :fin, FIN_FLAG
279
+
280
+ ### Returns +true+ if the request's RSV1 flag is set. RSV1-3 MUST be 0 unless
281
+ ### an extension is negotiated that defines meanings for non-zero values. If
282
+ ### a nonzero value is received and none of the negotiated extensions defines
283
+ ### the meaning of such a nonzero value, the receiving endpoint MUST _fail the
284
+ ### WebSocket connection_.
285
+ attr_flag :rsv1, RSV1_FLAG
286
+ attr_flag :rsv2, RSV2_FLAG
287
+ attr_flag :rsv3, RSV3_FLAG
288
+
289
+
290
+ ### Returns true if one or more of the RSV1-3 bits is set.
291
+ def has_rsv_flags?
292
+ return ( self.flags & RSV_FLAG_MASK ).nonzero?
293
+ end
294
+
295
+
296
+ ### Returns the name of the frame's opcode as a Symbol. The #numeric_opcode method
297
+ ### returns the numeric one.
298
+ def opcode
299
+ return OPCODE_NAME[ self.numeric_opcode ]
300
+ end
301
+
302
+
303
+ ### Return the numeric opcode of the frame.
304
+ def numeric_opcode
305
+ return self.flags & OPCODE_BITMASK
306
+ end
307
+
308
+
309
+ ### Set the frame's opcode to +code+, which should be either a numeric opcode or
310
+ ### its equivalent name (i.e., :continuation, :text, :binary, :close, :ping, :pong)
311
+ def opcode=( code )
312
+ opcode = OPCODE[ code.to_sym ] or
313
+ raise ArgumentError, "unknown opcode %p" % [ code ]
314
+
315
+ self.flags ^= ( self.flags & OPCODE_BITMASK )
316
+ self.flags |= opcode
317
+ end
318
+
319
+
320
+ ### Returns +true+ if the request is a WebSocket control frame.
321
+ def control?
322
+ return ( self.flags & OPCODE_CONTROL_MASK ).nonzero?
323
+ end
324
+
325
+
326
+ ### Append the given +object+ to the payload. Returns the Frame for
327
+ ### chaining.
328
+ def <<( object )
329
+ self.payload << object
330
+ return self
331
+ end
332
+
333
+
334
+ ### Write the given +objects+ to the payload, calling #to_s on each one.
335
+ def puts( *objects )
336
+ objects.each do |obj|
337
+ self << obj.to_s.chomp << $/
338
+ end
339
+ end
340
+
341
+
342
+ ### Overwrite the frame's payload with a status message based on
343
+ ### +statuscode+.
344
+ def set_status( statuscode )
345
+ self.log.warn "Unknown status code %d" unless CLOSING_STATUS_DESC.key?( statuscode )
346
+ status_msg = "%d %s" % [ statuscode, CLOSING_STATUS_DESC[statuscode] ]
347
+
348
+ self.payload.replace( status_msg )
349
+ end
350
+
351
+
352
+ ### Check the frame for problems, appending descriptions of any issues to
353
+ ### the #errors array.
354
+ def validate
355
+ self.errors.clear
356
+
357
+ self.validate_payload_encoding
358
+ self.validate_control_frame
359
+ self.validate_opcode
360
+ self.validate_reserved_flags
361
+ end
362
+
363
+
364
+ ### Sanity-checks the frame and returns +false+ if any problems are found.
365
+ ### Error messages will be in #errors.
366
+ def valid?
367
+ self.validate
368
+ return self.errors.empty?
369
+ end
370
+
371
+
372
+ ### Stringify into a response suitable for sending to the client.
373
+ def to_s
374
+ data = self.payload.to_s
375
+
376
+ # Make sure the outgoing payload is UTF-8, except in the case of a
377
+ # binary frame.
378
+ if self.opcode != :binary && data.encoding != Encoding::UTF_8
379
+ self.log.debug "Transcoding %s payload data to UTF-8" % [ data.encoding.name ]
380
+ data.encode!( Encoding::UTF_8 )
381
+ end
382
+
383
+ # Make sure everything's in order
384
+ unless self.valid?
385
+ self.log.error "Validation failed."
386
+ raise Mongrel2::WebSocket::FrameError, "invalid frame: %s" %
387
+ [ self.errors.join( ', ' ) ]
388
+ end
389
+
390
+ # Now force everything into binary so it can be catenated
391
+ data.force_encoding( Encoding::ASCII_8BIT )
392
+ return [
393
+ self.make_header( data ),
394
+ data
395
+ ].join
396
+ end
397
+
398
+
399
+ ### Return an Enumerator for the bytes of the raw frame as it appears
400
+ ### on the wire.
401
+ def bytes
402
+ return self.to_s.bytes
403
+ end
404
+
405
+
406
+ ### Create a Mongrel2::Response that will respond to the same server/connection as
407
+ ### the receiver. If you wish your specialized Request class to have a corresponding
408
+ ### response type, you can override the Mongrel2::Request.response_class method
409
+ ### to achieve that.
410
+ def response( *flags )
411
+ unless @response
412
+ @response = super()
413
+
414
+ # Set the opcode
415
+ self.log.debug "Setting up response %p with symmetrical flags" % [ @response ]
416
+ if self.opcode == :ping
417
+ @response.opcode = :pong
418
+ @response.payload = self.payload
419
+ else
420
+ @response.opcode = self.opcode
421
+ end
422
+
423
+ # Set flags in the response
424
+ unless flags.empty?
425
+ self.log.debug " applying custom flags: %p" % [ flags ]
426
+ @response.set_flags( *flags )
427
+ end
428
+
429
+ end
430
+
431
+ return @response
432
+ end
433
+
434
+
435
+ ### Apply flag bits and opcodes: (:fin, :rsv1, :rsv2, :rsv3, :continuation,
436
+ ### :text, :binary, :close, :ping, :pong) to the frame.
437
+ ###
438
+ ### # Transform the frame into a CLOSE frame and set its FIN flag
439
+ ### frame.set_flags( :fin, :close )
440
+ ###
441
+ def set_flags( *flag_symbols )
442
+ flag_symbols.flatten!
443
+ flag_symbols.compact!
444
+
445
+ self.log.debug "Setting flags for symbols: %p" % [ flag_symbols ]
446
+
447
+ flag_symbols.each do |flag|
448
+ case flag
449
+ when :fin, :rsv1, :rsv2, :rsv3
450
+ self.__send__( "#{flag}=", true )
451
+ when :continuation, :text, :binary, :close, :ping, :pong
452
+ self.opcode = flag
453
+ when Integer
454
+ self.log.debug " setting Integer flags directly: 0b%08b" % [ integer ]
455
+ self.flags |= flag
456
+ else
457
+ raise ArgumentError, "Don't know what the %p flag is." % [ flag ]
458
+ end
459
+ end
460
+ end
461
+
462
+
463
+ #########
464
+ protected
465
+ #########
466
+
467
+ ### Return the details to include in the contents of the #inspected object.
468
+ def inspect_details
469
+ return %Q{FIN:%d RSV1:%d RSV2:%d RSV3:%d OPCODE:%s (0x%x) -- %0.2fK body} % [
470
+ self.fin? ? 1 : 0,
471
+ self.rsv1? ? 1 : 0,
472
+ self.rsv2? ? 1 : 0,
473
+ self.rsv3? ? 1 : 0,
474
+ self.opcode,
475
+ self.numeric_opcode,
476
+ (self.payload.bytesize / 1024.0),
477
+ ]
478
+ end
479
+
480
+
481
+ ### Make a WebSocket header for the receiving frame and return it.
482
+ def make_header( data )
483
+ header = ''.force_encoding( Encoding::ASCII_8BIT )
484
+ length = data.bytesize
485
+ self.log.debug "Making wire protocol header for payload of %d bytes" % [ length ]
486
+
487
+ # Pack the frame according to its size
488
+ if length >= 2**16
489
+ self.log.debug " giant size, using 8-byte (64-bit int) length field"
490
+ header = [ self.flags, 127, length ].pack( 'c2q>' )
491
+ elsif length > 125
492
+ self.log.debug " big size, using 2-byte (16-bit int) length field"
493
+ header = [ self.flags, 126, length ].pack( 'c2n' )
494
+ else
495
+ self.log.debug " small size, using payload length field"
496
+ header = [ self.flags, length ].pack( 'c2' )
497
+ end
498
+
499
+ self.log.debug " header is: 0: %02x %02x" % header.unpack('C*')
500
+ return header
501
+ end
502
+
503
+
504
+ ### Validate that the payload encoding is correct for its opcode, attempting
505
+ ### to transcode it if it's not. If the transcoding fails, adds an error to
506
+ ### #errors.
507
+ def validate_payload_encoding
508
+ if self.opcode == :binary
509
+ self.log.debug "Binary payload: forcing to ASCII-8BIT"
510
+ self.payload.force_encoding( Encoding::ASCII_8BIT )
511
+ else
512
+ self.log.debug "Non-binary payload: forcing to UTF-8"
513
+ self.payload.force_encoding( Encoding::UTF_8 )
514
+ self.errors << "Invalid UTF8 in payload" unless self.payload.valid_encoding?
515
+ end
516
+ end
517
+
518
+
519
+ ### Sanity-check control frame +data+, raising a Mongrel2::WebSocket::FrameError
520
+ ### if there's a problem.
521
+ def validate_control_frame
522
+ return unless self.control?
523
+
524
+ if self.payload.bytesize > 125
525
+ self.log.error "Payload of control frame exceeds 125 bytes (%d)" % [ self.payload.bytesize ]
526
+ self.errors << "payload of control frame cannot exceed 125 bytes"
527
+ end
528
+
529
+ unless self.fin?
530
+ self.log.error "Control frame fragmented (FIN is unset)"
531
+ self.errors << "control frame is fragmented (no FIN flag set)"
532
+ end
533
+ end
534
+
535
+
536
+ ### Ensure that the frame has a valid opcode in its header. If you're using reserved
537
+ ### opcodes, you'll want to override this.
538
+ def validate_opcode
539
+ if self.opcode == :reserved
540
+ self.log.error "Frame uses reserved opcode 0x%x" % [ self.numeric_opcode ]
541
+ self.errors << "Frame uses reserved opcode"
542
+ end
543
+ end
544
+
545
+
546
+ ### Ensure that the frame doesn't have any of the reserved flags set (RSV1-3). If your
547
+ ### subprotocol uses one or more of these, you'll want to override this method.
548
+ def validate_reserved_flags
549
+ if self.has_rsv_flags?
550
+ self.log.error "Frame has one or more reserved flags set."
551
+ self.errors << "Frame has one or more reserved flags set."
552
+ end
553
+ end
554
+
555
+
556
+ end # class Frame
557
+
558
+ end # module Mongrel2::WebSocket
559
+
560
+ # vim: set nosta noet ts=4 sw=4:
561
+