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 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