mongrel2 0.15.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+