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.
- 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
|
+
|