1
0
mirror of https://github.com/bitwarden/help synced 2025-12-24 04:04:27 +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,3 @@
*.gemspec
pkg
Gemfile.lock

View File

@@ -0,0 +1,149 @@
= Changelog
== 0.5.1 / 2014-04-23
- new features:
- Support for receiving binary messages
- changed:
- Allow additional close codes to be sent by apps
- Raise better errors on missing Sec-WebSocket-Key2
- Updated http_parser.rb dependency to 0.6.0
- bug fixes:
- Abort if HTTP request URI is invalid
- Force close connections that have been sent a close handshake after a timeout
- improved spec compliance on:
- Missing continuation frames
- Fragmented control frames
- Close behaviour after protocol errors
== 0.5.0 / 2013-03-05
- new features:
- onclose handler is now passed a hash containing was_clean (set to true in drafts 03 and above when a connection is closed with a closing handshake, either by the server or the client), the close code, and reason (drafts 06 and above). Close code 1005 indicates that no code was supplied, and 1006 that the connection was closed abnormally.
- use Connection#support_close_codes? to easily check whether close codes are supported by the WebSocket protocol (drafts 06 and above)
- closes connection with 1007 close code if text frame contains invalid UTF8
- added Handshake#secure? for checking whether the connection is secure (either ssl or behind an ssl proxy)
- changed:
- Defaults to sending no close code rather than 1000 (consistent with browsers)
- Allows sending a 3xxx close code
- Renamed Connection#close_websocket to Connection#close (again for consistency with browsers). Old method is available till 0.6.
- Sends reasons with internally generated closure (previously only sent code)
- Echos close code when replying to close handshake
== 0.4.0 / 2013-01-22
- new features:
- on_open handler is now passed a handshake object which exposes the request headers, path, and query parameters
- Easily access the protocol version via Handshake#protocol_version
- Easily access the origin via Handshake#origin
- changed:
- Removed Connection#request - change to using handshake passed to on_open
- internals:
- Uses the http_parser.rb gem
== 0.3.8 / 2012-07-12
- bug fixes:
- Fixed support for Ruby 1.8.7 which was broken in 0.3.7
== 0.3.7 / 2012-07-11
- new features:
- Supports sending 1009 error code when incoming frame is too large to handle, and added associated exception class WSMessageTooBigError [Martyn Loughran]
- Supports overriding the maximum frame size by setting the max_frame_size accessor on the connection object (in bytes). Default unchanged at 10MB. [Martyn Loughran]
- bug fixes:
- Fixes some encoding issues on Ruby 1.9 [Dingding Ye]
- Raises a HandshakeError if WS header is empty [Markus Fenske]
- Connection#send would mutate passed string to BINARY encoding. The fix still mutates the string by forcing the encoding back to UTF-8 before returning, but if the passed string was encoded as UTF-8 this is equivalent [Martyn Loughran]
== 0.3.6 / 2011-12-23
- new features:
- Supports sending ping & pong messages
- Supports binding to received ping & pong messages
== 0.3.5 / 2011-10-24
- new features:
- Support WebSocket draft 13
== 0.3.2 / 2011-10-09
- bugfixes:
- Handling of messages with > 2 frames
- Encode string passed to onmessage handler as UTF-8 on Ruby 1.9
- Add 10MB frame length limit to all draft versions
== 0.3.1 / 2011-07-28
- new features:
- Support WebSocket drafts 07 & 08
== 0.3.0 / 2011-05-06
- new features:
- Support WebSocket drafts 05 & 06
- changes:
- Accept request headers in a case insensitive manner
- Change handling of errors. Previously some application errors were caught
internally and were invisible unless an onerror callback was supplied. See
readme for details
== 0.2.1 / 2011-03-01
- bugfixes:
- Performance improvements to draft 76 framing
- Limit frame lengths for draft 76
- Better error handling for draft 76 handshake
- Ruby 1.9 support
== 0.2.0 / 2010-11-23
- new features:
- Support for WebSocket draft 03
- bugfixes:
- Handle case when handshake split into two receive_data calls
- Stricter regexp matching of frames
== 0.1.4 / 2010-08-23
- new features:
- Allow custom ssl certificate to be used by passing :tls_options
- Protect against errors caused by non limited frame lengths
- Use custom exceptions rather than RuntimeError
- bugfixes:
- Handle invalid HTTP request with HandshakeError
== 0.1.3 / 2010-07-18
- new features:
- onerror callback
- bugfixes:
- proper handling of zero spaces in key1 or key2(prevent ZeroDivisionError)
- convert received data to utf-8 to prevent ruby 1.9 errors
- fix handling of null bytes within a frame
== 0.1.2 / 2010-06-16
- new features:
- none
- bugfixes:
- allow $ character inside header key
== 0.1.1 / 2010-06-13
- new features:
- wss/ssl support
- bugfixes:
- can't & strings
== 0.1.0 / 2010-06-12
- initial release

View File

@@ -0,0 +1,9 @@
source "http://rubygems.org"
gemspec
gem "em-websocket-client", git: "git@github.com:movitto/em-websocket-client.git", branch: "expose-websocket-api"
gem "em-spec", "~> 0.2.6"
gem "em-http-request", "~> 1.1.1"
gem "rspec", "~> 3.5.0"
gem "rake"

View File

