em-websocket 0.1.4 → 0.2.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/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
|