Files
pjaspers 202e1800e0 Allow rack.input.read to return a frozen string
We're seeing this occasionally in our error logs, where the `env`
passed into to the driver will return a frozen string when read.

The stack trace always includes ActionCable in to the mix, so
something like this:

      "/app/vendor/bundle/ruby/2.6.0/gems/websocket-driver-0.7.0/lib/websocket/driver/draft76.rb" line 11 in force_encoding
      "/app/vendor/bundle/ruby/2.6.0/gems/websocket-driver-0.7.0/lib/websocket/driver/draft76.rb" line 11 in initialize
      "/app/vendor/bundle/ruby/2.6.0/gems/websocket-driver-0.7.0/lib/websocket/driver.rb" line 162 in new
      "/app/vendor/bundle/ruby/2.6.0/gems/websocket-driver-0.7.0/lib/websocket/driver.rb" line 162 in rack
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/client_socket.rb" line 47 in initialize
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/web_socket.rb" line 10 in new
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/web_socket.rb" line 10 in initialize
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/base.rb" line 59 in new
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/base.rb" line 59 in initialize
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/server/base.rb" line 30 in new
      "/app/vendor/bundle/ruby/2.6.0/gems/actioncable-5.2.3/lib/action_cable/server/base.rb" line 30 in call

I still don't quite know why someone is sometimes returning a frozen
string (it could also have to with any of the current rack middleware
we're using), but I do know that we can handle it here.

So this adds a spec and some behavior to fix that.
2020-05-14 16:22:20 +01:00

100 lines
2.6 KiB
Ruby

module WebSocket
class Driver
class Draft76 < Draft75
BODY_SIZE = 8
def initialize(socket, options = {})
super
input = (@socket.env['rack.input'] || StringIO.new('')).read
input = input.dup if input.frozen?
@stage = -1
@body = input.force_encoding(BINARY)
@headers.clear
@headers['Upgrade'] = 'WebSocket'
@headers['Connection'] = 'Upgrade'
@headers['Sec-WebSocket-Origin'] = @socket.env['HTTP_ORIGIN']
@headers['Sec-WebSocket-Location'] = @socket.url
end
def version
'hixie-76'
end
def start
return false unless super
send_handshake_body
true
end
def close(reason = nil, code = nil)
return false if @ready_state == 3
@socket.write([0xFF, 0x00].pack('C*')) if @ready_state == 1
@ready_state = 3
emit(:close, CloseEvent.new(nil, nil))
true
end
private
def handshake_response
env = @socket.env
key1 = env['HTTP_SEC_WEBSOCKET_KEY1']
key2 = env['HTTP_SEC_WEBSOCKET_KEY2']
raise ProtocolError.new('Missing required header: Sec-WebSocket-Key1') unless key1
raise ProtocolError.new('Missing required header: Sec-WebSocket-Key2') unless key2
number1 = number_from_key(key1)
spaces1 = spaces_in_key(key1)
number2 = number_from_key(key2)
spaces2 = spaces_in_key(key2)
if number1 % spaces1 != 0 or number2 % spaces2 != 0
raise ProtocolError.new('Client sent invalid Sec-WebSocket-Key headers')
end
@key_values = [number1 / spaces1, number2 / spaces2]
start = 'HTTP/1.1 101 WebSocket Protocol Handshake'
headers = [start, @headers.to_s, '']
headers.join("\r\n")
end
def handshake_signature
return nil unless @body.bytesize >= BODY_SIZE
head = @body[0...BODY_SIZE]
Digest::MD5.digest((@key_values + [head]).pack('N2A*'))
end
def send_handshake_body
return unless signature = handshake_signature
@socket.write(signature)
@stage = 0
open
parse(@body[BODY_SIZE..-1]) if @body.bytesize > BODY_SIZE
end
def parse_leading_byte(octet)
return super unless octet == 0xFF
@closing = true
@length = 0
@stage = 1
end
def number_from_key(key)
number = key.scan(/[0-9]/).join('')
number == '' ? Float::NAN : number.to_i(10)
end
def spaces_in_key(key)
key.scan(/ /).size
end
end
end
end