@@ -0,0 +1,7 @@
Copyright (c) 2009-2014 Ilya Grigorik, Martyn Loughran
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,142 @@
# EM-WebSocket
[![Gem Version](https://badge.fury.io/rb/em-websocket.png)](http://rubygems.org/gems/em-websocket)
[![Analytics](https://ga-beacon.appspot.com/UA-71196-10/em-websocket/readme)](https://github.com/igrigorik/ga-beacon)
EventMachine based, async, Ruby WebSocket server. Take a look at examples directory, or check out the blog post: [Ruby & Websockets: TCP for the Web](http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/).
## Simple server example
```ruby
require 'em-websocket'
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080) do |ws|
ws.onopen { |handshake|
puts "WebSocket connection open"
# Access properties on the EM::WebSocket::Handshake object, e.g.
# path, query_string, origin, headers
# Publish message to the client
ws.send "Hello Client, you connected to #{handshake.path}"
}
ws.onclose { puts "Connection closed" }
ws.onmessage { |msg|
puts "Recieved message: #{msg}"
ws.send "Pong: #{msg}"
}
end
}
```
## Protocols supported, and protocol specific functionality
Supports all WebSocket protocols in use in the wild (and a few that are not): drafts 75, 76, 1-17, rfc.
While some of the changes between protocols are unimportant from the point of view of application developers, a few drafts did introduce new functionality. It's possible to easily test for this functionality by using
### Ping & pong supported
Call `ws.pingable?` to check whether ping & pong is supported by the protocol in use.
It's possible to send a ping frame (`ws.ping(body = '')`), which the client must respond to with a pong, or the server can send an unsolicited pong frame (`ws.pong(body = '')`) which the client should not respond to. These methods can be used regardless of protocol version; they return true if the protocol supports ping&pong or false otherwise.
When receiving a ping, the server will automatically respond with a pong as the spec requires (so you should _not_ write an onping handler that replies with a pong), however it is possible to bind to ping & pong events if desired by using the `onping` and `onpong` methods.
### Close codes and reasons
A WebSocket connection can be closed cleanly, regardless of protocol, by calling `ws.close(code = nil, body = nil)`.
Early protocols just close the TCP connection, draft 3 introduced a close handshake, and draft 6 added close codes and reasons to the close handshake. Call `ws.supports_close_codes?` to check whether close codes are supported (i.e. the protocol version is 6 or above).
The `onclose` callback is passed a hash which may contain following keys (depending on the protocol version):
* `was_clean`: boolean indicating whether the connection was closed via the close handshake.
* `code`: the close code. There are two special close codes which the server may set (as defined in the WebSocket spec):
* 1005: no code was supplied
* 1006: abnormal closure (the same as `was_clean: false`)
* `reason`: the close reason
Acceptable close codes are defined in the WebSocket rfc (<http://tools.ietf.org/html/rfc6455#section-7.4>). The following codes can be supplies when calling `ws.close(code)`:
* 1000: a generic normal close
* range 3xxx: reserved for libraries, frameworks, and applications (and can be registered with IANA)
* range 4xxx: for private use
If unsure use a code in the 4xxx range. em-websocket may also close a connection with one of the following close codes:
* 1002: WebSocket protocol error.
* 1009: Message too big to process. By default em-websocket will accept frames up to 10MB in size. If a frame is larger than this the connection will be closed without reading the frame data. The limit can be overriden globally (`EM::WebSocket.max_frame_size = bytes`) or on a specific connection (`ws.max_frame_size = bytes`).
## Secure server
It is possible to accept secure `wss://` connections by passing `:secure => true` when opening the connection. Pass a `:tls_options` hash containing keys as described in http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection:start_tls
**Warning**: Safari 5 does not currently support prompting on untrusted SSL certificates therefore using a self signed certificate may leave you scratching your head.
```ruby
EM::WebSocket.start({
:host => "0.0.0.0",
:port => 443,
:secure => true,
:tls_options => {
:private_key_file => "/private/key",
:cert_chain_file => "/ssl/certificate"
}
}) do |ws|
# ...
end
```
It's possible to check whether an incoming connection is secure by reading `handshake.secure?` in the onopen callback.
## Running behind an SSL Proxy/Terminator, like Stunnel
The `:secure_proxy => true` option makes it possible to use em-websocket behind a secure SSL proxy/terminator like [Stunnel](http://www.stunnel.org/) which does the actual encryption & decryption.
Note that this option is only required to support drafts 75 & 76 correctly (e.g. Safari 5.1.x & earlier, and Safari on iOS 5.x & earlier).
```ruby
EM::WebSocket.start({
:host => "0.0.0.0",
:port => 8080,
:secure_proxy => true
}) do |ws|
# ...
end
```
## Handling errors
There are two kinds of errors that need to be handled -- WebSocket protocol errors and errors in application code.
WebSocket protocol errors (for example invalid data in the handshake or invalid message frames) raise errors which descend from `EM::WebSocket::WebSocketError`. Such errors are rescued internally and the WebSocket connection will be closed immediately or an error code sent to the browser in accordance to the WebSocket specification. It is possible to be notified in application code of such errors by including an `onerror` callback.
```ruby
ws.onerror { |error|
if error.kind_of?(EM::WebSocket::WebSocketError)
# ...
end
}
```
Application errors are treated differently. If no `onerror` callback has been defined these errors will propagate to the EventMachine reactor, typically causing your program to terminate. If you wish to handle exceptions, simply supply an `onerror callback` and check for exceptions which are not descendant from `EM::WebSocket::WebSocketError`.
It is also possible to log all errors when developing by including the `:debug => true` option when initialising the WebSocket server.
## Emulating WebSockets in older browsers
It is possible to emulate WebSockets in older browsers using flash emulation. For example take a look at the [web-socket-js](https://github.com/gimite/web-socket-js) project.
Using flash emulation does require some minimal support from em-websocket which is enabled by default. If flash connects to the WebSocket port and requests a policy file (which it will do if it fails to receive a policy file on port 843 after a timeout), em-websocket will return one. Also see <https://github.com/igrigorik/em-websocket/issues/61> for an example policy file server which you can run on port 843.
## Examples & Projects using em-websocket
* [Pusher](http://pusher.com) - Realtime Messaging Service
* [Livereload](https://github.com/mockko/livereload) - LiveReload applies CSS/JS changes to Safari or Chrome w/o reloading
* [Twitter AMQP WebSocket Example](http://github.com/rubenfonseca/twitter-amqp-websocket-example)
* examples/multicast.rb - broadcast all ruby tweets to all subscribers
* examples/echo.rb - server <> client exchange via a websocket

View File

@@ -0,0 +1,11 @@
require 'bundler'
Bundler::GemHelper.install_tasks
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new do |t|
t.rspec_opts = ["-c", "-f progress", "-r ./spec/helper.rb"]
t.pattern = 'spec/**/*_spec.rb'
end
task :default => :spec

View File

@@ -0,0 +1,24 @@
require File.expand_path('../../lib/em-websocket', __FILE__)
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
puts "WebSocket opened #{{
:path => handshake.path,
:query => handshake.query,
:origin => handshake.origin,
}}"
ws.send "Hello Client!"
}
ws.onmessage { |msg|
ws.send "Pong: #{msg}"
}
ws.onclose {
puts "WebSocket closed"
}
ws.onerror { |e|
puts "Error: #{e.message}"
}
end
}

View File

@@ -0,0 +1,47 @@
require 'em-websocket'
# requires the twitter-stream gem
require 'twitter/json_stream'
require 'json'
#
# broadcast all ruby related tweets to all connected users!
#
username = ARGV.shift
password = ARGV.shift
raise "need username and password" if !username or !password
EventMachine.run {
@channel = EM::Channel.new
@twitter = Twitter::JSONStream.connect(
:path => '/1/statuses/filter.json?track=ruby',
:auth => "#{username}:#{password}",
:ssl => true
)
@twitter.each_item do |status|
status = JSON.parse(status)
@channel.push "#{status['user']['screen_name']}: #{status['text']}"
end
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws|
ws.onopen {
sid = @channel.subscribe { |msg| ws.send msg }
@channel.push "#{sid} connected!"
ws.onmessage { |msg|
@channel.push "<#{sid}>: #{msg}"
}
ws.onclose {
@channel.unsubscribe(sid)
}
}
end
puts "Server started"
}

View File

@@ -0,0 +1,24 @@
require File.expand_path('../../lib/em-websocket', __FILE__)
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
timer = nil
ws.onopen {
puts "Ping supported: #{ws.pingable?}"
timer = EM.add_periodic_timer(1) {
p ["Sent ping", ws.ping('hello')]
}
}
ws.onpong { |value|
puts "Received pong: #{value}"
}
ws.onping { |value|
puts "Received ping: #{value}"
}
ws.onclose {
EM.cancel_timer(timer)
puts "WebSocket closed"
}
ws.onerror { |e|
puts "Error: #{e.message}"
}
end

View File

@@ -0,0 +1,29 @@
<html>
<head>
<script>
function init() {
function debug(string) {
var element = document.getElementById("debug");
var p = document.createElement("p");
p.appendChild(document.createTextNode(string));
element.appendChild(p);
}
var Socket = "MozWebSocket" in window ? MozWebSocket : WebSocket;
var ws = new Socket("ws://localhost:8080/foo/bar?hello=world");
ws.onmessage = function(evt) { debug("Received: " + evt.data); };
ws.onclose = function(event) {
debug("Closed - code: " + event.code + ", reason: " + event.reason + ", wasClean: " + event.wasClean);
};
ws.onopen = function() {
debug("connected...");
ws.send("hello server");
ws.send("hello again");
};
};
</script>
</head>
<body onload="init();">
<div id="debug"></div>
</body>
</html>

View File

@@ -0,0 +1,24 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require "eventmachine"
%w[
debugger websocket connection
handshake
handshake75 handshake76 handshake04
framing76 framing03 framing04 framing05 framing07
close75 close03 close05 close06
masking04
message_processor_03 message_processor_06
handler handler75 handler76 handler03 handler05 handler06 handler07 handler08 handler13
].each do |file|
require "em-websocket/#{file}"
end
unless ''.respond_to?(:getbyte)
class String
def getbyte(i)
self[i]
end
end
end

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

View File

@@ -0,0 +1,173 @@
# encoding: BINARY
require 'rubygems'
require 'rspec'
require 'em-spec/rspec'
require 'em-http'
require 'em-websocket'
require 'em-websocket-client'
require 'integration/shared_examples'
require 'integration/gte_03_examples'
RSpec.configure do |c|
c.mock_with :rspec
end
class FakeWebSocketClient < EM::Connection
attr_reader :handshake_response, :packets
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 initialize
@state = :new
@packets = []
end
def receive_data(data)
# puts "RECEIVE DATA #{data}"
if @state == :new
@handshake_response = data
@onopen.call if defined? @onopen
@state = :open
else
@onmessage.call(data) if defined? @onmessage
@packets << data
end
end
def send(application_data)
send_frame(:text, application_data)
end
def send_frame(type, application_data)
send_data construct_frame(type, application_data)
end
def unbind
@onclose.call if defined? @onclose
end
private
def construct_frame(type, data)
"\x00#{data}\xff"
end
end
class Draft03FakeWebSocketClient < FakeWebSocketClient
private
def construct_frame(type, data)
frame = ""
frame << EM::WebSocket::Framing03::FRAME_TYPES[type]
frame << encoded_length(data.size)
frame << data
end
def encoded_length(length)
if length <= 125
[length].pack('C') # since rsv4 is 0
elsif length < 65536 # write 2 byte length
"\126#{[length].pack('n')}"
else # write 8 byte length
"\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}"
end
end
end
class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient
private
def construct_frame(type, data)
frame = ""
frame << "\x00\x00\x00\x00" # Mask with nothing for simplicity
frame << (EM::WebSocket::Framing05::FRAME_TYPES[type] | 0b10000000)
frame << encoded_length(data.size)
frame << data
end
end
class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient
private
def construct_frame(type, data)
frame = ""
frame << (EM::WebSocket::Framing07::FRAME_TYPES[type] | 0b10000000)
# Should probably mask the data, but I get away without bothering since
# the server doesn't enforce that incoming frames are masked
frame << encoded_length(data.size)
frame << data
end
end
# Wrapper around em-websocket-client
class Draft75WebSocketClient
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 initialize
@ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/',
:version => 75,
:origin => 'http://example.com')
@ws.errback { |err| @onerror.call if defined? @onerror }
@ws.callback { @onopen.call if defined? @onopen }
@ws.stream { |msg| @onmessage.call(msg) if defined? @onmessage }
@ws.disconnect { @onclose.call if defined? @onclose }
end
def send(message)
@ws.send_msg(message)
end
def close_connection
@ws.close_connection
end
end
def start_server(opts = {})
EM::WebSocket.run({:host => "0.0.0.0", :port => 12345}.merge(opts)) { |ws|
yield ws if block_given?
}
end
def format_request(r)
data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n"
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
data << [header_lines, '', r[:body]].join("\r\n")
data
end
def format_response(r)
data = r[:protocol] || "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
header_lines = r[:headers].map { |k,v| "#{k}: #{v}" }
data << [header_lines, '', r[:body]].join("\r\n")
data
end
RSpec::Matchers.define :succeed_with_upgrade do |response|
match do |actual|
success = nil
actual.callback { |upgrade_response, handler_klass|
success = (upgrade_response.lines.sort == format_response(response).lines.sort)
}
success
end
end
RSpec::Matchers.define :fail_with_error do |error_klass, error_message|
match do |actual|
success = nil
actual.errback { |e|
success = (e.class == error_klass)
success &= (e.message == error_message) if error_message
}
success
end
end

View File

@@ -0,0 +1,138 @@
require 'helper'
# These tests are not specific to any particular draft of the specification
#
describe "WebSocket server" do
include EM::SpecHelper
default_timeout 1
it "should fail on non WebSocket requests" do
em {
EM.add_timer(0.1) do
http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
http.errback { done }
http.callback { fail }
end
start_server
}
end
it "should expose the WebSocket request headers, path and query params" do
em {
EM.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/',
:origin => 'http://example.com')
ws.errback { fail }
ws.callback { ws.close_connection }
ws.stream { |msg| }
end
start_server do |ws|
ws.onopen { |handshake|
headers = handshake.headers
headers["Connection"].should == "Upgrade"
headers["Upgrade"].should == "websocket"
headers["Host"].to_s.should == "127.0.0.1:12345"
handshake.path.should == "/"
handshake.query.should == {}
handshake.origin.should == 'http://example.com'
}
ws.onclose {
ws.state.should == :closed
done
}
end
}
end
it "should expose the WebSocket path and query params when nonempty" do
em {
EM.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/hello?foo=bar&baz=qux')
ws.errback { fail }
ws.callback {
ws.close_connection
}
ws.stream { |msg| }
end
start_server do |ws|
ws.onopen { |handshake|
handshake.path.should == '/hello'
handshake.query_string.split('&').sort.
should == ["baz=qux", "foo=bar"]
handshake.query.should == {"foo"=>"bar", "baz"=>"qux"}
}
ws.onclose {
ws.state.should == :closed
done
}
end
}
end
it "should raise an exception if frame sent before handshake complete" do
em {
# 1. Start WebSocket server
start_server { |ws|
# 3. Try to send a message to the socket
lambda {
ws.send('early message')
}.should raise_error('Cannot send data before onopen callback')
done
}
# 2. Connect a dumb TCP connection (will not send handshake)
EM.connect('0.0.0.0', 12345, EM::Connection)
}
end
it "should allow the server to be started inside an existing EM" do
em {
EM.add_timer(0.1) do
http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
http.errback { |e| done }
http.callback { fail }
end
start_server do |ws|
ws.onopen { |handshake|
headers = handshake.headers
headers["Host"].to_s.should == "127.0.0.1:12345"
}
ws.onclose {
ws.state.should == :closed
done
}
end
}
end
context "outbound limit set" do
it "should close the connection if the limit is reached" do
em {
start_server(:outbound_limit => 150) do |ws|
# Increase the message size by one on each loop
ws.onmessage{|msg| ws.send(msg + "x") }
ws.onclose{|status|
status[:code].should == 1006 # Unclean
status[:was_clean].should be false
}
end
EM.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
ws.callback { ws.send_msg "hello" }
ws.disconnect { done } # Server closed the connection
ws.stream { |msg|
# minus frame size ? (getting 146 max here)
msg.data.size.should <= 150
# Return back the message
ws.send_msg(msg.data)
}
end
}
end
end
end

View File

@@ -0,0 +1,298 @@
require 'helper'
describe "draft03" do
include EM::SpecHelper
default_timeout 1
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
'Sec-WebSocket-Protocol' => 'sample',
'Upgrade' => 'WebSocket',
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
'Origin' => 'http://example.com',
'Sec-WebSocket-Draft' => '3'
},
:body => '^n:ds[4U'
}
@response = {
:headers => {
"Upgrade" => "WebSocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Location" => "ws://example.com/demo",
"Sec-WebSocket-Origin" => "http://example.com",
"Sec-WebSocket-Protocol" => "sample"
},
:body => "8jKS\'y:G*Co,Wxa-"
}
end
def start_client
client = EM.connect('0.0.0.0', 12345, Draft03FakeWebSocketClient)
client.send_data(format_request(@request))
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 3 }
end
it_behaves_like "a WebSocket server drafts 3 and above" do
let(:version) { 3 }
end
# These examples are straight from the spec
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6
describe "examples from the spec" do
it "should accept a single-frame text message" do
em {
start_server { |ws|
ws.onmessage { |msg|
msg.should == 'Hello'
done
}
}
start_client { |client|
client.onopen {
client.send_data("\x04\x05Hello")
}
}
}
end
it "should accept a fragmented text message" do
em {
start_server { |ws|
ws.onmessage { |msg|
msg.should == 'Hello'
done
}
}
connection = start_client
# Send frame
connection.onopen {
connection.send_data("\x84\x03Hel")
connection.send_data("\x00\x02lo")
}
}
end
it "should accept a ping request and respond with the same body" do
em {
start_server
connection = start_client
# Send frame
connection.onopen {
connection.send_data("\x02\x05Hello")
}
connection.onmessage { |frame|
next if frame.nil?
frame.should == "\x03\x05Hello"
done
}
}
end
it "should accept a 256 bytes binary message in a single frame" do
em {
data = "a" * 256
start_server { |ws|
ws.onbinary { |msg|
msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding)
msg.should == data
done
}
}
connection = start_client
# Send frame
connection.onopen {
connection.send_data("\x05\x7E\x01\x00" + data)
}
}
end
it "should accept a 64KiB binary message in a single frame" do
em {
data = "a" * 65536
start_server { |ws|
ws.onbinary { |msg|
msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding)
msg.should == data
done
}
}
connection = start_client
# Send frame
connection.onopen {
connection.send_data("\x05\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data)
}
}
end
end
describe "close handling" do
it "should respond to a new close frame with a close frame" do
em {
start_server
connection = start_client
# Send close frame
connection.onopen {
connection.send_data("\x01\x00")
}
# Check that close ack received
connection.onmessage { |frame|
frame.should == "\x01\x00"
done
}
}
end
it "should close the connection on receiving a close acknowlegement and call onclose with close code 1005 and was_clean=true (initiated by server)" do
em {
ack_received = false
start_server { |ws|
ws.onopen {
# 2. Send a close frame
EM.next_tick {
ws.close
}
}
# 5. Onclose event on server
ws.onclose { |event|
event.should == {
:code => 1005,
:reason => "",
:was_clean => true,
}
done
}
}
# 1. Create a fake client which sends draft 76 handshake
connection = start_client
# 3. Check that close frame recieved and acknowlege it
connection.onmessage { |frame|
frame.should == "\x01\x00"
ack_received = true
connection.send_data("\x01\x00")
}
# 4. Check that connection is closed _after_ the ack
connection.onclose {
ack_received.should == true
}
}
end
# it "should repur"
#
it "should return close code 1005 and was_clean=true after closing handshake (initiated by client)" do
em {
start_server { |ws|
ws.onclose { |event|
event.should == {
:code => 1005,
:reason => "",
:was_clean => true,
}
done
}
}
start_client { |client|
client.onopen {
client.send_data("\x01\x00")
}
}
}
end
it "should not allow data frame to be sent after close frame sent" do
em {
start_server { |ws|
ws.onopen {
# 2. Send a close frame
EM.next_tick {
ws.close
}
# 3. Check that exception raised if I attempt to send more data
EM.add_timer(0.1) {
lambda {
ws.send('hello world')
}.should raise_error(EM::WebSocket::WebSocketError, 'Cannot send data frame since connection is closing')
done
}
}
}
# 1. Create a fake client which sends draft 76 handshake
start_client
}
end
it "should still respond to control frames after close frame sent" do
em {
start_server { |ws|
ws.onopen {
# 2. Send a close frame
EM.next_tick {
ws.close
}
}
}
# 1. Create a fake client which sends draft 76 handshake
connection = start_client
connection.onmessage { |frame|
if frame == "\x01\x00"
# 3. After the close frame is received send a ping frame, but
# don't respond with a close ack
connection.send_data("\x02\x05Hello")
else
# 4. Check that the pong is received
frame.should == "\x03\x05Hello"
done
end
}
}
end
it "should report that close codes are not supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == false
done
}
}
start_client
}
end
end
end

