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 ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -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 'rake'
2
- require 'spec/rake/spectask'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
3
 
4
- begin
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
- Jeweler::GemcutterTasks.new
20
- rescue LoadError
21
- puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
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
-
@@ -1,8 +1,12 @@
1
1
  $:.unshift(File.dirname(__FILE__) + '/../lib')
2
2
 
3
- #require "rubygems"
4
3
  require "eventmachine"
5
4
 
6
- %w[ debugger websocket connection handler_factory handler handler75 handler76 ].each do |file|
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
- @data << data
40
- dispatch
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
- @state = :closed
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 handshake
61
- if @data.match(/<policy-file-request\s*\/>/)
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, @data]
72
+ debug [:inbound_headers, data]
66
73
  begin
67
- @handler = HandlerFactory.build(@data, @secure, @debug)
68
- @data = ''
69
- send_data @handler.handshake
70
-
71
- @request = @handler.request
72
- @state = :connected
73
- @onopen.call if @onopen
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
- ary = ["\x00", data, "\xff"]
181
- ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
182
- send_data(ary.join)
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