1
0
mirror of https://github.com/bitwarden/help synced 2025-12-20 10:13:15 +00:00

Promote to Master (#748)

* initial commit

* adding quotes for the array error

* Create Gemfile

* Create Gemfile.lock

* add .nvmrc and .node-version

* removed /article from URL

* update links to work with netlify

* more fixed links

* link fixes

* update bad links

* Update netlify.toml

toml test for redirects

* article redirect

* link fixes

* Update index.html

* Update netlify.toml

* Update _config.yml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* add article back into URL for launch

* Update netlify.toml

* Update netlify.toml

* add order to categories front matter

* Update netlify.toml

* update

* sidemenu update

* Revert "sidemenu update"

This reverts commit 5441c3d35c.

* update order prop

* Navbar updates per Gary and compiler warnings

* font/style tweaks

* Update sidebar.html

* Stage Release Documentation (#739)

* initial drafts

* rewrite Custom Fields article to prioritize new context-menu option & better organize ancillary information

* edit

* edit

* Custom Field Context Menu & CAPTCHA item in release notes

* SSO relink event

* update rn

* small edits

* improve release notes titles

* fix side menu

* Edits courtest of mportune!

* update order

* link fixes

* link cleanup

* image updates and a link

* fix trailing slash

Co-authored-by: DanHillesheim <79476558+DanHillesheim@users.noreply.github.com>
This commit is contained in:
fred_the_tech_writer
2021-09-21 13:21:11 -04:00
committed by GitHub
parent 63f78e8979
commit 906e2ca0dd
3304 changed files with 386714 additions and 8864 deletions

View File

@@ -0,0 +1,14 @@
module EventMachine
module WebSocket
module Close03
def close_websocket(code, body)
# TODO: Ideally send body data and check that it matches in ack
send_frame(:close, '')
@state = :closing
start_close_timeout
end
def supports_close_codes?; false; end
end
end
end

View File

@@ -0,0 +1,14 @@
module EventMachine
module WebSocket
module Close05
def close_websocket(code, body)
# TODO: Ideally send body data and check that it matches in ack
send_frame(:close, "\x53")
@state = :closing
start_close_timeout
end
def supports_close_codes?; false; end
end
end
end

View File

@@ -0,0 +1,19 @@
module EventMachine
module WebSocket
module Close06
def close_websocket(code, body)
if code
close_data = [code].pack('n')
close_data << body if body
send_frame(:close, close_data)
else
send_frame(:close, '')
end
@state = :closing
start_close_timeout
end
def supports_close_codes?; true; end
end
end
end

View File

@@ -0,0 +1,11 @@
module EventMachine
module WebSocket
module Close75
def close_websocket(code, body)
@connection.close_connection_after_writing
end
def supports_close_codes?; false; end
end
end
end

View File

@@ -0,0 +1,349 @@
module EventMachine
module WebSocket
class Connection < EventMachine::Connection
include Debugger
attr_writer :max_frame_size
# define WebSocket callbacks
def onopen(&blk); @onopen = blk; end
def onclose(&blk); @onclose = blk; end
def onerror(&blk); @onerror = blk; end
def onmessage(&blk); @onmessage = blk; end
def onbinary(&blk); @onbinary = blk; end
def onping(&blk); @onping = blk; end
def onpong(&blk); @onpong = blk; end
def trigger_on_message(msg)
@onmessage.call(msg) if defined? @onmessage
end
def trigger_on_binary(msg)
@onbinary.call(msg) if defined? @onbinary
end
def trigger_on_open(handshake)
@onopen.call(handshake) if defined? @onopen
end
def trigger_on_close(event = {})
@onclose.call(event) if defined? @onclose
end
def trigger_on_ping(data)
@onping.call(data) if defined? @onping
end
def trigger_on_pong(data)
@onpong.call(data) if defined? @onpong
end
def trigger_on_error(reason)
return false unless defined? @onerror
@onerror.call(reason)
true
end
def initialize(options)
@options = options
@debug = options[:debug] || false
@secure = options[:secure] || false
@secure_proxy = options[:secure_proxy] || false
@tls_options = options[:tls_options] || {}
@close_timeout = options[:close_timeout]
@outbound_limit = options[:outbound_limit] || 0
@handler = nil
debug [:initialize]
end
# Use this method to close the websocket connection cleanly
# This sends a close frame and waits for acknowlegement before closing
# the connection
def close(code = nil, body = nil)
if code && !acceptable_close_code?(code)
raise "Application code may only use codes from 1000, 3000-4999"
end
close_websocket_private(code, body)
end
# Deprecated, to be removed in version 0.6
alias :close_websocket :close
def post_init
start_tls(@tls_options) if @secure
end
def receive_data(data)
debug [:receive_data, data]
if @handler
@handler.receive_data(data)
else
dispatch(data)
end
rescue => e
debug [:error, e]
# There is no code defined for application errors, so use 3000
# (which is reserved for frameworks)
close_websocket_private(3000, "Application error")
# These are application errors - raise unless onerror defined
trigger_on_error(e) || raise(e)
end
def send_data(data)
if @outbound_limit > 0 &&
get_outbound_data_size + data.bytesize > @outbound_limit
abort(:outbound_limit_reached)
return 0
end
super(data)
end
def unbind
debug [:unbind, :connection]
@handler.unbind if @handler
rescue => e
debug [:error, e]
# These are application errors - raise unless onerror defined
trigger_on_error(e) || raise(e)
end
def dispatch(data)
if data.match(%r|^GET /healthcheck|)
send_healthcheck_response
elsif data.match(/\A<policy-file-request\s*\/>/)
send_flash_cross_domain_file
else
@handshake ||= begin
handshake = Handshake.new(@secure || @secure_proxy)
handshake.callback { |upgrade_response, handler_klass|
debug [:accepting_ws_version, handshake.protocol_version]
debug [:upgrade_response, upgrade_response]
self.send_data(upgrade_response)
@handler = handler_klass.new(self, @debug)
@handshake = nil
trigger_on_open(handshake)
}
handshake.errback { |e|
debug [:error, e]
trigger_on_error(e)
# Handshake errors require the connection to be aborted
abort(:handshake_error)
}
handshake
end
@handshake.receive_data(data)
end
end
def send_healthcheck_response
debug [:healthcheck, 'OK']
healthcheck_res = ["HTTP/1.1 200 OK"]
healthcheck_res << "Content-Type: text/plain"
healthcheck_res << "Content-Length: 2"
healthcheck_res = healthcheck_res.join("\r\n") + "\r\n\r\nOK"
send_data healthcheck_res
# handle the healthcheck request transparently
# no need to notify the user about this connection
@onclose = nil
close_connection_after_writing
end
def send_flash_cross_domain_file
file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
debug [:cross_domain, file]
send_data file
# handle the cross-domain request transparently
# no need to notify the user about this connection
@onclose = nil
close_connection_after_writing
end
# Cache encodings since it's moderately expensive to look them up each time
ENCODING_SUPPORTED = "string".respond_to?(:force_encoding)
UTF8 = Encoding.find("UTF-8") if ENCODING_SUPPORTED
BINARY = Encoding.find("BINARY") if ENCODING_SUPPORTED
# Send a WebSocket text frame.
#
# A WebSocketError may be raised if the connection is in an opening or a
# closing state, or if the passed in data is not valid UTF-8
#
def send_text(data)
# If we're using Ruby 1.9, be pedantic about encodings
if ENCODING_SUPPORTED
# Also accept ascii only data in other encodings for convenience
unless (data.encoding == UTF8 && data.valid_encoding?) || data.ascii_only?
raise WebSocketError, "Data sent to WebSocket must be valid UTF-8 but was #{data.encoding} (valid: #{data.valid_encoding?})"
end
# This labels the encoding as binary so that it can be combined with
# the BINARY framing
data.force_encoding(BINARY)
else
# TODO: Check that data is valid UTF-8
end
if @handler
@handler.send_text_frame(data)
else
raise WebSocketError, "Cannot send data before onopen callback"
end
# Revert data back to the original encoding (which we assume is UTF-8)
# Doing this to avoid duping the string - there may be a better way
data.force_encoding(UTF8) if ENCODING_SUPPORTED
return nil
end
alias :send :send_text
# Send a WebSocket binary frame.
#
def send_binary(data)
if @handler
@handler.send_frame(:binary, data)
else
raise WebSocketError, "Cannot send binary before onopen callback"
end
end
# Send a ping to the client. The client must respond with a pong.
#
# In the case that the client is running a WebSocket draft < 01, false
# is returned since ping & pong are not supported
#
def ping(body = '')
if @handler
@handler.pingable? ? @handler.send_frame(:ping, body) && true : false
else
raise WebSocketError, "Cannot ping before onopen callback"
end
end
# Send an unsolicited pong message, as allowed by the protocol. The
# client is not expected to respond to this message.
#
# em-websocket automatically takes care of sending pong replies to
# incoming ping messages, as the protocol demands.
#
def pong(body = '')
if @handler
@handler.pingable? ? @handler.send_frame(:pong, body) && true : false
else
raise WebSocketError, "Cannot ping before onopen callback"
end
end
# Test whether the connection is pingable (i.e. the WebSocket draft in
# use is >= 01)
def pingable?
if @handler
@handler.pingable?
else
raise WebSocketError, "Cannot test whether pingable before onopen callback"
end
end
def supports_close_codes?
if @handler
@handler.supports_close_codes?
else
raise WebSocketError, "Cannot test before onopen callback"
end
end
def state
@handler ? @handler.state : :handshake
end
# Returns the IP address for the remote peer
def remote_ip
get_peername[2,6].unpack('nC4')[1..4].join('.')
end
# Returns the maximum frame size which this connection is configured to
# accept. This can be set globally or on a per connection basis, and
# defaults to a value of 10MB if not set.
#
# The behaviour when a too large frame is received varies by protocol,
# but in the newest protocols the connection will be closed with the
# correct close code (1009) immediately after receiving the frame header
#
def max_frame_size
defined?(@max_frame_size) ? @max_frame_size : WebSocket.max_frame_size
end
def close_timeout
@close_timeout || WebSocket.close_timeout
end
private
# As definited in draft 06 7.2.2, some failures require that the server
# abort the websocket connection rather than close cleanly
def abort(reason)
debug [:abort, reason]
close_connection
end
def close_websocket_private(code, body)
if @handler
debug [:closing, code]
@handler.close_websocket(code, body)
else
# The handshake hasn't completed - should be safe to terminate
abort(:handshake_incomplete)
end
end
# Allow applications to close with 1000, 1003, 1008, 1011, 3xxx or 4xxx.
#
# em-websocket uses a few other codes internally which should not be
# used by applications
#
# Browsers generally allow connections to be closed with code 1000,
# 3xxx, and 4xxx. em-websocket allows closing with a few other codes
# which seem reasonable (for discussion see
# https://github.com/igrigorik/em-websocket/issues/98)
#
# Usage from the rfc:
#
# 1000 indicates a normal closure
#
# 1003 indicates that an endpoint is terminating the connection
# because it has received a type of data it cannot accept
#
# 1008 indicates that an endpoint is terminating the connection because
# it has received a message that violates its policy
#
# 1011 indicates that a server is terminating the connection because it
# encountered an unexpected condition that prevented it from fulfilling
# the request
#
# Status codes in the range 3000-3999 are reserved for use by libraries,
# frameworks, and applications
#
# Status codes in the range 4000-4999 are reserved for private use and
# thus can't be registered
#
def acceptable_close_code?(code)
case code
when 1000, 1003, 1008, 1011, (3000..4999)
true
else
false
end
end
end
end
end

View File

@@ -0,0 +1,17 @@
module EventMachine
module WebSocket
module Debugger
private
def debug(*data)
if @debug
require 'pp'
pp data
puts
end
end
end
end
end

View File

@@ -0,0 +1,162 @@
# encoding: BINARY
module EventMachine
module WebSocket
module Framing03
def initialize_framing
@data = ''
@application_data_buffer = '' # Used for MORE frames
@frame_type = nil
end
def process_data
error = false
while !error && @data.size > 1
pointer = 0
more = ((@data.getbyte(pointer) & 0b10000000) == 0b10000000) ^ fin
# Ignoring rsv1-3 for now
opcode = @data.getbyte(0) & 0b00001111
pointer += 1
# Ignoring rsv4
length = @data.getbyte(pointer) & 0b01111111
pointer += 1
payload_length = case length
when 127 # Length defined by 8 bytes
# Check buffer size
if @data.getbyte(pointer+8-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Only using the last 4 bytes for now, till I work out how to
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
l = @data[(pointer+4)..(pointer+7)].unpack('N').first
pointer += 8
l
when 126 # Length defined by 2 bytes
# Check buffer size
if @data.getbyte(pointer+2-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
l = @data[pointer..(pointer+1)].unpack('n').first
pointer += 2
l
else
length
end
if payload_length > @connection.max_frame_size
raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)"
end
# Check buffer size
if @data.getbyte(pointer+payload_length-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Throw away data up to pointer
@data.slice!(0...pointer)
# Read application data
application_data = @data.slice!(0...payload_length)
frame_type = opcode_to_type(opcode)
if frame_type == :continuation && !@frame_type
raise WSProtocolError, 'Continuation frame not expected'
end
if more
debug [:moreframe, frame_type, application_data]
@application_data_buffer << application_data
# The message type is passed in the first frame
@frame_type ||= frame_type
else
# Message is complete
if frame_type == :continuation
@application_data_buffer << application_data
message(@frame_type, '', @application_data_buffer)
@application_data_buffer = ''
@frame_type = nil
else
message(frame_type, '', application_data)
end
end
end # end while
end
def send_frame(frame_type, application_data)
debug [:sending_frame, frame_type, application_data]
if @state == :closing && data_frame?(frame_type)
raise WebSocketError, "Cannot send data frame since connection is closing"
end
frame = ''
opcode = type_to_opcode(frame_type)
byte1 = opcode # since more, rsv1-3 are 0
frame << byte1
length = application_data.size
if length <= 125
byte2 = length # since rsv4 is 0
frame << byte2
elsif length < 65536 # write 2 byte length
frame << 126
frame << [length].pack('n')
else # write 8 byte length
frame << 127
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
end
frame << application_data
@connection.send_data(frame)
end
def send_text_frame(data)
send_frame(:text, data)
end
private
# This allows flipping the more bit to fin for draft 04
def fin; false; end
FRAME_TYPES = {
:continuation => 0,
:close => 1,
:ping => 2,
:pong => 3,
:text => 4,
:binary => 5
}
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
# Frames are either data frames or control frames
DATA_FRAMES = [:text, :binary, :continuation]
def type_to_opcode(frame_type)
FRAME_TYPES[frame_type] || raise("Unknown frame type")
end
def opcode_to_type(opcode)
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
end
def data_frame?(type)
DATA_FRAMES.include?(type)
end
end
end
end

View File

@@ -0,0 +1,15 @@
# encoding: BINARY
module EventMachine
module WebSocket
# The only difference between draft 03 framing and draft 04 framing is
# that the MORE bit has been changed to a FIN bit
module Framing04
include Framing03
private
def fin; true; end
end
end
end

View File

@@ -0,0 +1,163 @@
# encoding: BINARY
module EventMachine
module WebSocket
module Framing05
def initialize_framing
@data = MaskedString.new
@application_data_buffer = '' # Used for MORE frames
@frame_type = nil
end
def process_data
error = false
while !error && @data.size > 5 # mask plus first byte present
pointer = 0
@data.read_mask
pointer += 4
fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
# Ignoring rsv1-3 for now
opcode = @data.getbyte(pointer) & 0b00001111
pointer += 1
# Ignoring rsv4
length = @data.getbyte(pointer) & 0b01111111
pointer += 1
payload_length = case length
when 127 # Length defined by 8 bytes
# Check buffer size
if @data.getbyte(pointer+8-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Only using the last 4 bytes for now, till I work out how to
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
l = @data.getbytes(pointer+4, 4).unpack('N').first
pointer += 8
l
when 126 # Length defined by 2 bytes
# Check buffer size
if @data.getbyte(pointer+2-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
l = @data.getbytes(pointer, 2).unpack('n').first
pointer += 2
l
else
length
end
if payload_length > @connection.max_frame_size
raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)"
end
# Check buffer size
if @data.getbyte(pointer+payload_length-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Read application data
application_data = @data.getbytes(pointer, payload_length)
pointer += payload_length
# Throw away data up to pointer
@data.unset_mask
@data.slice!(0...pointer)
frame_type = opcode_to_type(opcode)
if frame_type == :continuation && !@frame_type
raise WSProtocolError, 'Continuation frame not expected'
end
if !fin
debug [:moreframe, frame_type, application_data]
@application_data_buffer << application_data
@frame_type = frame_type
else
# Message is complete
if frame_type == :continuation
@application_data_buffer << application_data
message(@frame_type, '', @application_data_buffer)
@application_data_buffer = ''
@frame_type = nil
else
message(frame_type, '', application_data)
end
end
end # end while
end
def send_frame(frame_type, application_data)
debug [:sending_frame, frame_type, application_data]
if @state == :closing && data_frame?(frame_type)
raise WebSocketError, "Cannot send data frame since connection is closing"
end
frame = ''
opcode = type_to_opcode(frame_type)
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
frame << byte1
length = application_data.size
if length <= 125
byte2 = length # since rsv4 is 0
frame << byte2
elsif length < 65536 # write 2 byte length
frame << 126
frame << [length].pack('n')
else # write 8 byte length
frame << 127
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
end
frame << application_data
@connection.send_data(frame)
end
def send_text_frame(data)
send_frame(:text, data)
end
private
FRAME_TYPES = {
:continuation => 0,
:close => 1,
:ping => 2,
:pong => 3,
:text => 4,
:binary => 5
}
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
# Frames are either data frames or control frames
DATA_FRAMES = [:text, :binary, :continuation]
def type_to_opcode(frame_type)
FRAME_TYPES[frame_type] || raise("Unknown frame type")
end
def opcode_to_type(opcode)
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
end
def data_frame?(type)
DATA_FRAMES.include?(type)
end
end
end
end

View File

@@ -0,0 +1,185 @@
# encoding: BINARY
module EventMachine
module WebSocket
module Framing07
def initialize_framing
@data = MaskedString.new
@application_data_buffer = '' # Used for MORE frames
@frame_type = nil
end
def process_data
error = false
while !error && @data.size >= 2
pointer = 0
fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
# Ignoring rsv1-3 for now
opcode = @data.getbyte(pointer) & 0b00001111
pointer += 1
mask = (@data.getbyte(pointer) & 0b10000000) == 0b10000000
length = @data.getbyte(pointer) & 0b01111111
pointer += 1
# raise WebSocketError, 'Data from client must be masked' unless mask
payload_length = case length
when 127 # Length defined by 8 bytes
# Check buffer size
if @data.getbyte(pointer+8-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Only using the last 4 bytes for now, till I work out how to
# unpack 8 bytes. I'm sure 4GB frames will do for now :)
l = @data.getbytes(pointer+4, 4).unpack('N').first
pointer += 8
l
when 126 # Length defined by 2 bytes
# Check buffer size
if @data.getbyte(pointer+2-1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
l = @data.getbytes(pointer, 2).unpack('n').first
pointer += 2
l
else
length
end
# Compute the expected frame length
frame_length = pointer + payload_length
frame_length += 4 if mask
if frame_length > @connection.max_frame_size
raise WSMessageTooBigError, "Frame length too long (#{frame_length} bytes)"
end
# Check buffer size
if @data.getbyte(frame_length - 1) == nil
debug [:buffer_incomplete, @data]
error = true
next
end
# Remove frame header
@data.slice!(0...pointer)
pointer = 0
# Read application data (unmasked if required)
@data.read_mask if mask
pointer += 4 if mask
application_data = @data.getbytes(pointer, payload_length)
pointer += payload_length
@data.unset_mask if mask
# Throw away data up to pointer
@data.slice!(0...pointer)
frame_type = opcode_to_type(opcode)
if frame_type == :continuation
if !@frame_type
raise WSProtocolError, 'Continuation frame not expected'
end
else # Not a continuation frame
if @frame_type && data_frame?(frame_type)
raise WSProtocolError, "Continuation frame expected"
end
end
# Validate that control frames are not fragmented
if !fin && !data_frame?(frame_type)
raise WSProtocolError, 'Control frames must not be fragmented'
end
if !fin
debug [:moreframe, frame_type, application_data]
@application_data_buffer << application_data
# The message type is passed in the first frame
@frame_type ||= frame_type
else
# Message is complete
if frame_type == :continuation
@application_data_buffer << application_data
message(@frame_type, '', @application_data_buffer)
@application_data_buffer = ''
@frame_type = nil
else
message(frame_type, '', application_data)
end
end
end # end while
end
def send_frame(frame_type, application_data)
debug [:sending_frame, frame_type, application_data]
if @state == :closing && data_frame?(frame_type)
raise WebSocketError, "Cannot send data frame since connection is closing"
end
frame = ''
opcode = type_to_opcode(frame_type)
byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
frame << byte1
length = application_data.size
if length <= 125
byte2 = length # since rsv4 is 0
frame << byte2
elsif length < 65536 # write 2 byte length
frame << 126
frame << [length].pack('n')
else # write 8 byte length
frame << 127
frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
end
frame << application_data
@connection.send_data(frame)
end
def send_text_frame(data)
send_frame(:text, data)
end
private
FRAME_TYPES = {
:continuation => 0,
:text => 1,
:binary => 2,
:close => 8,
:ping => 9,
:pong => 10,
}
FRAME_TYPES_INVERSE = FRAME_TYPES.invert
# Frames are either data frames or control frames
DATA_FRAMES = [:text, :binary, :continuation]
def type_to_opcode(frame_type)
FRAME_TYPES[frame_type] || raise("Unknown frame type")
end
def opcode_to_type(opcode)
FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
end
def data_frame?(type)
DATA_FRAMES.include?(type)
end
end
end
end

View File

@@ -0,0 +1,105 @@
# encoding: BINARY
module EventMachine
module WebSocket
module Framing76
def initialize_framing
@data = ''
end
def process_data
debug [:message, @data]
# This algorithm comes straight from the spec
# http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-5.3
error = false
while !error
return if @data.size == 0
pointer = 0
frame_type = @data.getbyte(pointer)
pointer += 1
if (frame_type & 0x80) == 0x80
# If the high-order bit of the /frame type/ byte is set
length = 0
loop do
return false if !@data.getbyte(pointer)
b = @data.getbyte(pointer)
pointer += 1
b_v = b & 0x7F
length = length * 128 + b_v
break unless (b & 0x80) == 0x80
end
if length > @connection.max_frame_size
raise WSMessageTooBigError, "Frame length too long (#{length} bytes)"
end
if @data.getbyte(pointer+length-1) == nil
debug [:buffer_incomplete, @data]
# Incomplete data - leave @data to accumulate
error = true
else
# Straight from spec - I'm sure this isn't crazy...
# 6. Read /length/ bytes.
# 7. Discard the read bytes.
@data = @data[(pointer+length)..-1]
# If the /frame type/ is 0xFF and the /length/ was 0, then close
if length == 0
@connection.send_data("\xff\x00")
@state = :closing
@connection.close_connection_after_writing
else
error = true
end
end
else
# If the high-order bit of the /frame type/ byte is _not_ set
if @data.getbyte(0) != 0x00
# Close the connection since this buffer can never match
raise WSProtocolError, "Invalid frame received"
end
# Addition to the spec to protect against malicious requests
if @data.size > @connection.max_frame_size
raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
end
msg = @data.slice!(/\A\x00[^\xff]*\xff/)
if msg
msg.gsub!(/\A\x00|\xff\z/, '')
if @state == :closing
debug [:ignored_message, msg]
else
msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
@connection.trigger_on_message(msg)
end
else
error = true
end
end
end
false
end
# frames need to start with 0x00-0x7f byte and end with
# an 0xFF byte. Per spec, we can also set the first
# byte to a value betweent 0x80 and 0xFF, followed by
# a leading length indicator
def send_text_frame(data)
debug [:sending_text_frame, data]
ary = ["\x00", data, "\xff"]
ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
@connection.send_data(ary.join)
end
end
end
end

View File

@@ -0,0 +1,97 @@
module EventMachine
module WebSocket
class Handler
def self.klass_factory(version)
case version
when 75
Handler75
when 76
Handler76
when 1..3
# We'll use handler03 - I believe they're all compatible
Handler03
when 5
Handler05
when 6
Handler06
when 7
Handler07
when 8
# drafts 9, 10, 11 and 12 should never change the version
# number as they are all the same as version 08.
Handler08
when 13
# drafts 13 to 17 all identify as version 13 as they are
# only minor changes or text changes.
Handler13
else
# According to spec should abort the connection
raise HandshakeError, "Protocol version #{version} not supported"
end
end
include Debugger
attr_reader :request, :state
def initialize(connection, debug = false)
@connection = connection
@debug = debug
@state = :connected
@close_timer = nil
initialize_framing
end
def receive_data(data)
@data << data
process_data
rescue WSProtocolError => e
fail_websocket(e)
end
def close_websocket(code, body)
# Implemented in subclass
end
# Used to avoid un-acked and unclosed remaining open indefinitely
def start_close_timeout
@close_timer = EM::Timer.new(@connection.close_timeout) {
@connection.close_connection
e = WSProtocolError.new("Close handshake un-acked after #{@connection.close_timeout}s, closing tcp connection")
@connection.trigger_on_error(e)
}
end
# This corresponds to "Fail the WebSocket Connection" in the spec.
def fail_websocket(e)
debug [:error, e]
close_websocket(e.code, e.message)
@connection.close_connection_after_writing
@connection.trigger_on_error(e)
end
def unbind
@state = :closed
@close_timer.cancel if @close_timer
@close_info = defined?(@close_info) ? @close_info : {
:code => 1006,
:was_clean => false,
}
@connection.trigger_on_close(@close_info)
end
def ping
# Overridden in subclass
false
end
def pingable?
# Also Overridden
false
end
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler03 < Handler
include Framing03
include MessageProcessor03
include Close03
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler05 < Handler
include Framing05
include MessageProcessor03
include Close05
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler06 < Handler
include Framing05
include MessageProcessor06
include Close06
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler07 < Handler
include Framing07
include MessageProcessor06
include Close06
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler08 < Handler
include Framing07
include MessageProcessor06
include Close06
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler13 < Handler
include Framing07
include MessageProcessor06
include Close06
end
end
end

View File

@@ -0,0 +1,9 @@
module EventMachine
module WebSocket
class Handler75 < Handler
include Handshake75
include Framing76
include Close75
end
end
end

View File

@@ -0,0 +1,14 @@
# encoding: BINARY
module EventMachine
module WebSocket
class Handler76 < Handler
include Handshake76
include Framing76
include Close75
# "\377\000" is octet version and "\xff\x00" is hex version
TERMINATE_STRING = "\xff\x00"
end
end
end

View File

@@ -0,0 +1,156 @@
require "http/parser"
require "uri"
module EventMachine
module WebSocket
# Resposible for creating the server handshake response
class Handshake
include EM::Deferrable
attr_reader :parser, :protocol_version
# Unfortunately drafts 75 & 76 require knowledge of whether the
# connection is being terminated as ws/wss in order to generate the
# correct handshake response
def initialize(secure)
@parser = Http::Parser.new
@secure = secure
@parser.on_headers_complete = proc { |headers|
@headers = Hash[headers.map { |k,v| [k.downcase, v] }]
}
end
def receive_data(data)
@parser << data
if defined? @headers
process(@headers, @parser.upgrade_data)
end
rescue HTTP::Parser::Error => e
fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
end
# Returns the WebSocket upgrade headers as a hash.
#
# Keys are strings, unmodified from the request.
#
def headers
@parser.headers
end
# The same as headers, except that the hash keys are downcased
#
def headers_downcased
@headers
end
# Returns the request path (excluding any query params)
#
def path
@path
end
# Returns the query params as a string foo=bar&baz=...
def query_string
@query_string
end
def query
Hash[query_string.split('&').map { |c| c.split('=', 2) }]
end
# Returns the WebSocket origin header if provided
#
def origin
@headers["origin"] || @headers["sec-websocket-origin"] || nil
end
def secure?
@secure
end
private
def process(headers, remains)
unless @parser.http_method == "GET"
raise HandshakeError, "Must be GET request"
end
# Validate request path
#
# According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
# invalid Request-URI should result in a 400 status code, but
# HandshakeError's currently result in a WebSocket abort. It's not
# clear which should take precedence, but an abort will do just fine.
begin
uri = URI.parse(@parser.request_url)
@path = uri.path
@query_string = uri.query || ""
rescue URI::InvalidURIError
raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
end
# Validate Upgrade
unless @parser.upgrade?
raise HandshakeError, "Not an upgrade request"
end
upgrade = @headers['upgrade']
unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
end
# Determine version heuristically
version = if @headers['sec-websocket-version']
# Used from drafts 04 onwards
@headers['sec-websocket-version'].to_i
elsif @headers['sec-websocket-draft']
# Used in drafts 01 - 03
@headers['sec-websocket-draft'].to_i
elsif @headers['sec-websocket-key1']
76
else
75
end
# Additional handling of bytes after the header if required
case version
when 75
if !remains.empty?
raise HandshakeError, "Extra bytes after header"
end
when 76, 1..3
if remains.length < 8
# The whole third-key has not been received yet.
return nil
elsif remains.length > 8
raise HandshakeError, "Extra bytes after third key"
end
@headers['third-key'] = remains
end
handshake_klass = case version
when 75
Handshake75
when 76, 1..3
Handshake76
when 5, 6, 7, 8, 13
Handshake04
else
# According to spec should abort the connection
raise HandshakeError, "Protocol version #{version} not supported"
end
upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
handler_klass = Handler.klass_factory(version)
@protocol_version = version
succeed(upgrade_response, handler_klass)
rescue HandshakeError => e
fail(e)
end
end
end
end

View File

@@ -0,0 +1,37 @@
require 'digest/sha1'
require 'base64'
module EventMachine
module WebSocket
module Handshake04
def self.handshake(headers, _, __)
# Required
unless key = headers['sec-websocket-key']
raise HandshakeError, "sec-websocket-key header is required"
end
string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
upgrade = ["HTTP/1.1 101 Switching Protocols"]
upgrade << "Upgrade: websocket"
upgrade << "Connection: Upgrade"
upgrade << "Sec-WebSocket-Accept: #{signature}"
if protocol = headers['sec-websocket-protocol']
validate_protocol!(protocol)
upgrade << "Sec-WebSocket-Protocol: #{protocol}"
end
# TODO: Support sec-websocket-protocol selection
# TODO: sec-websocket-extensions
return upgrade.join("\r\n") + "\r\n\r\n"
end
def self.validate_protocol!(protocol)
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
# TODO: Validate characters
end
end
end
end

View File

@@ -0,0 +1,28 @@
module EventMachine
module WebSocket
module Handshake75
def self.handshake(headers, path, secure)
scheme = (secure ? "wss" : "ws")
location = "#{scheme}://#{headers['host']}#{path}"
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
upgrade << "Upgrade: WebSocket\r\n"
upgrade << "Connection: Upgrade\r\n"
upgrade << "WebSocket-Origin: #{headers['origin']}\r\n"
upgrade << "WebSocket-Location: #{location}\r\n"
if protocol = headers['sec-websocket-protocol']
validate_protocol!(protocol)
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
end
upgrade << "\r\n"
return upgrade
end
def self.validate_protocol!(protocol)
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
# TODO: Validate characters
end
end
end
end

View File

@@ -0,0 +1,72 @@
require 'digest/md5'
module EventMachine::WebSocket
module Handshake76
class << self
def handshake(headers, path, secure)
challenge_response = solve_challenge(
headers['sec-websocket-key1'],
headers['sec-websocket-key2'],
headers['third-key']
)
scheme = (secure ? "wss" : "ws")
location = "#{scheme}://#{headers['host']}#{path}"
upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
upgrade << "Upgrade: WebSocket\r\n"
upgrade << "Connection: Upgrade\r\n"
upgrade << "Sec-WebSocket-Location: #{location}\r\n"
upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n"
if protocol = headers['sec-websocket-protocol']
validate_protocol!(protocol)
upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
end
upgrade << "\r\n"
upgrade << challenge_response
return upgrade
end
private
def solve_challenge(first, second, third)
# Refer to 5.2 4-9 of the draft 76
sum = [numbers_over_spaces(first)].pack("N*") +
[numbers_over_spaces(second)].pack("N*") +
third
Digest::MD5.digest(sum)
end
def numbers_over_spaces(string)
unless string
raise HandshakeError, "WebSocket key1 or key2 is missing"
end
numbers = string.scan(/[0-9]/).join.to_i
spaces = string.scan(/ /).size
# As per 5.2.5, abort the connection if spaces are zero.
raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
# As per 5.2.6, abort if numbers is not an integral multiple of spaces
if numbers % spaces != 0
raise HandshakeError, "Invalid Key #{string.inspect}"
end
quotient = numbers / spaces
if quotient > 2**32-1
raise HandshakeError, "Challenge computation out of range for key #{string.inspect}"
end
return quotient
end
def validate_protocol!(protocol)
raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
# TODO: Validate characters
end
end
end
end

View File

@@ -0,0 +1,37 @@
module EventMachine
module WebSocket
class MaskedString < String
# Read a 4 bit XOR mask - further requested bytes will be unmasked
def read_mask
if respond_to?(:encoding) && encoding.name != "ASCII-8BIT"
raise "MaskedString only operates on BINARY strings"
end
raise "Too short" if bytesize < 4 # TODO - change
@masking_key = String.new(self[0..3])
end
# Removes the mask, behaves like a normal string again
def unset_mask
@masking_key = nil
end
def getbyte(index)
if defined?(@masking_key) && @masking_key
masked_char = super
masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
else
super
end
end
def getbytes(start_index, count)
data = ''
data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
count.times do |i|
data << getbyte(start_index + i)
end
data
end
end
end
end

View File

@@ -0,0 +1,47 @@
# encoding: BINARY
module EventMachine
module WebSocket
module MessageProcessor03
def message(message_type, extension_data, application_data)
case message_type
when :close
@close_info = {
:code => 1005,
:reason => "",
:was_clean => true,
}
if @state == :closing
# TODO: Check that message body matches sent data
# We can close connection immediately since there is no more data
# is allowed to be sent or received on this connection
@connection.close_connection
else
# Acknowlege close
# The connection is considered closed
send_frame(:close, application_data)
@connection.close_connection_after_writing
end
when :ping
# Pong back the same data
send_frame(:pong, application_data)
@connection.trigger_on_ping(application_data)
when :pong
@connection.trigger_on_pong(application_data)
when :text
if application_data.respond_to?(:force_encoding)
application_data.force_encoding("UTF-8")
end
@connection.trigger_on_message(application_data)
when :binary
@connection.trigger_on_binary(application_data)
end
end
# Ping & Pong supported
def pingable?
true
end
end
end
end

View File

@@ -0,0 +1,78 @@
module EventMachine
module WebSocket
module MessageProcessor06
def message(message_type, extension_data, application_data)
debug [:message_received, message_type, application_data]
case message_type
when :close
status_code = case application_data.length
when 0
# close messages MAY contain a body
nil
when 1
# Illegal close frame
raise WSProtocolError, "Close frames with a body must contain a 2 byte status code"
else
application_data.slice!(0, 2).unpack('n').first
end
debug [:close_frame_received, status_code, application_data]
@close_info = {
:code => status_code || 1005,
:reason => application_data,
:was_clean => true,
}
if @state == :closing
# We can close connection immediately since no more data may be
# sent or received on this connection
@connection.close_connection
elsif @state == :connected
# Acknowlege close & echo status back to client
# The connection is considered closed
close_data = [status_code || 1000].pack('n')
send_frame(:close, close_data)
@connection.close_connection_after_writing
end
when :ping
# There are a couple of protections here against malicious/broken WebSocket abusing ping frames.
#
# 1. Delay 200ms before replying. This reduces the number of pings from WebSocket clients behaving as
# `for (;;) { send_ping(conn); rcv_pong(conn); }`. The spec says we "SHOULD respond with Pong frame as soon
# as is practical".
# 2. Reply at most every 200ms. This reduces the number of pong frames sent to WebSocket clients behaving as
# `for (;;) { send_ping(conn); }`. The spec says "If an endpoint receives a Ping frame and has not yet sent
# Pong frame(s) in response to previous Ping frame(s), the endpoint MAY elect to send a Pong frame for only
# the most recently processed Ping frame."
@most_recent_pong_application_data = application_data
if @pong_timer == nil then
@pong_timer = EventMachine.add_timer(0.2) do
@pong_timer = nil
send_frame(:pong, @most_recent_pong_application_data)
end
end
@connection.trigger_on_ping(application_data)
when :pong
@connection.trigger_on_pong(application_data)
when :text
if application_data.respond_to?(:force_encoding)
application_data.force_encoding("UTF-8")
unless application_data.valid_encoding?
raise InvalidDataError, "Invalid UTF8 data"
end
end
@connection.trigger_on_message(application_data)
when :binary
@connection.trigger_on_binary(application_data)
end
end
# Ping & Pong supported
def pingable?
true
end
end
end
end

View File

@@ -0,0 +1,5 @@
module EventMachine
module Websocket
VERSION = "0.5.2"
end
end

View File

@@ -0,0 +1,56 @@
module EventMachine
module WebSocket
class << self
attr_accessor :max_frame_size
attr_accessor :close_timeout
end
@max_frame_size = 10 * 1024 * 1024 # 10MB
# Connections are given 60s to close after being sent a close handshake
@close_timeout = 60
# All errors raised by em-websocket should descend from this class
class WebSocketError < RuntimeError; end
# Used for errors that occur during WebSocket handshake
class HandshakeError < WebSocketError; end
# Used for errors which should cause the connection to close.
# See RFC6455 §7.4.1 for a full description of meanings
class WSProtocolError < WebSocketError
def code; 1002; end
end
class InvalidDataError < WSProtocolError
def code; 1007; end
end
# 1009: Message too big to process
class WSMessageTooBigError < WSProtocolError
def code; 1009; end
end
# Start WebSocket server, including starting eventmachine run loop
def self.start(options, &blk)
EM.epoll
EM.run {
trap("TERM") { stop }
trap("INT") { stop }
run(options, &blk)
}
end
# Start WebSocket server inside eventmachine run loop
def self.run(options)
host, port = options.values_at(:host, :port)
EM.start_server(host, port, Connection, options) do |c|
yield c
end
end
def self.stop
puts "Terminating WebSocket Server"
EM.stop
end
end
end