mongrel2 0.15.1 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/ChangeLog +51 -1
- data/History.rdoc +5 -0
- data/Manifest.txt +5 -0
- data/README.rdoc +14 -9
- data/bin/m2sh.rb +2 -0
- data/data/mongrel2/bootstrap.html +24 -22
- data/data/mongrel2/css/master.css +30 -1
- data/data/mongrel2/js/websock-test.js +108 -0
- data/data/mongrel2/websock-test.html +33 -0
- data/examples/config.rb +10 -10
- data/examples/ws-echo.rb +251 -0
- data/lib/mongrel2.rb +3 -2
- data/lib/mongrel2/config/server.rb +2 -0
- data/lib/mongrel2/connection.rb +2 -2
- data/lib/mongrel2/handler.rb +42 -28
- data/lib/mongrel2/request.rb +4 -3
- data/lib/mongrel2/testing.rb +201 -2
- data/lib/mongrel2/websocket.rb +561 -0
- data/spec/lib/constants.rb +2 -1
- data/spec/mongrel2/httprequest_spec.rb +1 -0
- data/spec/mongrel2/request_spec.rb +1 -1
- data/spec/mongrel2/websocket_spec.rb +295 -0
- metadata +48 -46
- metadata.gz.sig +0 -0
data/lib/mongrel2/request.rb
CHANGED
@@ -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 )
|
data/lib/mongrel2/testing.rb
CHANGED
@@ -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] =
|
74
|
-
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
|
+
|