View File

@@ -0,0 +1,50 @@
require 'helper'
describe "draft05" do
include EM::SpecHelper
default_timeout 1
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Upgrade' => 'websocket',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Protocol' => 'sample',
'Sec-WebSocket-Origin' => 'http://example.com',
'Sec-WebSocket-Version' => '5'
}
}
end
def start_client
client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient)
client.send_data(format_request(@request))
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 5 }
end
it_behaves_like "a WebSocket server drafts 3 and above" do
let(:version) { 5 }
end
it "should report that close codes are not supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == false
done
}
}
start_client
}
end
end

View File

@@ -0,0 +1,145 @@
require 'helper'
describe "draft06" do
include EM::SpecHelper
default_timeout 1
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Upgrade' => 'websocket',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Protocol' => 'sample',
'Sec-WebSocket-Origin' => 'http://example.com',
'Sec-WebSocket-Version' => '6'
}
}
@response = {
:protocol => "HTTP/1.1 101 Switching Protocols\r\n",
:headers => {
"Upgrade" => "websocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Accept" => "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
"Sec-WebSocket-Protocol" => "sample",
}
}
end
def start_client
client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient)
client.send_data(format_request(@request))
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 6 }
end
it_behaves_like "a WebSocket server drafts 3 and above" do
let(:version) { 6 }
end
it "should open connection" do
em {
start_server { |server|
server.onopen {
server.instance_variable_get(:@handler).class.should == EventMachine::WebSocket::Handler06
}
}
start_client { |client|
client.onopen {
client.handshake_response.lines.sort.
should == format_response(@response).lines.sort
done
}
}
}
end
it "should accept a single-frame text message (masked)" do
em {
start_server { |server|
server.onmessage { |msg|
msg.should == 'Hello'
if msg.respond_to?(:encoding)
msg.encoding.should == Encoding.find("UTF-8")
end
done
}
server.onerror {
fail
}
}
start_client { |client|
client.onopen {
client.send_data("\x00\x00\x01\x00\x84\x05Ielln")
}
}
}
end
it "should return close code and reason if closed via handshake" do
em {
start_server { |ws|
ws.onclose { |event|
# 2. Receive close event in server
event.should == {
:code => 4004,
:reason => "close reason",
:was_clean => true,
}
done
}
}
start_client { |client|
client.onopen {
# 1: Send close handshake
close_data = [4004].pack('n')
close_data << "close reason"
client.send_frame(:close, close_data)
}
}
}
end
it "should return close code 1005 if no code was specified" do
em {
start_server { |ws|
ws.onclose { |event|
event.should == {
:code => 1005,
:reason => "",
:was_clean => true,
}
done
}
}
start_client { |client|
client.onopen {
client.send_frame(:close, '')
}
}
}
end
it "should report that close codes are supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == true
done
}
}
start_client
}
end
end

