mirror of
https://github.com/faye/websocket-driver-ruby.git
synced 2025-11-01 13:59:38 +00:00
9ce857b3d4
When originally implemented, we still supported Ruby 1.8, which necessitated checking for encoding methods and using a regex to validate UTF-8. These checks are now gone. We tagged many strings as binary when not strictly necessary, either because we were just going to iterate their bytes or because we were going to hand them off to the caller which should just write them directly to a socket. Strings used as buffers to accumulate streaming input are still tagged as binary to avoid encoding collision/conversion. The places where we do need to tag as UTF-8 (i.e. just before emitting to the application) remain, but copy the string if necessary. This allows us to work with frozen strings. Finally, strings passed in via the Driver#text method should be *transcoded* to UTF-8 if necessary, not merely tagged. The Ruby String#encode method produces a new string so this should also be safe with frozen strings.
141 lines
3.9 KiB
Ruby
141 lines
3.9 KiB
Ruby
module WebSocket
|
|
class Driver
|
|
|
|
class Client < Hybi
|
|
VALID_SCHEMES = %w[ws wss]
|
|
|
|
def self.generate_key
|
|
Base64.strict_encode64(SecureRandom.random_bytes(16))
|
|
end
|
|
|
|
attr_reader :status, :headers
|
|
|
|
def initialize(socket, options = {})
|
|
super
|
|
|
|
@ready_state = -1
|
|
@key = Client.generate_key
|
|
@accept = Hybi.generate_accept(@key)
|
|
@http = HTTP::Response.new
|
|
|
|
uri = URI.parse(@socket.url)
|
|
unless VALID_SCHEMES.include?(uri.scheme)
|
|
raise URIError, "#{socket.url} is not a valid WebSocket URL"
|
|
end
|
|
|
|
host = uri.host + (uri.port ? ":#{uri.port}" : '')
|
|
path = (uri.path == '') ? '/' : uri.path
|
|
@pathname = path + (uri.query ? '?' + uri.query : '')
|
|
|
|
@headers['Host'] = host
|
|
@headers['Upgrade'] = 'websocket'
|
|
@headers['Connection'] = 'Upgrade'
|
|
@headers['Sec-WebSocket-Key'] = @key
|
|
@headers['Sec-WebSocket-Version'] = '13'
|
|
|
|
if @protocols.size > 0
|
|
@headers['Sec-WebSocket-Protocol'] = @protocols * ', '
|
|
end
|
|
|
|
if uri.user
|
|
auth = Base64.strict_encode64([uri.user, uri.password] * ':')
|
|
@headers['Authorization'] = 'Basic ' + auth
|
|
end
|
|
end
|
|
|
|
def version
|
|
'hybi-13'
|
|
end
|
|
|
|
def proxy(origin, options = {})
|
|
Proxy.new(self, origin, options)
|
|
end
|
|
|
|
def start
|
|
return false unless @ready_state == -1
|
|
@socket.write(handshake_request)
|
|
@ready_state = 0
|
|
true
|
|
end
|
|
|
|
def parse(chunk)
|
|
return if @ready_state == 3
|
|
return super if @ready_state > 0
|
|
|
|
@http.parse(chunk)
|
|
return fail_handshake('Invalid HTTP response') if @http.error?
|
|
return unless @http.complete?
|
|
|
|
validate_handshake
|
|
return if @ready_state == 3
|
|
|
|
open
|
|
parse(@http.body)
|
|
end
|
|
|
|
private
|
|
|
|
def handshake_request
|
|
extensions = @extensions.generate_offer
|
|
@headers['Sec-WebSocket-Extensions'] = extensions if extensions
|
|
|
|
start = "GET #{@pathname} HTTP/1.1"
|
|
headers = [start, @headers.to_s, '']
|
|
headers.join("\r\n")
|
|
end
|
|
|
|
def fail_handshake(message)
|
|
message = "Error during WebSocket handshake: #{message}"
|
|
@ready_state = 3
|
|
emit(:error, ProtocolError.new(message))
|
|
emit(:close, CloseEvent.new(ERRORS[:protocol_error], message))
|
|
end
|
|
|
|
def validate_handshake
|
|
@status = @http.code
|
|
@headers = Headers.new(@http.headers)
|
|
|
|
unless @http.code == 101
|
|
return fail_handshake("Unexpected response code: #{@http.code}")
|
|
end
|
|
|
|
upgrade = @http['Upgrade'] || ''
|
|
connection = @http['Connection'] || ''
|
|
accept = @http['Sec-WebSocket-Accept'] || ''
|
|
protocol = @http['Sec-WebSocket-Protocol'] || ''
|
|
|
|
if upgrade == ''
|
|
return fail_handshake("'Upgrade' header is missing")
|
|
elsif upgrade.downcase != 'websocket'
|
|
return fail_handshake("'Upgrade' header value is not 'WebSocket'")
|
|
end
|
|
|
|
if connection == ''
|
|
return fail_handshake("'Connection' header is missing")
|
|
elsif connection.downcase != 'upgrade'
|
|
return fail_handshake("'Connection' header value is not 'Upgrade'")
|
|
end
|
|
|
|
unless accept == @accept
|
|
return fail_handshake('Sec-WebSocket-Accept mismatch')
|
|
end
|
|
|
|
unless protocol == ''
|
|
if @protocols.include?(protocol)
|
|
@protocol = protocol
|
|
else
|
|
return fail_handshake('Sec-WebSocket-Protocol mismatch')
|
|
end
|
|
end
|
|
|
|
begin
|
|
@extensions.activate(@headers['Sec-WebSocket-Extensions'])
|
|
rescue ::WebSocket::Extensions::ExtensionError => error
|
|
return fail_handshake(error.message)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|