mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
155 lines
3.8 KiB
Ruby
155 lines
3.8 KiB
Ruby
require "websocket/driver"
|
|
|
|
module ActionCable
|
|
module Connection
|
|
#--
|
|
# This class is heavily based on faye-websocket-ruby
|
|
#
|
|
# Copyright (c) 2010-2015 James Coglan
|
|
class ClientSocket # :nodoc:
|
|
def self.determine_url(env)
|
|
scheme = secure_request?(env) ? "wss:" : "ws:"
|
|
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
|
end
|
|
|
|
def self.secure_request?(env)
|
|
return true if env["HTTPS"] == "on"
|
|
return true if env["HTTP_X_FORWARDED_SSL"] == "on"
|
|
return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
|
|
return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
|
|
return true if env["rack.url_scheme"] == "https"
|
|
|
|
return false
|
|
end
|
|
|
|
CONNECTING = 0
|
|
OPEN = 1
|
|
CLOSING = 2
|
|
CLOSED = 3
|
|
|
|
attr_reader :env, :url
|
|
|
|
def initialize(env, event_target, event_loop, protocols)
|
|
@env = env
|
|
@event_target = event_target
|
|
@event_loop = event_loop
|
|
|
|
@url = ClientSocket.determine_url(@env)
|
|
|
|
@driver = @driver_started = nil
|
|
@close_params = ["", 1006]
|
|
|
|
@ready_state = CONNECTING
|
|
|
|
# The driver calls +env+, +url+, and +write+
|
|
@driver = ::WebSocket::Driver.rack(self, protocols: protocols)
|
|
|
|
@driver.on(:open) { |e| open }
|
|
@driver.on(:message) { |e| receive_message(e.data) }
|
|
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
|
@driver.on(:error) { |e| emit_error(e.message) }
|
|
|
|
@stream = ActionCable::Connection::Stream.new(@event_loop, self)
|
|
end
|
|
|
|
def start_driver
|
|
return if @driver.nil? || @driver_started
|
|
@stream.hijack_rack_socket
|
|
|
|
if callback = @env["async.callback"]
|
|
callback.call([101, {}, @stream])
|
|
end
|
|
|
|
@driver_started = true
|
|
@driver.start
|
|
end
|
|
|
|
def rack_response
|
|
start_driver
|
|
[ -1, {}, [] ]
|
|
end
|
|
|
|
def write(data)
|
|
@stream.write(data)
|
|
rescue => e
|
|
emit_error e.message
|
|
end
|
|
|
|
def transmit(message)
|
|
return false if @ready_state > OPEN
|
|
case message
|
|
when Numeric then @driver.text(message.to_s)
|
|
when String then @driver.text(message)
|
|
when Array then @driver.binary(message)
|
|
else false
|
|
end
|
|
end
|
|
|
|
def close(code = nil, reason = nil)
|
|
code ||= 1000
|
|
reason ||= ""
|
|
|
|
unless code == 1000 || (code >= 3000 && code <= 4999)
|
|
raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
|
|
"The code must be either 1000, or between 3000 and 4999. " +
|
|
"#{code} is neither."
|
|
end
|
|
|
|
@ready_state = CLOSING unless @ready_state == CLOSED
|
|
@driver.close(reason, code)
|
|
end
|
|
|
|
def parse(data)
|
|
@driver.parse(data)
|
|
end
|
|
|
|
def client_gone
|
|
finalize_close
|
|
end
|
|
|
|
def alive?
|
|
@ready_state == OPEN
|
|
end
|
|
|
|
def protocol
|
|
@driver.protocol
|
|
end
|
|
|
|
private
|
|
def open
|
|
return unless @ready_state == CONNECTING
|
|
@ready_state = OPEN
|
|
|
|
@event_target.on_open
|
|
end
|
|
|
|
def receive_message(data)
|
|
return unless @ready_state == OPEN
|
|
|
|
@event_target.on_message(data)
|
|
end
|
|
|
|
def emit_error(message)
|
|
return if @ready_state >= CLOSING
|
|
|
|
@event_target.on_error(message)
|
|
end
|
|
|
|
def begin_close(reason, code)
|
|
return if @ready_state == CLOSED
|
|
@ready_state = CLOSING
|
|
@close_params = [reason, code]
|
|
|
|
@stream.shutdown if @stream
|
|
finalize_close
|
|
end
|
|
|
|
def finalize_close
|
|
return if @ready_state == CLOSED
|
|
@ready_state = CLOSED
|
|
|
|
@event_target.on_close(*@close_params)
|
|
end
|
|
end
|
|
end
|
|
end
|