websocket_parser 0.0.1 → 0.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.
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