View File

@@ -0,0 +1,105 @@
# encoding: BINARY
require 'helper'
describe "draft13" do
include EM::SpecHelper
default_timeout 1
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Upgrade' => 'websocket',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Protocol' => 'sample',
'Sec-WebSocket-Origin' => 'http://example.com',
'Sec-WebSocket-Version' => '13'
}
}
@response = {
:protocol => "HTTP/1.1 101 Switching Protocols\r\n",
:headers => {
"Upgrade" => "websocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Accept" => "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
"Sec-WebSocket-Protocol" => "sample",
}
}
end
def start_client
client = EM.connect('0.0.0.0', 12345, Draft07FakeWebSocketClient)
client.send_data(format_request(@request))
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 13 }
end
it_behaves_like "a WebSocket server drafts 3 and above" do
let(:version) { 13 }
end
it "should send back the correct handshake response" do
em {
start_server
connection = start_client
connection.onopen {
connection.handshake_response.lines.sort.
should == format_response(@response).lines.sort
done
}
}
end
# TODO: This test would be much nicer with a real websocket client...
it "should support sending pings and binding to onpong" do
em {
start_server { |ws|
ws.onopen {
ws.should be_pingable
EM.next_tick {
ws.ping('hello').should == true
}
}
ws.onpong { |data|
data.should == 'hello'
done
}
}
connection = start_client
# Confusing, fake onmessage means any data after the handshake
connection.onmessage { |data|
# This is what a ping looks like
data.should == "\x89\x05hello"
# This is what a pong looks like
connection.send_data("\x8a\x05hello")
}
}
end
it "should report that close codes are supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == true
done
}
}
start_client
}
end
end

