websocket 1.0.1 → 1.0.2

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 (36) hide show
  1. data/CHANGELOG.md +10 -0
  2. data/README.md +1 -1
  3. data/autobahn-server.json +1 -1
  4. data/examples/client.rb +1 -1
  5. data/examples/server.rb +1 -1
  6. data/examples/tests/autobahn_server.rb +24 -0
  7. data/lib/websocket.rb +11 -0
  8. data/lib/websocket/frame/base.rb +19 -4
  9. data/lib/websocket/frame/handler/base.rb +8 -12
  10. data/lib/websocket/frame/handler/handler03.rb +15 -1
  11. data/lib/websocket/frame/handler/handler05.rb +1 -0
  12. data/lib/websocket/frame/handler/handler07.rb +10 -0
  13. data/lib/websocket/frame/handler/handler75.rb +5 -2
  14. data/lib/websocket/frame/incoming.rb +8 -0
  15. data/lib/websocket/frame/outgoing.rb +6 -0
  16. data/lib/websocket/handshake/base.rb +38 -2
  17. data/lib/websocket/handshake/client.rb +66 -11
  18. data/lib/websocket/handshake/handler/base.rb +16 -0
  19. data/lib/websocket/handshake/handler/client.rb +1 -0
  20. data/lib/websocket/handshake/handler/client01.rb +3 -0
  21. data/lib/websocket/handshake/handler/client04.rb +9 -1
  22. data/lib/websocket/handshake/handler/client75.rb +1 -0
  23. data/lib/websocket/handshake/handler/client76.rb +35 -17
  24. data/lib/websocket/handshake/handler/server.rb +0 -19
  25. data/lib/websocket/handshake/handler/server04.rb +5 -0
  26. data/lib/websocket/handshake/handler/server75.rb +3 -1
  27. data/lib/websocket/handshake/handler/server76.rb +11 -1
  28. data/lib/websocket/handshake/server.rb +72 -0
  29. data/lib/websocket/version.rb +1 -1
  30. data/spec/frame/incoming_03_spec.rb +1 -1
  31. data/spec/frame/incoming_04_spec.rb +1 -1
  32. data/spec/frame/incoming_05_spec.rb +1 -1
  33. data/spec/frame/incoming_07_spec.rb +1 -1
  34. data/spec/frame/incoming_75_spec.rb +1 -1
  35. data/spec/handshake/client_76_spec.rb +1 -1
  36. metadata +4 -3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.2
4
+
5
+ - allow configuration of max frame size via WebSocket.max_frame_size option
6
+ - much better documentation
7
+ - remove handler-specific methods from public list
8
+ - refactor code for easier use
9
+ - make parsers return more consistent values
10
+ - fix server handshake #to_s when no version was found
11
+ - add #uri to server handshake
12
+
3
13
  ## 1.0.1
4
14
 
5
15
  - allow creating client with :uri and :url options
data/README.md CHANGED
@@ -9,7 +9,7 @@ Currently WebSocket Ruby supports all existing drafts of WebSocket, which includ
9
9
 
10
10
  - [hixie-75](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75)
11
11
  - [hixie-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76)
