1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
# frozen_string_literal: true
module WebSocket
module Handshake
# @abstract Subclass and override to implement custom handshakes
class Base
include ExceptionHandler
include NiceInspect
attr_reader :host, :path, :query,
:state, :version, :secure,
:headers, :protocols
# Initialize new WebSocket Handshake and set it's state to :new
def initialize(args = {})
args.each do |k, v|
value = begin
v.dup
rescue TypeError
v
end
instance_variable_set("@#{k}", value)
end
@state = :new
@handler = nil
@data = String.new('')
@headers ||= {}
@protocols ||= []
end
# @abstract Add data to handshake
def <<(data)
@data << data
end
# Return textual representation of handshake request or response
# @return [String] text of response
def to_s
@handler ? @handler.to_s : ''
end
rescue_method :to_s, return: ''
# Is parsing of data finished?
# @return [Boolena] True if request was completely parsed or error occured. False otherwise
def finished?
@state == :finished || @state == :error
end
# Is parsed data valid?
# @return [Boolean] False if some errors occured. Reason for error could be found in error method
def valid?
finished? && @error.nil? && @handler && @handler.valid?
end
rescue_method :valid?, return: false
# @abstract Should send data after parsing is finished?
def should_respond?
raise NotImplementedError
end
# Data left from parsing. Sometimes data that doesn't belong to handshake are added - use this method to retrieve them.
# @return [String] String if some data are available. Nil otherwise
def leftovers
(@leftovers.to_s.split("\n", reserved_leftover_lines + 1)[reserved_leftover_lines] || '').strip
end
# Return default port for protocol (80 for ws, 443 for wss)
def default_port
secure ? 443 : 80
end
# Check if provided port is a default one
def default_port?
port == default_port
end
def port
@port || default_port
end
# URI of request.
# @return [String] Full URI with protocol
# @example
# @handshake.uri #=> "ws://example.com/path?query=true"
def uri
uri = String.new(secure ? 'wss://' : 'ws://')
uri << host
uri << ":#{port}" unless default_port?
uri << path
uri << "?#{query}" if query
uri
end
private
# Number of lines after header that should be handled as belonging to handshake. Any data after those lines will be handled as leftovers.
# @return [Integer] Number of lines
def reserved_leftover_lines
0
end
# Changes state to error and sets error message
# @param [String] message Error message to set
def error=(message)
@state = :error
super
end
HEADER = /^([^:]+):\s*(.+)$/
# Parse data imported to handshake and sets state to finished if necessary.
# @return [Boolean] True if finished parsing. False if not all data received yet.
def parse_data
header, @leftovers = @data.split("\r\n\r\n", 2)
return false unless @leftovers # The whole header has not been received yet.
lines = header.split("\r\n")
first_line = lines.shift
parse_first_line(first_line)
lines.each do |line|
h = HEADER.match(line)
next unless h # Skip any invalid headers
key = h[1].strip.downcase
val = h[2].strip
# If the header is already set and refers to the websocket protocol, append the new value
if @headers.key?(key) && key =~ /^(sec-)?websocket-protocol$/
@headers[key] << ", #{val}"
else
@headers[key] = val
end
end
@state = :finished
true
end
end
end
end
|