websocket-rack 0.1.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/.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
|