em-websocket 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.md +76 -0
- data/Rakefile +6 -28
- data/lib/em-websocket.rb +6 -2
- data/lib/em-websocket/connection.rb +53 -112
- data/lib/em-websocket/framing03.rb +176 -0
- data/lib/em-websocket/framing76.rb +96 -0
- data/lib/em-websocket/handler.rb +28 -4
- data/lib/em-websocket/handler03.rb +14 -0
- data/lib/em-websocket/handler75.rb +2 -15
- data/lib/em-websocket/handler76.rb +3 -56
- data/lib/em-websocket/handler_factory.rb +37 -12
- data/lib/em-websocket/handshake75.rb +21 -0
- data/lib/em-websocket/handshake76.rb +61 -0
- data/lib/em-websocket/version.rb +5 -0
- data/spec/helper.rb +12 -7
- data/spec/integration/draft03_spec.rb +252 -0
- data/spec/integration/{integration_spec.rb → draft76_spec.rb} +26 -1
- data/spec/unit/framing_spec.rb +108 -0
- data/spec/unit/handler_spec.rb +12 -0
- data/spec/websocket_spec.rb +16 -1
- metadata +51 -17
- data/README.rdoc +0 -73
- data/VERSION +0 -1
- data/examples/srv.rb +0 -19
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# EM-WebSocket
|
2
|
+
|
3
|
+
EventMachine based, async, Ruby WebSocket server. Take a look at examples directory, or check out the blog post below:
|
4
|
+
|
5
|
+
* [Ruby & Websockets: TCP for the Web](http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/)
|
6
|
+
|
7
|
+
## Simple server example
|
8
|
+
|
9
|
+
EventMachine.run {
|
10
|
+
|
11
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080) do |ws|
|
12
|
+
ws.onopen {
|
13
|
+
puts "WebSocket connection open"
|
14
|
+
|
15
|
+
# publish message to the client
|
16
|
+
ws.send "Hello Client"
|
17
|
+
}
|
18
|
+
|
19
|
+
ws.onclose { puts "Connection closed" }
|
20
|
+
ws.onmessage { |msg|
|
21
|
+
puts "Recieved message: #{msg}"
|
22
|
+
ws.send "Pong: #{msg}"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
## Secure server
|
28
|
+
|
29
|
+
It is possible to accept secure wss:// connections by passing :secure => true when opening the connection. Safari 5 does not currently support prompting on untrusted SSL certificates therefore using signed certificates is highly recommended. Pass a :tls_options hash containing keys as described in http://eventmachine.rubyforge.org/EventMachine/Connection.html#M000296
|
30
|
+
|
31
|
+
For example,
|
32
|
+
|
33
|
+
EventMachine::WebSocket.start({
|
34
|
+
:host => "0.0.0.0",
|
35
|
+
:port => 443
|
36
|
+
:secure => true,
|
37
|
+
:tls_options => {
|
38
|
+
:private_key_file => "/private/key",
|
39
|
+
:cert_chain_file => "/ssl/certificate"
|
40
|
+
}
|
41
|
+
}) do |ws|
|
42
|
+
...
|
43
|
+
end
|
44
|
+
|
45
|
+
## Examples & Projects using em-websocket
|
46
|
+
|
47
|
+
* [Pusher](http://pusherapp.com) - Realtime client push
|
48
|
+
* [Livereload](https://github.com/mockko/livereload) - LiveReload applies CSS/JS changes to Safari or Chrome w/o reloading
|
49
|
+
* [Twitter AMQP WebSocket Example](http://github.com/rubenfonseca/twitter-amqp-websocket-example)
|
50
|
+
* examples/multicast.rb - broadcast all ruby tweets to all subscribers
|
51
|
+
* examples/echo.rb - server <> client exchange via a websocket
|
52
|
+
|
53
|
+
# License
|
54
|
+
|
55
|
+
(The MIT License)
|
56
|
+
|
57
|
+
Copyright (c) 2009 Ilya Grigorik
|
58
|
+
|
59
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
60
|
+
a copy of this software and associated documentation files (the
|
61
|
+
'Software'), to deal in the Software without restriction, including
|
62
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
63
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
64
|
+
permit persons to whom the Software is furnished to do so, subject to
|
65
|
+
the following conditions:
|
66
|
+
|
67
|
+
The above copyright notice and this permission notice shall be
|
68
|
+
included in all copies or substantial portions of the Software.
|
69
|
+
|
70
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
71
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
72
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
73
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
74
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
75
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
76
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1,31 +1,9 @@
|
|
1
|
-
require '
|
2
|
-
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
3
|
|
4
|
-
|
5
|
-
require 'jeweler'
|
6
|
-
Jeweler::Tasks.new do |gemspec|
|
7
|
-
gemspec.name = "em-websocket"
|
8
|
-
gemspec.summary = "EventMachine based WebSocket server"
|
9
|
-
gemspec.description = gemspec.summary
|
10
|
-
gemspec.email = "ilya@igvita.com"
|
11
|
-
gemspec.homepage = "http://github.com/igrigorik/em-websocket"
|
12
|
-
gemspec.authors = ["Ilya Grigorik"]
|
13
|
-
gemspec.add_dependency("eventmachine", ">= 0.12.9")
|
14
|
-
gemspec.add_dependency("addressable", '>= 2.1.1')
|
15
|
-
gemspec.add_development_dependency('em-http-request', '>= 0.2.6')
|
16
|
-
gemspec.rubyforge_project = "em-websocket"
|
17
|
-
end
|
4
|
+
require 'rspec/core/rake_task'
|
18
5
|
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
RSpec::Core::RakeTask.new do |t|
|
7
|
+
t.rspec_opts = ["-c", "-f progress", "-r ./spec/helper.rb"]
|
8
|
+
t.pattern = 'spec/**/*_spec.rb'
|
22
9
|
end
|
23
|
-
|
24
|
-
task :default => :spec
|
25
|
-
|
26
|
-
Spec::Rake::SpecTask.new do |t|
|
27
|
-
t.ruby_opts = ['-rtest/unit']
|
28
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
-
end
|
30
|
-
|
31
|
-
|
data/lib/em-websocket.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
2
|
|
3
|
-
#require "rubygems"
|
4
3
|
require "eventmachine"
|
5
4
|
|
6
|
-
%w[
|
5
|
+
%w[
|
6
|
+
debugger websocket connection
|
7
|
+
handshake75 handshake76
|
8
|
+
framing76 framing03
|
9
|
+
handler_factory handler handler75 handler76 handler03
|
10
|
+
].each do |file|
|
7
11
|
require "em-websocket/#{file}"
|
8
12
|
end
|
@@ -5,30 +5,45 @@ module EventMachine
|
|
5
5
|
class Connection < EventMachine::Connection
|
6
6
|
include Debugger
|
7
7
|
|
8
|
-
attr_reader :state, :request
|
9
|
-
|
10
|
-
# Set the max frame lenth to very high value (10MB) until there is a
|
11
|
-
# limit specified in the spec to protect against malicious attacks
|
12
|
-
MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
|
13
|
-
|
14
8
|
# define WebSocket callbacks
|
15
9
|
def onopen(&blk); @onopen = blk; end
|
16
10
|
def onclose(&blk); @onclose = blk; end
|
17
11
|
def onerror(&blk); @onerror = blk; end
|
18
12
|
def onmessage(&blk); @onmessage = blk; end
|
19
13
|
|
14
|
+
def trigger_on_message(msg)
|
15
|
+
@onmessage.call(msg) if @onmessage
|
16
|
+
end
|
17
|
+
def trigger_on_open
|
18
|
+
@onopen.call if @onopen
|
19
|
+
end
|
20
|
+
def trigger_on_close
|
21
|
+
@onclose.call if @onclose
|
22
|
+
end
|
23
|
+
|
20
24
|
def initialize(options)
|
21
25
|
@options = options
|
22
26
|
@debug = options[:debug] || false
|
23
27
|
@secure = options[:secure] || false
|
24
28
|
@tls_options = options[:tls_options] || {}
|
25
|
-
@state = :handshake
|
26
29
|
@request = {}
|
27
30
|
@data = ''
|
28
31
|
|
29
32
|
debug [:initialize]
|
30
33
|
end
|
31
34
|
|
35
|
+
# Use this method to close the websocket connection cleanly
|
36
|
+
# This sends a close frame and waits for acknowlegement before closing
|
37
|
+
# the connection
|
38
|
+
def close_websocket
|
39
|
+
if @handler
|
40
|
+
@handler.close_websocket
|
41
|
+
else
|
42
|
+
# The handshake hasn't completed - should be safe to terminate
|
43
|
+
close_connection
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
32
47
|
def post_init
|
33
48
|
start_tls(@tls_options) if @secure
|
34
49
|
end
|
@@ -36,41 +51,34 @@ module EventMachine
|
|
36
51
|
def receive_data(data)
|
37
52
|
debug [:receive_data, data]
|
38
53
|
|
39
|
-
@
|
40
|
-
|
54
|
+
if @handler
|
55
|
+
@handler.receive_data(data)
|
56
|
+
else
|
57
|
+
dispatch(data)
|
58
|
+
end
|
41
59
|
end
|
42
60
|
|
43
61
|
def unbind
|
44
62
|
debug [:unbind, :connection]
|
45
63
|
|
46
|
-
@
|
47
|
-
@onclose.call if @onclose
|
48
|
-
end
|
49
|
-
|
50
|
-
def dispatch
|
51
|
-
case @state
|
52
|
-
when :handshake
|
53
|
-
handshake
|
54
|
-
when :connected
|
55
|
-
process_message
|
56
|
-
else raise WebSocketError, "invalid state: #{@state}"
|
57
|
-
end
|
64
|
+
@handler.unbind if @handler
|
58
65
|
end
|
59
66
|
|
60
|
-
def
|
61
|
-
if
|
67
|
+
def dispatch(data)
|
68
|
+
if data.match(/\A<policy-file-request\s*\/>/)
|
62
69
|
send_flash_cross_domain_file
|
63
70
|
return false
|
64
71
|
else
|
65
|
-
debug [:inbound_headers,
|
72
|
+
debug [:inbound_headers, data]
|
66
73
|
begin
|
67
|
-
@
|
68
|
-
@
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
@
|
74
|
+
@data << data
|
75
|
+
@handler = HandlerFactory.build(self, @data, @secure, @debug)
|
76
|
+
unless @handler
|
77
|
+
# The whole header has not been received yet.
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
@data = nil
|
81
|
+
@handler.run
|
74
82
|
return true
|
75
83
|
rescue => e
|
76
84
|
debug [:error, e]
|
@@ -97,95 +105,28 @@ module EventMachine
|
|
97
105
|
close_connection_after_writing
|
98
106
|
end
|
99
107
|
|
100
|
-
def process_message
|
101
|
-
debug [:message, @data]
|
102
|
-
|
103
|
-
# This algorithm comes straight from the spec
|
104
|
-
# http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-4.2
|
105
|
-
|
106
|
-
error = false
|
107
|
-
|
108
|
-
while !error
|
109
|
-
pointer = 0
|
110
|
-
frame_type = @data[pointer].to_i
|
111
|
-
pointer += 1
|
112
|
-
|
113
|
-
if (frame_type & 0x80) == 0x80
|
114
|
-
# If the high-order bit of the /frame type/ byte is set
|
115
|
-
length = 0
|
116
|
-
|
117
|
-
loop do
|
118
|
-
b = @data[pointer].to_i
|
119
|
-
return false unless b
|
120
|
-
pointer += 1
|
121
|
-
b_v = b & 0x7F
|
122
|
-
length = length * 128 + b_v
|
123
|
-
break unless (b & 0x80) == 0x80
|
124
|
-
end
|
125
|
-
|
126
|
-
# Addition to the spec to protect against malicious requests
|
127
|
-
if length > MAXIMUM_FRAME_LENGTH
|
128
|
-
close_with_error(DataError.new("Frame length too long (#{length} bytes)"))
|
129
|
-
return false
|
130
|
-
end
|
131
|
-
|
132
|
-
if @data[pointer+length-1] == nil
|
133
|
-
debug [:buffer_incomplete, @data.inspect]
|
134
|
-
# Incomplete data - leave @data to accumulate
|
135
|
-
error = true
|
136
|
-
else
|
137
|
-
# Straight from spec - I'm sure this isn't crazy...
|
138
|
-
# 6. Read /length/ bytes.
|
139
|
-
# 7. Discard the read bytes.
|
140
|
-
@data = @data[(pointer+length)..-1]
|
141
|
-
|
142
|
-
# If the /frame type/ is 0xFF and the /length/ was 0, then close
|
143
|
-
if length == 0
|
144
|
-
send_data("\xff\x00")
|
145
|
-
@state = :closing
|
146
|
-
close_connection_after_writing
|
147
|
-
else
|
148
|
-
error = true
|
149
|
-
end
|
150
|
-
end
|
151
|
-
else
|
152
|
-
# If the high-order bit of the /frame type/ byte is _not_ set
|
153
|
-
msg = @data.slice!(/^\x00([^\xff]*)\xff/)
|
154
|
-
if msg
|
155
|
-
msg.gsub!(/\A\x00|\xff\z/, '')
|
156
|
-
if @state == :closing
|
157
|
-
debug [:ignored_message, msg]
|
158
|
-
else
|
159
|
-
msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
|
160
|
-
@onmessage.call(msg) if @onmessage
|
161
|
-
end
|
162
|
-
else
|
163
|
-
error = true
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
false
|
169
|
-
end
|
170
|
-
|
171
|
-
# should only be invoked after handshake, otherwise it
|
172
|
-
# will inject data into the header exchange
|
173
|
-
#
|
174
|
-
# frames need to start with 0x00-0x7f byte and end with
|
175
|
-
# an 0xFF byte. Per spec, we can also set the first
|
176
|
-
# byte to a value betweent 0x80 and 0xFF, followed by
|
177
|
-
# a leading length indicator
|
178
108
|
def send(data)
|
179
109
|
debug [:send, data]
|
180
|
-
|
181
|
-
|
182
|
-
|
110
|
+
|
111
|
+
if @handler
|
112
|
+
@handler.send_text_frame(data)
|
113
|
+
else
|
114
|
+
raise WebSocketError, "Cannot send data before onopen callback"
|
115
|
+
end
|
183
116
|
end
|
184
117
|
|
185
118
|
def close_with_error(message)
|
186
119
|
@onerror.call(message) if @onerror
|
187
120
|
close_connection_after_writing
|
188
121
|
end
|
122
|
+
|
123
|
+
def request
|
124
|
+
@handler ? @handler.request : {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def state
|
128
|
+
@handler ? @handler.state : :handshake
|
129
|
+
end
|
189
130
|
end
|
190
131
|
end
|
191
132
|
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module WebSocket
|
3
|
+
module Framing03
|
4
|
+
|
5
|
+
def initialize_framing
|
6
|
+
@data = ''
|
7
|
+
@application_data_buffer = '' # Used for MORE frames
|
8
|
+
end
|
9
|
+
|
10
|
+
def process_data
|
11
|
+
error = false
|
12
|
+
|
13
|
+
while !error && @data.size > 1
|
14
|
+
pointer = 0
|
15
|
+
|
16
|
+
more = (@data[pointer] & 0b10000000) == 0b10000000
|
17
|
+
# Ignoring rsv1-3 for now
|
18
|
+
opcode = @data[0] & 0b00001111
|
19
|
+
pointer += 1
|
20
|
+
|
21
|
+
# Ignoring rsv4
|
22
|
+
length = @data[pointer] & 0b01111111
|
23
|
+
pointer += 1
|
24
|
+
|
25
|
+
payload_length = case length
|
26
|
+
when 127 # Length defined by 8 bytes
|
27
|
+
# Check buffer size
|
28
|
+
if @data[pointer+8-1] == nil
|
29
|
+
debug [:buffer_incomplete, @data.inspect]
|
30
|
+
error = true
|
31
|
+
next
|
32
|
+
end
|
33
|
+
|
34
|
+
# Only using the last 4 bytes for now, till I work out how to
|
35
|
+
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
|
36
|
+
l = @data[(pointer+4)..(pointer+7)].unpack('N').first
|
37
|
+
pointer += 8
|
38
|
+
l
|
39
|
+
when 126 # Length defined by 2 bytes
|
40
|
+
# Check buffer size
|
41
|
+
if @data[pointer+2-1] == nil
|
42
|
+
debug [:buffer_incomplete, @data.inspect]
|
43
|
+
error = true
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
l = @data[pointer..(pointer+1)].unpack('n').first
|
48
|
+
pointer += 2
|
49
|
+
l
|
50
|
+
else
|
51
|
+
length
|
52
|
+
end
|
53
|
+
|
54
|
+
# Check buffer size
|
55
|
+
if @data[pointer+payload_length-1] == nil
|
56
|
+
debug [:buffer_incomplete, @data.inspect]
|
57
|
+
error = true
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
# Throw away data up to pointer
|
62
|
+
@data.slice!(0...pointer)
|
63
|
+
|
64
|
+
# Read application data
|
65
|
+
application_data = @data.slice!(0...payload_length)
|
66
|
+
|
67
|
+
frame_type = opcode_to_type(opcode)
|
68
|
+
|
69
|
+
if frame_type == :continuation && !@frame_type
|
70
|
+
raise WebSocketError, 'Continuation frame not expected'
|
71
|
+
end
|
72
|
+
|
73
|
+
if more
|
74
|
+
debug [:moreframe, frame_type, application_data]
|
75
|
+
@application_data_buffer << application_data
|
76
|
+
@frame_type = frame_type
|
77
|
+
else
|
78
|
+
# Message is complete
|
79
|
+
if frame_type == :continuation
|
80
|
+
@application_data_buffer << application_data
|
81
|
+
message(@frame_type, '', @application_data_buffer)
|
82
|
+
@application_data_buffer = ''
|
83
|
+
@frame_type = nil
|
84
|
+
else
|
85
|
+
message(frame_type, '', application_data)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end # end while
|
89
|
+
end
|
90
|
+
|
91
|
+
def send_frame(frame_type, application_data)
|
92
|
+
if @state == :closing && data_frame?(frame_type)
|
93
|
+
raise WebSocketError, "Cannot send data frame since connection is closing"
|
94
|
+
end
|
95
|
+
|
96
|
+
frame = ''
|
97
|
+
|
98
|
+
opcode = type_to_opcode(frame_type)
|
99
|
+
byte1 = opcode # since more, rsv1-3 are 0
|
100
|
+
frame << byte1
|
101
|
+
|
102
|
+
length = application_data.size
|
103
|
+
if length <= 125
|
104
|
+
byte2 = length # since rsv4 is 0
|
105
|
+
frame << byte2
|
106
|
+
elsif length < 65536 # write 2 byte length
|
107
|
+
frame << 126
|
108
|
+
frame << [length].pack('n')
|
109
|
+
else # write 8 byte length
|
110
|
+
frame << 127
|
111
|
+
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
|
112
|
+
end
|
113
|
+
|
114
|
+
frame << application_data
|
115
|
+
|
116
|
+
@connection.send_data(frame)
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_text_frame(data)
|
120
|
+
send_frame(:text, data)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def message(message_type, extension_data, application_data)
|
126
|
+
case message_type
|
127
|
+
when :close
|
128
|
+
if @state == :closing
|
129
|
+
# TODO: Check that message body matches sent data
|
130
|
+
# We can close connection immediately since there is no more data
|
131
|
+
# is allowed to be sent or received on this connection
|
132
|
+
@connection.close_connection
|
133
|
+
@state = :closed
|
134
|
+
else
|
135
|
+
# Acknowlege close
|
136
|
+
# The connection is considered closed
|
137
|
+
send_frame(:close, application_data)
|
138
|
+
@state = :closed
|
139
|
+
@connection.close_connection_after_writing
|
140
|
+
end
|
141
|
+
when :ping
|
142
|
+
# Pong back the same data
|
143
|
+
send_frame(:pong, application_data)
|
144
|
+
when :pong
|
145
|
+
# TODO: Do something. Complete a deferrable established by a ping?
|
146
|
+
when :text, :binary
|
147
|
+
@connection.trigger_on_message(application_data)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
FRAME_TYPES = {
|
152
|
+
:continuation => 0,
|
153
|
+
:close => 1,
|
154
|
+
:ping => 2,
|
155
|
+
:pong => 3,
|
156
|
+
:text => 4,
|
157
|
+
:binary => 5
|
158
|
+
}
|
159
|
+
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
|
160
|
+
# Frames are either data frames or control frames
|
161
|
+
DATA_FRAMES = [:text, :binary, :continuation]
|
162
|
+
|
163
|
+
def type_to_opcode(frame_type)
|
164
|
+
FRAME_TYPES[frame_type] || raise("Unknown frame type")
|
165
|
+
end
|
166
|
+
|
167
|
+
def opcode_to_type(opcode)
|
168
|
+
FRAME_TYPES_INVERSE[opcode] || raise("Unknown opcode")
|
169
|
+
end
|
170
|
+
|
171
|
+
def data_frame?(type)
|
172
|
+
DATA_FRAMES.include?(type)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|