12
- - [all hybi drafts (00-17)](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17)
12
+ - [all hybi drafts (00-13)](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17)
13
13
  - [RFC 6455](http://datatracker.ietf.org/doc/rfc6455/)
14
14
 
15
15
  ## Installation
data/autobahn-server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "options": {"failByDrop": false},
3
3
  "outdir": "./autobahn/server",
4
4
 
5
- "servers": [{"agent": "WebSocket-Ruby v1.0.1", "url": "ws://localhost:9001", "options": {"version": 18}}],
5
+ "servers": [{"agent": "WebSocket-Ruby v1.0.2", "url": "ws://localhost:9001", "options": {"version": 18}}],
6
6
 
7
7
  "cases": ["*"],
8
8
  "exclude-cases": [],
data/examples/client.rb CHANGED
@@ -2,7 +2,7 @@ require "#{File.expand_path(File.dirname(__FILE__))}/base"
2
2
  require 'uri'
3
3
 
4
4
  # Example WebSocket Client (using EventMachine)
5
- # @example Usage
5
+ # @example
6
6
  # ws = WebSocket::EventMachine::Client.connect(:host => "0.0.0.0", :port => 8080)
7
7
  # ws.onmessage { |msg| ws.send "Pong: #{msg}" }
8
8
  # ws.send "data"
data/examples/server.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "#{File.expand_path(File.dirname(__FILE__))}/base"
2
2
 
3
3
  # Example WebSocket Server (using EventMachine)
4
- # @example Usage
4
+ # @example
5
5
  # WebSocket::EventMachine::Server.start(:host => "0.0.0.0", :port => 8080) do |ws|
6
6
  # ws.onopen { ws.send "Hello Client!"}
7
7
  # ws.onmessage { |msg| ws.send "Pong: #{msg}" }
@@ -0,0 +1,24 @@
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.onmessage do |msg, type|
12
+ ws.send msg, :type => type
13
+ end
14
+
15
+ end
16
+
17
+ puts "Server started at port 9001"
18
+
19
+ def stop
20
+ puts "Terminating WebSocket Server"
21
+ EventMachine.stop
22
+ end
23
+
24
+ end
data/lib/websocket.rb CHANGED
@@ -6,6 +6,7 @@
6
6
  module WebSocket
7
7
  class Error < RuntimeError; end
8
8
 
9
+ # Default WebSocket version to use
9
10
  DEFAULT_VERSION = 13
10
11
  ROOT = File.expand_path(File.dirname(__FILE__))
11
12
 
@@ -13,4 +14,14 @@ module WebSocket
13
14
  autoload :Handler, "#{ROOT}/websocket/handler"
14
15
  autoload :Handshake, "#{ROOT}/websocket/handshake"
15
16
 
17
+ # Limit of frame size payload in bytes
18
+ def self.max_frame_size
19
+ @max_frame_size ||= 20 * 1024 * 1024 # 20MB
20
+ end
21
+
22
+ # Set limit of frame size payload in bytes
23
+ def self.max_frame_size=(val)
24
+ @max_frame_size = val
25
+ end
26
+
16
27
  end
@@ -1,19 +1,20 @@
1
1
  module WebSocket
2
2
  module Frame
3
+ # @abstract Subclass and override to implement custom frames
3
4
  class Base
4
5
 
5
6
  attr_reader :data, :type, :version, :error
6
7
 
7
8
  # Initialize frame
8
9
  # @param args [Hash] Arguments for frame
10
+ # @option args [String] :data default data for frame
11
+ # @option args [String] :type Type of frame - available types are "text", "binary", "ping", "pong" and "close"(support depends on draft version)
9
12
  # @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
13
  def initialize(args = {})
13
14
  @type = args[:type]
14
15
  @data = Data.new(args[:data].to_s)
15
16
  @version = args[:version] || DEFAULT_VERSION
16
- set_handler
17
+ include_version
17
18
  end
18
19
 
19
20
  # Check if some errors occured
@@ -22,9 +23,21 @@ module WebSocket
22
23
  !!@error
23
24
  end
24
25
 
26
+ # Is selected type supported for selected handler?
27
+ def support_type?
28
+ supported_frames.include?(@type)
29
+ end
30
+
31
+ # Implement in submodules
32
+ def supported_frames
33
+ raise NotImplementedError
34
+ end
35
+
25
36
  private
26
37
 
27
- def set_handler
38
+ # Include set of methods for selected protocol version
39
+ # @return [Boolean] false if protocol number is unknown, otherwise true
40
+ def include_version
28
41
  case @version
29
42
  when 75..76 then extend Handler::Handler75
30
43
  when 0..2 then extend Handler::Handler75
@@ -36,6 +49,8 @@ module WebSocket
36
49
  end
37
50
  end
38
51
 
52
+ # Changes state to error and sets error message
53
+ # @param [String] message Error message to set
39
54
  def set_error(message)
40
55
  @error = message
41
56
  end
@@ -3,34 +3,30 @@ module WebSocket
3
3
  module Handler
4
4
  module Base
5
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
6
  private
19
7
 
20
8
  # Convert data to raw frame ready to send to client
9
+ # @return [String] Encoded frame
21
10
  def encode_frame
22
11
  raise NotImplementedError
23
12
  end
24
13
 
25
14
  # Convert raw data to decoded frame
15
+ # @return [WebSocket::Frame::Incoming] Frame if found, nil otherwise
26
16
  def decode_frame
27
17
  raise NotImplementedError
28
18
  end
29
19
 
20
+ # Check if frame is one of control frames
21
+ # @param [Symbol] frame_type Frame type
22
+ # @return [Boolean] True if given frame type is control frame
30
23
  def control_frame?(frame_type)
31
24
  ![:text, :binary, :continuation].include?(frame_type)
32
25
  end
33
26
 
27
+ # Check if frame is one of data frames
28
+ # @param [Symbol] frame_type Frame type
29
+ # @return [Boolean] True if given frame type is data frame
34
30
  def data_frame?(frame_type)
35
31
  [:text, :binary].include?(frame_type)
36
32
  end
@@ -7,12 +7,14 @@ module WebSocket
7
7
 
8
8
  include Base
9
9
 
10
+ # @see WebSocket::Frame::Base#supported_frames
10
11
  def supported_frames
11
12
  [:text, :binary, :close, :ping, :pong]
12
13
  end
13
14
 
14
15
  private
15
16
 
17
+ # @see WebSocket::Frame::Handler::Base#encode_frame
16
18
  def encode_frame
17
19
  frame = ''
18
20
 
@@ -38,6 +40,7 @@ module WebSocket
38
40
  set_error(e.message.to_sym) and return
39
41
  end
40
42
 
43
+ # @see WebSocket::Frame::Handler::Base#decode_frame
41
44
  def decode_frame
42
45
  while @data.size > 1
43
46
  pointer = 0
@@ -85,7 +88,7 @@ module WebSocket
85
88
  frame_length = pointer + payload_length
86
89
  frame_length += 4 if mask
87
90
 
88
- raise(WebSocket::Error, :frame_too_long) if frame_length > MAX_FRAME_SIZE
91
+ raise(WebSocket::Error, :frame_too_long) if frame_length > WebSocket.max_frame_size
89
92
 
90
93
  # Check buffer size
91
94
  return if @data.getbyte(frame_length-1) == nil # Buffer incomplete
@@ -135,8 +138,10 @@ module WebSocket
135
138
  # This allows flipping the more bit to fin for draft 04
136
139
  def fin; false; end
137
140
 
141
+ # Allow turning on or off masking
138
142
  def masking?; false; end
139
143
 
144
+ # Hash of frame names and it's opcodes
140
145
  FRAME_TYPES = {
141
146
  :continuation => 0,
142
147
  :close => 1,
@@ -146,12 +151,21 @@ module WebSocket
146
151
  :binary => 5
147
152
  }
148
153
 
154
+ # Hash of frame opcodes and it's names
149
155
  FRAME_TYPES_INVERSE = FRAME_TYPES.invert
150
156
 
157
+ # Convert frame type name to opcode
158
+ # @param [Symbol] frame_type Frame type name
159
+ # @return [Integer] opcode or nil
160
+ # @raise [WebSocket::Error] if frame opcode is not known
151
161
  def type_to_opcode(frame_type)
152
162
  FRAME_TYPES[frame_type] || raise(WebSocket::Error, :unknown_frame_type)
153
163
  end
154
164
 
165
+ # Convert frame opcode to type name
166
+ # @param [Integer] opcode Opcode
167
+ # @return [Symbol] Frame type name or nil
168
+ # @raise [WebSocket::Error] if frame type name is not known
155
169
  def opcode_to_type(opcode)
156
170
  FRAME_TYPES_INVERSE[opcode] || raise(WebSocket::Error, :unknown_opcode)
157
171
  end
@@ -9,6 +9,7 @@ module WebSocket
9
9
 
10
10
  private
11
11
 
12
+ # Since handler 5 masking should be enabled by default
12
13
  def masking?; true; end
13
14
 
14
15
  end
@@ -9,6 +9,7 @@ module WebSocket
9
9
 
10
10
  private
11
11
 
12
+ # Hash of frame names and it's opcodes
12
13
  FRAME_TYPES = {
13
14
  :continuation => 0,
14
15
  :text => 1,
@@ -18,12 +19,21 @@ module WebSocket
18
19
  :pong => 10,
19
20
  }
20
21
 
22
+ # Hash of frame opcodes and it's names
21
23
  FRAME_TYPES_INVERSE = FRAME_TYPES.invert
22
24
 
25
+ # Convert frame type name to opcode
26
+ # @param [Symbol] frame_type Frame type name
27
+ # @return [Integer] opcode or nil
28
+ # @raise [WebSocket::Error] if frame opcode is not known
23
29
  def type_to_opcode(frame_type)
24
30
  FRAME_TYPES[frame_type] || raise(WebSocket::Error, :unknown_frame_type)
25
31
  end
26
32
 
33
+ # Convert frame opcode to type name
34
+ # @param [Integer] opcode Opcode
35
+ # @return [Symbol] Frame type name or nil
36
+ # @raise [WebSocket::Error] if frame type name is not known
27
37
  def opcode_to_type(opcode)
28
38
  FRAME_TYPES_INVERSE[opcode] || raise(WebSocket::Error, :unknown_opcode)
29
39
  end
@@ -7,12 +7,14 @@ module WebSocket
7
7
 
8
8
  include Base
9
9
 
10
+ # @see WebSocket::Frame::Base#supported_frames
10
11
  def supported_frames
11
12
  [:text, :close]
12
13
  end
13
14
 
14
15
  private
15
16
 
17
+ # @see WebSocket::Frame::Handler::Base#encode_frame
16
18
  def encode_frame
17
19
  case @type
18
20
  when :close then "\xff\x00"
@@ -23,6 +25,7 @@ module WebSocket
23
25
  end
24
26
  end
25
27
 
28
+ # @see WebSocket::Frame::Handler::Base#decode_frame
26
29
  def decode_frame
27
30
  return if @data.size == 0
28
31
 
@@ -43,7 +46,7 @@ module WebSocket
43
46
  break unless (b & 0x80) == 0x80
44
47
  end
45
48
 
46
- set_error(:frame_too_long) and return if length > MAX_FRAME_SIZE
49
+ set_error(:frame_too_long) and return if length > ::WebSocket.max_frame_size
47
50
 
48
51
  unless @data.getbyte(pointer+length-1) == nil
49
52
  # Straight from spec - I'm sure this isn't crazy...
@@ -62,7 +65,7 @@ module WebSocket
62
65
  set_error(:invalid_frame) and return if @data.getbyte(0) != 0x00
63
66
 
64
67
  # Addition to the spec to protect against malicious requests
65
- set_error(:frame_too_long) and return if @data.size > MAX_FRAME_SIZE
68
+ set_error(:frame_too_long) and return if @data.size > ::WebSocket.max_frame_size
66
69
 
67
70
  msg = @data.slice!(/\A\x00[^\xff]*\xff/)
68
71
  if msg
@@ -1,5 +1,13 @@
1
1
  module WebSocket
2
2
  module Frame
3
+ # Construct or parse incoming WebSocket Frame.
4
+ # @note You should NEVER use this class directly - use Client or Server subclasses instead, as they contain additional frame options(i.e. Client-side masking in draft 04)
5
+ #
6
+ # @example
7
+ # frame = WebSocket::Frame::Incoming::Server.new(:version => @handshake.version)
8
+ # frame << "\x81\x05\x48\x65\x6c\x6c\x6f\x81\x06\x77\x6f\x72\x6c\x64\x21"
9
+ # frame.next # "Hello"
10
+ # frame.next # "world!""
3
11
  class Incoming < Base
4
12
 
5
13
  autoload :Client, "#{::WebSocket::ROOT}/websocket/frame/incoming/client"
@@ -1,5 +1,11 @@
1
1
  module WebSocket
2
2
  module Frame
3
+ # Construct or parse incoming WebSocket Frame.
4
+ # @note You should NEVER use this class directly - use Client or Server subclasses instead, as they contain additional frame options(i.e. Client-side masking in draft 04)
5
+ #
6
+ # @example
7
+ # frame = WebSocket::Frame::Outgoing::Server.new(:version => @handshake.version, :data => "Hello", :type => :text)
8
+ # frame.to_s # "\x81\x05\x48\x65\x6c\x6c\x6f"
3
9
  class Outgoing < Base
4
10
 
5
11
  autoload :Client, "#{::WebSocket::ROOT}/websocket/frame/outgoing/client"
@@ -1,10 +1,12 @@
1
1
  module WebSocket
2
2
  module Handshake
3
+ # @abstract Subclass and override to implement custom handshakes
3
4
  class Base
4
5
 
5
6
  attr_reader :host, :port, :path, :query,
6
7
  :error, :state, :version, :secure
7
8
 
9
+ # Initialize new WebSocket Handshake and set it's state to :new
8
10
  def initialize(args = {})
9
11
  @state = :new
10
12
 
@@ -12,32 +14,63 @@ module WebSocket
12
14
  @headers = {}
13
15
  end
14
16
 
17
+ # @abstract Add data to handshake
15
18
  def <<(data)
16
19
  raise NotImplementedError
17
20
  end
18
21
 
22
+ # Return textual representation of handshake request or response
23
+ # @return [String] text of response
24
+ def to_s
25
+ ""
26
+ end
27
+
28
+ # Is parsing of data finished?
29
+ # @return [Boolena] True if request was completely parsed or error occured. False otherwise
19
30
  def finished?
20
31
  @state == :finished || @state == :error
21
32
  end
22
33
 
34
+ # Is parsed data valid?
35
+ # @return [Boolean] False if some errors occured. Reason for error could be found in error method
23
36
  def valid?
24
37
  finished? && @error == nil
25
38
  end
26
39
 
40
+ # @abstract Should send data after parsing is finished?
27
41
  def should_respond?
28
42
  raise NotImplementedError
29
43
  end
30
44
 
45
+ # Data left from parsing. Sometimes data that doesn't belong to handshake are added - use this method to retrieve them.
46
+ # @return [String] String if some data are available. Nil otherwise
31
47
  def leftovers
32
48
  @leftovers.split("\n", reserved_leftover_lines + 1)[reserved_leftover_lines]
33
49
  end
34
50
 
51
+ # URI of request.
52
+ # @return [String] Full URI with protocol
53
+ # @example
54
+ # @handshake.uri #=> "ws://example.com/path?query=true"
55
+ def uri
56
+ uri = secure ? "wss://" : "ws://"
57
+ uri << host
58
+ uri << ":#{port}" if port
59
+ uri << path
60
+ uri << "?#{query}" if query
61
+ uri
62
+ end
63
+
35
64
  private
36
65
 
66
+ # Number of lines after header that should be handled as belonging to handshake. Any data after those lines will be handled as leftovers.
67
+ # @return [Integer] Number of lines
37
68
  def reserved_leftover_lines
38
69
  0
39
70
  end
40
71
 
72
+ # Changes state to error and sets error message
73
+ # @param [String] message Error message to set
41
74
  def set_error(message)
42
75
  @state = :error
43
76
  @error = message
@@ -45,14 +78,16 @@ module WebSocket
45
78
 
46
79
  HEADER = /^([^:]+):\s*(.+)$/
47
80
 
81
+ # Parse data imported to handshake and sets state to finished if necessary.
82
+ # @return [Boolean] True if finished parsing. False if not all data received yet.
48
83
  def parse_data
49
84
  header, @leftovers = @data.split("\r\n\r\n", 2)
50
- return unless @leftovers # The whole header has not been received yet.
85
+ return false unless @leftovers # The whole header has not been received yet.
51
86
 
52
87
  lines = header.split("\r\n")
53
88
 
54
89
  first_line = lines.shift
55
- return unless parse_first_line(first_line)
90
+ return false unless parse_first_line(first_line)
56
91
 
57
92
  lines.each do |line|
58
93
  h = HEADER.match(line)
@@ -60,6 +95,7 @@ module WebSocket
60
95
  end
61
96
 
62
97
  @state = :finished
98
+ true
63
99
  end
64
100
 
65
101
  end
@@ -2,8 +2,53 @@ require 'URI' unless defined?(URI)
2
2
 
3
3
  module WebSocket
4
4
  module Handshake
5
+ # Construct or parse a client WebSocket handshake.
6
+ #
7
+ # @example
8
+ # @handshake = WebSocket::Handshake::Client.new(:url => 'ws://example.com')
9
+ #
10
+ # # Create request
11
+ # @handshake.to_s # GET /demo HTTP/1.1
12
+ # # Upgrade: websocket
13
+ # # Connection: Upgrade
14
+ # # Host: example.com
15
+ # # Sec-WebSocket-Origin: http://example.com
16
+ # # Sec-WebSocket-Version: 17
17
+ # # Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
18
+ #
19
+ # # Parse server response
20
+ # @handshake << <<EOF
21
+ # HTTP/1.1 101 Switching Protocols
22
+ # Upgrade: websocket
23
+ # Connection: Upgrade
24
+ # Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
25
+ #
26
+ # EOF
27
+ #
28
+ # # All data received?
29
+ # @handshake.finished?
30
+ #
31
+ # # No parsing errors?
32
+ # @handshake.valid?
33
+ #
5
34
  class Client < Base
6
35
 
36
+ # Initialize new WebSocket Client
37
+ #
38
+ # @param [Hash] args Arguments for client
39
+ #
40
+ # @option args [String] :host Host of request. Required if no :url param was provided.
41
+ # @option args [String] :origin Origin of request. Optional, should be used mostly by browsers. Default: nil
42
+ # @option args [String] :path Path of request. Should start with '/'. Default: '/'
43
+ # @option args [Integer] :port Port of request. Default: nil
44
+ # @option args [String] :query. Query for request. Should be in format "aaa=bbb&ccc=ddd"
45
+ # @option args [Boolean] :secure Defines protocol to use. If true then wss://, otherwise ws://. This option will not change default port - it should be handled by programmer.
46
+ # @option args [String] :url URL of request. Must by in format like ws://example.com/path?query=true. Every part of this url will be overriden by more specific arguments.
47
+ # @option args [String] :uri Alias to :url
48
+ # @option args [Integer] :version Version of WebSocket to use. Default: 13 (this is version from RFC)
49
+ #
50
+ # @example
51
+ # Websocket::Handshake::Client.new(:url => "ws://example.com/path?query=true")
7
52
  def initialize(args = {})
8
53
  super
9
54
 
@@ -32,6 +77,18 @@ module WebSocket
32
77
  include_version
33
78
  end
34
79
 
80
+ # Add text of response from Server. This method will parse content immediately and update state and error(if neccessary)
81
+ #
82
+ # @param [String] data Data to add
83
+ #
84
+ # @example
85
+ # @handshake << <<EOF
86
+ # HTTP/1.1 101 Switching Protocols
87
+ # Upgrade: websocket
88
+ # Connection: Upgrade
89
+ # Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
90
+ #
91
+ # EOF
35
92
  def <<(data)
36
93
  @data << data
37
94
  if parse_data
@@ -39,21 +96,16 @@ module WebSocket
39
96
  end
40
97
  end
41
98
 
99
+ # Should send content to server after finished parsing?
100
+ # @return [Boolean] false
42
101
  def should_respond?
43
102
  false
44
103
  end
45
104
 
46
- def uri
47
- uri = @secure ? "wss://" : "ws://"
48
- uri << @host
49
- uri << ":#{@port}" if @port
50
- uri << @path
51
- uri << "?#{@query}" if @query
52
- uri
53
- end
54
-
55
105
  private
56
106
 
107
+ # Include set of methods for selected protocol version
108
+ # @return [Boolean] false if protocol number is unknown, otherwise true
57
109
  def include_version
58
110
  case @version
59
111
  when 75 then extend Handler::Client75
@@ -67,11 +119,14 @@ module WebSocket
67
119
 
68
120
  FIRST_LINE = /^HTTP\/1\.1 (\d{3})[\w\s]*$/
69
121
 
122
+ # Parse first line of Server response.
123
+ # @param [String] line Line to parse
124
+ # @return [Boolean] True if parsed correctly. False otherwise
70
125
  def parse_first_line(line)
71
126
  line_parts = line.match(FIRST_LINE)
72
- set_error(:invalid_header) and return unless line_parts
127
+ set_error(:invalid_header) and return false unless line_parts
73
128
  status = line_parts[1]
74
- set_error(:invalid_status_code) and return unless status == '101'
129
+ set_error(:invalid_status_code) and return false unless status == '101'
75
130
 
76
131
  return true
77
132
  end
@@ -1,8 +1,10 @@
1
1
  module WebSocket
2
2
  module Handshake
3
3
  module Handler
4
+ # This module and it's descendants are included in client or server handshake in order to extend basic functionality
4
5
  module Base
5
6
 
7
+ # @see WebSocket::Handshake::Base#to_s
6
8
  def to_s
7
9
  result = [ header_line ]
8
10
  handshake_keys.each do |key|
@@ -15,6 +17,20 @@ module WebSocket
15
17
 
16
18
  private
17
19
 
20
+ # Set first line of text representation according to specification.
21
+ # @return [String] First line of HTTP header
22
+ def header_line
23
+ ""
24
+ end
25
+
26
+ # Set handshake headers. Provided as array because some protocol version require specific order of fields.
27
+ # @return [Array] List of headers as arrays [ key, value ]
28
+ def handshake_keys
29
+ []
30
+ end
31
+
32
+ # Set data to send after headers. In most cases it will be blank data.
33
+ # @return [String] data
18
34
  def finishing_line
19
35
  ""
20
36
  end
@@ -7,6 +7,7 @@ module WebSocket
7
7
 
8
8
  private
9
9
 
10
+ # @see WebSocket::Handshake::Handler::Base#header_line
10
11
  def header_line
11
12
  path = @path
12
13
  path += "?" + @query if @query
@@ -7,6 +7,9 @@ module WebSocket
7
7
 
8
8
  include Client76
9
9
 
10
+ private
11
+
12
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
10
13
  def handshake_keys
11
14
  keys = super
12
15
  keys << ['Sec-WebSocket-Draft', @version]
@@ -8,12 +8,14 @@ module WebSocket
8
8
 
9
9
  include Client
10
10
 
11
+ # @see WebSocket::Handshake::Base#valid?
11
12
  def valid?
12
13
  super && verify_accept
13
14
  end
14
15
 
15
16
  private
16
17
 
18
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
17
19
  def handshake_keys
18
20
  keys = [
19
21
  ["Upgrade", "websocket"],
@@ -23,19 +25,25 @@ module WebSocket
23
25
  host += ":#{@port}" if @port
24
26
  keys << ["Host", host]
25
27
  keys << ["Sec-WebSocket-Origin", @origin] if @origin
26
- keys << ["Sec-WebSocket-Version", @version]
28
+ keys << ["Sec-WebSocket-Version", @version ]
27
29
  keys << ["Sec-WebSocket-Key", key]
28
30
  keys
29
31
  end
30
32
 
33
+ # Sec-WebSocket-Key value
34
+ # @return [String] key
31
35
  def key
32
36
  @key ||= Base64.encode64((1..16).map { rand(255).chr } * '').strip
33
37
  end
34
38
 
39
+ # Value of Sec-WebSocket-Accept that should be delivered back by server
40
+ # @return [Sering] accept
35
41
  def accept
36
42
  @accept ||= Base64.encode64(Digest::SHA1.digest(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')).strip
37
43
  end
38
44
 
45
+ # Verify if received header Sec-WebSocket-Accept matches generated one.
46
+ # @return [Boolean] True if accept is matching. False otherwise(appropriate error is set)
39
47
  def verify_accept
40
48
  set_error(:invalid_accept) and return false unless @headers['sec-websocket-accept'] == accept
41
49
  true
@@ -7,6 +7,7 @@ module WebSocket
7
7
 
8
8
  private
9
9
 
10
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
10
11
  def handshake_keys
11
12
  keys = [
12
13
  ["Upgrade", "WebSocket"],
@@ -7,28 +7,19 @@ module WebSocket
7
7
 
8
8
  include Client75
9
9
 
10
- def key1
11
- @key1 ||= generate_key(:key1)
12
- end
13
-
14
- def key2
15
- @key2 ||= generate_key(:key2)
16
- end
17
-
18
- def key3
19
- @key3 ||= generate_key3
20
- end
21
-
10
+ # @see WebSocket::Handshake::Base#valid?
22
11
  def valid?
23
12
  super && verify_challenge
24
13
  end
25
14
 
26
15
  private
27
16
 
17
+ # @see WebSocket::Handshake::Base#reserved_leftover_lines
28
18
  def reserved_leftover_lines
29
19
  1
30
20
  end
31
21
 
22
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
32
23
  def handshake_keys
33
24
  keys = super
34
25
  keys << ['Sec-WebSocket-Key1', key1]
@@ -36,11 +27,31 @@ module WebSocket
36
27
  keys
37
28
  end
38
29
 
39
- def verify_challenge
40
- set_error(:invalid_challenge) and return false unless @leftovers == challenge
41
- true
30
+ # @see WebSocket::Handshake::Handler::Base#finishing_line
31
+ def finishing_line
32
+ key3
42
33
  end
43
34
 
35
+ # Sec-WebSocket-Key1 value
36
+ # @return [String] key
37
+ def key1
38
+ @key1 ||= generate_key(:key1)
39
+ end
40
+
41
+ # Sec-WebSocket-Key2 value
42
+ # @return [String] key
43
+ def key2
44
+ @key2 ||= generate_key(:key2)
45
+ end
46
+
47
+ # Value of third key, sent in body
48
+ # @return [String] key
49
+ def key3
50
+ @key3 ||= generate_key3
51
+ end
52
+
53
+ # Expected challenge that should be sent by server
54
+ # @return [String] challenge
44
55
  def challenge
45
56
  return @challenge if defined?(@challenge)
46
57
  key1 && key2
@@ -51,12 +62,18 @@ module WebSocket
51
62
  @challenge = Digest::MD5.digest(sum)
52
63
  end
53
64
 
54
- def finishing_line
55
- key3
65
+ # Verify if challenge sent by server match generated one
66
+ # @return [Boolena] True if challenge matches, false otherwise(sets appropriate error)
67
+ def verify_challenge
68
+ set_error(:invalid_challenge) and return false unless @leftovers == challenge
69
+ true
56
70
  end
57
71
 
58
72
  NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
59
73
 
74
+ # Generate Sec-WebSocket-Key1 and Sec-WebSocket-Key2
75
+ # @param [String] name of key. Will be used to set number variable needed later. Valid values: key1, key2
76
+ # @return [String] generated key
60
77
  def generate_key(key)
61
78
  spaces = 1 + rand(12)
62
79
  max = 0xffffffff / spaces
@@ -75,6 +92,7 @@ module WebSocket
75
92
  return key
76
93
  end
77
94
 
95
+ # Generate third key
78
96
  def generate_key3
79
97
  return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
80
98
  end
@@ -5,25 +5,6 @@ module WebSocket
5
5
 
6
6
  include Base
7
7
 
8
- def host
9
- @headers["host"].to_s.split(":")[0].to_s
10
- end
11
-
12
- def port
13
- @headers["host"].to_s.split(":")[1]
14
- end
15
-
16
- private
17
-
18
- def handshake_location
19
- location = @secure ? "wss://" : "ws://"
20
- location << host
21
- location << ":#{port}" if port
22
- location << path
23
- location << "?#{@query}" if @query
24
- location
25
- end
26
-
27
8
  end
28
9
  end
29
10
  end
@@ -8,16 +8,19 @@ module WebSocket
8
8
 
9
9
  include Server
10
10
 
11
+ # @see WebSocket::Handshake::Base#valid?
11
12
  def valid?
12
13
  super && (@headers['sec-websocket-key'] ? true : (set_error(:invalid_handshake_authentication) and false))
13
14
  end
14
15
 
15
16
  private
16
17
 
18
+ # @see WebSocket::Handshake::Handler::Base#header_line
17
19
  def header_line
18
20
  "HTTP/1.1 101 Switching Protocols"
19
21
  end
20
22
 
23
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
21
24
  def handshake_keys
22
25
  [
23
26
  ["Upgrade", "websocket"],
@@ -26,6 +29,8 @@ module WebSocket
26
29
  ]
27
30
  end
28
31
 
32
+ # Signature of response, created from client request Sec-WebSocket-Key
33
+ # @return [String] signature
29
34
  def signature
30
35
  return unless key = @headers['sec-websocket-key']
31
36
  string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
@@ -7,16 +7,18 @@ module WebSocket
7
7
 
8
8
  private
9
9
 
10
+ # @see WebSocket::Handshake::Handler::Base#header_line
10
11
  def header_line
11
12
  "HTTP/1.1 101 Web Socket Protocol Handshake"
12
13
  end
13
14
 
15
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
14
16
  def handshake_keys
15
17
  [
16
18
  ["Upgrade", "WebSocket"],
17
19
  ["Connection", "Upgrade"],
18
20
  ["WebSocket-Origin", @headers['origin']],
19
- ["WebSocket-Location", handshake_location]
21
+ ["WebSocket-Location", uri]
20
22
  ]
21
23
  end
22
24
 
@@ -7,35 +7,42 @@ module WebSocket
7
7
 
8
8
  include Server
9
9
 
10
+ # @see WebSocket::Handshake::Base#valid?
10
11
  def valid?
11
12
  super && !!finishing_line
12
13
  end
13
14
 
14
15
  private
15
16
 
17
+ # @see WebSocket::Handshake::Base#reserved_leftover_lines
16
18
  def reserved_leftover_lines
17
19
  1
18
20
  end
19
21
 
22
+ # @see WebSocket::Handshake::Handler::Base#header_line
20
23
  def header_line
21
24
  "HTTP/1.1 101 WebSocket Protocol Handshake"
22
25
  end
23
26
 
27
+ # @see WebSocket::Handshake::Handler::Base#handshake_keys
24
28
  def handshake_keys
25
29
  [
26
30
  ["Upgrade", "WebSocket"],
27
31
  ["Connection", "Upgrade"],
28
32
  ["Sec-WebSocket-Origin", @headers['origin']],
29
- ["Sec-WebSocket-Location", handshake_location]
33
+ ["Sec-WebSocket-Location", uri]
30
34
  ]
31
35
  end
32
36
 
37
+ # @see WebSocket::Handshake::Handler::Base#finishing_line
33
38
  def finishing_line
34
39
  @finishing_line ||= challenge_response
35
40
  end
36
41
 
37
42
  private
38
43
 
44
+ # Response to client challenge from request Sec-WebSocket-Key1, Sec-WebSocket-Key2 and leftovers
45
+ # @return [String] Challenge response or nil if error occured
39
46
  def challenge_response
40
47
  # Refer to 5.2 4-9 of the draft 76
41
48
  first = numbers_over_spaces(@headers['sec-websocket-key1']) || return
@@ -48,6 +55,9 @@ module WebSocket
48
55
  Digest::MD5.digest(sum)
49
56
  end
50
57
 
58
+ # Calculate numbers over spaces, according to spec 5.2
59
+ # @param [String] string Key to parse
60
+ # @return [Integer] Result of calculations or nil if error occured
51
61
  def numbers_over_spaces(string)
52
62
  numbers = string.scan(/[0-9]/).join.to_i
53
63
 
@@ -1,12 +1,64 @@
1
1
  module WebSocket
2
2
  module Handshake
3
+ # Construct or parse a server WebSocket handshake.
4
+ #
5
+ # @example
6
+ # handshake = WebSocket::Handshake::Server.new
7
+ #
8
+ # # Parse client request
9
+ # @handshake << <<EOF
10
+ # GET /demo HTTP/1.1
11
+ # Upgrade: websocket
12
+ # Connection: Upgrade
13
+ # Host: example.com
14
+ # Sec-WebSocket-Origin: http://example.com
15
+ # Sec-WebSocket-Version: 17
16
+ # Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
17
+ #
18
+ # EOF
19
+ #
20
+ # # All data received?
21
+ # @handshake.finished?
22
+ #
23
+ # # No parsing errors?
24
+ # @handshake.valid?
25
+ #
26
+ # # Create response
27
+ # @handshake.to_s # HTTP/1.1 101 Switching Protocols
28
+ # # Upgrade: websocket
29
+ # # Connection: Upgrade
30
+ # # Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
31
+ #
3
32
  class Server < Base
4
33
 
34
+ # Initialize new WebSocket Server
35
+ #
36
+ # @param [Hash] args Arguments for server
37
+ #
38
+ # @option args [Boolean] :secure If true then server will use wss:// protocol
39
+ #
40
+ # @example
41
+ # Websocket::Handshake::Server.new(:secure => true)
5
42
  def initialize(args = {})
6
43
  super
7
44
  @secure = !!args[:secure]
8
45
  end
9
46
 
47
+ # Add text of request from Client. This method will parse content immediately and update version, state and error(if neccessary)
48
+ #
49
+ # @param [String] data Data to add
50
+ #
51
+ # @example
52
+ # @handshake << <<EOF
53
+ # GET /demo HTTP/1.1
54
+ # Upgrade: websocket
55
+ # Connection: Upgrade
56
+ # Host: example.com
57
+ # Sec-WebSocket-Origin: http://example.com
58
+ # Sec-WebSocket-Version: 13
59
+ # Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
60
+ #
61
+ # EOF
10
62
  def <<(data)
11
63
  @data << data
12
64
  if parse_data
@@ -14,12 +66,27 @@ module WebSocket
14
66
  end
15
67
  end
16
68
 
69
+ # Should send content to client after finished parsing?
70
+ # @return [Boolean] true
17
71
  def should_respond?
18
72
  true
19
73
  end
20
74
 
75
+ # Host of server according to client header
76
+ # @return [String] host
77
+ def host
78
+ @headers["host"].to_s.split(":")[0].to_s
79
+ end
80
+
81
+ # Port of server according to client header
82
+ # @return [String] port
83
+ def port
84
+ @headers["host"].to_s.split(":")[1]
85
+ end
86
+
21
87
  private
22
88
 
89
+ # Set version of protocol basing on client requets. AFter cotting method calls include_version.
23
90
  def set_version
24
91
  @version = @headers['sec-websocket-version'].to_i if @headers['sec-websocket-version']
25
92
  @version ||= @headers['sec-websocket-draft'].to_i if @headers['sec-websocket-draft']
@@ -28,6 +95,8 @@ module WebSocket
28
95
  include_version
29
96
  end
30
97
 
98
+ # Include set of methods for selected protocol version
99
+ # @return [Boolean] false if protocol number is unknown, otherwise true
31
100
  def include_version
32
101
  case @version
33
102
  when 75 then extend Handler::Server75
@@ -40,6 +109,9 @@ module WebSocket
40
109
 
41
110
  PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
42
111
 
112
+ # Parse first line of Client response.
113
+ # @param [String] line Line to parse
114
+ # @return [Boolean] True if parsed correctly. False otherwise
43
115
  def parse_first_line(line)
44
116
  line_parts = line.match(PATH)
45
117
  set_error(:invalid_header) and return unless line_parts
@@ -1,3 +1,3 @@
1
1
  module WebSocket
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
@@ -99,7 +99,7 @@ describe 'Incoming frame draft 03' do
99
99
  end
100
100
 
101
101
  context "should raise error with too long frame" do
102
- let(:encoded_text) { "\x04\x7F" + "a" * WebSocket::Frame::Handler::Base::MAX_FRAME_SIZE }
102
+ let(:encoded_text) { "\x04\x7F" + "a" * WebSocket.max_frame_size }
103
103
  let(:decoded_text) { nil }
104
104
  let(:error) { :frame_too_long }
105
105
 
@@ -99,7 +99,7 @@ describe 'Incoming frame draft 04' do
99
99
  end
100
100
 
101
101
  context "should raise error with too long frame" do
102
- let(:encoded_text) { "\x84\x7F" + "a" * WebSocket::Frame::Handler::Base::MAX_FRAME_SIZE }
102
+ let(:encoded_text) { "\x84\x7F" + "a" * WebSocket.max_frame_size }
103
103
  let(:decoded_text) { nil }
104
104
  let(:error) { :frame_too_long }
105
105
 
@@ -115,7 +115,7 @@ describe 'Incoming frame draft 05' do
115
115
  end
116
116
 
117
117
  context "should raise error with too long frame" do
118
- let(:encoded_text) { "\x84\x7F" + "a" * WebSocket::Frame::Handler::Base::MAX_FRAME_SIZE }
118
+ let(:encoded_text) { "\x84\x7F" + "a" * WebSocket.max_frame_size }
119
119
  let(:decoded_text) { nil }
120
120
  let(:error) { :frame_too_long }
121
121
 
@@ -115,7 +115,7 @@ describe 'Incoming frame draft 07' do
115
115
  end
116
116
 
117
117
  context "should raise error with too long frame" do
118
- let(:encoded_text) { "\x81\x7F" + "a" * WebSocket::Frame::Handler::Base::MAX_FRAME_SIZE }
118
+ let(:encoded_text) { "\x81\x7F" + "a" * WebSocket.max_frame_size }
119
119
  let(:decoded_text) { nil }
120
120
  let(:error) { :frame_too_long }
121
121
 
@@ -50,7 +50,7 @@ describe 'Incoming frame draft 75' do
50
50
  end
51
51
 
52
52
  context "with too long frame" do
53
- let(:encoded_text) { "\x00" + "a" * WebSocket::Frame::Handler::Base::MAX_FRAME_SIZE + "\xFF" }
53
+ let(:encoded_text) { "\x00" + "a" * WebSocket.max_frame_size + "\xFF" }
54
54
  let(:error) { :frame_too_long }
55
55
 
56
56
  it_should_behave_like('valid_incoming_frame') unless RUBY_PLATFORM == "java"
@@ -4,7 +4,7 @@ describe 'Client draft 76 handshake' do
4
4
  let(:handshake) { WebSocket::Handshake::Client.new({ :uri => 'ws://example.com/demo', :origin => 'http://example.com', :version => version }.merge(@request_params || {})) }
5
5
 
6
6
  let(:version) { 76 }
7
- let(:client_request) { client_handshake_76({ :key1 => handshake.key1, :key2 => handshake.key2, :key3 => handshake.key3 }.merge(@request_params || {})) }
7
+ let(:client_request) { client_handshake_76({ :key1 => handshake.send(:key1), :key2 => handshake.send(:key2), :key3 => handshake.send(:key3) }.merge(@request_params || {})) }
8
8
  let(:server_response) { server_handshake_76({ :challenge => handshake.send(:challenge) }.merge(@request_params || {})) }
9
9
 
10
10
  it_should_behave_like 'all client drafts'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: websocket
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -46,6 +46,7 @@ files:
46
46
  - examples/client.rb
47
47
  - examples/server.rb
48
48
  - examples/tests/autobahn_client.rb
49
+ - examples/tests/autobahn_server.rb
49
50
  - examples/tests/echo_client.rb
50
51
  - examples/tests/echo_server.rb
51
52
  - lib/websocket.rb
@@ -120,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
121
  version: '0'
121
122
  segments:
122
123
  - 0
123
- hash: -2471372616907526003
124
+ hash: 221385565863631789
124
125
  required_rubygems_version: !ruby/object:Gem::Requirement
125
126
  none: false
126
127
  requirements:
@@ -129,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
130
  version: '0'
130
131
  segments:
131
132
  - 0
132
- hash: -2471372616907526003
133
+ hash: 221385565863631789
133
134
  requirements: []
134
135
  rubyforge_project:
135
136
  rubygems_version: 1.8.24