websocket 1.0.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.
Files changed (73) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +11 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +5 -0
  5. data/README.md +117 -0
  6. data/Rakefile +23 -0
  7. data/autobahn-client.json +11 -0
  8. data/autobahn-server.json +10 -0
  9. data/examples/base.rb +162 -0
  10. data/examples/client.rb +70 -0
  11. data/examples/server.rb +56 -0
  12. data/examples/tests/autobahn_client.rb +52 -0
  13. data/examples/tests/echo_client.rb +42 -0
  14. data/examples/tests/echo_server.rb +45 -0
  15. data/lib/websocket.rb +16 -0
  16. data/lib/websocket/frame.rb +11 -0
  17. data/lib/websocket/frame/base.rb +45 -0
  18. data/lib/websocket/frame/data.rb +52 -0
  19. data/lib/websocket/frame/handler.rb +15 -0
  20. data/lib/websocket/frame/handler/base.rb +41 -0
  21. data/lib/websocket/frame/handler/handler03.rb +162 -0
  22. data/lib/websocket/frame/handler/handler04.rb +19 -0
  23. data/lib/websocket/frame/handler/handler05.rb +17 -0
  24. data/lib/websocket/frame/handler/handler07.rb +34 -0
  25. data/lib/websocket/frame/handler/handler75.rb +79 -0
  26. data/lib/websocket/frame/incoming.rb +43 -0
  27. data/lib/websocket/frame/incoming/client.rb +9 -0
  28. data/lib/websocket/frame/incoming/server.rb +9 -0
  29. data/lib/websocket/frame/outgoing.rb +28 -0
  30. data/lib/websocket/frame/outgoing/client.rb +9 -0
  31. data/lib/websocket/frame/outgoing/server.rb +9 -0
  32. data/lib/websocket/handshake.rb +10 -0
  33. data/lib/websocket/handshake/base.rb +67 -0
  34. data/lib/websocket/handshake/client.rb +80 -0
  35. data/lib/websocket/handshake/handler.rb +20 -0
  36. data/lib/websocket/handshake/handler/base.rb +25 -0
  37. data/lib/websocket/handshake/handler/client.rb +19 -0
  38. data/lib/websocket/handshake/handler/client01.rb +19 -0
  39. data/lib/websocket/handshake/handler/client04.rb +47 -0
  40. data/lib/websocket/handshake/handler/client75.rb +25 -0
  41. data/lib/websocket/handshake/handler/client76.rb +85 -0
  42. data/lib/websocket/handshake/handler/server.rb +30 -0
  43. data/lib/websocket/handshake/handler/server04.rb +38 -0
  44. data/lib/websocket/handshake/handler/server75.rb +26 -0
  45. data/lib/websocket/handshake/handler/server76.rb +71 -0
  46. data/lib/websocket/handshake/server.rb +56 -0
  47. data/lib/websocket/version.rb +3 -0
  48. data/spec/frame/incoming_03_spec.rb +117 -0
  49. data/spec/frame/incoming_04_spec.rb +117 -0
  50. data/spec/frame/incoming_05_spec.rb +133 -0
  51. data/spec/frame/incoming_07_spec.rb +133 -0
  52. data/spec/frame/incoming_75_spec.rb +59 -0
  53. data/spec/frame/incoming_common_spec.rb +22 -0
  54. data/spec/frame/outgoing_03_spec.rb +77 -0
  55. data/spec/frame/outgoing_04_spec.rb +77 -0
  56. data/spec/frame/outgoing_05_spec.rb +77 -0
  57. data/spec/frame/outgoing_07_spec.rb +77 -0
  58. data/spec/frame/outgoing_75_spec.rb +41 -0
  59. data/spec/frame/outgoing_common_spec.rb +15 -0
  60. data/spec/handshake/client_04_spec.rb +20 -0
  61. data/spec/handshake/client_75_spec.rb +11 -0
  62. data/spec/handshake/client_76_spec.rb +20 -0
  63. data/spec/handshake/server_04_spec.rb +18 -0
  64. data/spec/handshake/server_75_spec.rb +11 -0
  65. data/spec/handshake/server_76_spec.rb +46 -0
  66. data/spec/spec_helper.rb +4 -0
  67. data/spec/support/all_client_drafts.rb +77 -0
  68. data/spec/support/all_server_drafts.rb +86 -0
  69. data/spec/support/handshake_requests.rb +72 -0
  70. data/spec/support/incoming_frames.rb +30 -0
  71. data/spec/support/outgoing_frames.rb +14 -0
  72. data/websocket.gemspec +21 -0
  73. metadata +163 -0
