websocket_parser 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -20,7 +20,7 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- ```
23
+ ```ruby
24
24
  require 'websocket_parser'
25
25
 
26
26
  socket = # Handle I/O with your server/event loop.
@@ -36,12 +36,17 @@ parser.on_error do |m|
36
36
  socket.close!
37
37
  end
38
38
 
39
- parser.on_close do |m|
40
- puts "Client closed connection. Reason: #{m}"
39
+ parser.on_close do |status, message|
40
+ # According to the spec the server must respond with another
41
+ # close message before closing the connection
42
+
43
+ socket << WebSocket::Message.close.to_data
41
44
  socket.close!
45
+
46
+ puts "Client closed connection. Status: #{status}. Reason: #{m}"
42
47
  end
43
48
 
44
- parser.on_ping do |m|
49
+ parser.on_ping do
45
50
  socket << WebSocket::Message.pong.to_data
46
51
  end
47
52
 
@@ -53,6 +58,10 @@ socket << WebSocket::Message.new('Hi there!').to_data
53
58
 
54
59
  ```
55
60
 
61
+ ## Status
62
+
63
+ Websocket Parser is still in early development phase.
64
+
56
65
  ## Contributing
57
66
 
58
67
  1. Fork it
@@ -3,6 +3,10 @@ require 'digest/sha1'
3
3
  module WebSocket
4
4
  class ClientHandshake < Http::Request
5
5
 
6
+ def self.accept_token_for(websocket_key)
7
+ Digest::SHA1.base64digest(websocket_key.strip + GUID)
8
+ end
9
+
6
10
  def initialize(method, uri, headers = {}, proxy = {}, body = nil, version = "1.1")
7
11
  @method = method.to_s.downcase.to_sym
8
12
  @uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
@@ -21,16 +25,10 @@ module WebSocket
21
25
  response_headers = {
22
26
  'Upgrade' => 'websocket',
23
27
  'Connection' => 'Upgrade',
24
- 'Sec-WebSocket-Accept' => accept_token
28
+ 'Sec-WebSocket-Accept' => ClientHandshake.accept_token_for(headers['Sec-WebSocket-Key'])
25
29
  }
26
30
 
27
31
  ServerHandshake.new(101, '1.1', response_headers)
28
32
  end
29
-
30
- private
31
-
32
- def accept_token
33
- Digest::SHA1.base64digest(headers['Sec-WebSocket-Key'].strip + GUID)
34
- end
35
33
  end
36
34
  end
@@ -1,34 +1,49 @@
1
1
  module WebSocket
2
2
  class Message
3
- attr_reader :type, :payload
3
+ attr_reader :type, :mask_key, :status_code, :payload, :status_message
4
4
 
5
5
  # Return a new ping message
6
- def self.ping(message = '')
7
- new(message, :ping)
6
+ def self.ping
7
+ new('', :ping)
8
8
  end
9
9
 
10
10
  # Return a new pong message
11
- def self.pong(message = '')
12
- new(message, :pong)
11
+ def self.pong
12
+ new('', :pong)
13
13
  end
14
14
 
15
15
  # Return a new close message
16
- def self.close(reason = '')
17
- new(reason, :close)
18
- end
16
+ def self.close(status_code = nil, reason = nil)
17
+ if status_code && STATUS_CODES[status_code] == nil
18
+ raise ArgumentError.new('Invalid status')
19
+ end
20
+
21
+ if reason && status_code == nil
22
+ raise ArgumentError.new("Can't set a status message without status code")
23
+ end
19
24
 
20
- def initialize(message, type = :text)
21
- @type, @payload = type, message.force_encoding("ASCII-8BIT")
25
+ new(reason, :close, status_code)
22
26
  end
23
27
 
24
- def first_byte
25
- @first_byte ||= if type == :continuation
26
- OPCODE_VALUES[type]
28
+ def initialize(message = '', type = :text, status_code = nil)
29
+ @type = type
30
+
31
+ @payload = if status_code
32
+ @status_code = status_code
33
+ @status_message = message
34
+
35
+ [status_code, message].pack('S<a*')
27
36
  else
28
- 0b10000000 | OPCODE_VALUES[type] # set FIN bit to true
37
+ message.force_encoding("ASCII-8BIT") if message
29
38
  end
30
39
  end
31
40
 
41
+ def mask!
42
+ @second_byte = second_byte | 0b10000000 # Set masked bit
43
+ @mask_key = Random.new.bytes(4)
44
+ @payload = WebSocket.mask(@payload, @mask_key)
45
+ end
46
+
32
47
  def message_size
33
48
  if payload_length < 126
34
49
  :small
@@ -39,36 +54,58 @@ module WebSocket
39
54
  end
40
55
  end
41
56
 
42
- def second_byte
43
- case message_size
44
- when :small then payload_length
45
- when :medium then 126
46
- when :large then 127
47
- end
57
+ def payload_length
58
+ @payload ? @payload.length : 0
48
59
  end
49
60
 
50
- def payload_length
51
- @payload.length
61
+ def masked?
62
+ second_byte & 0b10000000 != 0
52
63
  end
53
64
 
54
65
  def extended_payload_length
55
66
  message_size == :small ? nil : payload_length
56
67
  end
57
68
 
69
+ def to_data
70
+ to_a.pack(pack_format)
71
+ end
72
+
73
+ def write(io)
74
+ io << to_data
75
+ end
76
+
77
+ def control_frame?
78
+ [:close, :ping, :pong].include?(type)
79
+ end
80
+
81
+ def status
82
+ STATUS_CODES[status_code]
83
+ end
84
+
85
+ private
86
+
58
87
  def to_a
59
- [first_byte, second_byte, extended_payload_length, payload].compact
88
+ [first_byte, second_byte, extended_payload_length, mask_key, payload].compact
60
89
  end
61
90
 
62
91
  def pack_format
63
- "#{FRAME_FORMAT[message_size]}#{payload_length}"
92
+ WebSocket.frame_format(payload_length, masked?)
64
93
  end
65
94
 
66
- def to_data
67
- to_a.pack(pack_format)
95
+ def first_byte
96
+ @first_byte ||= if type == :continuation
97
+ OPCODE_VALUES[type]
98
+ else
99
+ 0b10000000 | OPCODE_VALUES[type] # set FIN bit to true
100
+ end
68
101
  end
69
102
 
70
- def write(io)
71
- io << to_data
103
+ def second_byte
104
+ @second_byte ||= case message_size
105
+ when :small then payload_length
106
+ when :medium then 126
107
+ when :large then 127
108
+ end
72
109
  end
73
110
  end
74
111
  end
@@ -36,15 +36,15 @@ module WebSocket
36
36
  puts "WebSocket error: #{error}"
37
37
  end
38
38
 
39
- @on_close = Proc.new do |reason|
40
- puts "Should close connection. Reason: #{reason}"
39
+ @on_close = Proc.new do |status, message|
40
+ puts "Should close connection. Status: #{status} Message: #{message}"
41
41
  end
42
42
 
43
- @on_ping = Proc.new do |ping|
43
+ @on_ping = Proc.new do
44
44
  puts "Ping received"
45
45
  end
46
46
 
47
- @on_pong = Proc.new do |pong|
47
+ @on_pong = Proc.new do
48
48
  puts "Pong received"
49
49
  end
50
50
 
@@ -76,6 +76,7 @@ module WebSocket
76
76
 
77
77
  read_header if @state == :header
78
78
  read_payload_length if @state == :payload_length
79
+ read_mask_key if @state == :mask
79
80
  read_payload if @state == :payload
80
81
 
81
82
  process_frame if @state == :complete
@@ -97,7 +98,9 @@ module WebSocket
97
98
  read_extended_payload_length
98
99
  end
99
100
 
100
- @state = :payload if @payload_length
101
+ return unless @payload_length
102
+
103
+ @state = masked? ? :mask : :payload
101
104
  end
102
105
 
103
106
  def read_extended_payload_length
@@ -108,11 +111,23 @@ module WebSocket
108
111
  end
109
112
  end
110
113
 
114
+ def read_mask_key
115
+ return unless @data.size >= 4
116
+
117
+ @mask_key = unpack_bytes(4,'a4')
118
+ @state = :payload
119
+ end
120
+
111
121
  def read_payload
112
122
  return unless @data.length >= @payload_length # Not enough data
113
123
 
114
124
  payload_data = unpack_bytes(@payload_length, "a#{@payload_length}")
115
- @payload = masked? ? unmask(payload_data) : payload_data
125
+
126
+ @payload = if masked?
127
+ WebSocket.unmask(payload_data, @mask_key)
128
+ else
129
+ payload_data
130
+ end
116
131
 
117
132
  @state = :complete if @payload
118
133
  end
@@ -121,10 +136,6 @@ module WebSocket
121
136
  @data.slice!(0,num).unpack(format).first
122
137
  end
123
138
 
124
- def completed_message?
125
- fin? || payload
126
- end
127
-
128
139
  def control_frame?
129
140
  [:close, :ping, :pong].include?(opcode)
130
141
  end
@@ -150,11 +161,14 @@ module WebSocket
150
161
  when :binary
151
162
  @on_message.call(@current_message)
152
163
  when :close
153
- @on_close.call(@current_message.encode("UTF-8"))
164
+ status_code, message = @current_message.unpack('S<a*')
165
+ status = STATUS_CODES[status_code]
166
+
167
+ @on_close.call(status, message)
154
168
  when :ping
155
- @on_ping.call(@current_message.encode("UTF-8"))
169
+ @on_ping.call
156
170
  when :pong
157
- @on_pong.call(@current_message.encode("UTF-8"))
171
+ @on_pong.call
158
172
  end
159
173
 
160
174
  @current_message = nil
@@ -166,18 +180,6 @@ module WebSocket
166
180
  @first_byte & 0b10000000 != 0
167
181
  end
168
182
 
169
- def svr1?
170
- @first_byte & 0b01000000 != 0
171
- end
172
-
173
- def svr2?
174
- @first_byte & 0b00100000 != 0
175
- end
176
-
177
- def svr3?
178
- @first_byte & 0b00010000 != 0
179
- end
180
-
181
183
  def opcode
182
184
  @opcode ||= OPCODES[@first_byte & 0b00001111]
183
185
  end
@@ -190,14 +192,6 @@ module WebSocket
190
192
  @second_byte & 0b01111111
191
193
  end
192
194
 
193
- def mask_key
194
- @mask_key ||= if masked?
195
- @second_byte && read_uint32!
196
- else
197
- nil
198
- end
199
- end
200
-
201
195
  def message_size
202
196
  if payload_length_field < 126
203
197
  :small
@@ -209,21 +203,9 @@ module WebSocket
209
203
  end
210
204
 
211
205
  def pack_format
212
- "#{FRAME_FORMAT[message_size]}#{actual_payload_length}"
213
- end
214
-
215
- def mask(data)
216
- masked_data = ''.encode!("ASCII-8BIT")
217
-
218
- data.each_byte.each_with_index do |byte, i|
219
- masked_data << byte ^ mask_key.chars[i%4]
220
- end
206
+ WebSocket.frame_format(actual_payload_length, masked?)
221
207
  end
222
208
 
223
- # The same algorithm applies regardless of the direction of the translation,
224
- # e.g., the same steps are applied to mask the data as to unmask the data.
225
- alias_method :unmask, :mask
226
-
227
209
  def reset_frame!
228
210
  @state = :header
229
211
 
@@ -1,9 +1,23 @@
1
1
  module WebSocket
2
2
  class ServerHandshake < Http::Response
3
3
 
4
- def initialize(status = nil, version = "1.1", headers = {}, body = nil, &body_proc)
4
+ def initialize(status = 101, version = "1.1", headers = {}, body = nil, &body_proc)
5
5
  @status, @version, @body, @body_proc = status, version, body, body_proc
6
6
  @headers = headers
7
7
  end
8
+
9
+ def render(out)
10
+ response_header = "#{@version} #{@status} #{@reason}#{CRLF}"
11
+
12
+ unless @headers.empty?
13
+ response_header << @headers.map do |header, value|
14
+ "#{header}: #{value}"
15
+ end.join(CRLF) << CRLF
16
+ end
17
+
18
+ response_header << CRLF
19
+
20
+ out << response_header
21
+ end
8
22
  end
9
23
  end
@@ -1,3 +1,3 @@
1
1
  module WebSocket
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -5,6 +5,8 @@ require "websocket/message"
5
5
  require "websocket/parser"
6
6
 
7
7
  module WebSocket
8
+ extend self
9
+
8
10
  PROTOCOL_VERSION = 13 # RFC 6455
9
11
  GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
10
12
 
@@ -27,9 +29,54 @@ module WebSocket
27
29
  :pong => 10
28
30
  }
29
31
 
30
- FRAME_FORMAT = {
31
- :small => 'CCa', # 2 bytes for header. N bytes for payload.
32
- :medium => 'CCS<a', # 2 bytes for header. 2 bytes for extended length. N bytes for payload.
33
- :large => 'CCQ<a' # 2 bytes for header. 4 bytes for extended length. N bytes for payload.
32
+ # See: http://tools.ietf.org/html/rfc6455#section-7.4.1
33
+ STATUS_CODES = {
34
+ 1000 => :normal_closure,
35
+ 1001 => :peer_going_away,
36
+ 1002 => :protocol_error,
37
+ 1003 => :data_error,
38
+ 1007 => :data_not_consistent,
39
+ 1008 => :policy_violation,
40
+ 1009 => :message_too_big,
41
+ 1010 => :extension_required,
42
+ 1011 => :unexpected_condition
34
43
  }
44
+
45
+ # Determines how to unpack the frame depending on
46
+ # the payload length and wether the frame is masked
47
+ def frame_format(payload_length, masked = false)
48
+ format = 'CC'
49
+
50
+ if payload_length > 65_535
51
+ format += 'Q<'
52
+ elsif payload_length > 125
53
+ format += 'S<'
54
+ end
55
+
56
+ if masked
57
+ format += 'a4'
58
+ end
59
+
60
+ if payload_length > 0
61
+ format += "a#{payload_length}"
62
+ end
63
+
64
+ format
65
+ end
66
+
67
+ def mask(data, mask_key)
68
+ masked_data = ''.encode!("ASCII-8BIT")
69
+ mask_bytes = mask_key.bytes.to_a
70
+
71
+ data.bytes.each_with_index do |byte, i|
72
+ masked_data << (byte ^ mask_bytes[i%4])
73
+ end
74
+
75
+ masked_data
76
+ end
77
+
78
+ # The same algorithm applies regardless of the direction of the translation,
79
+ # e.g., the same steps are applied to mask the data as to unmask the data.
80
+ alias_method :unmask, :mask
81
+
35
82
  end
@@ -17,4 +17,26 @@ describe WebSocket::Message do
17
17
  ext_length.should == text.length
18
18
  payload.should == text
19
19
  end
20
+
21
+ it "can be masked" do
22
+ message = WebSocket::Message.new('The man with the Iron Mask')
23
+ message.masked?.should be_false
24
+
25
+ message.mask!
26
+
27
+ message.masked?.should be_true
28
+ end
29
+
30
+ it "allows status codes for control frames" do
31
+ msg = WebSocket::Message.close(1001, 'Bye')
32
+
33
+ msg.status_code.should == 1001
34
+ msg.payload.should == [1001, 'Bye'].pack('S<a*')
35
+ msg.status.should == :peer_going_away
36
+ msg.status_message.should == 'Bye'
37
+ end
38
+
39
+ it "does not allow a status message without status code" do
40
+ expect{ WebSocket::Message.close(nil, 'Bye') }.to raise_error(ArgumentError)
41
+ end
20
42
  end
@@ -13,9 +13,9 @@ describe WebSocket::Parser do
13
13
 
14
14
  parser.on_message { |m| received_messages << m }
15
15
  parser.on_error { |m| received_errors << m }
16
- parser.on_close { |m| received_closes << m }
17
- parser.on_ping { |m| received_pings << m }
18
- parser.on_pong { |m| received_pongs << m }
16
+ parser.on_close { |status, message| received_closes << [status, message] }
17
+ parser.on_ping { received_pings << 'ping' }
18
+ parser.on_pong { received_pongs << 'pong' }
19
19
 
20
20
  parser
21
21
  end
@@ -93,20 +93,39 @@ describe WebSocket::Parser do
93
93
  end
94
94
 
95
95
  it "recognizes a ping message" do
96
- parser << WebSocket::Message.ping('Oh, hai!').to_data
96
+ parser << WebSocket::Message.ping.to_data
97
97
 
98
- received_pings.first.should == 'Oh, hai!'
98
+ received_pings.size.should == 1
99
99
  end
100
100
 
101
101
  it "recognizes a pong message" do
102
- parser << WebSocket::Message.pong('Hi there!').to_data
102
+ parser << WebSocket::Message.pong.to_data
103
103
 
104
- received_pongs.first.should == 'Hi there!'
104
+ received_pongs.size.should == 1
105
105
  end
106
106
 
107
- it "recognizes a close message" do
108
- parser << WebSocket::Message.close('Browser leaving page').to_data
107
+ it "recognizes a close message with status code and message" do
108
+ parser << WebSocket::Message.close(1001, 'Browser leaving page').to_data
109
109
 
110
- received_closes.first.should == 'Browser leaving page'
110
+ status, message = received_closes.first
111
+ status.should == :peer_going_away # Status code 1001
112
+ message.should == 'Browser leaving page'
113
+ end
114
+
115
+ it "recognizes a close message without status code" do
116
+ parser << WebSocket::Message.close.to_data
117
+
118
+ status, message = received_closes.first
119
+ status.should be_nil
120
+ message.should be_empty
121
+ end
122
+
123
+ it "recognizes a masked frame" do
124
+ msg = WebSocket::Message.new('Once upon a time')
125
+ msg.mask!
126
+
127
+ parser << msg.to_data
128
+
129
+ received_messages.first.should == 'Once upon a time'
111
130
  end
112
131
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: websocket_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -96,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
96
  version: '0'
97
97
  segments:
98
98
  - 0
99
- hash: 3979494630006513518
99
+ hash: -3359756691541156568
100
100
  required_rubygems_version: !ruby/object:Gem::Requirement
101
101
  none: false
102
102
  requirements:
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
105
  version: '0'
106
106
  segments:
107
107
  - 0
108
- hash: 3979494630006513518
108
+ hash: -3359756691541156568
109
109
  requirements: []
110
110
  rubyforge_project:
111
111
  rubygems_version: 1.8.23