View File

@@ -0,0 +1,123 @@
require 'helper'
# These integration tests are older and use a different testing style to the
# integration tests for newer drafts. They use EM::HttpRequest which happens
# to currently estabish a websocket connection using the draft75 protocol.
#
describe "WebSocket server draft75" do
include EM::SpecHelper
default_timeout 1
def start_client
client = Draft75WebSocketClient.new
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 75 }
end
it "should automatically complete WebSocket handshake" do
em {
MSG = "Hello World!"
EventMachine.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
ws.errback { fail }
ws.callback { }
ws.stream { |msg|
msg.data.should == MSG
EventMachine.stop
}
end
start_server { |ws|
ws.onopen {
ws.send MSG
}
}
}
end
it "should split multiple messages into separate callbacks" do
em {
messages = %w[1 2]
received = []
EventMachine.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
ws.errback { fail }
ws.stream {|msg|}
ws.callback {
ws.send_msg messages[0]
ws.send_msg messages[1]
}
end
start_server { |ws|
ws.onopen {}
ws.onclose {}
ws.onmessage {|msg|
msg.should == messages[received.size]
received.push msg
EventMachine.stop if received.size == messages.size
}
}
}
end
it "should call onclose callback when client closes connection" do
em {
EventMachine.add_timer(0.1) do
ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/')
ws.errback { fail }
ws.callback {
ws.close_connection
}
ws.stream{|msg|}
end
start_server { |ws|
ws.onopen {}
ws.onclose {
ws.state.should == :closed
EventMachine.stop
}
}
}
end
it "should call onerror callback with raised exception and close connection on bad handshake" do
em {
EventMachine.add_timer(0.1) do
http = EM::HttpRequest.new('http://127.0.0.1:12345/').get
http.errback { }
http.callback { fail }
end
start_server { |ws|
ws.onopen { fail }
ws.onclose { EventMachine.stop }
ws.onerror {|e|
e.should be_an_instance_of EventMachine::WebSocket::HandshakeError
e.message.should match('Not an upgrade request')
EventMachine.stop
}
}
}
end
it "should report that close codes are not supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == false
done
}
}
start_client
}
end
end

View File

@@ -0,0 +1,234 @@
# encoding: BINARY
require 'helper'
describe "WebSocket server draft76" do
include EM::SpecHelper
default_timeout 1
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
'Sec-WebSocket-Protocol' => 'sample',
'Upgrade' => 'WebSocket',
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
'Origin' => 'http://example.com'
},
:body => '^n:ds[4U'
}
@response = {
:headers => {
"Upgrade" => "WebSocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Location" => "ws://example.com/demo",
"Sec-WebSocket-Origin" => "http://example.com",
"Sec-WebSocket-Protocol" => "sample"
},
:body => "8jKS\'y:G*Co,Wxa-"
}
end
def start_client
client = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
client.send_data(format_request(@request))
yield client if block_given?
return client
end
it_behaves_like "a websocket server" do
let(:version) { 76 }
end
it "should send back the correct handshake response" do
em {
start_server
start_client { |connection|
connection.onopen {
connection.handshake_response.lines.sort.
should == format_response(@response).lines.sort
done
}
}
}
end
it "should send closing frame back and close the connection after recieving closing frame" do
em {
start_server
connection = start_client
# Send closing frame after handshake complete
connection.onopen {
connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING)
}
# Check that this causes a termination string to be returned and the
# connection close
connection.onclose {
connection.packets[0].should ==
EM::WebSocket::Handler76::TERMINATE_STRING
done
}
}
end
it "should ignore any data received after the closing frame" do
em {
start_server { |ws|
# Fail if foobar message is received
ws.onmessage { |msg|
fail
}
}
connection = start_client
# Send closing frame after handshake complete, followed by another msg
connection.onopen {
connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING)
connection.send('foobar')
}
connection.onclose {
done
}
}
end
it "should accept null bytes within the frame after a line return" do
em {
start_server { |ws|
ws.onmessage { |msg|
msg.should == "\n\000"
}
}
connection = start_client
# Send closing frame after handshake complete
connection.onopen {
connection.send_data("\000\n\000\377")
connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING)
}
connection.onclose {
done
}
}
end
it "should handle unreasonable frame lengths by calling onerror callback" do
em {
start_server { |server|
server.onerror { |error|
error.should be_an_instance_of EM::WebSocket::WSMessageTooBigError
error.message.should == "Frame length too long (1180591620717411303296 bytes)"
done
}
}
client = start_client
# This particular frame indicates a message length of
# 1180591620717411303296 bytes. Such a message would previously cause
# a "bignum too big to convert into `long'" error.
# However it is clearly unreasonable and should be rejected.
client.onopen {
client.send_data("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00")
}
}
end
it "should handle impossible frames by calling onerror callback" do
em {
start_server { |server|
server.onerror { |error|
error.should be_an_instance_of EM::WebSocket::WSProtocolError
error.message.should == "Invalid frame received"
done
}
}
client = start_client
client.onopen {
client.send_data("foobar") # Does not start with \x00 or \xff
}
}
end
it "should handle invalid http requests by raising HandshakeError passed to onerror callback" do
em {
start_server { |server|
server.onerror { |error|
error.should be_an_instance_of EM::WebSocket::HandshakeError
error.message.should == "Invalid HTTP header: Could not parse data entirely (1 != 29)"
done
}
}
client = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
client.send_data("This is not a HTTP header\r\n\r\n")
}
end
it "should handle handshake request split into two TCP packets" do
em {
start_server
# Create a fake client which sends draft 76 handshake
connection = EM.connect('0.0.0.0', 12345, FakeWebSocketClient)
data = format_request(@request)
# Sends first half of the request
connection.send_data(data[0...(data.length / 2)])
connection.onopen {
connection.handshake_response.lines.sort.
should == format_response(@response).lines.sort
done
}
EM.add_timer(0.1) do
# Sends second half of the request
connection.send_data(data[(data.length / 2)..-1])
end
}
end
it "should report that close codes are not supported" do
em {
start_server { |ws|
ws.onopen {
ws.supports_close_codes?.should == false
done
}
}
start_client
}
end
it "should call onclose when the server closes the connection [antiregression]" do
em {
start_server { |ws|
ws.onopen {
EM.add_timer(0.1) {
ws.close()
}
}
ws.onclose {
done
}
}
start_client
}
end
end