@@ -0,0 +1,52 @@
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/../client"
2
+ require 'cgi'
3
+
4
+ EM.epoll
5
+ EM.run do
6
+
7
+ host = 'ws://localhost:9001'
8
+ agent = "WebSocket-Ruby (Ruby #{RUBY_VERSION})"
9
+ cases = 0
10
+ skip = []
11
+
12
+ ws = WebSocket::EventMachine::Client.connect(:uri => "#{host}/getCaseCount")
13
+
14
+ ws.onmessage do |msg, type|
15
+ puts "$ Total cases to run: #{msg}"
16
+ cases = msg.to_i
17
+ end
18
+
19
+ ws.onclose do
20
+
21
+ run_case = lambda do |n|
22
+
23
+ if n > cases
24
+ puts "$ Requesting report"
25
+ ws = WebSocket::EventMachine::Client.connect(:uri => "#{host}/updateReports?agent=#{CGI.escape agent}")
26
+ ws.onclose do
27
+ EM.stop
28
+ end
29
+
30
+ elsif skip.include?(n)
31
+ EM.next_tick { run_case.call(n+1) }
32
+
33
+ else
34
+ puts "$ Test number #{n}"
35
+ ws = WebSocket::EventMachine::Client.connect(:uri => "#{host}/runCase?case=#{n}&agent=#{CGI.escape agent}")
36
+
37
+ ws.onmessage do |msg, type|
38
+ puts "Received #{msg}(#{type})"
39
+ ws.send(msg, :type => type)
40
+ end
41
+
42
+ ws.onclose do |msg|
43
+ puts("Closing: #{msg}") if msg
44
+ run_case.call(n + 1)
45
+ end
46
+ end
47
+ end
48
+
49
+ run_case.call(1)
50
+ end
51
+
52
+ end
@@ -0,0 +1,42 @@
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/../client"
2
+
3
+ EM.epoll
4
+ EM.run do
5
+
6
+ trap("TERM") { stop }
7
+ trap("INT") { stop }
8
+
9
+ ws = WebSocket::EventMachine::Client.connect(:host => "localhost", :port => 9001);
10
+
11
+ ws.onopen do
12
+ puts "Connected"
13
+ ws.send "Hello"
14
+ end
15
+
16
+ ws.onmessage do |msg, type|
17
+ puts "Received message: #{msg}"
18
+ ws.send msg, :type => type
19
+ end
20
+
21
+ ws.onclose do
22
+ puts "Disconnected"
23
+ end
24
+
25
+ ws.onerror do |e|
26
+ puts "Error: #{e}"
27
+ end
28
+
29
+ ws.onping do |msg|
30
+ puts "Receied ping: #{msg}"
31
+ end
32
+
33
+ ws.onpong do |msg|
34
+ puts "Received pong: #{msg}"
35
+ end
36
+
37
+ def stop
38
+ puts "Terminating connection"
39
+ EventMachine.stop
40
+ end
41
+
42
+ end
@@ -0,0 +1,45 @@
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/../server"
2
+
3
+ EM.epoll
4
+ EM.run do
5
+
6
+ trap("TERM") { stop }
7
+ trap("INT") { stop }
8
+
9
+ WebSocket::EventMachine::Server.start(:host => "0.0.0.0", :port => 9001) do |ws|
10
+
11
+ ws.onopen do
12
+ puts "Client connected"
13
+ end
14
+
15
+ ws.onmessage do |msg, type|
16
+ puts "Received message: #{msg}"
17
+ ws.send msg, :type => type
18
+ end
19
+
20
+ ws.onclose do
21
+ puts "Client disconnected"
22
+ end
23
+
24
+ ws.onerror do |e|
25
+ puts "Error: #{e}"
26
+ end
27
+
28
+ ws.onping do |msg|
29
+ puts "Receied ping: #{msg}"
30
+ end
31
+
32
+ ws.onpong do |msg|
33
+ puts "Received pong: #{msg}"
34
+ end
35
+
36
+ end
37
+
38
+ puts "Server started at port 9001"
39
+
40
+ def stop
41
+ puts "Terminating WebSocket Server"
42
+ EventMachine.stop
43
+ end
44
+
45
+ end
data/lib/websocket.rb ADDED
@@ -0,0 +1,16 @@
1
+ # WebSocket protocol implementation in Ruby
2
+ # This module does not provide a WebSocket server or client, but is made for using
3
+ # in http servers or clients to provide WebSocket support.
4
+ # @author Bernard "Imanel" Potocki
5
+ # @see http://github.com/imanel/websocket-ruby main repository
6
+ module WebSocket
7
+ class Error < RuntimeError; end
8
+
9
+ DEFAULT_VERSION = 13
10
+ ROOT = File.expand_path(File.dirname(__FILE__))
11
+
12
+ autoload :Frame, "#{ROOT}/websocket/frame"
13
+ autoload :Handler, "#{ROOT}/websocket/handler"
14
+ autoload :Handshake, "#{ROOT}/websocket/handshake"
15
+
16
+ end
@@ -0,0 +1,11 @@
1
+ module WebSocket
2
+ module Frame
3
+
4
+ autoload :Base, "#{::WebSocket::ROOT}/websocket/frame/base"
5
+ autoload :Data, "#{::WebSocket::ROOT}/websocket/frame/data"
6
+ autoload :Handler, "#{::WebSocket::ROOT}/websocket/frame/handler"
7
+ autoload :Incoming, "#{::WebSocket::ROOT}/websocket/frame/incoming"
8
+ autoload :Outgoing, "#{::WebSocket::ROOT}/websocket/frame/outgoing"
9
+
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module WebSocket
2
+ module Frame
3
+ class Base
4
+
5
+ attr_reader :data, :type, :version, :error
6
+
7
+ # Initialize frame
8
+ # @param args [Hash] Arguments for frame
9
+ # @option args [Integer] :version Version of draft. Currently supported version are 75, 76 and 00-13.
10
+ # @option args [String] :type Type of frame - available types are "text", "binary", "ping", "pong" and "close"(support depends on draft version)
11
+ # @option args [String] :data default data for frame
12
+ def initialize(args = {})
13
+ @type = args[:type]
14
+ @data = Data.new(args[:data].to_s)
15
+ @version = args[:version] || DEFAULT_VERSION
16
+ set_handler
17
+ end
18
+
19
+ # Check if some errors occured
20
+ # @return [Boolean] True if error is set
21
+ def error?
22
+ !!@error
23
+ end
24
+
25
+ private
26
+
27
+ def set_handler
28
+ case @version
29
+ when 75..76 then extend Handler::Handler75
30
+ when 0..2 then extend Handler::Handler75
31
+ when 3 then extend Handler::Handler03
32
+ when 4 then extend Handler::Handler04
33
+ when 5..6 then extend Handler::Handler05
34
+ when 7..13 then extend Handler::Handler07
35
+ else set_error(:unknown_protocol_version) and return
36
+ end
37
+ end
38
+
39
+ def set_error(message)
40
+ @error = message
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ module WebSocket
2
+ module Frame
3
+ class Data < String
4
+
5
+ def initialize(*args)
6
+ super *args.each { |arg| arg.force_encoding('ASCII-8BIT') if respond_to?(:force_encoding) }
7
+ end
8
+
9
+ def <<(*args)
10
+ super *args.each { |arg| arg.force_encoding('ASCII-8BIT') if respond_to?(:force_encoding) }
11
+ end
12
+
13
+ def set_mask
14
+ raise "Too short" if bytesize < 4 # TODO - change
15
+ @masking_key = Data.new(self[0..3])
16
+ end
17
+
18
+ def unset_mask
19
+ @masking_key = nil
20
+ end
21
+
22
+ def getbytes(start_index, count)
23
+ data = ''
24
+ data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
25
+ count.times do |i|
26
+ data << getbyte(start_index + i)
27
+ end
28
+ data
29
+ end
30
+
31
+ # Required for support of Ruby 1.8
32
+ unless new.respond_to?(:getbyte)
33
+ def getbyte(index)
34
+ self[index]
35
+ end
36
+ end
37
+
38
+ def getbyte_with_masking(index)
39
+ if @masking_key
40
+ masked_char = getbyte_without_masking(index)
41
+ masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
42
+ else
43
+ getbyte_without_masking(index)
44
+ end
45
+ end
46
+
47
+ alias_method :getbyte_without_masking, :getbyte
48
+ alias_method :getbyte, :getbyte_with_masking
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
1
+ module WebSocket
2
+ module Frame
3
+ module Handler
4
+
5
+ autoload :Base, "#{::WebSocket::ROOT}/websocket/frame/handler/base"
6
+
7
+ autoload :Handler03, "#{::WebSocket::ROOT}/websocket/frame/handler/handler03"
8
+ autoload :Handler04, "#{::WebSocket::ROOT}/websocket/frame/handler/handler04"
9
+ autoload :Handler05, "#{::WebSocket::ROOT}/websocket/frame/handler/handler05"
10
+ autoload :Handler07, "#{::WebSocket::ROOT}/websocket/frame/handler/handler07"
11
+ autoload :Handler75, "#{::WebSocket::ROOT}/websocket/frame/handler/handler75"
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ module WebSocket
2
+ module Frame
3
+ module Handler
4
+ module Base
5
+
6
+ MAX_FRAME_SIZE = 20 * 1024 * 1024 # 20MB
7
+
8
+ # Is selected type supported for selected handler?
9
+ def support_type?
10
+ supported_frames.include?(@type)
11
+ end
12
+
13
+ # Implement in submodules
14
+ def supported_frames
15
+ raise NotImplementedError
16
+ end
17
+
18
+ private
19
+
20
+ # Convert data to raw frame ready to send to client
21
+ def encode_frame
22
+ raise NotImplementedError
23
+ end
24
+
25
+ # Convert raw data to decoded frame
26
+ def decode_frame
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def control_frame?(frame_type)
31
+ ![:text, :binary, :continuation].include?(frame_type)
32
+ end
33
+
34
+ def data_frame?(frame_type)
35
+ [:text, :binary].include?(frame_type)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,162 @@
1
+ # encoding: binary
2
+
3
+ module WebSocket
4
+ module Frame
5
+ module Handler
6
+ module Handler03
7
+
8
+ include Base
9
+
10
+ def supported_frames
11
+ [:text, :binary, :close, :ping, :pong]
12
+ end
13
+
14
+ private
15
+
16
+ def encode_frame
17
+ frame = ''
18
+
19
+ opcode = type_to_opcode(@type)
20
+ byte1 = opcode | (fin ? 0b10000000 : 0b00000000) # since more, rsv1-3 are 0 and 0x80 for Draft 4
21
+ frame << byte1
22
+
23
+ length = @data.size
24
+ if length <= 125
25
+ byte2 = length # since rsv4 is 0
26
+ frame << byte2
27
+ elsif length < 65536 # write 2 byte length
28
+ frame << 126
29
+ frame << [length].pack('n')
30
+ else # write 8 byte length
31
+ frame << 127
32
+ frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
33
+ end
34
+
35
+ frame << @data
36
+ frame
37
+ rescue WebSocket::Error => e
38
+ set_error(e.message.to_sym) and return
39
+ end
40
+
41
+ def decode_frame
42
+ while @data.size > 1
43
+ pointer = 0
44
+
45
+ more = ((@data.getbyte(pointer) & 0b10000000) == 0b10000000) ^ fin
46
+
47
+ raise(WebSocket::Error, :reserved_bit_used) if @data.getbyte(pointer) & 0b01110000 != 0b00000000
48
+
49
+ opcode = @data.getbyte(pointer) & 0b00001111
50
+ frame_type = opcode_to_type(opcode)
51
+ pointer += 1
52
+
53
+ raise(WebSocket::Error, :fragmented_control_frame) if more && control_frame?(frame_type)
54
+ raise(WebSocket::Error, :data_frame_instead_continuation) if data_frame?(frame_type) && !@application_data_buffer.nil?
55
+
56
+ mask = masking? && (@data.getbyte(pointer) & 0b10000000) == 0b10000000
57
+ length = @data.getbyte(pointer) & 0b01111111
58
+
59
+ raise(WebSocket::Error, :control_frame_payload_too_long) if length > 125 && control_frame?(frame_type)
60
+
61
+ pointer += 1
62
+
63
+ payload_length = case length
64
+ when 127 # Length defined by 8 bytes
65
+ # Check buffer size
66
+ return if @data.getbyte(pointer+8-1) == nil # Buffer incomplete
67
+
68
+ # Only using the last 4 bytes for now, till I work out how to
69
+ # unpack 8 bytes. I'm sure 4GB frames will do for now :)
70
+ l = @data.getbytes(pointer+4, 4).unpack('N').first
71
+ pointer += 8
72
+ l
73
+ when 126 # Length defined by 2 bytes
74
+ # Check buffer size
75
+ return if @data.getbyte(pointer+2-1) == nil # Buffer incomplete
76
+
77
+ l = @data.getbytes(pointer, 2).unpack('n').first
78
+ pointer += 2
79
+ l
80
+ else
81
+ length
82
+ end
83
+
84
+ # Compute the expected frame length
85
+ frame_length = pointer + payload_length
86
+ frame_length += 4 if mask
87
+
88
+ raise(WebSocket::Error, :frame_too_long) if frame_length > MAX_FRAME_SIZE
89
+
90
+ # Check buffer size
91
+ return if @data.getbyte(frame_length-1) == nil # Buffer incomplete
92
+
93
+ # Remove frame header
94
+ @data.slice!(0...pointer)
95
+ pointer = 0
96
+
97
+ # Read application data (unmasked if required)
98
+ @data.set_mask if mask
99
+ pointer += 4 if mask
100
+ application_data = @data.getbytes(pointer, payload_length)
101
+ application_data.force_encoding('UTF-8') if application_data.respond_to?(:force_encoding)
102
+ pointer += payload_length
103
+ @data.unset_mask if mask
104
+
105
+ # Throw away data up to pointer
106
+ @data.slice!(0...pointer)
107
+
108
+ raise(WebSocket::Error, :unexpected_continuation_frame) if frame_type == :continuation && !@frame_type
109
+
110
+ if more
111
+ @application_data_buffer ||= ''
112
+ @application_data_buffer << application_data
113
+ @frame_type ||= frame_type
114
+ else
115
+ # Message is complete
116
+ if frame_type == :continuation
117
+ @application_data_buffer << application_data
118
+ # Test valid UTF-8 encoding
119
+ raise(WebSocket::Error, :invalid_payload_encoding) if @frame_type == :text && @application_data_buffer.respond_to?(:valid_encoding?) && !@application_data_buffer.valid_encoding?
120
+ message = self.class.new(:version => version, :type => @frame_type, :data => @application_data_buffer, :decoded => true)
121
+ @application_data_buffer = nil
122
+ @frame_type = nil
123
+ return message
124
+ else
125
+ raise(WebSocket::Error, :invalid_payload_encoding) if frame_type == :text && application_data.respond_to?(:valid_encoding?) && !application_data.valid_encoding?
126
+ return self.class.new(:version => version, :type => frame_type, :data => application_data, :decoded => true)
127
+ end
128
+ end
129
+ end
130
+ return nil
131
+ rescue WebSocket::Error => e
132
+ set_error(e.message.to_sym) and return
133
+ end
134
+
135
+ # This allows flipping the more bit to fin for draft 04
136
+ def fin; false; end
137
+
138
+ def masking?; false; end
139
+
140
+ FRAME_TYPES = {
141
+ :continuation => 0,
142
+ :close => 1,
143
+ :ping => 2,
144
+ :pong => 3,
145
+ :text => 4,
146
+ :binary => 5
147
+ }
148
+
149
+ FRAME_TYPES_INVERSE = FRAME_TYPES.invert
150
+
151
+ def type_to_opcode(frame_type)
152
+ FRAME_TYPES[frame_type] || raise(WebSocket::Error, :unknown_frame_type)
153
+ end
154
+
155
+ def opcode_to_type(opcode)
156
+ FRAME_TYPES_INVERSE[opcode] || raise(WebSocket::Error, :unknown_opcode)
157
+ end
158
+
159
+ end
160
+ end
161
+ end
162
+ end