websocket-rack 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.md +105 -0
- data/Rakefile +11 -0
- data/example/example.ru +31 -0
- data/example/html/FABridge.js +604 -0
- data/example/html/WebSocketMain.swf +0 -0
- data/example/html/index.html +76 -0
- data/example/html/swfobject.js +4 -0
- data/example/html/web_socket.js +388 -0
- data/lib/rack/websocket/application.rb +65 -0
- data/lib/rack/websocket/connection.rb +105 -0
- data/lib/rack/websocket/debugger.rb +17 -0
- data/lib/rack/websocket/extensions/thin/connection.rb +71 -0
- data/lib/rack/websocket/extensions/thin.rb +16 -0
- data/lib/rack/websocket/framing03.rb +178 -0
- data/lib/rack/websocket/framing76.rb +115 -0
- data/lib/rack/websocket/handler.rb +43 -0
- data/lib/rack/websocket/handler03.rb +14 -0
- data/lib/rack/websocket/handler75.rb +8 -0
- data/lib/rack/websocket/handler76.rb +11 -0
- data/lib/rack/websocket/handler_factory.rb +61 -0
- data/lib/rack/websocket/handshake75.rb +21 -0
- data/lib/rack/websocket/handshake76.rb +71 -0
- data/lib/rack/websocket.rb +40 -0
- data/spec/helper.rb +44 -0
- data/spec/integration/draft03_spec.rb +252 -0
- data/spec/integration/draft76_spec.rb +212 -0
- data/spec/unit/framing_spec.rb +108 -0
- data/spec/unit/handler_spec.rb +136 -0
- data/spec/websocket_spec.rb +210 -0
- data/websocket-rack.gemspec +23 -0
- metadata +147 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
module Rack
|
2
|
+
module WebSocket
|
3
|
+
class Application
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {}
|
6
|
+
attr_accessor :options
|
7
|
+
|
8
|
+
def on_open; end # Fired when a client is connected.
|
9
|
+
def on_message(msg); end # Fired when a message from a client is received.
|
10
|
+
def on_close; end # Fired when a client is disconnected.
|
11
|
+
def on_error(error); end # Fired when error occurs.
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
app, options = args[0], args[1]
|
15
|
+
app, options = nil, app if app.is_a?(Hash)
|
16
|
+
@options = DEFAULT_OPTIONS.merge(options || {})
|
17
|
+
@app = app
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
if(env['HTTP_CONNECTION'].to_s.downcase == 'upgrade' && env['HTTP_UPGRADE'].to_s.downcase == 'websocket')
|
22
|
+
@env = env
|
23
|
+
socket = env['async.connection']
|
24
|
+
@connection = Connection.new(self, socket)
|
25
|
+
@connection.dispatch(env) ? async_response : failure_response
|
26
|
+
elsif @app
|
27
|
+
@app.call(env)
|
28
|
+
else
|
29
|
+
not_fount_response
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def close_websocket
|
34
|
+
if @connection
|
35
|
+
@connection.close_websocket
|
36
|
+
else
|
37
|
+
raise WebSocketError, "WebSocket not opened"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def send_data(data)
|
42
|
+
if @connection
|
43
|
+
@connection.send data
|
44
|
+
else
|
45
|
+
raise WebSocketError, "WebSocket not opened"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def async_response
|
52
|
+
[-1, {}, []]
|
53
|
+
end
|
54
|
+
|
55
|
+
def failure_response
|
56
|
+
[ 400, { "Content-Type" => "text/plain" }, [ 'invalid data' ] ]
|
57
|
+
end
|
58
|
+
|
59
|
+
def not_fount_response
|
60
|
+
[ 404, { "Content-Type" => "text/plain" }, [ 'not found' ] ]
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module WebSocket
|
5
|
+
class Connection
|
6
|
+
include Debugger
|
7
|
+
|
8
|
+
def initialize(app, socket, options = {})
|
9
|
+
@app = app
|
10
|
+
@socket = socket
|
11
|
+
@options = options
|
12
|
+
@debug = options[:debug] || false
|
13
|
+
|
14
|
+
socket.websocket = self
|
15
|
+
|
16
|
+
debug [:initialize]
|
17
|
+
end
|
18
|
+
|
19
|
+
def trigger_on_message(msg)
|
20
|
+
@app.on_message(msg)
|
21
|
+
end
|
22
|
+
def trigger_on_open
|
23
|
+
@app.on_open
|
24
|
+
end
|
25
|
+
def trigger_on_close
|
26
|
+
@app.on_close
|
27
|
+
end
|
28
|
+
def trigger_on_error(error)
|
29
|
+
@app.on_error(error)
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(sym, *args, &block)
|
33
|
+
@socket.send sym, *args, &block
|
34
|
+
end
|
35
|
+
|
36
|
+
# Use this method to close the websocket connection cleanly
|
37
|
+
# This sends a close frame and waits for acknowlegement before closing
|
38
|
+
# the connection
|
39
|
+
def close_websocket
|
40
|
+
if @handler
|
41
|
+
@handler.close_websocket
|
42
|
+
else
|
43
|
+
# The handshake hasn't completed - should be safe to terminate
|
44
|
+
close_connection
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def receive_data(data)
|
49
|
+
debug [:receive_data, data]
|
50
|
+
|
51
|
+
@handler.receive_data(data)
|
52
|
+
end
|
53
|
+
|
54
|
+
def unbind
|
55
|
+
debug [:unbind, :connection]
|
56
|
+
|
57
|
+
@handler.unbind if @handler
|
58
|
+
end
|
59
|
+
|
60
|
+
def dispatch(data)
|
61
|
+
debug [:inbound_headers, data]
|
62
|
+
@handler = HandlerFactory.build(self, data, @debug)
|
63
|
+
unless @handler
|
64
|
+
# The whole header has not been received yet.
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
@handler.run
|
68
|
+
return true
|
69
|
+
rescue => e
|
70
|
+
debug [:error, e]
|
71
|
+
process_bad_request(e)
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_bad_request(reason)
|
76
|
+
trigger_on_error(reason)
|
77
|
+
send_data "HTTP/1.1 400 Bad request\r\n\r\n"
|
78
|
+
close_connection_after_writing
|
79
|
+
end
|
80
|
+
|
81
|
+
def send(data)
|
82
|
+
debug [:send, data]
|
83
|
+
|
84
|
+
if @handler
|
85
|
+
@handler.send_text_frame(data)
|
86
|
+
else
|
87
|
+
raise WebSocketError, "Cannot send data before onopen callback"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def close_with_error(message)
|
92
|
+
trigger_on_error(message)
|
93
|
+
close_connection_after_writing
|
94
|
+
end
|
95
|
+
|
96
|
+
def request
|
97
|
+
@handler ? @handler.request : {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def state
|
101
|
+
@handler ? @handler.state : :handshake
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Rack
|
2
|
+
module WebSocket
|
3
|
+
module Extensions
|
4
|
+
module Thin
|
5
|
+
module Connection
|
6
|
+
|
7
|
+
def self.included(thin_conn)
|
8
|
+
thin_conn.class_eval do
|
9
|
+
alias :pre_process_without_websocket :pre_process
|
10
|
+
alias :pre_process :pre_process_with_websocket
|
11
|
+
|
12
|
+
alias :receive_data_without_websocket :receive_data
|
13
|
+
alias :receive_data :receive_data_with_websocket
|
14
|
+
|
15
|
+
alias :unbind_without_websocket :unbind
|
16
|
+
alias :unbind :unbind_with_websocket
|
17
|
+
|
18
|
+
alias :receive_data_without_flash_policy_file :receive_data
|
19
|
+
alias :receive_data :receive_data_with_flash_policy_file
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :websocket
|
24
|
+
|
25
|
+
def websocket?
|
26
|
+
!self.websocket.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def pre_process_with_websocket
|
30
|
+
@request.env['async.connection'] = self
|
31
|
+
pre_process_without_websocket
|
32
|
+
end
|
33
|
+
|
34
|
+
def receive_data_with_websocket(data)
|
35
|
+
if self.websocket?
|
36
|
+
self.websocket.receive_data(data)
|
37
|
+
else
|
38
|
+
receive_data_without_websocket(data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def unbind_with_websocket
|
43
|
+
if self.websocket?
|
44
|
+
self.websocket.unbind
|
45
|
+
else
|
46
|
+
unbind_without_websocket
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def receive_data_with_flash_policy_file(data)
|
51
|
+
# thin require data to be proper http request - in it's not
|
52
|
+
# then @request.parse raises exception and data isn't parsed
|
53
|
+
# by futher methods. Here we only check if it is flash
|
54
|
+
# policy file request ("<policy-file-request/>\000") and
|
55
|
+
# if so then flash policy file is returned. if not then
|
56
|
+
# rest of request is handled.
|
57
|
+
if (data == "<policy-file-request/>\000")
|
58
|
+
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
|
59
|
+
# ignore errors - we will close this anyway
|
60
|
+
send_data(file) rescue nil
|
61
|
+
close_connection_after_writing
|
62
|
+
else
|
63
|
+
receive_data_without_flash_policy_file(data)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
module WebSocket
|
3
|
+
module Extensions
|
4
|
+
module Thin
|
5
|
+
|
6
|
+
autoload :Connection, "#{::File.dirname(__FILE__)}/thin/connection"
|
7
|
+
|
8
|
+
def self.included(thin)
|
9
|
+
thin_connection = thin.const_get(:Connection)
|
10
|
+
thin_connection.send(:include, Thin.const_get(:Connection))
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module WebSocket
|
5
|
+
module Framing03
|
6
|
+
|
7
|
+
def initialize_framing
|
8
|
+
@data = ''
|
9
|
+
@application_data_buffer = '' # Used for MORE frames
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_data(newdata)
|
13
|
+
error = false
|
14
|
+
|
15
|
+
while !error && @data.size > 1
|
16
|
+
pointer = 0
|
17
|
+
|
18
|
+
more = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
|
19
|
+
# Ignoring rsv1-3 for now
|
20
|
+
opcode = @data.getbyte(0) & 0b00001111
|
21
|
+
pointer += 1
|
22
|
+
|
23
|
+
# Ignoring rsv4
|
24
|
+
length = @data.getbyte(pointer) & 0b01111111
|
25
|
+
pointer += 1
|
26
|
+
|
27
|
+
payload_length = case length
|
28
|
+
when 127 # Length defined by 8 bytes
|
29
|
+
# Check buffer size
|
30
|
+
if @data.getbyte(pointer+8-1) == nil
|
31
|
+
debug [:buffer_incomplete, @data.inspect]
|
32
|
+
error = true
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
# Only using the last 4 bytes for now, till I work out how to
|
37
|
+
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
|
38
|
+
l = @data[(pointer+4)..(pointer+7)].unpack('N').first
|
39
|
+
pointer += 8
|
40
|
+
l
|
41
|
+
when 126 # Length defined by 2 bytes
|
42
|
+
# Check buffer size
|
43
|
+
if @data.getbyte(pointer+2-1) == nil
|
44
|
+
debug [:buffer_incomplete, @data.inspect]
|
45
|
+
error = true
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
l = @data[pointer..(pointer+1)].unpack('n').first
|
50
|
+
pointer += 2
|
51
|
+
l
|
52
|
+
else
|
53
|
+
length
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check buffer size
|
57
|
+
if @data.getbyte(pointer+payload_length-1) == nil
|
58
|
+
debug [:buffer_incomplete, @data.inspect]
|
59
|
+
error = true
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
# Throw away data up to pointer
|
64
|
+
@data.slice!(0...pointer)
|
65
|
+
|
66
|
+
# Read application data
|
67
|
+
application_data = @data.slice!(0...payload_length)
|
68
|
+
|
69
|
+
frame_type = opcode_to_type(opcode)
|
70
|
+
|
71
|
+
if frame_type == :continuation && !@frame_type
|
72
|
+
raise WebSocketError, 'Continuation frame not expected'
|
73
|
+
end
|
74
|
+
|
75
|
+
if more
|
76
|
+
debug [:moreframe, frame_type, application_data]
|
77
|
+
@application_data_buffer << application_data
|
78
|
+
@frame_type = frame_type
|
79
|
+
else
|
80
|
+
# Message is complete
|
81
|
+
if frame_type == :continuation
|
82
|
+
@application_data_buffer << application_data
|
83
|
+
message(@frame_type, '', @application_data_buffer)
|
84
|
+
@application_data_buffer = ''
|
85
|
+
@frame_type = nil
|
86
|
+
else
|
87
|
+
message(frame_type, '', application_data)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end # end while
|
91
|
+
end
|
92
|
+
|
93
|
+
def send_frame(frame_type, application_data)
|
94
|
+
if @state == :closing && data_frame?(frame_type)
|
95
|
+
raise WebSocketError, "Cannot send data frame since connection is closing"
|
96
|
+
end
|
97
|
+
|
98
|
+
frame = ''
|
99
|
+
|
100
|
+
opcode = type_to_opcode(frame_type)
|
101
|
+
byte1 = opcode # since more, rsv1-3 are 0
|
102
|
+
frame << byte1
|
103
|
+
|
104
|
+
length = application_data.size
|
105
|
+
if length <= 125
|
106
|
+
byte2 = length # since rsv4 is 0
|
107
|
+
frame << byte2
|
108
|
+
elsif length < 65536 # write 2 byte length
|
109
|
+
frame << 126
|
110
|
+
frame << [length].pack('n')
|
111
|
+
else # write 8 byte length
|
112
|
+
frame << 127
|
113
|
+
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
114
|
+
end
|
115
|
+
|
116
|
+
frame << application_data
|
117
|
+
|
118
|
+
@connection.send_data(frame)
|
119
|
+
end
|
120
|
+
|
121
|
+
def send_text_frame(data)
|
122
|
+
send_frame(:text, data)
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def message(message_type, extension_data, application_data)
|
128
|
+
case message_type
|
129
|
+
when :close
|
130
|
+
if @state == :closing
|
131
|
+
# TODO: Check that message body matches sent data
|
132
|
+
# We can close connection immediately since there is no more data
|
133
|
+
# is allowed to be sent or received on this connection
|
134
|
+
@connection.close_connection
|
135
|
+
@state = :closed
|
136
|
+
else
|
137
|
+
# Acknowlege close
|
138
|
+
# The connection is considered closed
|
139
|
+
send_frame(:close, application_data)
|
140
|
+
@state = :closed
|
141
|
+
@connection.close_connection_after_writing
|
142
|
+
end
|
143
|
+
when :ping
|
144
|
+
# Pong back the same data
|
145
|
+
send_frame(:pong, application_data)
|
146
|
+
when :pong
|
147
|
+
# TODO: Do something. Complete a deferrable established by a ping?
|
148
|
+
when :text, :binary
|
149
|
+
@connection.trigger_on_message(application_data)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
FRAME_TYPES = {
|
154
|
+
:continuation => 0,
|
155
|
+
:close => 1,
|
156
|
+
:ping => 2,
|
157
|
+
:pong => 3,
|
158
|
+
:text => 4,
|
159
|
+
:binary => 5
|
160
|
+
}
|
161
|
+
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
|
162
|
+
# Frames are either data frames or control frames
|
163
|
+
DATA_FRAMES = [:text, :binary, :continuation]
|
164
|
+
|
165
|
+
def type_to_opcode(frame_type)
|
166
|
+
FRAME_TYPES[frame_type] || raise("Unknown frame type")
|
167
|
+
end
|
168
|
+
|
169
|
+
def opcode_to_type(opcode)
|
170
|
+
FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
|
171
|
+
end
|
172
|
+
|
173
|
+
def data_frame?(type)
|
174
|
+
DATA_FRAMES.include?(type)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|