View File

@@ -0,0 +1,42 @@
shared_examples_for "a WebSocket server drafts 3 and above" do
it "should force close connections after a timeout if close handshake is not sent by the client" do
em {
server_onerror_fired = false
server_onclose_fired = false
client_got_close_handshake = false
start_server(:close_timeout => 0.1) { |ws|
ws.onopen {
# 1: Send close handshake to client
EM.next_tick { ws.close(4999, "Close message") }
}
ws.onerror { |e|
# 3: Client should receive onerror
e.class.should == EM::WebSocket::WSProtocolError
e.message.should == "Close handshake un-acked after 0.1s, closing tcp connection"
server_onerror_fired = true
}
ws.onclose {
server_onclose_fired = true
}
}
start_client { |client|
client.onmessage { |msg|
# 2: Client does not respond to close handshake (the fake client
# doesn't understand them at all hence this is in onmessage)
msg.should =~ /Close message/ if version >= 6
client_got_close_handshake = true
}
client.onclose {
server_onerror_fired.should == true
server_onclose_fired.should == true
client_got_close_handshake.should == true
done
}
}
}
end
end

View File

@@ -0,0 +1,265 @@
# encoding: UTF-8
# These tests are run against all draft versions
#
shared_examples_for "a websocket server" do
it "should expose the protocol version" do
em {
start_server { |ws|
ws.onopen { |handshake|
handshake.protocol_version.should == version
done
}
}
start_client
}
end
it "should expose the origin header" do
em {
start_server { |ws|
ws.onopen { |handshake|
handshake.origin.should == 'http://example.com'
done
}
}
start_client
}
end
it "should expose the remote IP address" do
em {
start_server { |ws|
ws.onopen {
ws.remote_ip.should == "127.0.0.1"
done
}
}
start_client
}
end
it "should send messages successfully" do
em {
start_server { |ws|
ws.onmessage { |message|
message.should == "hello server"
done
}
}
start_client { |client|
client.onopen {
client.send("hello server")
}
}
}
end
it "should allow connection to be closed with valid close code" do
em {
start_server { |ws|
ws.onopen {
ws.close(4004, "Bye bye")
done
}
}
start_client
# TODO: Use a real client which understands how to respond to closing
# handshakes, sending the handshake currently untested
}
end
it "should raise error if if invalid close code is used" do
em {
start_server { |ws|
ws.onopen {
lambda {
ws.close(2000)
}.should raise_error("Application code may only use codes from 1000, 3000-4999")
done
}
}
start_client
}
end
it "should call onclose with was_clean set to false if connection closed without closing handshake by server" do
em {
start_server { |ws|
ws.onopen {
# Close tcp connection (no close handshake)
ws.close_connection
}
ws.onclose { |event|
event.should == {:code => 1006, :was_clean => false}
done
}
}
start_client
}
end
it "should call onclose with was_clean set to false if connection closed without closing handshake by client" do
em {
start_server { |ws|
ws.onclose { |event|
event.should == {:code => 1006, :was_clean => false}
done
}
}
start_client { |client|
client.onopen {
# Close tcp connection (no close handshake)
client.close_connection
}
}
}
end
it "should call onerror if an application error raised in onopen" do
em {
start_server { |ws|
ws.onopen {
raise "application error"
}
ws.onerror { |e|
e.message.should == "application error"
done
}
}
start_client
}
end
it "should call onerror if an application error raised in onmessage" do
em {
start_server { |server|
server.onmessage {
raise "application error"
}
server.onerror { |e|
e.message.should == "application error"
done
}
}
start_client { |client|
client.onopen {
client.send('a message')
}
}
}
end
it "should call onerror in an application error raised in onclose" do
em {
start_server { |server|
server.onclose {
raise "application error"
}
server.onerror { |e|
e.message.should == "application error"
done
}
}
start_client { |client|
client.onopen {
EM.add_timer(0.1) {
client.close_connection
}
}
}
}
end
it "should close the connection when a too long frame is sent" do
em {
start_server { |server|
server.max_frame_size = 20
server.onerror { |e|
# 3: Error should be reported to server
e.class.should == EventMachine::WebSocket::WSMessageTooBigError
e.message.should =~ /Frame length too long/
}
}
start_client { |client|
client.onopen {
EM.next_tick {
client.send("This message is longer than 20 characters")
}
}
client.onmessage { |msg|
# 4: This is actually the close message. Really need to use a real
# WebSocket client in these tests...
done
}
client.onclose {
# 4: Drafts 75 & 76 don't send a close message, they just close the
# connection
done
}
}
}
end
# Only run these tests on ruby 1.9
if "a".respond_to?(:force_encoding)
it "should raise error if you try to send non utf8 text data to ws" do
em {
start_server { |server|
server.onopen {
# Create a string which claims to be UTF-8 but which is not
s = "ê" # utf-8 string
s.encode!("ISO-8859-1")
s.force_encoding("UTF-8")
s.valid_encoding?.should == false # now invalid utf8
# Send non utf8 encoded data
server.send(s)
}
server.onerror { |error|
error.class.should == EventMachine::WebSocket::WebSocketError
error.message.should == "Data sent to WebSocket must be valid UTF-8 but was UTF-8 (valid: false)"
done
}
}
start_client { }
}
end
it "should not change the encoding of strings sent to send [antiregression]" do
em {
start_server { |server|
server.onopen {
s = "example string"
s.force_encoding("UTF-8")
server.send(s)
s.encoding.should == Encoding.find("UTF-8")
done
}
}
start_client { }
}
end
end
end

