Rename the module to websocket-driver.

This commit is contained in:
James Coglan
2013-05-04 16:43:25 +01:00
parent e274a521ef
commit be1f1cdc35
17 changed files with 285 additions and 286 deletions
+47 -48
View File
@@ -1,4 +1,4 @@
# websocket-protocol [![Build Status](https://travis-ci.org/faye/websocket-protocol-ruby.png)](https://travis-ci.org/faye/websocket-protocol-ruby)
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-ruby.png)](https://travis-ci.org/faye/websocket-driver-ruby)
This module provides a complete implementation of the WebSocket protocols that
can be hooked up to any TCP library. It aims to simplify things by decoupling
@@ -10,7 +10,7 @@ pluggable I/O.
Due to this design, you get a lot of things for free. In particular, if you
hook this module up to some I/O object, it will do all of this for you:
* Select the correct server-side protocol handler to talk to the client
* Select the correct server-side driver to talk to the client
* Generate and send both server- and client-side handshakes
* Recognize when the handshake phase completes and the WS protocol begins
* Negotiate subprotocol selection based on `Sec-WebSocket-Protocol`
@@ -31,7 +31,7 @@ I/O system.
## Installation
```
$ gem install websocket-protocol
$ gem install websocket-driver
```
@@ -64,12 +64,12 @@ Server-side sockets require one additional method:
### Server-side
To handle a server-side WebSocket connection, you need to check whether the
request is a WebSocket handshake, and if so create a protocol handler for it.
You must give the handler an object with the `env`, `url` and `write` methods.
request is a WebSocket handshake, and if so create a protocol driver for it.
You must give the driver an object with the `env`, `url` and `write` methods.
A simple example might be:
```ruby
require 'websocket/protocol'
require 'websocket/driver'
require 'eventmachine'
class WS
@@ -82,14 +82,14 @@ class WS
scheme = secure ? 'wss:' : 'ws:'
@url = scheme + '//' + env['HTTP_HOST'] + env['REQUEST_URI']
@handler = WebSocket::Protocol.rack(self)
@driver = WebSocket::Driver.rack(self)
env['rack.hijack'].call
@io = env['rack.hijack_io']
EM.attach(@io, Reader) { |conn| conn.handler = @handler }
EM.attach(@io, Reader) { |conn| conn.driver = @driver }
@handler.start
@driver.start
end
def write(string)
@@ -97,10 +97,10 @@ class WS
end
module Reader
attr_writer :handler
attr_writer :driver
def receive_data(string)
@handler.parse(string)
@driver.parse(string)
end
end
end
@@ -109,132 +109,131 @@ end
To explain what's going on here: the `WS` class implements the `env`, `url` and
`write(string)` methods as required. When instantiated with a Rack environment,
it stores the environment and infers the complete URL from it. Having set up
the `env` and `url`, it asks `WebSocket::Protocol` for a server-side handler
for the socket. Then it uses the Rack hijack API to gain access to the TCP
stream, and uses EventMachine to stream in incoming data from the client,
handing incoming data off to the handler for parsing. Finally, we tell the
handler to `start`, which will begin sending the handshake response. This will
invoke the `WS#write` method, which will send the response out over the TCP
socket.
the `env` and `url`, it asks `WebSocket::Driver` for a server-side driver for
the socket. Then it uses the Rack hijack API to gain access to the TCP stream,
and uses EventMachine to stream in incoming data from the client, handing
incoming data off to the driver for parsing. Finally, we tell the driver to
`start`, which will begin sending the handshake response. This will invoke the
`WS#write` method, which will send the response out over the TCP socket.
Having defined this class we could use it like this when handling a request:
```ruby
if WebSocket::Protocol.websocket?(env)
if WebSocket::Driver.websocket?(env)
socket = WS.new(env)
end
```
The handler API is described in full below.
The driver API is described in full below.
### Client-side
Similarly, to implement a WebSocket client you need an object with `url` and
`write` methods. Once you have one such object, you ask for a handler for it:
`write` methods. Once you have one such object, you ask for a driver for it:
```ruby
handler = WebSocket::Protocol.client(socket)
driver = WebSocket::Driver.client(socket)
```
After this you use the handler API as described below to process incoming data
After this you use the driver API as described below to process incoming data
and send outgoing data.
### Handler API
### Driver API
Handlers are created using one of the following methods:
Drivers are created using one of the following methods:
```ruby
handler = WebSocket::Protocol.rack(socket, options)
handler = WebSocket::Protocol.client(socket, options)
driver = WebSocket::Driver.rack(socket, options)
driver = WebSocket::Driver.client(socket, options)
```
The `rack` method returns a handler chosen using the socket's `env`. The
`client` method always returns a handler for the RFC version of the protocol
The `rack` method returns a driver chosen using the socket's `env`. The
`client` method always returns a driver for the RFC version of the protocol
with masking enabled on outgoing frames.
The `options` argument is optional, and is a hash. It may contain the following
keys:
* `:protocols` - an array of strings representing acceptable subprotocols for
use over the socket. The handler will negotiate one of these to use via the
use over the socket. The driver will negotiate one of these to use via the
`Sec-WebSocket-Protocol` header if supported by the other peer.
All handlers respond to the following API methods, but some of them are no-ops
All drivers respond to the following API methods, but some of them are no-ops
depending on whether the client supports the behaviour.
Note that most of these methods are commands: if they produce data that should
be sent over the socket, they will give this to you by calling
`socket.write(string)`.
#### `handler.on('open') { |event| }`
#### `driver.on('open') { |event| }`
Sets the callback block to execute when the socket becomes open.
#### `handler.on('message') { |event| }`
#### `driver.on('message') { |event| }`
Sets the callback block to execute when a message is received. `event` will
have a `data` attribute containing either a string in the case of a text
message or an array of integers in the case of a binary message.
#### `handler.on('error') { |event| }`
#### `driver.on('error') { |event| }`
Sets the callback to execute when a protocol error occurs due to the other peer
sending an invalid byte sequence. `event` will have a `message` attribute
describing the error.
#### `handler.on('close') { |event| }`
#### `driver.on('close') { |event| }`
Sets the callback block to execute when the socket becomes closed. The `event`
object has `code` and `reason` attributes.
#### `handler.start`
#### `driver.start`
Initiates the protocol by sending the handshake - either the response for a
server-side handler or the request for a client-side one. This should be the
server-side driver or the request for a client-side one. This should be the
first method you invoke. Returns `true` iff a handshake was sent.
#### `handler.parse(string)`
#### `driver.parse(string)`
Takes a string and parses it, potentially resulting in message events being
emitted (see `on('message')` above) or in data being sent to `socket.write`.
You should send all data you receive via I/O to this method.
#### `handler.text(string)`
#### `driver.text(string)`
Sends a text message over the socket. If the socket handshake is not yet
complete, the message will be queued until it is. Returns `true` if the message
was sent or queued, and `false` if the socket can no longer send messages.
#### `handler.binary(array)`
#### `driver.binary(array)`
Takes an array of byte-sized integers and sends them as a binary message. Will
queue and return `true` or `false` the same way as the `text` method. It will
also return `false` if the handler does not support binary messages.
also return `false` if the driver does not support binary messages.
#### `handler.ping(string = '', &callback)`
#### `driver.ping(string = '', &callback)`
Sends a ping frame over the socket, queueing it if necessary. `string` and the
`callback` block are both optional. If a callback is given, it will be invoked
when the socket receives a pong frame whose content matches `string`. Returns
`false` if frames can no longer be sent, or if the handler does not support
`false` if frames can no longer be sent, or if the driver does not support
ping/pong.
#### `handler.close`
#### `driver.close`
Initiates the closing handshake if the socket is still open. For handlers with
Initiates the closing handshake if the socket is still open. For drivers with
no closing handshake, this will result in the immediate execution of the
`on('close')` handler. For handlers with a closing handshake, this sends a
`on('close')` callback. For drivers with a closing handshake, this sends a
closing frame and `emit('close')` will execute when a response is received or a
protocol error occurs.
#### `handler.version`
#### `driver.version`
Returns the WebSocket version in use as a string. Will either be `hixie-75`,
`hixie-76` or `hybi-$version`.
#### `handler.protocol`
#### `driver.protocol`
Returns a string containing the selected subprotocol, if any was agreed upon
using the `Sec-WebSocket-Protocol` mechanism. This value becomes available
+1 -1
View File
@@ -1,6 +1,6 @@
require 'rubygems/package_task'
spec = Gem::Specification.load('websocket-protocol.gemspec')
spec = Gem::Specification.load('websocket-driver.gemspec')
Gem::PackageTask.new(spec) do |pkg|
end
@@ -12,9 +12,9 @@ require 'stringio'
require 'uri'
module WebSocket
class Protocol
class Driver
root = File.expand_path('../protocol', __FILE__)
root = File.expand_path('../driver', __FILE__)
require root + '/../../websocket_mask'
if RUBY_PLATFORM =~ /java/
@@ -1,5 +1,5 @@
module WebSocket
class Protocol
class Driver
class Client < Hybi
def self.generate_key
@@ -71,7 +71,7 @@ module WebSocket
end
def validate_handshake
data = Protocol.encode(@buffer)
data = Driver.encode(@buffer)
@buffer = []
response = Net::HTTPResponse.read_new(Net::BufferedIO.new(StringIO.new(data)))
@@ -1,7 +1,7 @@
module WebSocket
class Protocol
class Driver
class Draft75 < Protocol
class Draft75 < Driver
def initialize(socket, options = {})
super
@stage = 0
@@ -41,7 +41,7 @@ module WebSocket
when 2 then
if data == 0xFF
emit(:message, MessageEvent.new(Protocol.encode(@buffer)))
emit(:message, MessageEvent.new(Driver.encode(@buffer)))
@stage = 0
else
if @length
@@ -57,8 +57,8 @@ module WebSocket
def frame(data, type = nil, error_type = nil)
return queue([data, type, error_type]) if @ready_state == 0
data = Protocol.encode(data)
frame = ["\x00", data, "\xFF"].map(&Protocol.method(:encode)) * ''
data = Driver.encode(data)
frame = ["\x00", data, "\xFF"].map(&Driver.method(:encode)) * ''
@socket.write(frame)
true
end
@@ -1,5 +1,5 @@
module WebSocket
class Protocol
class Driver
class Draft76 < Draft75
BODY_SIZE = 8
@@ -1,5 +1,5 @@
module WebSocket
class Protocol
class Driver
module EventEmitter
def initialize
@@ -1,7 +1,7 @@
module WebSocket
class Protocol
class Driver
class Hybi < Protocol
class Hybi < Driver
root = File.expand_path('../hybi', __FILE__)
autoload :StreamReader, root + '/stream_reader'
@@ -107,7 +107,7 @@ module WebSocket
return false unless @ready_state == 1
data = data.to_s unless Array === data
data = Protocol.encode(data) if String === data
data = Driver.encode(data) if String === data
is_text = (String === data)
opcode = OPCODES[type || (is_text ? :text : :binary)]
@@ -151,7 +151,7 @@ module WebSocket
frame.concat(buffer)
@socket.write(Protocol.encode(frame))
@socket.write(Driver.encode(frame))
true
end
@@ -292,7 +292,7 @@ module WebSocket
@buffer.concat(payload)
if @final
message = @buffer
message = Protocol.encode(message, true) if @mode == :text
message = Driver.encode(message, true) if @mode == :text
reset
if message
emit(:message, MessageEvent.new(message))
@@ -303,7 +303,7 @@ module WebSocket
when OPCODES[:text] then
if @final
message = Protocol.encode(payload, true)
message = Driver.encode(payload, true)
if message
emit(:message, MessageEvent.new(message))
else
@@ -331,18 +331,18 @@ module WebSocket
code = ERRORS[:protocol_error]
end
if payload.size > 125 or not Protocol.valid_utf8?(payload[2..-1] || [])
if payload.size > 125 or not Driver.valid_utf8?(payload[2..-1] || [])
code = ERRORS[:protocol_error]
end
reason = (payload.size > 2) ? Protocol.encode(payload[2..-1], true) : ''
reason = (payload.size > 2) ? Driver.encode(payload[2..-1], true) : ''
shutdown(code, reason || '')
when OPCODES[:ping] then
frame(payload, :pong)
when OPCODES[:pong] then
message = Protocol.encode(payload, true)
message = Driver.encode(payload, true)
callback = @ping_callbacks[message]
@ping_callbacks.delete(message)
callback.call if callback
@@ -1,5 +1,5 @@
module WebSocket
class Protocol
class Driver
class Hybi
class StreamReader
@@ -1,5 +1,5 @@
module WebSocket
class Protocol
class Driver
# http://www.w3.org/International/questions/qa-forms-utf-8.en.php
UTF8_MATCH = /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/
end
+2 -2
View File
@@ -1,8 +1,8 @@
require 'rubygems'
require 'bundler/setup'
require File.expand_path('../../lib/websocket/protocol', __FILE__)
require File.expand_path('../websocket/protocol/draft75_examples', __FILE__)
require File.expand_path('../../lib/websocket/driver', __FILE__)
require File.expand_path('../websocket/driver/draft75_examples', __FILE__)
module EncodingHelper
def encode(message)
@@ -1,6 +1,6 @@
require "spec_helper"
describe WebSocket::Protocol::Client do
describe WebSocket::Driver::Client do
include EncodingHelper
let :socket do
@@ -18,13 +18,13 @@ describe WebSocket::Protocol::Client do
nil
end
let :protocol do
protocol = WebSocket::Protocol::Client.new(socket, options)
protocol.on(:open) { |e| @open = true }
protocol.on(:message) { |e| @message += e.data }
protocol.on(:error) { |e| @error = e }
protocol.on(:close) { |e| @close = [e.code, e.reason] }
protocol
let :driver do
driver = WebSocket::Driver::Client.new(socket, options)
driver.on(:open) { |e| @open = true }
driver.on(:message) { |e| @message += e.data }
driver.on(:error) { |e| @error = e }
driver.on(:close) { |e| @close = [e.code, e.reason] }
driver
end
let :key do
@@ -40,14 +40,14 @@ describe WebSocket::Protocol::Client do
end
before do
WebSocket::Protocol::Client.stub(:generate_key).and_return(key)
WebSocket::Driver::Client.stub(:generate_key).and_return(key)
@open = @error = @close = false
@message = ""
end
describe "in the beginning state" do
it "starts in no state" do
protocol.state.should == nil
driver.state.should == nil
end
describe :start do
@@ -60,11 +60,11 @@ describe WebSocket::Protocol::Client do
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"\r\n")
protocol.start
driver.start
end
it "returns true" do
protocol.start.should == true
driver.start.should == true
end
describe "with subprotocols" do
@@ -80,40 +80,40 @@ describe WebSocket::Protocol::Client do
"Sec-WebSocket-Version: 13\r\n" +
"Sec-WebSocket-Protocol: foo, bar, xmpp\r\n" +
"\r\n")
protocol.start
driver.start
end
end
it "changes the state to :connecting" do
protocol.start
protocol.state.should == :connecting
driver.start
driver.state.should == :connecting
end
end
end
describe "in the :connecting state" do
before { protocol.start }
before { driver.start }
describe "with a valid response" do
before { protocol.parse(response) }
before { driver.parse(response) }
it "changes the state to :open" do
@open.should == true
@close.should == false
protocol.state.should == :open
driver.state.should == :open
end
end
describe "with a valid response followed by a frame" do
before do
resp = response + WebSocket::Protocol.encode([0x81, 0x02, 72, 105])
protocol.parse(resp)
resp = response + WebSocket::Driver.encode([0x81, 0x02, 72, 105])
driver.parse(resp)
end
it "changes the state to :open" do
@open.should == true
@close.should == false
protocol.state.should == :open
driver.state.should == :open
end
it "parses the frame" do
@@ -124,28 +124,28 @@ describe WebSocket::Protocol::Client do
describe "with a bad Upgrade header" do
before do
resp = response.gsub(/websocket/, "wrong")
protocol.parse(resp)
driver.parse(resp)
end
it "changes the state to :closed" do
@open.should == false
@error.message.should == "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"
@close.should == [1002, "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"]
protocol.state.should == :closed
driver.state.should == :closed
end
end
describe "with a bad Accept header" do
before do
resp = response.gsub(/QV3/, "wrong")
protocol.parse(resp)
driver.parse(resp)
end
it "changes the state to :closed" do
@open.should == false
@error.message.should == "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"
@close.should == [1002, "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"]
protocol.state.should == :closed
driver.state.should == :closed
end
end
@@ -154,17 +154,17 @@ describe WebSocket::Protocol::Client do
before do
resp = response.gsub(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n")
protocol.parse(resp)
driver.parse(resp)
end
it "changes the state to :open" do
@open.should == true
@close.should == false
protocol.state.should == :open
driver.state.should == :open
end
it "selects the subprotocol" do
protocol.protocol.should == "xmpp"
driver.protocol.should == "xmpp"
end
end
@@ -173,18 +173,18 @@ describe WebSocket::Protocol::Client do
before do
resp = response.gsub(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n")
protocol.parse(resp)
driver.parse(resp)
end
it "changes the state to :closed" do
@open.should == false
@error.message.should == "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"
@close.should == [1002, "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"]
protocol.state.should == :closed
driver.state.should == :closed
end
it "selects no subprotocol" do
protocol.protocol.should == nil
driver.protocol.should == nil
end
end
end
@@ -4,111 +4,111 @@ require "spec_helper"
shared_examples_for "draft-75 protocol" do
describe "in the :open state" do
before { protocol.start }
before { driver.start }
describe :parse do
it "parses text frames" do
protocol.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
driver.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
@message.should == "Hello"
end
it "parses multiple frames from the same packet" do
protocol.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
driver.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
@message.should == "HelloHello"
end
it "parses text frames beginning 0x00-0x7F" do
protocol.parse [0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
driver.parse [0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
@message.should == "Hello"
end
it "ignores frames with a length header" do
protocol.parse [0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
driver.parse [0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
@message.should == "Hello"
end
it "parses multibyte text frames" do
protocol.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
driver.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
@message.should == encode("Apple = ")
end
it "parses frames received in several packets" do
protocol.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65]
protocol.parse [0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
driver.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65]
driver.parse [0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
@message.should == encode("Apple = ")
end
it "parses fragmented frames" do
protocol.parse [0x00, 0x48, 0x65, 0x6c]
protocol.parse [0x6c, 0x6f, 0xff]
driver.parse [0x00, 0x48, 0x65, 0x6c]
driver.parse [0x6c, 0x6f, 0xff]
@message.should == "Hello"
end
end
describe :frame do
it "formats the given string as a WebSocket frame" do
protocol.frame "Hello"
driver.frame "Hello"
@bytes.should == [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
end
it "encodes multibyte characters correctly" do
message = encode "Apple = "
protocol.frame message
driver.frame message
@bytes.should == [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]
end
it "returns true" do
protocol.frame("lol").should == true
driver.frame("lol").should == true
end
end
describe :ping do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.ping
driver.ping
end
it "returns false" do
protocol.ping.should == false
driver.ping.should == false
end
end
describe :close do
it "triggers the onclose event" do
protocol.close
driver.close
@close.should == true
end
it "returns true" do
protocol.close.should == true
driver.close.should == true
end
it "changes the state to :closed" do
protocol.close
protocol.state.should == :closed
driver.close
driver.state.should == :closed
end
end
end
describe "in the :closed state" do
before do
protocol.start
protocol.close
driver.start
driver.close
end
describe :close do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.close
driver.close
end
it "returns false" do
protocol.close.should == false
driver.close.should == false
end
it "leaves the protocol in the :closed state" do
protocol.close
protocol.state.should == :closed
driver.close
driver.state.should == :closed
end
end
end
@@ -2,7 +2,7 @@
require "spec_helper"
describe WebSocket::Protocol::Draft75 do
describe WebSocket::Driver::Draft75 do
include EncodingHelper
let :env do
@@ -22,12 +22,12 @@ describe WebSocket::Protocol::Draft75 do
socket
end
let :protocol do
protocol = WebSocket::Protocol::Draft75.new(socket)
protocol.on(:open) { |e| @open = true }
protocol.on(:message) { |e| @message += e.data }
protocol.on(:close) { |e| @close = true }
protocol
let :driver do
driver = WebSocket::Driver::Draft75.new(socket)
driver.on(:open) { |e| @open = true }
driver.on(:message) { |e| @message += e.data }
driver.on(:close) { |e| @close = true }
driver
end
before do
@@ -37,7 +37,7 @@ describe WebSocket::Protocol::Draft75 do
describe "in the :connecting state" do
it "starts in the :connecting state" do
protocol.state.should == :connecting
driver.state.should == :connecting
end
describe :start do
@@ -49,37 +49,37 @@ describe WebSocket::Protocol::Draft75 do
"WebSocket-Origin: http://www.example.com\r\n" +
"WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n")
protocol.start
driver.start
end
it "returns true" do
protocol.start.should == true
driver.start.should == true
end
it "triggers the onopen event" do
protocol.start
driver.start
@open.should == true
end
it "changes the state to :open" do
protocol.start
protocol.state.should == :open
driver.start
driver.state.should == :open
end
it "sets the protocol version" do
protocol.start
protocol.version.should == "hixie-75"
driver.start
driver.version.should == "hixie-75"
end
end
describe :frame do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.frame("Hello, world")
driver.frame("Hello, world")
end
it "returns true" do
protocol.frame("whatever").should == true
driver.frame("whatever").should == true
end
it "queues the frames until the handshake has been sent" do
@@ -92,8 +92,8 @@ describe WebSocket::Protocol::Draft75 do
"\r\n")
socket.should_receive(:write).with("\x00Hi\xFF")
protocol.frame("Hi")
protocol.start
driver.frame("Hi")
driver.start
@bytes.should == [0x00, 72, 105, 0xFF]
end
@@ -2,11 +2,11 @@
require "spec_helper"
describe WebSocket::Protocol::Draft76 do
describe WebSocket::Driver::Draft76 do
include EncodingHelper
let :body do
WebSocket::Protocol.encode [0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88]
WebSocket::Driver.encode [0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88]
end
let :response do
@@ -35,12 +35,12 @@ describe WebSocket::Protocol::Draft76 do
socket
end
let :protocol do
protocol = WebSocket::Protocol::Draft76.new(socket)
protocol.on(:open) { |e| @open = true }
protocol.on(:message) { |e| @message += e.data }
protocol.on(:close) { |e| @close = true }
protocol
let :driver do
driver = WebSocket::Driver::Draft76.new(socket)
driver.on(:open) { |e| @open = true }
driver.on(:message) { |e| @message += e.data }
driver.on(:close) { |e| @close = true }
driver
end
before do
@@ -50,7 +50,7 @@ describe WebSocket::Protocol::Draft76 do
describe "in the :connecting state" do
it "starts in the connecting state" do
protocol.state.should == :connecting
driver.state.should == :connecting
end
describe :start do
@@ -63,37 +63,37 @@ describe WebSocket::Protocol::Draft76 do
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n")
socket.should_receive(:write).with(response)
protocol.start
driver.start
end
it "returns true" do
protocol.start.should == true
driver.start.should == true
end
it "triggers the onopen event" do
protocol.start
driver.start
@open.should == true
end
it "changes the state to :open" do
protocol.start
protocol.state.should == :open
driver.start
driver.state.should == :open
end
it "sets the protocol version" do
protocol.start
protocol.version.should == "hixie-76"
driver.start
driver.version.should == "hixie-76"
end
end
describe :frame do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.frame("Hello, world")
driver.frame("Hello, world")
end
it "returns true" do
protocol.frame("whatever").should == true
driver.frame("whatever").should == true
end
it "queues the frames until the handshake has been sent" do
@@ -107,8 +107,8 @@ describe WebSocket::Protocol::Draft76 do
socket.should_receive(:write).with(response)
socket.should_receive(:write).with("\x00Hi\xFF")
protocol.frame("Hi")
protocol.start
driver.frame("Hi")
driver.start
@bytes.should == [0x00, 72, 105, 0xff]
end
@@ -126,42 +126,42 @@ describe WebSocket::Protocol::Draft76 do
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n")
protocol.start
driver.start
end
it "does not trigger the onopen event" do
protocol.start
driver.start
@open.should == false
end
it "leaves the protocol in the :connecting state" do
protocol.start
protocol.state.should == :connecting
driver.start
driver.state.should == :connecting
end
describe "when the request body is received" do
before { protocol.start }
before { driver.start }
it "sends the response body" do
socket.should_receive(:write).with(response)
protocol.parse(body)
driver.parse(body)
end
it "triggers the onopen event" do
protocol.parse(body)
driver.parse(body)
@open.should == true
end
it "changes the state to :open" do
protocol.parse(body)
protocol.state.should == :open
driver.parse(body)
driver.state.should == :open
end
it "sends any frames queued before the handshake was complete" do
socket.should_receive(:write).with(response)
socket.should_receive(:write).with("\x00hello\xFF")
protocol.frame("hello")
protocol.parse(body)
driver.frame("hello")
driver.parse(body)
@bytes.should == [0, 104, 101, 108, 108, 111, 255]
end
end
@@ -172,19 +172,19 @@ describe WebSocket::Protocol::Draft76 do
it_should_behave_like "draft-75 protocol"
describe "in the :open state" do
before { protocol.start }
before { driver.start }
describe :parse do
it "closes the socket if a close frame is received" do
protocol.parse [0xff, 0x00]
driver.parse [0xff, 0x00]
@close.should == true
protocol.state.should == :closed
driver.state.should == :closed
end
end
describe :close do
it "writes a close message to the socket" do
protocol.close
driver.close
@bytes.should == [0xff, 0x00]
end
end
@@ -2,7 +2,7 @@
require "spec_helper"
describe WebSocket::Protocol::Hybi do
describe WebSocket::Driver::Hybi do
include EncodingHelper
let :env do
@@ -28,13 +28,13 @@ describe WebSocket::Protocol::Hybi do
socket
end
let :protocol do
protocol = WebSocket::Protocol::Hybi.new(socket, options)
protocol.on(:open) { |e| @open = true }
protocol.on(:message) { |e| @message += e.data }
protocol.on(:error) { |e| @error = e }
protocol.on(:close) { |e| @close = [e.code, e.reason] }
protocol
let :driver do
driver = WebSocket::Driver::Hybi.new(socket, options)
driver.on(:open) { |e| @open = true }
driver.on(:message) { |e| @message += e.data }
driver.on(:error) { |e| @error = e }
driver.on(:close) { |e| @close = [e.code, e.reason] }
driver
end
before do
@@ -44,7 +44,7 @@ describe WebSocket::Protocol::Hybi do
describe "in the :connecting state" do
it "starts in the :connecting state" do
protocol.state.should == :connecting
driver.state.should == :connecting
end
describe :start do
@@ -55,11 +55,11 @@ describe WebSocket::Protocol::Hybi do
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n")
protocol.start
driver.start
end
it "returns true" do
protocol.start.should == true
driver.start.should == true
end
describe "with subprotocols" do
@@ -76,39 +76,39 @@ describe WebSocket::Protocol::Hybi do
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"Sec-WebSocket-Protocol: xmpp\r\n" +
"\r\n")
protocol.start
driver.start
end
it "sets the subprotocol" do
protocol.start
protocol.protocol.should == "xmpp"
driver.start
driver.protocol.should == "xmpp"
end
end
it "triggers the onopen event" do
protocol.start
driver.start
@open.should == true
end
it "changes the state to :open" do
protocol.start
protocol.state.should == :open
driver.start
driver.state.should == :open
end
it "sets the protocol version" do
protocol.start
protocol.version.should == "hybi-13"
driver.start
driver.version.should == "hybi-13"
end
end
describe :frame do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.frame("Hello, world")
driver.frame("Hello, world")
end
it "returns true" do
protocol.frame("whatever").should == true
driver.frame("whatever").should == true
end
it "queues the frames until the handshake has been sent" do
@@ -118,21 +118,21 @@ describe WebSocket::Protocol::Hybi do
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n")
socket.should_receive(:write).with(WebSocket::Protocol.encode [0x81, 0x02, 72, 105])
socket.should_receive(:write).with(WebSocket::Driver.encode [0x81, 0x02, 72, 105])
protocol.frame("Hi")
protocol.start
driver.frame("Hi")
driver.start
end
end
describe :ping do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.ping
driver.ping
end
it "returns true" do
protocol.ping.should == true
driver.ping.should == true
end
it "queues the ping until the handshake has been sent" do
@@ -142,37 +142,37 @@ describe WebSocket::Protocol::Hybi do
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n")
socket.should_receive(:write).with(WebSocket::Protocol.encode [0x89, 0])
socket.should_receive(:write).with(WebSocket::Driver.encode [0x89, 0])
protocol.ping
protocol.start
driver.ping
driver.start
end
end
describe :close do
it "does not write anything to the socket" do
socket.should_not_receive(:write)
protocol.close
driver.close
end
it "returns true" do
protocol.close.should == true
driver.close.should == true
end
it "triggers the onclose event" do
protocol.close
driver.close
@close.should == [1000, ""]
end
it "changes the state to :closed" do
protocol.close
protocol.state.should == :closed
driver.close
driver.state.should == :closed
end
end
end
describe "in the :open state" do
before { protocol.start }
before { driver.start }
describe :parse do
let(:mask) { (1..4).map { rand 255 } }
@@ -186,181 +186,181 @@ describe WebSocket::Protocol::Hybi do
end
it "parses unmasked text frames" do
protocol.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@message.should == "Hello"
end
it "parses multiple frames from the same packet" do
protocol.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@message.should == "HelloHello"
end
it "parses empty text frames" do
protocol.parse [0x81, 0x00]
driver.parse [0x81, 0x00]
@message.should == ""
end
it "parses fragmented text frames" do
protocol.parse [0x01, 0x03, 0x48, 0x65, 0x6c]
protocol.parse [0x80, 0x02, 0x6c, 0x6f]
driver.parse [0x01, 0x03, 0x48, 0x65, 0x6c]
driver.parse [0x80, 0x02, 0x6c, 0x6f]
@message.should == "Hello"
end
it "parses masked text frames" do
protocol.parse [0x81, 0x85] + mask + mask_message(0x48, 0x65, 0x6c, 0x6c, 0x6f)
driver.parse [0x81, 0x85] + mask + mask_message(0x48, 0x65, 0x6c, 0x6c, 0x6f)
@message.should == "Hello"
end
it "parses masked empty text frames" do
protocol.parse [0x81, 0x80] + mask + mask_message()
driver.parse [0x81, 0x80] + mask + mask_message()
@message.should == ""
end
it "parses masked fragmented text frames" do
protocol.parse [0x01, 0x81] + mask + mask_message(0x48)
protocol.parse [0x80, 0x84] + mask + mask_message(0x65, 0x6c, 0x6c, 0x6f)
driver.parse [0x01, 0x81] + mask + mask_message(0x48)
driver.parse [0x80, 0x84] + mask + mask_message(0x65, 0x6c, 0x6c, 0x6f)
@message.should == "Hello"
end
it "closes the socket if the frame has an unrecognized opcode" do
protocol.parse [0x83, 0x00]
driver.parse [0x83, 0x00]
@bytes[0..3].should == [0x88, 0x1e, 0x03, 0xea]
@error.message.should == "Unrecognized frame opcode: 3"
@close.should == [1002, "Unrecognized frame opcode: 3"]
protocol.state.should == :closed
driver.state.should == :closed
end
it "closes the socket if a close frame is received" do
protocol.parse [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
driver.parse [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@bytes.should == [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@close.should == [1000, "Hello"]
protocol.state.should == :closed
driver.state.should == :closed
end
it "parses unmasked multibyte text frames" do
protocol.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]
driver.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]
@message.should == encode("Apple = ")
end
it "parses frames received in several packets" do
protocol.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c]
protocol.parse [0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]
driver.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c]
driver.parse [0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]
@message.should == encode("Apple = ")
end
it "parses fragmented multibyte text frames" do
protocol.parse [0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]
protocol.parse [0x80, 0x01, 0xbf]
driver.parse [0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]
driver.parse [0x80, 0x01, 0xbf]
@message.should == encode("Apple = ")
end
it "parses masked multibyte text frames" do
protocol.parse [0x81, 0x8b] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf)
driver.parse [0x81, 0x8b] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf)
@message.should == encode("Apple = ")
end
it "parses masked fragmented multibyte text frames" do
protocol.parse [0x01, 0x8a] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3)
protocol.parse [0x80, 0x81] + mask + mask_message(0xbf)
driver.parse [0x01, 0x8a] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3)
driver.parse [0x80, 0x81] + mask + mask_message(0xbf)
@message.should == encode("Apple = ")
end
it "parses unmasked medium-length text frames" do
protocol.parse [0x81, 0x7e, 0x00, 0xc8] + [0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40
driver.parse [0x81, 0x7e, 0x00, 0xc8] + [0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40
@message.should == "Hello" * 40
end
it "parses masked medium-length text frames" do
protocol.parse [0x81, 0xfe, 0x00, 0xc8] + mask + mask_message(*([0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40))
driver.parse [0x81, 0xfe, 0x00, 0xc8] + mask + mask_message(*([0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40))
@message.should == "Hello" * 40
end
it "replies to pings with a pong" do
protocol.parse [0x89, 0x04, 0x4f, 0x48, 0x41, 0x49]
driver.parse [0x89, 0x04, 0x4f, 0x48, 0x41, 0x49]
@bytes.should == [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49]
end
end
describe :frame do
it "formats the given string as a WebSocket frame" do
protocol.frame "Hello"
driver.frame "Hello"
@bytes.should == [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
end
it "formats a byte array as a binary WebSocket frame" do
protocol.frame [0x48, 0x65, 0x6c]
driver.frame [0x48, 0x65, 0x6c]
@bytes.should == [0x82, 0x03, 0x48, 0x65, 0x6c]
end
it "encodes multibyte characters correctly" do
message = encode "Apple = "
protocol.frame message
driver.frame message
@bytes.should == [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]
end
it "encodes medium-length strings using extra length bytes" do
message = "Hello" * 40
protocol.frame message
driver.frame message
@bytes.should == [0x81, 0x7e, 0x00, 0xc8] + [0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40
end
it "encodes close frames with an error code" do
protocol.frame "Hello", :close, 1002
driver.frame "Hello", :close, 1002
@bytes.should == [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
end
it "encodes pong frames" do
protocol.frame "", :pong
driver.frame "", :pong
@bytes.should == [0x8a, 0x00]
end
end
describe :ping do
it "writes a ping frame to the socket" do
protocol.ping("mic check")
driver.ping("mic check")
@bytes.should == [0x89, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b]
end
it "returns true" do
protocol.ping.should == true
driver.ping.should == true
end
it "runs the given callback on matching pong" do
protocol.ping("Hi") { @reply = true }
protocol.parse [0x8a, 0x02, 72, 105]
driver.ping("Hi") { @reply = true }
driver.parse [0x8a, 0x02, 72, 105]
@reply.should == true
end
it "does not run the callback on non-matching pong" do
protocol.ping("Hi") { @reply = true }
protocol.parse [0x8a, 0x03, 119, 97, 116]
driver.ping("Hi") { @reply = true }
driver.parse [0x8a, 0x03, 119, 97, 116]
@reply.should == nil
end
end
describe :close do
it "writes a close frame to the socket" do
protocol.close("<%= reasons %>", 1003)
driver.close("<%= reasons %>", 1003)
@bytes.should == [0x88, 0x10, 0x03, 0xeb, 0x3c, 0x25, 0x3d, 0x20, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x25, 0x3e]
end
it "returns true" do
protocol.close.should == true
driver.close.should == true
end
it "does not trigger the onclose event" do
protocol.close
driver.close
@close.should == false
end
it "does not trigger the onerror event" do
protocol.close
driver.close
@error.should == false
end
it "changes the state to :closing" do
protocol.close
protocol.state.should == :closing
driver.close
driver.state.should == :closing
end
end
end
@@ -368,62 +368,62 @@ describe WebSocket::Protocol::Hybi do
describe "when masking is required" do
before do
options[:require_masking] = true
protocol.start
driver.start
end
it "does not emit a message" do
protocol.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@message.should == ""
end
it "returns an error" do
protocol.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
@close.should == [1003, "Received unmasked frame but masking is required"]
end
end
describe "in the :closing state" do
before do
protocol.start
protocol.close
driver.start
driver.close
end
describe :frame do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.frame("dropped")
driver.frame("dropped")
end
it "returns false" do
protocol.frame("wut").should == false
driver.frame("wut").should == false
end
end
describe :ping do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.ping
driver.ping
end
it "returns false" do
protocol.ping.should == false
driver.ping.should == false
end
end
describe :close do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.close
driver.close
end
it "returns false" do
protocol.close.should == false
driver.close.should == false
end
end
describe "receiving a close frame" do
before do
protocol.parse [0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b]
driver.parse [0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b]
end
it "triggers the onclose event" do
@@ -431,53 +431,53 @@ describe WebSocket::Protocol::Hybi do
end
it "changes the state to :closed" do
protocol.state.should == :closed
driver.state.should == :closed
end
end
end
describe "in the :closed state" do
before do
protocol.start
protocol.close
protocol.parse [0x88, 0x02, 0x03, 0xea]
driver.start
driver.close
driver.parse [0x88, 0x02, 0x03, 0xea]
end
describe :frame do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.frame("dropped")
driver.frame("dropped")
end
it "returns false" do
protocol.frame("wut").should == false
driver.frame("wut").should == false
end
end
describe :ping do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.ping
driver.ping
end
it "returns false" do
protocol.ping.should == false
driver.ping.should == false
end
end
describe :close do
it "does not write to the socket" do
socket.should_not_receive(:write)
protocol.close
driver.close
end
it "returns false" do
protocol.close.should == false
driver.close.should == false
end
it "leaves the state as :closed" do
protocol.close
protocol.state.should == :closed
driver.close
driver.state.should == :closed
end
end
end
@@ -1,10 +1,10 @@
Gem::Specification.new do |s|
s.name = 'websocket-protocol'
s.name = 'websocket-driver'
s.version = '0.1.0'
s.summary = 'WebSocket protocol handler with pluggable I/O'
s.author = 'James Coglan'
s.email = 'jcoglan@gmail.com'
s.homepage = 'http://github.com/faye/websocket-protocol-ruby'
s.homepage = 'http://github.com/faye/websocket-driver-ruby'
s.extra_rdoc_files = %w[README.md]
s.rdoc_options = %w[--main README.md --markup markdown]