View File

@@ -0,0 +1,298 @@
# encoding: BINARY
require 'helper'
describe EM::WebSocket::Framing03 do
class FramingContainer
include EM::WebSocket::Framing03
def initialize
@connection = Object.new
def @connection.max_frame_size
1000000
end
end
def <<(data)
@data << data
process_data
end
def debug(*args); end
end
before :each do
@f = FramingContainer.new
@f.initialize_framing
end
describe "basic examples" do
it "connection close" do
@f.should_receive(:message).with(:close, '', '')
@f << 0b00000001
@f << 0b00000000
end
it "ping" do
@f.should_receive(:message).with(:ping, '', '')
@f << 0b00000010
@f << 0b00000000
end
it "pong" do
@f.should_receive(:message).with(:pong, '', '')
@f << 0b00000011
@f << 0b00000000
end
it "text" do
@f.should_receive(:message).with(:text, '', 'foo')
@f << 0b00000100
@f << 0b00000011
@f << 'foo'
end
it "Text in two frames" do
@f.should_receive(:message).with(:text, '', 'hello world')
@f << 0b10000100
@f << 0b00000110
@f << "hello "
@f << 0b00000000
@f << 0b00000101
@f << "world"
end
it "2 byte extended payload length text frame" do
data = 'a' * 256
@f.should_receive(:message).with(:text, '', data)
@f << 0b00000100 # Single frame, text
@f << 0b01111110 # Length 126 (so read 2 bytes)
@f << 0b00000001 # Two bytes in network byte order (256)
@f << 0b00000000
@f << data
end
end
# These examples are straight from the spec
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6
describe "examples from the spec" do
it "a single-frame text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x04\x05Hello"
end
it "a fragmented text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x84\x03Hel"
@f << "\x00\x02lo"
end
it "Ping request and response" do
@f.should_receive(:message).with(:ping, '', 'Hello')
@f << "\x02\x05Hello"
end
it "256 bytes binary message in a single frame" do
data = "a"*256
@f.should_receive(:message).with(:binary, '', data)
@f << "\x05\x7E\x01\x00" + data
end
it "64KiB binary message in a single frame" do
data = "a"*65536
@f.should_receive(:message).with(:binary, '', data)
@f << "\x05\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
end
end
describe "other tests" do
it "should accept a fragmented unmasked text message in 3 frames" do
@f.should_receive(:message).with(:text, '', 'Hello world')
@f << "\x84\x03Hel"
@f << "\x80\x02lo"
@f << "\x00\x06 world"
end
end
describe "error cases" do
it "should raise an exception on continuation frame without preceeding more frame" do
lambda {
@f << 0b00000000 # Single frame, continuation
@f << 0b00000001 # Length 1
@f << 'f'
}.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame not expected')
end
end
end
# These examples are straight from the spec
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6
describe EM::WebSocket::Framing04 do
class FramingContainer04
include EM::WebSocket::Framing04
def initialize
@connection = Object.new
def @connection.max_frame_size
1000000
end
end
def <<(data)
@data << data
process_data
end
def debug(*args); end
end
before :each do
@f = FramingContainer04.new
@f.initialize_framing
end
describe "examples from the spec" do
it "a single-frame text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x84\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
end
it "a fragmented text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x04\x03Hel"
@f << "\x80\x02lo"
end
it "Ping request" do
@f.should_receive(:message).with(:ping, '', 'Hello')
@f << "\x82\x05Hello"
end
it "a pong response" do
@f.should_receive(:message).with(:pong, '', 'Hello')
@f << "\x83\x05Hello"
end
it "256 bytes binary message in a single frame" do
data = "a"*256
@f.should_receive(:message).with(:binary, '', data)
@f << "\x85\x7E\x01\x00" + data
end
it "64KiB binary message in a single frame" do
data = "a"*65536
@f.should_receive(:message).with(:binary, '', data)
@f << "\x85\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
end
end
describe "other tests" do
it "should accept a fragmented unmasked text message in 3 frames" do
@f.should_receive(:message).with(:text, '', 'Hello world')
@f << "\x04\x03Hel"
@f << "\x00\x02lo"
@f << "\x80\x06 world"
end
end
end
describe EM::WebSocket::Framing07 do
class FramingContainer07
include EM::WebSocket::Framing07
def initialize
@connection = Object.new
def @connection.max_frame_size
1000000
end
end
def <<(data)
@data << data
process_data
end
def debug(*args); end
end
before :each do
@f = FramingContainer07.new
@f.initialize_framing
end
# These examples are straight from the spec
# http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07#section-4.6
describe "examples from the spec" do
it "a single-frame unmakedtext message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello"
end
it "a single-frame masked text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello"
end
it "a fragmented unmasked text message" do
@f.should_receive(:message).with(:text, '', 'Hello')
@f << "\x01\x03Hel"
@f << "\x80\x02lo"
end
it "Ping request" do
@f.should_receive(:message).with(:ping, '', 'Hello')
@f << "\x89\x05Hello"
end
it "a pong response" do
@f.should_receive(:message).with(:pong, '', 'Hello')
@f << "\x8a\x05Hello"
end
it "256 bytes binary message in a single unmasked frame" do
data = "a"*256
@f.should_receive(:message).with(:binary, '', data)
@f << "\x82\x7E\x01\x00" + data
end
it "64KiB binary message in a single unmasked frame" do
data = "a"*65536
@f.should_receive(:message).with(:binary, '', data)
@f << "\x82\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data
end
end
describe "other tests" do
it "should raise a WSProtocolError if an invalid frame type is requested" do
lambda {
# Opcode 3 is not supported by this draft
@f << "\x83\x05Hello"
}.should raise_error(EventMachine::WebSocket::WSProtocolError, "Unknown opcode 3")
end
it "should accept a fragmented unmasked text message in 3 frames" do
@f.should_receive(:message).with(:text, '', 'Hello world')
@f << "\x01\x03Hel"
@f << "\x00\x02lo"
@f << "\x80\x06 world"
end
it "should raise if non-fin frame is followed by a non-continuation data frame (continuation frame would be expected)" do
lambda {
@f << 0b00000001 # Not fin, text
@f << 0b00000001 # Length 1
@f << 'f'
@f << 0b10000001 # fin, text (continutation expected)
@f << 0b00000001 # Length 1
@f << 'b'
}.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame expected')
end
it "should raise on non-fin control frames (control frames must not be fragmented)" do
lambda {
@f << 0b00001010 # Not fin, pong (opcode 10)
@f << 0b00000000 # Length 1
}.should raise_error(EM::WebSocket::WebSocketError, 'Control frames must not be fragmented')
end
end
end

View File

@@ -0,0 +1,216 @@
require 'helper'
describe EM::WebSocket::Handshake do
def handshake(request, secure = false)
handshake = EM::WebSocket::Handshake.new(secure)
handshake.receive_data(format_request(request))
handshake
end
before :each do
@request = {
:port => 80,
:method => "GET",
:path => "/demo",
:headers => {
'Host' => 'example.com',
'Connection' => 'Upgrade',
'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00',
'Sec-WebSocket-Protocol' => 'sample',
'Upgrade' => 'WebSocket',
'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5',
'Origin' => 'http://example.com'
},
:body => '^n:ds[4U'
}
@secure_request = @request.merge(:port => 443)
@response = {
:headers => {
"Upgrade" => "WebSocket",
"Connection" => "Upgrade",
"Sec-WebSocket-Location" => "ws://example.com/demo",
"Sec-WebSocket-Origin" => "http://example.com",
"Sec-WebSocket-Protocol" => "sample"
},
:body => "8jKS\'y:G*Co,Wxa-"
}
@secure_response = @response.merge(:headers => @response[:headers].merge('Sec-WebSocket-Location' => "wss://example.com/demo"))
end
it "should handle good request" do
handshake(@request).should succeed_with_upgrade(@response)
end
it "should handle good request to secure default port if secure mode is enabled" do
handshake(@secure_request, true).
should succeed_with_upgrade(@secure_response)
end
it "should not handle good request to secure default port if secure mode is disabled" do
handshake(@secure_request, false).
should_not succeed_with_upgrade(@secure_response)
end
it "should handle good request on nondefault port" do
@request[:port] = 8081
@request[:headers]['Host'] = 'example.com:8081'
@response[:headers]['Sec-WebSocket-Location'] =
'ws://example.com:8081/demo'
handshake(@request).should succeed_with_upgrade(@response)
end
it "should handle good request to secure nondefault port" do
@secure_request[:port] = 8081
@secure_request[:headers]['Host'] = 'example.com:8081'
@secure_response[:headers]['Sec-WebSocket-Location'] = 'wss://example.com:8081/demo'
handshake(@secure_request, true).
should succeed_with_upgrade(@secure_response)
end
it "should handle good request with no protocol" do
@request[:headers].delete('Sec-WebSocket-Protocol')
@response[:headers].delete("Sec-WebSocket-Protocol")
handshake(@request).should succeed_with_upgrade(@response)
end
it "should handle extra headers by simply ignoring them" do
@request[:headers]['EmptyValue'] = ""
@request[:headers]['AKey'] = "AValue"
handshake(@request).should succeed_with_upgrade(@response)
end
it "should raise error on HTTP request" do
@request[:headers] = {
'Host' => 'www.google.com',
'User-Agent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 GTB6 GTBA',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-us,en;q=0.5',
'Accept-Encoding' => 'gzip,deflate',
'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
'Keep-Alive' => '300',
'Connection' => 'keep-alive',
}
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
end
it "should raise error on wrong method" do
@request[:method] = 'POST'
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
end
it "should raise error if upgrade header incorrect" do
@request[:headers]['Upgrade'] = 'NonWebSocket'
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
end
it "should raise error if Sec-WebSocket-Protocol is empty" do
@request[:headers]['Sec-WebSocket-Protocol'] = ''
handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError)
end
%w[Sec-WebSocket-Key1 Sec-WebSocket-Key2].each do |header|
it "should raise error if #{header} has zero spaces" do
@request[:headers][header] = 'nospaces'
handshake(@request).
should fail_with_error(EM::WebSocket::HandshakeError, 'Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack')
end
end
it "should raise error if Sec-WebSocket-Key1 is missing" do
@request[:headers].delete("Sec-WebSocket-Key1")
# The error message isn't correct since key1 is used to heuristically
# determine the protocol version in use, however this test at least checks
# that the handshake does correctly fail
handshake(@request).
should fail_with_error(EM::WebSocket::HandshakeError, 'Extra bytes after header')
end
it "should raise error if Sec-WebSocket-Key2 is missing" do
@request[:headers].delete("Sec-WebSocket-Key2")
handshake(@request).
should fail_with_error(EM::WebSocket::HandshakeError, 'WebSocket key1 or key2 is missing')
end
it "should raise error if spaces do not divide numbers in Sec-WebSocket-Key* " do
@request[:headers]['Sec-WebSocket-Key2'] = '12998 5 Y3 1.P00'
handshake(@request).
should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid Key "12998 5 Y3 1.P00"')
end
it "should raise error if the HTTP header is empty" do
handshake = EM::WebSocket::Handshake.new(false)
handshake.receive_data("\r\n\r\nfoobar")
handshake.
should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid HTTP header: Could not parse data entirely (4 != 10)')
end
# This might seems crazy, but very occasionally we saw multiple "Upgrade:
# WebSocket" headers in the wild. RFC 4.2.1 isn't particularly clear on this
# point, so for now I have decided not to accept --@mloughran
it "should raise error on multiple upgrade headers" do
handshake = EM::WebSocket::Handshake.new(false)
# Add a duplicate upgrade header
headers = format_request(@request)
upgrade_header = "Upgrade: WebSocket\r\n"
headers.gsub!(upgrade_header, "#{upgrade_header}#{upgrade_header}")
handshake.receive_data(headers)
handshake.errback { |e|
e.class.should == EM::WebSocket::HandshakeError
e.message.should == 'Invalid upgrade header: ["WebSocket", "WebSocket"]'
}
end
it "should cope with requests where the header is split" do
request = format_request(@request)
incomplete_request = request[0...(request.length / 2)]
rest = request[(request.length / 2)..-1]
handshake = EM::WebSocket::Handshake.new(false)
handshake.receive_data(incomplete_request)
handshake.instance_variable_get(:@deferred_status).should == nil
# Send the remaining header
handshake.receive_data(rest)
handshake(@request).should succeed_with_upgrade(@response)
end
it "should cope with requests where the third key is split" do
request = format_request(@request)
# Removes last two bytes of the third key
incomplete_request = request[0..-3]
rest = request[-2..-1]
handshake = EM::WebSocket::Handshake.new(false)
handshake.receive_data(incomplete_request)
handshake.instance_variable_get(:@deferred_status).should == nil
# Send the remaining third key
handshake.receive_data(rest)
handshake(@request).should succeed_with_upgrade(@response)
end
it "should fail if the request URI is invalid" do
@request[:path] = "/%"
handshake(@request).should \
fail_with_error(EM::WebSocket::HandshakeError, 'Invalid request URI: /%')
end
end

View File

@@ -0,0 +1,29 @@
# encoding: BINARY
require 'helper'
describe EM::WebSocket::MaskedString do
it "should allow reading 4 byte mask and unmasking byte / bytes" do
t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x01\x00\x01\x00\x01")
t.read_mask
t.getbyte(3).should == 0x00
t.getbytes(4, 4).should == "\x00\x01\x00\x00"
t.getbytes(5, 3).should == "\x01\x00\x00"
end
it "should return nil from getbyte if index requested is out of range" do
t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x00\x53")
t.read_mask
t.getbyte(4).should == 0x53
t.getbyte(5).should == nil
end
it "should allow switching masking on and off" do
t = EM::WebSocket::MaskedString.new("\x02\x00\x00\x00\x03")
t.getbyte(4).should == 0x03
t.read_mask
t.getbyte(4).should == 0x01
t.unset_mask
t.getbyte(4).should == 0x03
end
end