├── Gemfile ├── .gitignore ├── lib └── websocket │ ├── websocket_mask.rb │ ├── mask.rb │ ├── http.rb │ ├── driver │ ├── hybi │ │ ├── frame.rb │ │ └── message.rb │ ├── headers.rb │ ├── event_emitter.rb │ ├── stream_reader.rb │ ├── proxy.rb │ ├── server.rb │ ├── draft75.rb │ ├── draft76.rb │ ├── client.rb │ └── hybi.rb │ ├── http │ ├── response.rb │ ├── request.rb │ └── headers.rb │ └── driver.rb ├── ext └── websocket-driver │ ├── extconf.rb │ ├── websocket_mask.c │ └── WebsocketMaskService.java ├── CODE_OF_CONDUCT.md ├── spec ├── spec_helper.rb └── websocket │ └── driver │ ├── draft75_spec.rb │ ├── draft75_examples.rb │ ├── draft76_spec.rb │ ├── client_spec.rb │ └── hybi_spec.rb ├── LICENSE.md ├── examples ├── em_server.rb ├── em_client.rb └── tcp_client.rb ├── Rakefile ├── .github └── workflows │ └── test.yml ├── websocket-driver.gemspec ├── CHANGELOG.md └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | Makefile 3 | pkg 4 | tmp 5 | *.gem 6 | *.jar 7 | -------------------------------------------------------------------------------- /lib/websocket/websocket_mask.rb: -------------------------------------------------------------------------------- 1 | # Load C native extension 2 | require "websocket-driver/websocket_mask" 3 | -------------------------------------------------------------------------------- /ext/websocket-driver/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | extension_name = 'websocket_mask' 3 | dir_config(extension_name) 4 | create_makefile(extension_name) 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All projects under the [Faye](https://github.com/faye) umbrella are covered by 4 | the [Code of Conduct](https://github.com/faye/code-of-conduct). 5 | -------------------------------------------------------------------------------- /lib/websocket/mask.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | module Mask 3 | def self.mask(payload, mask) 4 | return payload if mask.nil? || payload.empty? 5 | 6 | payload.tap do |result| 7 | payload.bytesize.times do |i| 8 | result.setbyte(i, payload.getbyte(i) ^ mask.getbyte(i % 4)) 9 | end 10 | end 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | 3 | require File.expand_path('../../lib/websocket/driver', __FILE__) 4 | require File.expand_path('../websocket/driver/draft75_examples', __FILE__) 5 | 6 | module EncodingHelper 7 | def encode(message, encoding = nil) 8 | WebSocket::Driver.encode(message, encoding) 9 | end 10 | 11 | def bytes(string) 12 | string.bytes.to_a 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/websocket/http.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | module HTTP 3 | 4 | root = File.expand_path('../http', __FILE__) 5 | 6 | autoload :Headers, root + '/headers' 7 | autoload :Request, root + '/request' 8 | autoload :Response, root + '/response' 9 | 10 | def self.normalize_header(name) 11 | name.to_s.strip.downcase.gsub(/^http_/, '').gsub(/_/, '-') 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi/frame.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | class Hybi 4 | 5 | class Frame 6 | attr_accessor :final, 7 | :rsv1, 8 | :rsv2, 9 | :rsv3, 10 | :opcode, 11 | :masked, 12 | :masking_key, 13 | :length_bytes, 14 | :length, 15 | :payload 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2010-2025 James Coglan 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /lib/websocket/http/response.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | module HTTP 3 | 4 | class Response 5 | include Headers 6 | 7 | STATUS_LINE = /^(HTTP\/[0-9]+\.[0-9]+) ([0-9]{3}) ([\x20-\x7e]*)$/ 8 | 9 | attr_reader :code 10 | 11 | def [](name) 12 | @headers[HTTP.normalize_header(name)] 13 | end 14 | 15 | def body 16 | @buffer.pack('C*') 17 | end 18 | 19 | private 20 | 21 | def start_line(line) 22 | return false unless parsed = line.scan(STATUS_LINE).first 23 | @code = parsed[1].to_i 24 | true 25 | end 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/em_server.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'eventmachine' 3 | require 'websocket/driver' 4 | require 'permessage_deflate' 5 | 6 | module Connection 7 | def initialize 8 | @driver = WebSocket::Driver.server(self) 9 | @driver.add_extension(PermessageDeflate) 10 | 11 | @driver.on(:connect) { |e| @driver.start if WebSocket::Driver.websocket? @driver.env } 12 | @driver.on(:message) { |e| @driver.frame(e.data) } 13 | @driver.on(:close) { |e| close_connection_after_writing } 14 | end 15 | 16 | def receive_data(data) 17 | @driver.parse(data) 18 | end 19 | 20 | def write(data) 21 | send_data(data) 22 | end 23 | end 24 | 25 | EM.run { 26 | EM.start_server('127.0.0.1', ARGV[0], Connection) 27 | } 28 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems/package_task' 2 | 3 | spec = Gem::Specification.load('websocket-driver.gemspec') 4 | 5 | Gem::PackageTask.new(spec) do |pkg| 6 | end 7 | 8 | if RUBY_PLATFORM =~ /java/ 9 | require 'rake/javaextensiontask' 10 | Rake::JavaExtensionTask.new('websocket-driver', spec) do |ext| 11 | ext.name = 'websocket_mask' 12 | ext.source_version = '8' 13 | ext.target_version = '8' 14 | end 15 | else 16 | require 'rake/extensiontask' 17 | Rake::ExtensionTask.new('websocket-driver', spec) do |ext| 18 | ext.name = 'websocket_mask' 19 | end 20 | end 21 | 22 | task :clean do 23 | Dir['./**/*.{bundle,jar,o,so}'].each do |path| 24 | puts "Deleting #{path} ..." 25 | File.delete(path) 26 | end 27 | FileUtils.rm_rf('./pkg') 28 | FileUtils.rm_rf('./tmp') 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - push 3 | - pull_request 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | ruby: 11 | - ruby-2.3 12 | - ruby-2.4 13 | - ruby-2.5 14 | - ruby-2.6 15 | - ruby-2.7 16 | - ruby-3.0 17 | - ruby-3.1 18 | - ruby-3.2 19 | - ruby-3.3 20 | - ruby-3.4 21 | - jruby-9.3 22 | - jruby-9.4 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | - run: ruby --version 31 | - run: bundle exec rake compile 32 | - run: bundle exec rspec 33 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi/message.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | class Hybi 4 | 5 | class Message 6 | attr_accessor :rsv1, 7 | :rsv2, 8 | :rsv3, 9 | :opcode, 10 | :data 11 | 12 | def initialize 13 | @rsv1 = false 14 | @rsv2 = false 15 | @rsv3 = false 16 | @opcode = nil 17 | @data = String.new('').force_encoding(Encoding::BINARY) 18 | end 19 | 20 | def <<(frame) 21 | @rsv1 ||= frame.rsv1 22 | @rsv2 ||= frame.rsv2 23 | @rsv3 ||= frame.rsv3 24 | @opcode ||= frame.opcode 25 | @data << frame.payload 26 | end 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /ext/websocket-driver/websocket_mask.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | VALUE method_websocket_mask(VALUE self, VALUE payload, VALUE mask) 4 | { 5 | char *payload_s, *mask_s, *unmasked_s; 6 | long i, n; 7 | VALUE unmasked; 8 | 9 | if (mask == Qnil || RSTRING_LEN(mask) != 4) { 10 | return payload; 11 | } 12 | 13 | payload_s = RSTRING_PTR(payload); 14 | mask_s = RSTRING_PTR(mask); 15 | n = RSTRING_LEN(payload); 16 | 17 | unmasked = rb_str_new(0, n); 18 | unmasked_s = RSTRING_PTR(unmasked); 19 | 20 | for (i = 0; i < n; i++) { 21 | unmasked_s[i] = payload_s[i] ^ mask_s[i % 4]; 22 | } 23 | return unmasked; 24 | } 25 | 26 | void Init_websocket_mask() 27 | { 28 | VALUE WebSocket = rb_define_module("WebSocket"); 29 | VALUE Mask = rb_define_module_under(WebSocket, "Mask"); 30 | 31 | rb_define_singleton_method(Mask, "mask", method_websocket_mask, 2); 32 | } 33 | -------------------------------------------------------------------------------- /examples/em_client.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'eventmachine' 3 | require 'websocket/driver' 4 | require 'permessage_deflate' 5 | 6 | module Connection 7 | attr_accessor :url 8 | 9 | def connection_completed 10 | @driver = WebSocket::Driver.client(self) 11 | @driver.add_extension(PermessageDeflate) 12 | 13 | @driver.on(:open) { |event| @driver.text('Hello, world') } 14 | @driver.on(:message) { |event| p [:message, event.data] } 15 | @driver.on(:close) { |event| finalize(event) } 16 | 17 | @driver.start 18 | end 19 | 20 | def receive_data(data) 21 | @driver.parse(data) 22 | end 23 | 24 | def write(data) 25 | send_data(data) 26 | end 27 | 28 | def finalize(event) 29 | p [:close, event.code, event.reason] 30 | close_connection 31 | end 32 | end 33 | 34 | EM.run { 35 | url = ARGV.first 36 | uri = URI.parse(url) 37 | 38 | EM.connect(uri.host, uri.port, Connection) do |conn| 39 | conn.url = url 40 | end 41 | } 42 | -------------------------------------------------------------------------------- /lib/websocket/driver/headers.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Headers 5 | ALLOWED_DUPLICATES = %w[set-cookie set-cookie2 warning www-authenticate] 6 | 7 | def initialize(received = {}) 8 | @raw = received 9 | clear 10 | 11 | @received = {} 12 | @raw.each { |k,v| @received[HTTP.normalize_header(k)] = v } 13 | end 14 | 15 | def clear 16 | @sent = Set.new 17 | @lines = [] 18 | end 19 | 20 | def [](name) 21 | @received[HTTP.normalize_header(name)] 22 | end 23 | 24 | def []=(name, value) 25 | return if value.nil? 26 | key = HTTP.normalize_header(name) 27 | return unless @sent.add?(key) or ALLOWED_DUPLICATES.include?(key) 28 | @lines << "#{ name.strip }: #{ value.to_s.strip }\r\n" 29 | end 30 | 31 | def inspect 32 | @raw.inspect 33 | end 34 | 35 | def to_h 36 | @raw.dup 37 | end 38 | 39 | def to_s 40 | @lines.join('') 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /examples/tcp_client.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'websocket/driver' 3 | require 'permessage_deflate' 4 | require 'socket' 5 | require 'uri' 6 | 7 | class WSClient 8 | attr_reader :url, :thread 9 | 10 | def initialize(url) 11 | uri = URI.parse(url) 12 | 13 | @url = url 14 | @tcp = TCPSocket.new(uri.host, uri.port) 15 | @dead = false 16 | 17 | @driver = WebSocket::Driver.client(self) 18 | @driver.add_extension(PermessageDeflate) 19 | 20 | @driver.on(:open) { |event| send "Hello world!" } 21 | @driver.on(:message) { |event| p [:message, event.data] } 22 | @driver.on(:close) { |event| finalize(event) } 23 | 24 | @thread = Thread.new do 25 | @driver.parse(@tcp.read(1)) until @dead 26 | end 27 | 28 | @driver.start 29 | end 30 | 31 | def send(message) 32 | @driver.text(message) 33 | end 34 | 35 | def write(data) 36 | @tcp.write(data) 37 | end 38 | 39 | def close 40 | @driver.close 41 | end 42 | 43 | def finalize(event) 44 | p [:close, event.code, event.reason] 45 | @dead = true 46 | @thread.kill 47 | end 48 | end 49 | 50 | ws = WSClient.new(ARGV.first) 51 | ws.thread.join 52 | -------------------------------------------------------------------------------- /websocket-driver.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'websocket-driver' 3 | s.version = '0.8.0' 4 | s.summary = 'WebSocket protocol handler with pluggable I/O' 5 | s.author = 'James Coglan' 6 | s.email = 'jcoglan@gmail.com' 7 | s.homepage = 'https://github.com/faye/websocket-driver-ruby' 8 | s.license = 'Apache-2.0' 9 | 10 | s.metadata['changelog_uri'] = s.homepage + '/blob/main/CHANGELOG.md' 11 | 12 | s.extra_rdoc_files = %w[README.md] 13 | s.rdoc_options = %w[--main README.md --markup markdown] 14 | s.require_paths = %w[lib] 15 | 16 | files = %w[CHANGELOG.md LICENSE.md README.md] + 17 | Dir.glob('ext/**/*.{c,java,rb}') + 18 | Dir.glob('lib/**/*.rb') 19 | 20 | if RUBY_PLATFORM =~ /java/ 21 | s.platform = 'java' 22 | files << 'lib/websocket_mask.jar' 23 | else 24 | s.extensions << 'ext/websocket-driver/extconf.rb' 25 | end 26 | 27 | s.files = files 28 | 29 | s.add_dependency 'base64' 30 | s.add_dependency 'websocket-extensions', '>= 0.1.0' 31 | 32 | s.add_development_dependency 'eventmachine' 33 | s.add_development_dependency 'permessage_deflate' 34 | s.add_development_dependency 'rake-compiler' 35 | s.add_development_dependency 'rspec' 36 | 37 | if RUBY_VERSION < '2.0.0' 38 | s.add_development_dependency 'rake', '< 12.3.0' 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/websocket/http/request.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | module HTTP 3 | 4 | class Request 5 | include Headers 6 | 7 | REQUEST_LINE = /^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) ([\x21-\x7e]+) (HTTP\/[0-9]+\.[0-9]+)$/ 8 | REQUEST_TARGET = /^(.*?)(\?(.*))?$/ 9 | RESERVED_HEADERS = %w[content-length content-type] 10 | 11 | attr_reader :env 12 | 13 | private 14 | 15 | def start_line(line) 16 | return false unless parsed = line.scan(REQUEST_LINE).first 17 | 18 | target = parsed[1].scan(REQUEST_TARGET).first 19 | 20 | @env = { 21 | 'REQUEST_METHOD' => parsed[0], 22 | 'SCRIPT_NAME' => '', 23 | 'PATH_INFO' => target[0], 24 | 'QUERY_STRING' => target[2] || '' 25 | } 26 | true 27 | end 28 | 29 | def complete 30 | super 31 | @headers.each do |name, value| 32 | rack_name = name.upcase.gsub(/-/, '_') 33 | rack_name = "HTTP_#{ rack_name }" unless RESERVED_HEADERS.include?(name) 34 | @env[rack_name] = value 35 | end 36 | if host = @env['HTTP_HOST'] 37 | uri = URI.parse("http://#{ host }") 38 | @env['SERVER_NAME'] = uri.host 39 | @env['SERVER_PORT'] = uri.port.to_s 40 | end 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/websocket/driver/event_emitter.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | module EventEmitter 5 | def initialize 6 | @listeners = Hash.new { |h,k| h[k] = [] } 7 | end 8 | 9 | def add_listener(event, callable = nil, &block) 10 | listener = callable || block 11 | @listeners[event.to_s] << listener 12 | listener 13 | end 14 | 15 | def on(event, callable = nil, &block) 16 | if callable 17 | add_listener(event, callable) 18 | else 19 | add_listener(event, &block) 20 | end 21 | end 22 | 23 | def remove_listener(event, callable = nil, &block) 24 | listener = callable || block 25 | @listeners[event.to_s].delete(listener) 26 | listener 27 | end 28 | 29 | def remove_all_listeners(event = nil) 30 | if event 31 | @listeners.delete(event.to_s) 32 | else 33 | @listeners.clear 34 | end 35 | end 36 | 37 | def emit(event, *args) 38 | @listeners[event.to_s].dup.each do |listener| 39 | listener.call(*args) 40 | end 41 | end 42 | 43 | def listener_count(event) 44 | return 0 unless @listeners.has_key?(event.to_s) 45 | @listeners[event.to_s].size 46 | end 47 | 48 | def listeners(event) 49 | @listeners[event.to_s] 50 | end 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/websocket/driver/stream_reader.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class StreamReader 5 | # Try to minimise the number of reallocations done: 6 | MINIMUM_AUTOMATIC_PRUNE_OFFSET = 128 7 | 8 | def initialize 9 | @buffer = String.new('').force_encoding(Encoding::BINARY) 10 | @offset = 0 11 | end 12 | 13 | def put(chunk) 14 | return unless chunk and chunk.bytesize > 0 15 | @buffer << chunk.force_encoding(Encoding::BINARY) 16 | end 17 | 18 | # Read bytes from the data: 19 | def read(length) 20 | return nil if (@offset + length) > @buffer.bytesize 21 | 22 | chunk = @buffer.byteslice(@offset, length) 23 | @offset += chunk.bytesize 24 | 25 | prune if @offset > MINIMUM_AUTOMATIC_PRUNE_OFFSET 26 | 27 | return chunk 28 | end 29 | 30 | def each_byte 31 | prune 32 | 33 | @buffer.each_byte do |octet| 34 | @offset += 1 35 | yield octet 36 | end 37 | end 38 | 39 | private 40 | 41 | def prune 42 | buffer_size = @buffer.bytesize 43 | 44 | if @offset > buffer_size 45 | @buffer = String.new('').force_encoding(Encoding::BINARY) 46 | else 47 | @buffer = @buffer.byteslice(@offset, buffer_size - @offset) 48 | end 49 | 50 | @offset = 0 51 | end 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/websocket/driver/proxy.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Proxy 5 | include EventEmitter 6 | 7 | attr_reader :status, :headers 8 | 9 | def initialize(client, origin, options) 10 | super() 11 | 12 | @client = client 13 | @http = HTTP::Response.new 14 | @socket = client.instance_variable_get(:@socket) 15 | @origin = URI.parse(@socket.url) 16 | @url = URI.parse(origin) 17 | @options = options 18 | @state = 0 19 | 20 | @headers = Headers.new 21 | @headers['Host'] = Driver.host_header(@origin) 22 | @headers['Connection'] = 'keep-alive' 23 | @headers['Proxy-Connection'] = 'keep-alive' 24 | 25 | if @url.user 26 | auth = Base64.strict_encode64([@url.user, @url.password] * ':') 27 | @headers['Proxy-Authorization'] = 'Basic ' + auth 28 | end 29 | end 30 | 31 | def set_header(name, value) 32 | return false unless @state == 0 33 | @headers[name] = value 34 | true 35 | end 36 | 37 | def start 38 | return false unless @state == 0 39 | @state = 1 40 | 41 | port = @origin.port || PORTS[@origin.scheme] 42 | start = "CONNECT #{ @origin.host }:#{ port } HTTP/1.1" 43 | headers = [start, @headers.to_s, ''] 44 | 45 | @socket.write(headers.join("\r\n")) 46 | true 47 | end 48 | 49 | def parse(chunk) 50 | @http.parse(chunk) 51 | return unless @http.complete? 52 | 53 | @status = @http.code 54 | @headers = Headers.new(@http.headers) 55 | 56 | if @status == 200 57 | emit(:connect, ConnectEvent.new) 58 | else 59 | message = "Can't establish a connection to the server at #{ @socket.url }" 60 | emit(:error, ProtocolError.new(message)) 61 | end 62 | end 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /ext/websocket-driver/WebsocketMaskService.java: -------------------------------------------------------------------------------- 1 | package com.jcoglan.websocket; 2 | 3 | import java.io.IOException; 4 | import org.jruby.Ruby; 5 | import org.jruby.RubyClass; 6 | import org.jruby.RubyModule; 7 | import org.jruby.RubyObject; 8 | import org.jruby.RubyString; 9 | import org.jruby.anno.JRubyMethod; 10 | import org.jruby.runtime.ObjectAllocator; 11 | import org.jruby.runtime.ThreadContext; 12 | import org.jruby.runtime.builtin.IRubyObject; 13 | import org.jruby.runtime.load.BasicLibraryService; 14 | 15 | public class WebsocketMaskService implements BasicLibraryService { 16 | private Ruby runtime; 17 | 18 | public boolean basicLoad(Ruby runtime) throws IOException { 19 | this.runtime = runtime; 20 | 21 | RubyModule websocket = runtime.defineModule("WebSocket"); 22 | RubyClass webSocketMask = websocket.defineClassUnder("Mask", runtime.getObject(), getAllocator()); 23 | 24 | webSocketMask.defineAnnotatedMethods(WebsocketMask.class); 25 | return true; 26 | } 27 | 28 | ObjectAllocator getAllocator() { 29 | return new ObjectAllocator() { 30 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 31 | return new WebsocketMask(runtime, rubyClass); 32 | } 33 | }; 34 | } 35 | 36 | public class WebsocketMask extends RubyObject { 37 | public WebsocketMask(final Ruby runtime, RubyClass rubyClass) { 38 | super(runtime, rubyClass); 39 | } 40 | 41 | @JRubyMethod 42 | public IRubyObject mask(ThreadContext context, IRubyObject payload, IRubyObject mask) { 43 | if (mask.isNil()) return payload; 44 | 45 | byte[] payload_a = ((RubyString)payload).getBytes(); 46 | byte[] mask_a = ((RubyString)mask).getBytes(); 47 | int i, n = payload_a.length; 48 | 49 | if (n == 0) return payload; 50 | 51 | for (i = 0; i < n; i++) { 52 | payload_a[i] ^= mask_a[i % 4]; 53 | } 54 | return RubyString.newStringNoCopy(runtime, payload_a); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/websocket/driver/server.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Server < Driver 5 | EVENTS = %w[open message error close ping pong] 6 | 7 | def initialize(socket, options = {}) 8 | super 9 | @http = HTTP::Request.new 10 | @delegate = nil 11 | end 12 | 13 | def env 14 | @http.complete? ? @http.env : nil 15 | end 16 | 17 | def url 18 | return nil unless e = env 19 | 20 | url = "ws://#{ e['HTTP_HOST'] }" 21 | url << e['PATH_INFO'] 22 | url << "?#{ e['QUERY_STRING'] }" unless e['QUERY_STRING'] == '' 23 | url 24 | end 25 | 26 | %w[add_extension set_header start frame text binary ping close].each do |method| 27 | define_method(method) do |*args, &block| 28 | if @delegate 29 | @delegate.__send__(method, *args, &block) 30 | else 31 | @queue << [method, args, block] 32 | true 33 | end 34 | end 35 | end 36 | 37 | %w[protocol version].each do |method| 38 | define_method(method) do 39 | @delegate && @delegate.__send__(method) 40 | end 41 | end 42 | 43 | def parse(chunk) 44 | return @delegate.parse(chunk) if @delegate 45 | 46 | @http.parse(chunk) 47 | return fail_request('Invalid HTTP request') if @http.error? 48 | return unless @http.complete? 49 | 50 | @delegate = Driver.rack(self, @options) 51 | open 52 | 53 | EVENTS.each do |event| 54 | @delegate.on(event) { |e| emit(event, e) } 55 | end 56 | 57 | emit(:connect, ConnectEvent.new) 58 | end 59 | 60 | def write(buffer) 61 | @socket.write(buffer) 62 | end 63 | 64 | private 65 | 66 | def fail_request(message) 67 | emit(:error, ProtocolError.new(message)) 68 | emit(:close, CloseEvent.new(Hybi::ERRORS[:protocol_error], message)) 69 | end 70 | 71 | def open 72 | @queue.each do |method, args, block| 73 | @delegate.__send__(method, *args, &block) 74 | end 75 | @queue = [] 76 | end 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/websocket/driver/draft75.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Draft75 < Driver 5 | def initialize(socket, options = {}) 6 | super 7 | 8 | @stage = 0 9 | @closing = false 10 | 11 | @headers['Upgrade'] = 'WebSocket' 12 | @headers['Connection'] = 'Upgrade' 13 | @headers['WebSocket-Origin'] = @socket.env['HTTP_ORIGIN'] 14 | @headers['WebSocket-Location'] = @socket.url 15 | end 16 | 17 | def version 18 | 'hixie-75' 19 | end 20 | 21 | def close(reason = nil, code = nil) 22 | return false if @ready_state == 3 23 | @ready_state = 3 24 | emit(:close, CloseEvent.new(nil, nil)) 25 | true 26 | end 27 | 28 | def parse(chunk) 29 | return if @ready_state > 1 30 | 31 | @reader.put(chunk) 32 | 33 | @reader.each_byte do |octet| 34 | case @stage 35 | when -1 then 36 | @body << octet 37 | send_handshake_body 38 | 39 | when 0 then 40 | parse_leading_byte(octet) 41 | 42 | when 1 then 43 | @length = (octet & 0x7F) + 128 * @length 44 | 45 | if @closing and @length.zero? 46 | return close 47 | elsif (octet & 0x80) != 0x80 48 | if @length.zero? 49 | @stage = 0 50 | else 51 | @skipped = 0 52 | @stage = 2 53 | end 54 | end 55 | 56 | when 2 then 57 | if octet == 0xFF 58 | @stage = 0 59 | emit(:message, MessageEvent.new(Driver.encode(@buffer, Encoding::UTF_8))) 60 | else 61 | if @length 62 | @skipped += 1 63 | @stage = 0 if @skipped == @length 64 | else 65 | @buffer << octet 66 | return close if @buffer.size > @max_length 67 | end 68 | end 69 | end 70 | end 71 | end 72 | 73 | def frame(buffer, type = nil, error_type = nil) 74 | return queue([buffer, type, error_type]) if @ready_state == 0 75 | frame = [0x00, buffer, 0xFF].pack('CA*C') 76 | @socket.write(frame) 77 | true 78 | end 79 | 80 | private 81 | 82 | def handshake_response 83 | start = 'HTTP/1.1 101 Web Socket Protocol Handshake' 84 | headers = [start, @headers.to_s, ''] 85 | headers.join("\r\n") 86 | end 87 | 88 | def parse_leading_byte(octet) 89 | if (octet & 0x80) == 0x80 90 | @length = 0 91 | @stage = 1 92 | else 93 | @length = nil 94 | @skipped = nil 95 | @buffer = [] 96 | @stage = 2 97 | end 98 | end 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/websocket/driver/draft76.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Draft76 < Draft75 5 | BODY_SIZE = 8 6 | 7 | def initialize(socket, options = {}) 8 | super 9 | input = (@socket.env['rack.input'] || StringIO.new('')).read 10 | input = input.dup if input.frozen? 11 | @stage = -1 12 | @body = input.force_encoding(Encoding::BINARY) 13 | 14 | @headers.clear 15 | @headers['Upgrade'] = 'WebSocket' 16 | @headers['Connection'] = 'Upgrade' 17 | @headers['Sec-WebSocket-Origin'] = @socket.env['HTTP_ORIGIN'] 18 | @headers['Sec-WebSocket-Location'] = @socket.url 19 | end 20 | 21 | def version 22 | 'hixie-76' 23 | end 24 | 25 | def start 26 | return false unless super 27 | send_handshake_body 28 | true 29 | end 30 | 31 | def close(reason = nil, code = nil) 32 | return false if @ready_state == 3 33 | @socket.write([0xFF, 0x00].pack('C*')) if @ready_state == 1 34 | @ready_state = 3 35 | emit(:close, CloseEvent.new(nil, nil)) 36 | true 37 | end 38 | 39 | private 40 | 41 | def handshake_response 42 | env = @socket.env 43 | key1 = env['HTTP_SEC_WEBSOCKET_KEY1'] 44 | key2 = env['HTTP_SEC_WEBSOCKET_KEY2'] 45 | 46 | raise ProtocolError.new('Missing required header: Sec-WebSocket-Key1') unless key1 47 | raise ProtocolError.new('Missing required header: Sec-WebSocket-Key2') unless key2 48 | 49 | number1 = number_from_key(key1) 50 | spaces1 = spaces_in_key(key1) 51 | 52 | number2 = number_from_key(key2) 53 | spaces2 = spaces_in_key(key2) 54 | 55 | if number1 % spaces1 != 0 or number2 % spaces2 != 0 56 | raise ProtocolError.new('Client sent invalid Sec-WebSocket-Key headers') 57 | end 58 | 59 | @key_values = [number1 / spaces1, number2 / spaces2] 60 | 61 | start = 'HTTP/1.1 101 WebSocket Protocol Handshake' 62 | headers = [start, @headers.to_s, ''] 63 | headers.join("\r\n") 64 | end 65 | 66 | def handshake_signature 67 | return nil unless @body.bytesize >= BODY_SIZE 68 | 69 | head = @body[0...BODY_SIZE] 70 | Digest::MD5.digest((@key_values + [head]).pack('N2A*')) 71 | end 72 | 73 | def send_handshake_body 74 | return unless signature = handshake_signature 75 | @socket.write(signature) 76 | @stage = 0 77 | open 78 | parse(@body[BODY_SIZE..-1]) if @body.bytesize > BODY_SIZE 79 | end 80 | 81 | def parse_leading_byte(octet) 82 | return super unless octet == 0xFF 83 | @closing = true 84 | @length = 0 85 | @stage = 1 86 | end 87 | 88 | def number_from_key(key) 89 | number = key.scan(/[0-9]/).join('') 90 | number == '' ? Float::NAN : number.to_i(10) 91 | end 92 | 93 | def spaces_in_key(key) 94 | key.scan(/ /).size 95 | end 96 | end 97 | 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft75_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | require "spec_helper" 4 | 5 | describe WebSocket::Driver::Draft75 do 6 | include EncodingHelper 7 | 8 | let :env do 9 | { 10 | "REQUEST_METHOD" => "GET", 11 | "HTTP_CONNECTION" => "Upgrade", 12 | "HTTP_UPGRADE" => "WebSocket", 13 | "HTTP_ORIGIN" => "http://www.example.com" 14 | } 15 | end 16 | 17 | let :socket do 18 | socket = double(WebSocket) 19 | allow(socket).to receive(:env).and_return(env) 20 | allow(socket).to receive(:url).and_return("ws://www.example.com/socket") 21 | allow(socket).to receive(:write) { |message| @bytes = bytes(message) } 22 | socket 23 | end 24 | 25 | let :driver do 26 | driver = WebSocket::Driver::Draft75.new(socket) 27 | driver.on(:open) { |e| @open = true } 28 | driver.on(:message) { |e| @message += e.data } 29 | driver.on(:close) { |e| @close = true } 30 | driver 31 | end 32 | 33 | before do 34 | @open = @close = false 35 | @message = "" 36 | end 37 | 38 | describe "in the :connecting state" do 39 | it "starts in the :connecting state" do 40 | expect(driver.state).to eq :connecting 41 | end 42 | 43 | describe :start do 44 | it "writes the handshake response to the socket" do 45 | expect(socket).to receive(:write).with( 46 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + 47 | "Upgrade: WebSocket\r\n" + 48 | "Connection: Upgrade\r\n" + 49 | "WebSocket-Origin: http://www.example.com\r\n" + 50 | "WebSocket-Location: ws://www.example.com/socket\r\n" + 51 | "\r\n") 52 | driver.start 53 | end 54 | 55 | it "returns true" do 56 | expect(driver.start).to eq true 57 | end 58 | 59 | it "triggers the onopen event" do 60 | driver.start 61 | expect(@open).to eq true 62 | end 63 | 64 | it "changes the state to :open" do 65 | driver.start 66 | expect(driver.state).to eq :open 67 | end 68 | 69 | it "sets the protocol version" do 70 | driver.start 71 | expect(driver.version).to eq "hixie-75" 72 | end 73 | end 74 | 75 | describe :frame do 76 | it "does not write to the socket" do 77 | expect(socket).not_to receive(:write) 78 | driver.frame("Hello, world") 79 | end 80 | 81 | it "returns true" do 82 | expect(driver.frame("whatever")).to eq true 83 | end 84 | 85 | it "queues the frames until the handshake has been sent" do 86 | expect(socket).to receive(:write).with( 87 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + 88 | "Upgrade: WebSocket\r\n" + 89 | "Connection: Upgrade\r\n" + 90 | "WebSocket-Origin: http://www.example.com\r\n" + 91 | "WebSocket-Location: ws://www.example.com/socket\r\n" + 92 | "\r\n") 93 | expect(socket).to receive(:write).with(encode "\x00Hi\xFF".bytes) 94 | 95 | driver.frame("Hi") 96 | driver.start 97 | 98 | expect(@bytes).to eq [0x00, 72, 105, 0xFF] 99 | end 100 | end 101 | end 102 | 103 | it_should_behave_like "draft-75 protocol" 104 | end 105 | -------------------------------------------------------------------------------- /lib/websocket/http/headers.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | module HTTP 3 | 4 | module Headers 5 | MAX_LINE_LENGTH = 4096 6 | CR = 0x0D 7 | LF = 0x0A 8 | 9 | # RFC 2616 grammar rules: 10 | # 11 | # CHAR = 12 | # 13 | # CTL = 15 | # 16 | # SP = 17 | # 18 | # HT = 19 | # 20 | # token = 1* 21 | # 22 | # separators = "(" | ")" | "<" | ">" | "@" 23 | # | "," | ";" | ":" | "\" | <"> 24 | # | "/" | "[" | "]" | "?" | "=" 25 | # | "{" | "}" | SP | HT 26 | # 27 | # Or, as redefined in RFC 7230: 28 | # 29 | # token = 1*tchar 30 | # 31 | # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" 32 | # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" 33 | # / DIGIT / ALPHA 34 | # ; any VCHAR, except delimiters 35 | 36 | HEADER_LINE = /^([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+):\s*([\x20-\x7e]*?)\s*$/i 37 | 38 | attr_reader :headers 39 | 40 | def initialize 41 | @buffer = [] 42 | @env = {} 43 | @headers = {} 44 | @stage = 0 45 | end 46 | 47 | def complete? 48 | @stage == 2 49 | end 50 | 51 | def error? 52 | @stage == -1 53 | end 54 | 55 | def parse(chunk) 56 | chunk.each_byte do |octet| 57 | if octet == LF and @stage < 2 58 | @buffer.pop if @buffer.last == CR 59 | if @buffer.empty? 60 | complete if @stage == 1 61 | else 62 | result = case @stage 63 | when 0 then start_line(string_buffer) 64 | when 1 then header_line(string_buffer) 65 | end 66 | 67 | if result 68 | @stage = 1 69 | else 70 | error 71 | end 72 | end 73 | @buffer = [] 74 | else 75 | @buffer << octet if @stage >= 0 76 | error if @stage < 2 and @buffer.size > MAX_LINE_LENGTH 77 | end 78 | end 79 | @env['rack.input'] = StringIO.new(string_buffer) 80 | end 81 | 82 | private 83 | 84 | def complete 85 | @stage = 2 86 | end 87 | 88 | def error 89 | @stage = -1 90 | end 91 | 92 | def header_line(line) 93 | return false unless parsed = line.scan(HEADER_LINE).first 94 | 95 | key = HTTP.normalize_header(parsed[0]) 96 | value = parsed[1].strip 97 | 98 | if @headers.has_key?(key) 99 | @headers[key] << ', ' << value 100 | else 101 | @headers[key] = value 102 | end 103 | true 104 | end 105 | 106 | def string_buffer 107 | @buffer.pack('C*') 108 | end 109 | end 110 | 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft75_examples.rb: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | shared_examples_for "draft-75 protocol" do 4 | describe "in the :open state" do 5 | before { driver.start } 6 | 7 | describe :parse do 8 | it "parses text frames" do 9 | driver.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff].pack("C*") 10 | expect(@message).to eq "Hello" 11 | end 12 | 13 | it "parses multiple frames from the same packet" do 14 | driver.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff].pack("C*") 15 | expect(@message).to eq "HelloHello" 16 | end 17 | 18 | it "parses text frames beginning 0x00-0x7F" do 19 | driver.parse [0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff].pack("C*") 20 | expect(@message).to eq "Hello" 21 | end 22 | 23 | it "ignores frames with a length header" do 24 | driver.parse [0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff].pack("C*") 25 | expect(@message).to eq "Hello" 26 | end 27 | 28 | it "parses multibyte text frames" do 29 | driver.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff].pack("C*") 30 | expect(@message).to eq encode("Apple = ") 31 | end 32 | 33 | it "parses frames received in several packets" do 34 | driver.parse [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65].pack("C*") 35 | driver.parse [0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff].pack("C*") 36 | expect(@message).to eq encode("Apple = ") 37 | end 38 | 39 | it "parses fragmented frames" do 40 | driver.parse [0x00, 0x48, 0x65, 0x6c].pack("C*") 41 | driver.parse [0x6c, 0x6f, 0xff].pack("C*") 42 | expect(@message).to eq "Hello" 43 | end 44 | 45 | describe "when a message listener raises an error" do 46 | before do 47 | @messages = [] 48 | 49 | driver.on :message do |msg| 50 | @messages << msg.data 51 | raise "an error" 52 | end 53 | end 54 | 55 | it "is not trapped by the parser" do 56 | buffer = [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff].pack('C*') 57 | expect { driver.parse buffer }.to raise_error(RuntimeError, "an error") 58 | end 59 | 60 | it "parses text frames without dropping input" do 61 | driver.parse [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x57].pack("C*") rescue nil 62 | driver.parse [0x6f, 0x72, 0x6c, 0x64, 0xff].pack("C*") rescue nil 63 | expect(@messages).to eq(["Hello", "World"]) 64 | end 65 | end 66 | end 67 | 68 | describe :frame do 69 | it "formats the given string as a WebSocket frame" do 70 | driver.frame "Hello" 71 | expect(@bytes).to eq [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff] 72 | end 73 | 74 | it "encodes multibyte characters correctly" do 75 | message = encode("Apple = ") 76 | driver.frame message 77 | expect(@bytes).to eq [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff] 78 | end 79 | 80 | it "returns true" do 81 | expect(driver.frame("lol")).to eq true 82 | end 83 | end 84 | 85 | describe :ping do 86 | it "does not write to the socket" do 87 | expect(socket).not_to receive(:write) 88 | driver.ping 89 | end 90 | 91 | it "returns false" do 92 | expect(driver.ping).to eq false 93 | end 94 | end 95 | 96 | describe :close do 97 | it "triggers the onclose event" do 98 | driver.close 99 | expect(@close).to eq true 100 | end 101 | 102 | it "returns true" do 103 | expect(driver.close).to eq true 104 | end 105 | 106 | it "changes the state to :closed" do 107 | driver.close 108 | expect(driver.state).to eq :closed 109 | end 110 | end 111 | end 112 | 113 | describe "in the :closed state" do 114 | before do 115 | driver.start 116 | driver.close 117 | end 118 | 119 | describe :close do 120 | it "does not write to the socket" do 121 | expect(socket).not_to receive(:write) 122 | driver.close 123 | end 124 | 125 | it "returns false" do 126 | expect(driver.close).to eq false 127 | end 128 | 129 | it "leaves the protocol in the :closed state" do 130 | driver.close 131 | expect(driver.state).to eq :closed 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/websocket/driver/client.rb: -------------------------------------------------------------------------------- 1 | module WebSocket 2 | class Driver 3 | 4 | class Client < Hybi 5 | VALID_SCHEMES = %w[ws wss] 6 | 7 | def self.generate_key 8 | Base64.strict_encode64(SecureRandom.random_bytes(16)) 9 | end 10 | 11 | attr_reader :status, :headers 12 | 13 | def initialize(socket, options = {}) 14 | super 15 | 16 | @ready_state = -1 17 | @key = Client.generate_key 18 | @accept = Hybi.generate_accept(@key) 19 | @http = HTTP::Response.new 20 | 21 | uri = URI.parse(@socket.url) 22 | unless VALID_SCHEMES.include?(uri.scheme) 23 | raise URIError, "#{ socket.url } is not a valid WebSocket URL" 24 | end 25 | 26 | path = (uri.path == '') ? '/' : uri.path 27 | @pathname = path + (uri.query ? '?' + uri.query : '') 28 | 29 | @headers['Host'] = Driver.host_header(uri) 30 | @headers['Upgrade'] = 'websocket' 31 | @headers['Connection'] = 'Upgrade' 32 | @headers['Sec-WebSocket-Key'] = @key 33 | @headers['Sec-WebSocket-Version'] = VERSION 34 | 35 | if @protocols.size > 0 36 | @headers['Sec-WebSocket-Protocol'] = @protocols * ', ' 37 | end 38 | 39 | if uri.user 40 | auth = Base64.strict_encode64([uri.user, uri.password] * ':') 41 | @headers['Authorization'] = 'Basic ' + auth 42 | end 43 | end 44 | 45 | def version 46 | "hybi-#{ VERSION }" 47 | end 48 | 49 | def proxy(origin, options = {}) 50 | Proxy.new(self, origin, options) 51 | end 52 | 53 | def start 54 | return false unless @ready_state == -1 55 | @socket.write(handshake_request) 56 | @ready_state = 0 57 | true 58 | end 59 | 60 | def parse(chunk) 61 | return if @ready_state == 3 62 | return super if @ready_state > 0 63 | 64 | @http.parse(chunk) 65 | return fail_handshake('Invalid HTTP response') if @http.error? 66 | return unless @http.complete? 67 | 68 | validate_handshake 69 | return if @ready_state == 3 70 | 71 | open 72 | parse(@http.body) 73 | end 74 | 75 | private 76 | 77 | def handshake_request 78 | extensions = @extensions.generate_offer 79 | @headers['Sec-WebSocket-Extensions'] = extensions if extensions 80 | 81 | start = "GET #{ @pathname } HTTP/1.1" 82 | headers = [start, @headers.to_s, ''] 83 | headers.join("\r\n") 84 | end 85 | 86 | def fail_handshake(message) 87 | message = "Error during WebSocket handshake: #{ message }" 88 | @ready_state = 3 89 | emit(:error, ProtocolError.new(message)) 90 | emit(:close, CloseEvent.new(ERRORS[:protocol_error], message)) 91 | end 92 | 93 | def validate_handshake 94 | @status = @http.code 95 | @headers = Headers.new(@http.headers) 96 | 97 | unless @http.code == 101 98 | return fail_handshake("Unexpected response code: #{ @http.code }") 99 | end 100 | 101 | upgrade = @http['Upgrade'] || '' 102 | connection = @http['Connection'] || '' 103 | accept = @http['Sec-WebSocket-Accept'] || '' 104 | protocol = @http['Sec-WebSocket-Protocol'] || '' 105 | 106 | if upgrade == '' 107 | return fail_handshake("'Upgrade' header is missing") 108 | elsif upgrade.downcase != 'websocket' 109 | return fail_handshake("'Upgrade' header value is not 'WebSocket'") 110 | end 111 | 112 | if connection == '' 113 | return fail_handshake("'Connection' header is missing") 114 | elsif connection.downcase != 'upgrade' 115 | return fail_handshake("'Connection' header value is not 'Upgrade'") 116 | end 117 | 118 | unless accept == @accept 119 | return fail_handshake('Sec-WebSocket-Accept mismatch') 120 | end 121 | 122 | unless protocol == '' 123 | if @protocols.include?(protocol) 124 | @protocol = protocol 125 | else 126 | return fail_handshake('Sec-WebSocket-Protocol mismatch') 127 | end 128 | end 129 | 130 | begin 131 | @extensions.activate(@headers['Sec-WebSocket-Extensions']) 132 | rescue ::WebSocket::Extensions::ExtensionError => error 133 | return fail_handshake(error.message) 134 | end 135 | end 136 | end 137 | 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.8.0 / 2025-05-25 2 | 3 | - Emit binary message as a string with `Encoding::BINARY` instead of an array 4 | - Add the option `:binary_data_format` to force the previous behaviour 5 | 6 | ### 0.7.7 / 2025-01-04 7 | 8 | - Add `base64` gem to the dependencies to support Ruby 3.4 9 | 10 | ### 0.7.6 / 2023-07-25 11 | 12 | - Fix handling of default ports in `Host` headers on Ruby 3.1+ 13 | 14 | ### 0.7.5 / 2021-06-12 15 | 16 | - Do not change the encoding of strings passed to `Driver#text` 17 | 18 | ### 0.7.4 / 2021-05-24 19 | 20 | - Optimise conversions between strings and byte arrays and related encoding 21 | operations, to reduce amount of allocation and copying 22 | 23 | ### 0.7.3 / 2020-07-09 24 | 25 | - Let the client accept HTTP responses that have an empty reason phrase 26 | following the `101` status code 27 | 28 | ### 0.7.2 / 2020-05-22 29 | 30 | - Emit `ping` and `pong` events from the `Server` driver 31 | - Handle draft-76 handshakes correctly if the request's body is a frozen string 32 | 33 | ### 0.7.1 / 2019-06-10 34 | 35 | - Catch any exceptions produced while generating a handshake response and send a 36 | `400 Bad Request` response to the client 37 | - Pick the RFC-6455 protocol version if the request contains any of the headers 38 | used by that version 39 | - Handle errors encountered while handling malformed draft-76 requests 40 | - Change license from MIT to Apache 2.0 41 | 42 | ### 0.7.0 / 2017-09-11 43 | 44 | - Add `ping` and `pong` to the set of events users can listen to 45 | 46 | ### 0.6.5 / 2017-01-22 47 | 48 | - Provide a pure-Ruby fallback for the native unmasking code 49 | 50 | ### 0.6.4 / 2016-05-20 51 | 52 | - Amend warnings issued when running with -W2 53 | - Make sure message strings passed in by the app are transcoded to UTF-8 54 | - Copy strings if necessary for frozen-string compatibility 55 | 56 | ### 0.6.3 / 2015-11-06 57 | 58 | - Reject draft-76 handshakes if their Sec-WebSocket-Key headers are invalid 59 | - Throw a more helpful error if a client is created with an invalid URL 60 | 61 | ### 0.6.2 / 2015-07-18 62 | 63 | - When the peer sends a close frame with no error code, emit 1000 64 | 65 | ### 0.6.1 / 2015-07-13 66 | 67 | - Fix how events are stored in `EventEmitter` to fix a backward-compatibility 68 | violation introduced in the last release 69 | - Use the `Array#pack` and `String#unpack` methods for reading/writing numbers 70 | to buffers rather than including duplicate logic for this 71 | 72 | ### 0.6.0 / 2015-07-08 73 | 74 | - Use `SecureRandom` to generate the `Sec-WebSocket-Key` header 75 | - Allow the parser to recover cleanly if event listeners raise an error 76 | - Let the `on()` method take a lambda as a positional argument rather than a 77 | block 78 | - Add a `pong` method for sending unsolicited pong frames 79 | 80 | ### 0.5.4 / 2015-03-29 81 | 82 | - Don't emit extra close frames if we receive a close frame after we already 83 | sent one 84 | - Fail the connection when the driver receives an invalid 85 | `Sec-WebSocket-Extensions` header 86 | 87 | ### 0.5.3 / 2015-02-22 88 | 89 | - Don't treat incoming data as WebSocket frames if a client driver is closed 90 | before receiving the server handshake 91 | 92 | ### 0.5.2 / 2015-02-19 93 | 94 | - Don't emit multiple `error` events 95 | 96 | ### 0.5.1 / 2014-12-18 97 | 98 | - Don't allow drivers to be created with unrecognized options 99 | 100 | ### 0.5.0 / 2014-12-13 101 | 102 | - Support protocol extensions via the websocket-extensions module 103 | 104 | ### 0.4.0 / 2014-11-08 105 | 106 | - Support connection via HTTP proxies using `CONNECT` 107 | 108 | ### 0.3.5 / 2014-10-04 109 | 110 | - Fix bug where the `Server` driver doesn't pass `ping` callbacks to its 111 | delegate 112 | - Fix an arity error when calling `fail_request` 113 | - Allow `close` to be called before `start` to close the driver 114 | 115 | ### 0.3.4 / 2014-07-06 116 | 117 | - Don't hold references to frame buffers after a message has been emitted 118 | - Make sure that `protocol` and `version` are exposed properly by the TCP driver 119 | - Correct HTTP header parsing based on RFC 7230; header names cannot contain 120 | backslashes 121 | 122 | ### 0.3.3 / 2014-04-24 123 | 124 | - Fix problems with loading C and Java native extension code 125 | - Correct the acceptable characters used in the HTTP parser 126 | - Correct the draft-76 status line reason phrase 127 | 128 | ### 0.3.2 / 2013-12-29 129 | 130 | - Expand `max_length` to cover sequences of continuation frames and 131 | `draft-{75,76}` 132 | - Decrease default maximum frame buffer size to 64MB 133 | - Stop parsing when the protocol enters a failure mode, to save CPU cycles 134 | 135 | ### 0.3.1 / 2013-12-03 136 | 137 | - Add a `max_length` option to limit allowed frame size 138 | 139 | ### 0.3.0 / 2013-09-09 140 | 141 | - Support client URLs with Basic Auth credentials 142 | 143 | ### 0.2.3 / 2013-08-04 144 | 145 | - Fix bug in EventEmitter#emit when listeners are removed 146 | 147 | ### 0.2.2 / 2013-08-04 148 | 149 | - Fix bug in EventEmitter#listener_count for unregistered events 150 | 151 | ### 0.2.1 / 2013-07-05 152 | 153 | - Queue sent messages if the client has not begun trying to connect 154 | - Encode all strings sent to I/O as `ASCII-8BIT` 155 | 156 | ### 0.2.0 / 2013-05-12 157 | 158 | - Add API for setting and reading headers 159 | - Add Driver.server() method for getting a driver for TCP servers 160 | 161 | ### 0.1.0 / 2013-05-04 162 | 163 | - First stable release 164 | 165 | ### 0.0.0 / 2013-04-22 166 | 167 | - First release 168 | - Proof of concept for people to try out 169 | - Might be unstable 170 | -------------------------------------------------------------------------------- /lib/websocket/driver.rb: -------------------------------------------------------------------------------- 1 | # Protocol references: 2 | # 3 | # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 4 | # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 5 | # * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 6 | 7 | require 'base64' 8 | require 'digest/md5' 9 | require 'digest/sha1' 10 | require 'securerandom' 11 | require 'set' 12 | require 'stringio' 13 | require 'uri' 14 | require 'websocket/extensions' 15 | 16 | module WebSocket 17 | autoload :HTTP, File.expand_path('../http', __FILE__) 18 | 19 | class Driver 20 | 21 | root = File.expand_path('../driver', __FILE__) 22 | 23 | begin 24 | # Load C native extension 25 | require 'websocket_mask' 26 | rescue LoadError 27 | # Fall back to pure-Ruby implementation 28 | require 'websocket/mask' 29 | end 30 | 31 | 32 | if RUBY_PLATFORM =~ /java/ 33 | require 'jruby' 34 | com.jcoglan.websocket.WebsocketMaskService.new.basicLoad(JRuby.runtime) 35 | end 36 | 37 | unless Mask.respond_to?(:mask) 38 | def Mask.mask(payload, mask) 39 | @instance ||= new 40 | @instance.mask(payload, mask) 41 | end 42 | end 43 | 44 | MAX_LENGTH = 0x3ffffff 45 | PORTS = { 'ws' => 80, 'wss' => 443 } 46 | STATES = [:connecting, :open, :closing, :closed] 47 | 48 | ConnectEvent = Struct.new(nil) 49 | OpenEvent = Struct.new(nil) 50 | MessageEvent = Struct.new(:data) 51 | PingEvent = Struct.new(:data) 52 | PongEvent = Struct.new(:data) 53 | CloseEvent = Struct.new(:code, :reason) 54 | 55 | ProtocolError = Class.new(StandardError) 56 | URIError = Class.new(ArgumentError) 57 | ConfigurationError = Class.new(ArgumentError) 58 | 59 | autoload :Client, root + '/client' 60 | autoload :Draft75, root + '/draft75' 61 | autoload :Draft76, root + '/draft76' 62 | autoload :EventEmitter, root + '/event_emitter' 63 | autoload :Headers, root + '/headers' 64 | autoload :Hybi, root + '/hybi' 65 | autoload :Proxy, root + '/proxy' 66 | autoload :Server, root + '/server' 67 | autoload :StreamReader, root + '/stream_reader' 68 | 69 | include EventEmitter 70 | attr_reader :protocol, :ready_state 71 | 72 | def initialize(socket, options = {}) 73 | super() 74 | Driver.validate_options(options, [:max_length, :masking, :require_masking, :protocols, :binary_data_format]) 75 | 76 | @socket = socket 77 | @reader = StreamReader.new 78 | @options = options 79 | @max_length = options[:max_length] || MAX_LENGTH 80 | @headers = Headers.new 81 | @queue = [] 82 | @ready_state = 0 83 | 84 | @binary_data_format = options[:binary_data_format] || :string 85 | end 86 | 87 | def state 88 | return nil unless @ready_state >= 0 89 | STATES[@ready_state] 90 | end 91 | 92 | def add_extension(extension) 93 | false 94 | end 95 | 96 | def set_header(name, value) 97 | return false unless @ready_state <= 0 98 | @headers[name] = value 99 | true 100 | end 101 | 102 | def start 103 | return false unless @ready_state == 0 104 | 105 | unless Driver.websocket?(@socket.env) 106 | return fail_handshake(ProtocolError.new('Not a WebSocket request')) 107 | end 108 | 109 | begin 110 | response = handshake_response 111 | rescue => error 112 | return fail_handshake(error) 113 | end 114 | 115 | @socket.write(response) 116 | open unless @stage == -1 117 | true 118 | end 119 | 120 | def text(message) 121 | message = Driver.encode(message, Encoding::UTF_8) 122 | frame(message, :text) 123 | end 124 | 125 | def binary(message) 126 | false 127 | end 128 | 129 | def ping(*args) 130 | false 131 | end 132 | 133 | def pong(*args) 134 | false 135 | end 136 | 137 | def close(reason = nil, code = nil) 138 | return false unless @ready_state == 1 139 | @ready_state = 3 140 | emit(:close, CloseEvent.new(nil, nil)) 141 | true 142 | end 143 | 144 | private 145 | 146 | def fail_handshake(error) 147 | headers = Headers.new 148 | headers['Content-Type'] = 'text/plain' 149 | headers['Content-Length'] = error.message.bytesize 150 | 151 | headers = ['HTTP/1.1 400 Bad Request', headers.to_s, error.message] 152 | @socket.write(headers.join("\r\n")) 153 | fail(:protocol_error, error.message) 154 | 155 | false 156 | end 157 | 158 | def fail(type, message) 159 | @ready_state = 2 160 | emit(:error, ProtocolError.new(message)) 161 | close 162 | end 163 | 164 | def open 165 | @ready_state = 1 166 | @queue.each { |message| frame(*message) } 167 | @queue = [] 168 | emit(:open, OpenEvent.new) 169 | end 170 | 171 | def queue(message) 172 | @queue << message 173 | true 174 | end 175 | 176 | def self.client(socket, options = {}) 177 | Client.new(socket, options.merge(:masking => true)) 178 | end 179 | 180 | def self.server(socket, options = {}) 181 | Server.new(socket, options.merge(:require_masking => true)) 182 | end 183 | 184 | def self.rack(socket, options = {}) 185 | env = socket.env 186 | version = env['HTTP_SEC_WEBSOCKET_VERSION'] 187 | key = env['HTTP_SEC_WEBSOCKET_KEY'] 188 | key1 = env['HTTP_SEC_WEBSOCKET_KEY1'] 189 | key2 = env['HTTP_SEC_WEBSOCKET_KEY2'] 190 | 191 | if version or key 192 | Hybi.new(socket, options.merge(:require_masking => true)) 193 | elsif key1 or key2 194 | Draft76.new(socket, options) 195 | else 196 | Draft75.new(socket, options) 197 | end 198 | end 199 | 200 | def self.encode(data, encoding = nil) 201 | if Array === data 202 | data = data.pack('C*') 203 | encoding ||= Encoding::BINARY 204 | end 205 | 206 | return data if encoding.nil? or data.encoding == encoding 207 | 208 | if data.encoding == Encoding::BINARY 209 | data = data.dup if data.frozen? 210 | data.force_encoding(encoding) 211 | else 212 | data.encode(encoding) 213 | end 214 | end 215 | 216 | def self.host_header(uri) 217 | host = uri.host 218 | if uri.port and uri.port != PORTS[uri.scheme] 219 | host += ":#{uri.port}" 220 | end 221 | host 222 | end 223 | 224 | def self.validate_options(options, valid_keys) 225 | options.keys.each do |key| 226 | unless valid_keys.include?(key) 227 | raise ConfigurationError, "Unrecognized option: #{ key.inspect }" 228 | end 229 | end 230 | 231 | if options[:binary_data_format] 232 | unless [:array, :string].include?(options[:binary_data_format]) 233 | raise ConfigurationError, "Invalid :binary_data_format: #{options[:binary_data_format].inspect}" 234 | end 235 | end 236 | end 237 | 238 | def self.websocket?(env) 239 | connection = env['HTTP_CONNECTION'] || '' 240 | upgrade = env['HTTP_UPGRADE'] || '' 241 | 242 | env['REQUEST_METHOD'] == 'GET' and 243 | connection.downcase.split(/ *, */).include?('upgrade') and 244 | upgrade.downcase == 'websocket' 245 | end 246 | 247 | end 248 | end 249 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft76_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | # frozen_string_literal: false 3 | 4 | require "spec_helper" 5 | 6 | describe WebSocket::Driver::Draft76 do 7 | include EncodingHelper 8 | 9 | let :body do 10 | encode [0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88] 11 | end 12 | 13 | let :response do 14 | string = "\xB4\x9Cn@S\x04\x04&\xE5\e\xBFl\xB7\x9F\x1D\xF9" 15 | string.force_encoding(Encoding::BINARY) if string.respond_to?(:force_encoding) 16 | string 17 | end 18 | 19 | let :env do 20 | { 21 | "REQUEST_METHOD" => "GET", 22 | "HTTP_CONNECTION" => "Upgrade", 23 | "HTTP_UPGRADE" => "WebSocket", 24 | "HTTP_ORIGIN" => "http://www.example.com", 25 | "HTTP_SEC_WEBSOCKET_KEY1" => "1 38 wZ3f9 23O0 3l 0r", 26 | "HTTP_SEC_WEBSOCKET_KEY2" => "27 0E 6 2 1665:< ;U 1H", 27 | "rack.input" => StringIO.new(body) 28 | } 29 | end 30 | 31 | let :socket do 32 | socket = double(WebSocket) 33 | allow(socket).to receive(:env).and_return(env) 34 | allow(socket).to receive(:url).and_return("ws://www.example.com/socket") 35 | allow(socket).to receive(:write) { |message| @bytes = bytes(message) } 36 | socket 37 | end 38 | 39 | let :driver do 40 | driver = WebSocket::Driver::Draft76.new(socket) 41 | driver.on(:open) { |e| @open = true } 42 | driver.on(:message) { |e| @message += e.data } 43 | driver.on(:error) { |e| @error = e } 44 | driver.on(:close) { |e| @close = true } 45 | driver 46 | end 47 | 48 | before do 49 | @open = @close = false 50 | @message = "" 51 | end 52 | 53 | describe "in the :connecting state" do 54 | it "starts in the connecting state" do 55 | expect(driver.state).to eq :connecting 56 | end 57 | 58 | describe :start do 59 | it "writes the handshake response to the socket" do 60 | expect(socket).to receive(:write).with( 61 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 62 | "Upgrade: WebSocket\r\n" + 63 | "Connection: Upgrade\r\n" + 64 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 65 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 66 | "\r\n") 67 | expect(socket).to receive(:write).with(response) 68 | driver.start 69 | end 70 | 71 | it "returns true" do 72 | expect(driver.start).to eq true 73 | end 74 | 75 | it "triggers the onopen event" do 76 | driver.start 77 | expect(@open).to eq true 78 | end 79 | 80 | it "changes the state to :open" do 81 | driver.start 82 | expect(driver.state).to eq :open 83 | end 84 | 85 | it "sets the protocol version" do 86 | driver.start 87 | expect(driver.version).to eq "hixie-76" 88 | end 89 | 90 | describe "with an invalid key header" do 91 | before do 92 | env["HTTP_SEC_WEBSOCKET_KEY1"] = "2 L785 8o% s9Sy9@V. 4<1P5" 93 | end 94 | 95 | it "writes a handshake error response" do 96 | expect(socket).to receive(:write).with( 97 | "HTTP/1.1 400 Bad Request\r\n" + 98 | "Content-Type: text/plain\r\n" + 99 | "Content-Length: 45\r\n" + 100 | "\r\n" + 101 | "Client sent invalid Sec-WebSocket-Key headers") 102 | driver.start 103 | end 104 | 105 | it "does not trigger the onopen event" do 106 | driver.start 107 | expect(@open).to eq false 108 | end 109 | 110 | it "triggers the onerror event" do 111 | driver.start 112 | expect(@error.message).to eq "Client sent invalid Sec-WebSocket-Key headers" 113 | end 114 | 115 | it "triggers the onclose event" do 116 | driver.start 117 | expect(@close).to eq true 118 | end 119 | 120 | it "changes the state to closed" do 121 | driver.start 122 | expect(driver.state).to eq :closed 123 | end 124 | end 125 | end 126 | 127 | describe :frame do 128 | it "does not write to the socket" do 129 | expect(socket).not_to receive(:write) 130 | driver.frame("Hello, world") 131 | end 132 | 133 | it "returns true" do 134 | expect(driver.frame("whatever")).to eq true 135 | end 136 | 137 | it "queues the frames until the handshake has been sent" do 138 | expect(socket).to receive(:write).with( 139 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 140 | "Upgrade: WebSocket\r\n" + 141 | "Connection: Upgrade\r\n" + 142 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 143 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 144 | "\r\n") 145 | expect(socket).to receive(:write).with(response) 146 | expect(socket).to receive(:write).with(encode "\x00Hi\xFF".bytes) 147 | 148 | driver.frame("Hi") 149 | driver.start 150 | 151 | expect(@bytes).to eq [0x00, 72, 105, 0xff] 152 | end 153 | end 154 | 155 | describe "with no request body" do 156 | before { env.delete("rack.input") } 157 | 158 | describe :start do 159 | it "writes the handshake response with no body" do 160 | expect(socket).to receive(:write).with( 161 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 162 | "Upgrade: WebSocket\r\n" + 163 | "Connection: Upgrade\r\n" + 164 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 165 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 166 | "\r\n") 167 | driver.start 168 | end 169 | 170 | it "does not trigger the onopen event" do 171 | driver.start 172 | expect(@open).to eq false 173 | end 174 | 175 | it "leaves the protocol in the :connecting state" do 176 | driver.start 177 | expect(driver.state).to eq :connecting 178 | end 179 | 180 | describe "when the request body is received" do 181 | before { driver.start } 182 | 183 | it "sends the response body" do 184 | expect(socket).to receive(:write).with(response) 185 | driver.parse(body) 186 | end 187 | 188 | it "triggers the onopen event" do 189 | driver.parse(body) 190 | expect(@open).to eq true 191 | end 192 | 193 | it "changes the state to :open" do 194 | driver.parse(body) 195 | expect(driver.state).to eq :open 196 | end 197 | 198 | it "sends any frames queued before the handshake was complete" do 199 | expect(socket).to receive(:write).with(response) 200 | expect(socket).to receive(:write).with(encode "\x00hello\xFF".bytes) 201 | driver.frame("hello") 202 | driver.parse(body) 203 | expect(@bytes).to eq [0, 104, 101, 108, 108, 111, 255] 204 | end 205 | end 206 | end 207 | end 208 | end 209 | 210 | it_should_behave_like "draft-75 protocol" 211 | 212 | describe "in the :open state" do 213 | before { driver.start } 214 | 215 | describe :parse do 216 | it "closes the socket if a close frame is received" do 217 | driver.parse [0xff, 0x00].pack("C*") 218 | expect(@close).to eq true 219 | expect(driver.state).to eq :closed 220 | end 221 | end 222 | 223 | describe :close do 224 | it "writes a close message to the socket" do 225 | driver.close 226 | expect(@bytes).to eq [0xff, 0x00] 227 | end 228 | end 229 | end 230 | 231 | describe "frozen rack.input.read" do 232 | let :frozen_env do 233 | # Make up a rack.input that somehow returns a frozen string on input.read 234 | # We're seeing this error occasionally when using ActionCable in Rails 5.2 235 | env.merge("rack.input" => Struct.new(:read).new(''.freeze)) 236 | end 237 | 238 | it 'edge case where rack.input.read returns a frozen string' do 239 | frozen_socket = socket 240 | allow(frozen_socket).to receive(:env).and_return(frozen_env) 241 | 242 | expect { 243 | WebSocket::Driver::Draft76.new(frozen_socket) 244 | }.to_not raise_error 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /spec/websocket/driver/client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe WebSocket::Driver::Client do 4 | include EncodingHelper 5 | 6 | let :socket do 7 | socket = double(WebSocket) 8 | allow(socket).to receive(:write) { |message| @bytes = bytes(message) } 9 | allow(socket).to receive(:url).and_return(url) 10 | socket 11 | end 12 | 13 | let :options do 14 | { :protocols => protocols } 15 | end 16 | 17 | let :protocols do 18 | nil 19 | end 20 | 21 | let :url do 22 | "ws://www.example.com/socket" 23 | end 24 | 25 | let :driver do 26 | driver = WebSocket::Driver::Client.new(socket, options) 27 | driver.on(:open) { |e| @open = true } 28 | driver.on(:message) { |e| @message += e.data } 29 | driver.on(:error) { |e| @error = e } 30 | driver.on(:close) { |e| @close = [e.code, e.reason] } 31 | driver 32 | end 33 | 34 | let :key do 35 | "2vBVWg4Qyk3ZoM/5d3QD9Q==" 36 | end 37 | 38 | let :response do 39 | "HTTP/1.1 101 Switching Protocols\r\n" + 40 | "Upgrade: websocket\r\n" + 41 | "Connection: Upgrade\r\n" + 42 | "Sec-WebSocket-Accept: QV3I5XUXU2CdhtjixE7QCkCcMZM=\r\n" + 43 | "\r\n" 44 | end 45 | 46 | before do 47 | allow(WebSocket::Driver::Client).to receive(:generate_key).and_return(key) 48 | @open = @error = @close = false 49 | @message = "" 50 | end 51 | 52 | describe "in the beginning state" do 53 | it "starts in no state" do 54 | expect(driver.state).to eq nil 55 | end 56 | 57 | describe :close do 58 | it "changes the state to :closed" do 59 | driver.close 60 | expect(driver.state).to eq :closed 61 | expect(@close).to eq [1000, ""] 62 | end 63 | end 64 | 65 | describe :start do 66 | it "writes the handshake request to the socket" do 67 | expect(socket).to receive(:write).with( 68 | "GET /socket HTTP/1.1\r\n" + 69 | "Host: www.example.com\r\n" + 70 | "Upgrade: websocket\r\n" + 71 | "Connection: Upgrade\r\n" + 72 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 73 | "Sec-WebSocket-Version: 13\r\n" + 74 | "\r\n") 75 | driver.start 76 | end 77 | 78 | it "returns true" do 79 | expect(driver.start).to eq true 80 | end 81 | 82 | describe "with subprotocols" do 83 | let(:protocols) { ["foo", "bar", "xmpp"] } 84 | 85 | it "writes the handshake with Sec-WebSocket-Protocol" do 86 | expect(socket).to receive(:write).with( 87 | "GET /socket HTTP/1.1\r\n" + 88 | "Host: www.example.com\r\n" + 89 | "Upgrade: websocket\r\n" + 90 | "Connection: Upgrade\r\n" + 91 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 92 | "Sec-WebSocket-Version: 13\r\n" + 93 | "Sec-WebSocket-Protocol: foo, bar, xmpp\r\n" + 94 | "\r\n") 95 | driver.start 96 | end 97 | end 98 | 99 | describe "with basic auth" do 100 | let(:url) { "ws://user:pass@www.example.com/socket" } 101 | 102 | it "writes the handshake with Sec-WebSocket-Protocol" do 103 | expect(socket).to receive(:write).with( 104 | "GET /socket HTTP/1.1\r\n" + 105 | "Host: www.example.com\r\n" + 106 | "Upgrade: websocket\r\n" + 107 | "Connection: Upgrade\r\n" + 108 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 109 | "Sec-WebSocket-Version: 13\r\n" + 110 | "Authorization: Basic dXNlcjpwYXNz\r\n" + 111 | "\r\n") 112 | driver.start 113 | end 114 | end 115 | 116 | describe "with an invalid URL" do 117 | let(:url) { "stream.wikimedia.org/rc" } 118 | 119 | it "throws an URIError error" do 120 | expect { driver }.to raise_error(WebSocket::Driver::URIError) 121 | end 122 | end 123 | 124 | describe "with an explicit port" do 125 | let(:url) { "ws://www.example.com:3000/socket" } 126 | 127 | it "includes the port in the Host header" do 128 | expect(socket).to receive(:write).with( 129 | "GET /socket HTTP/1.1\r\n" + 130 | "Host: www.example.com:3000\r\n" + 131 | "Upgrade: websocket\r\n" + 132 | "Connection: Upgrade\r\n" + 133 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 134 | "Sec-WebSocket-Version: 13\r\n" + 135 | "\r\n") 136 | driver.start 137 | end 138 | end 139 | 140 | describe "with a wss: URL" do 141 | let(:url) { "wss://www.example.com/socket" } 142 | 143 | it "does not include the port in the Host header" do 144 | expect(socket).to receive(:write).with( 145 | "GET /socket HTTP/1.1\r\n" + 146 | "Host: www.example.com\r\n" + 147 | "Upgrade: websocket\r\n" + 148 | "Connection: Upgrade\r\n" + 149 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 150 | "Sec-WebSocket-Version: 13\r\n" + 151 | "\r\n") 152 | driver.start 153 | end 154 | end 155 | 156 | describe "with a wss: URL and explicit port" do 157 | let(:url) { "wss://www.example.com:3000/socket" } 158 | 159 | it "includes the port in the Host header" do 160 | expect(socket).to receive(:write).with( 161 | "GET /socket HTTP/1.1\r\n" + 162 | "Host: www.example.com:3000\r\n" + 163 | "Upgrade: websocket\r\n" + 164 | "Connection: Upgrade\r\n" + 165 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 166 | "Sec-WebSocket-Version: 13\r\n" + 167 | "\r\n") 168 | driver.start 169 | end 170 | end 171 | 172 | describe "with custom headers" do 173 | before do 174 | driver.set_header "User-Agent", "Chrome" 175 | end 176 | 177 | it "writes the handshake with custom headers" do 178 | expect(socket).to receive(:write).with( 179 | "GET /socket HTTP/1.1\r\n" + 180 | "Host: www.example.com\r\n" + 181 | "Upgrade: websocket\r\n" + 182 | "Connection: Upgrade\r\n" + 183 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 184 | "Sec-WebSocket-Version: 13\r\n" + 185 | "User-Agent: Chrome\r\n" + 186 | "\r\n") 187 | driver.start 188 | end 189 | end 190 | 191 | it "changes the state to :connecting" do 192 | driver.start 193 | expect(driver.state).to eq :connecting 194 | end 195 | end 196 | end 197 | 198 | describe "using a proxy" do 199 | it "sends a CONNECT request" do 200 | proxy = driver.proxy("http://proxy.example.com") 201 | expect(socket).to receive(:write).with( 202 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 203 | "Host: www.example.com\r\n" + 204 | "Connection: keep-alive\r\n" + 205 | "Proxy-Connection: keep-alive\r\n" + 206 | "\r\n") 207 | proxy.start 208 | end 209 | 210 | it "sends an authenticated CONNECT request" do 211 | proxy = driver.proxy("http://user:pass@proxy.example.com") 212 | expect(socket).to receive(:write).with( 213 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 214 | "Host: www.example.com\r\n" + 215 | "Connection: keep-alive\r\n" + 216 | "Proxy-Connection: keep-alive\r\n" + 217 | "Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" + 218 | "\r\n") 219 | proxy.start 220 | end 221 | 222 | it "sends a CONNECT request with custom headers" do 223 | proxy = driver.proxy("http://proxy.example.com") 224 | proxy.set_header("User-Agent", "Chrome") 225 | expect(socket).to receive(:write).with( 226 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 227 | "Host: www.example.com\r\n" + 228 | "Connection: keep-alive\r\n" + 229 | "Proxy-Connection: keep-alive\r\n" + 230 | "User-Agent: Chrome\r\n" + 231 | "\r\n") 232 | proxy.start 233 | end 234 | 235 | describe "receiving a response" do 236 | let(:proxy) { driver.proxy("http://proxy.example.com") } 237 | 238 | before do 239 | @connect = nil 240 | proxy.on(:connect) { @connect = true } 241 | proxy.on(:error) { |e| @error = e } 242 | end 243 | 244 | it "emits a 'connect' event when the proxy connects" do 245 | proxy.parse("HTTP/1.1 200 OK\r\n\r\n") 246 | expect(@connect).to eq true 247 | expect(@error).to eq false 248 | end 249 | 250 | it "emits an 'error' event if the proxy does not connect" do 251 | proxy.parse("HTTP/1.1 403 Forbidden\r\n\r\n") 252 | expect(@connect).to eq nil 253 | expect(@error.message).to eq "Can't establish a connection to the server at ws://www.example.com/socket" 254 | end 255 | end 256 | end 257 | 258 | describe "in the :connecting state" do 259 | before { driver.start } 260 | 261 | describe "with a valid response" do 262 | before { driver.parse(response) } 263 | 264 | it "changes the state to :open" do 265 | expect(@open).to eq true 266 | expect(@close).to eq false 267 | expect(driver.state).to eq :open 268 | end 269 | 270 | it "makes the response status available" do 271 | expect(driver.status).to eq 101 272 | end 273 | 274 | it "makes the response headers available" do 275 | expect(driver.headers["Upgrade"]).to eq "websocket" 276 | end 277 | end 278 | 279 | describe "with a valid response followed by a frame" do 280 | before do 281 | resp = response + encode([0x81, 0x02, 72, 105]) 282 | driver.parse(resp) 283 | end 284 | 285 | it "changes the state to :open" do 286 | expect(@open).to eq true 287 | expect(@close).to eq false 288 | expect(driver.state).to eq :open 289 | end 290 | 291 | it "parses the frame" do 292 | expect(@message).to eq "Hi" 293 | end 294 | end 295 | 296 | describe "with a bad status code" do 297 | before do 298 | resp = response.gsub(/101/, "4") 299 | driver.parse(resp) 300 | end 301 | 302 | it "changes the state to :closed" do 303 | expect(@open).to eq false 304 | expect(@error.message).to eq "Error during WebSocket handshake: Invalid HTTP response" 305 | expect(@close).to eq [1002, "Error during WebSocket handshake: Invalid HTTP response"] 306 | expect(driver.state).to eq :closed 307 | end 308 | end 309 | 310 | describe "with a bad Upgrade header" do 311 | before do 312 | resp = response.gsub(/websocket/, "wrong") 313 | driver.parse(resp) 314 | end 315 | 316 | it "changes the state to :closed" do 317 | expect(@open).to eq false 318 | expect(@error.message).to eq "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'" 319 | expect(@close).to eq [1002, "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"] 320 | expect(driver.state).to eq :closed 321 | end 322 | end 323 | 324 | describe "with a bad Accept header" do 325 | before do 326 | resp = response.gsub(/QV3/, "wrong") 327 | driver.parse(resp) 328 | end 329 | 330 | it "changes the state to :closed" do 331 | expect(@open).to eq false 332 | expect(@error.message).to eq "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch" 333 | expect(@close).to eq [1002, "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"] 334 | expect(driver.state).to eq :closed 335 | end 336 | end 337 | 338 | describe "with valid subprotocols" do 339 | let(:protocols) { ["foo", "xmpp"] } 340 | 341 | before do 342 | resp = response.gsub(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n") 343 | driver.parse(resp) 344 | end 345 | 346 | it "changes the state to :open" do 347 | expect(@open).to eq true 348 | expect(@close).to eq false 349 | expect(driver.state).to eq :open 350 | end 351 | 352 | it "selects the subprotocol" do 353 | expect(driver.protocol).to eq "xmpp" 354 | end 355 | end 356 | 357 | describe "with invalid subprotocols" do 358 | let(:protocols) { ["foo", "xmpp"] } 359 | 360 | before do 361 | resp = response.gsub(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n") 362 | driver.parse(resp) 363 | end 364 | 365 | it "changes the state to :closed" do 366 | expect(@open).to eq false 367 | expect(@error.message).to eq "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch" 368 | expect(@close).to eq [1002, "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"] 369 | expect(driver.state).to eq :closed 370 | end 371 | 372 | it "selects no subprotocol" do 373 | expect(driver.protocol).to eq nil 374 | end 375 | end 376 | end 377 | end 378 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | module WebSocket 4 | class Driver 5 | 6 | class Hybi < Driver 7 | root = File.expand_path('../hybi', __FILE__) 8 | 9 | autoload :Frame, root + '/frame' 10 | autoload :Message, root + '/message' 11 | 12 | def self.generate_accept(key) 13 | Base64.strict_encode64(Digest::SHA1.digest(key + GUID)) 14 | end 15 | 16 | VERSION = '13' 17 | GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 18 | 19 | BYTE = 0b11111111 20 | FIN = MASK = 0b10000000 21 | RSV1 = 0b01000000 22 | RSV2 = 0b00100000 23 | RSV3 = 0b00010000 24 | OPCODE = 0b00001111 25 | LENGTH = 0b01111111 26 | 27 | OPCODES = { 28 | :continuation => 0, 29 | :text => 1, 30 | :binary => 2, 31 | :close => 8, 32 | :ping => 9, 33 | :pong => 10 34 | } 35 | 36 | OPCODE_CODES = OPCODES.values 37 | MESSAGE_OPCODES = OPCODES.values_at(:continuation, :text, :binary) 38 | OPENING_OPCODES = OPCODES.values_at(:text, :binary) 39 | 40 | ERRORS = { 41 | :normal_closure => 1000, 42 | :going_away => 1001, 43 | :protocol_error => 1002, 44 | :unacceptable => 1003, 45 | :encoding_error => 1007, 46 | :policy_violation => 1008, 47 | :too_large => 1009, 48 | :extension_error => 1010, 49 | :unexpected_condition => 1011 50 | } 51 | 52 | ERROR_CODES = ERRORS.values 53 | DEFAULT_ERROR_CODE = 1000 54 | MIN_RESERVED_ERROR = 3000 55 | MAX_RESERVED_ERROR = 4999 56 | 57 | PACK_FORMATS = { 2 => 'S>', 8 => 'Q>' } 58 | 59 | def initialize(socket, options = {}) 60 | super 61 | 62 | @extensions = ::WebSocket::Extensions.new 63 | @stage = 0 64 | @masking = options[:masking] 65 | @protocols = options[:protocols] || [] 66 | @protocols = @protocols.strip.split(/ *, */) if String === @protocols 67 | @require_masking = options[:require_masking] 68 | @ping_callbacks = {} 69 | 70 | @frame = @message = nil 71 | 72 | return unless @socket.respond_to?(:env) 73 | 74 | if protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL'] 75 | protos = protos.split(/ *, */) if String === protos 76 | @protocol = protos.find { |p| @protocols.include?(p) } 77 | else 78 | @protocol = nil 79 | end 80 | end 81 | 82 | def version 83 | "hybi-#{ VERSION }" 84 | end 85 | 86 | def add_extension(extension) 87 | @extensions.add(extension) 88 | true 89 | end 90 | 91 | def parse(chunk) 92 | @reader.put(chunk) 93 | buffer = true 94 | while buffer 95 | case @stage 96 | when 0 then 97 | buffer = @reader.read(1) 98 | parse_opcode(buffer.getbyte(0)) if buffer 99 | 100 | when 1 then 101 | buffer = @reader.read(1) 102 | parse_length(buffer.getbyte(0)) if buffer 103 | 104 | when 2 then 105 | buffer = @reader.read(@frame.length_bytes) 106 | parse_extended_length(buffer) if buffer 107 | 108 | when 3 then 109 | buffer = @reader.read(4) 110 | if buffer 111 | @stage = 4 112 | @frame.masking_key = buffer 113 | end 114 | 115 | when 4 then 116 | buffer = @reader.read(@frame.length) 117 | 118 | if buffer 119 | @stage = 0 120 | emit_frame(buffer) 121 | end 122 | 123 | else 124 | buffer = nil 125 | end 126 | end 127 | end 128 | 129 | def binary(message) 130 | frame(message, :binary) 131 | end 132 | 133 | def ping(message = '', &callback) 134 | @ping_callbacks[message] = callback if callback 135 | frame(message, :ping) 136 | end 137 | 138 | def pong(message = '') 139 | frame(message, :pong) 140 | end 141 | 142 | def close(reason = nil, code = nil) 143 | reason ||= '' 144 | code ||= ERRORS[:normal_closure] 145 | 146 | if @ready_state <= 0 147 | @ready_state = 3 148 | emit(:close, CloseEvent.new(code, reason)) 149 | true 150 | elsif @ready_state == 1 151 | frame(reason, :close, code) 152 | @ready_state = 2 153 | true 154 | else 155 | false 156 | end 157 | end 158 | 159 | def frame(buffer, type = nil, code = nil) 160 | return queue([buffer, type, code]) if @ready_state <= 0 161 | return false unless @ready_state == 1 162 | 163 | message = Message.new 164 | frame = Frame.new 165 | 166 | is_binary = (Array === buffer or buffer.encoding == Encoding::BINARY) 167 | payload = Driver.encode(buffer, is_binary ? nil : Encoding::UTF_8) 168 | payload = [code, payload].pack('S>a*') if code 169 | type ||= is_binary ? :binary : :text 170 | 171 | message.rsv1 = message.rsv2 = message.rsv3 = false 172 | message.opcode = OPCODES[type] 173 | message.data = payload 174 | 175 | if MESSAGE_OPCODES.include?(message.opcode) 176 | message = @extensions.process_outgoing_message(message) 177 | end 178 | 179 | frame.final = true 180 | frame.rsv1 = message.rsv1 181 | frame.rsv2 = message.rsv2 182 | frame.rsv3 = message.rsv3 183 | frame.opcode = message.opcode 184 | frame.masked = !!@masking 185 | frame.masking_key = SecureRandom.random_bytes(4) if frame.masked 186 | frame.length = message.data.bytesize 187 | frame.payload = message.data 188 | 189 | send_frame(frame) 190 | true 191 | 192 | rescue ::WebSocket::Extensions::ExtensionError => error 193 | fail(:extension_error, error.message) 194 | end 195 | 196 | private 197 | 198 | def send_frame(frame) 199 | length = frame.length 200 | values = [] 201 | format = 'C2' 202 | masked = frame.masked ? MASK : 0 203 | 204 | values[0] = (frame.final ? FIN : 0) | 205 | (frame.rsv1 ? RSV1 : 0) | 206 | (frame.rsv2 ? RSV2 : 0) | 207 | (frame.rsv3 ? RSV3 : 0) | 208 | frame.opcode 209 | 210 | if length <= 125 211 | values[1] = masked | length 212 | elsif length <= 65535 213 | values[1] = masked | 126 214 | values[2] = length 215 | format << 'S>' 216 | else 217 | values[1] = masked | 127 218 | values[2] = length 219 | format << 'Q>' 220 | end 221 | 222 | if frame.masked 223 | values << frame.masking_key 224 | values << Mask.mask(frame.payload, frame.masking_key) 225 | format << 'a4a*' 226 | else 227 | values << frame.payload 228 | format << 'a*' 229 | end 230 | 231 | @socket.write(values.pack(format)) 232 | end 233 | 234 | def handshake_response 235 | sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY'] 236 | version = @socket.env['HTTP_SEC_WEBSOCKET_VERSION'] 237 | 238 | unless version == VERSION 239 | raise ProtocolError.new("Unsupported WebSocket version: #{ VERSION }") 240 | end 241 | 242 | unless sec_key 243 | raise ProtocolError.new('Missing handshake request header: Sec-WebSocket-Key') 244 | end 245 | 246 | @headers['Upgrade'] = 'websocket' 247 | @headers['Connection'] = 'Upgrade' 248 | @headers['Sec-WebSocket-Accept'] = Hybi.generate_accept(sec_key) 249 | 250 | @headers['Sec-WebSocket-Protocol'] = @protocol if @protocol 251 | 252 | extensions = @extensions.generate_response(@socket.env['HTTP_SEC_WEBSOCKET_EXTENSIONS']) 253 | @headers['Sec-WebSocket-Extensions'] = extensions if extensions 254 | 255 | start = 'HTTP/1.1 101 Switching Protocols' 256 | headers = [start, @headers.to_s, ''] 257 | headers.join("\r\n") 258 | end 259 | 260 | def shutdown(code, reason, error = false) 261 | @frame = @message = nil 262 | @stage = 5 263 | @extensions.close 264 | 265 | frame(reason, :close, code) if @ready_state < 2 266 | @ready_state = 3 267 | 268 | emit(:error, ProtocolError.new(reason)) if error 269 | emit(:close, CloseEvent.new(code, reason)) 270 | end 271 | 272 | def fail(type, message) 273 | return if @ready_state > 1 274 | shutdown(ERRORS[type], message, true) 275 | end 276 | 277 | def parse_opcode(octet) 278 | rsvs = [RSV1, RSV2, RSV3].map { |rsv| (octet & rsv) == rsv } 279 | 280 | @frame = Frame.new 281 | 282 | @frame.final = (octet & FIN) == FIN 283 | @frame.rsv1 = rsvs[0] 284 | @frame.rsv2 = rsvs[1] 285 | @frame.rsv3 = rsvs[2] 286 | @frame.opcode = (octet & OPCODE) 287 | 288 | @stage = 1 289 | 290 | unless @extensions.valid_frame_rsv?(@frame) 291 | return fail(:protocol_error, 292 | "One or more reserved bits are on: reserved1 = #{ @frame.rsv1 ? 1 : 0 }" + 293 | ", reserved2 = #{ @frame.rsv2 ? 1 : 0 }" + 294 | ", reserved3 = #{ @frame.rsv3 ? 1 : 0 }") 295 | end 296 | 297 | unless OPCODES.values.include?(@frame.opcode) 298 | return fail(:protocol_error, "Unrecognized frame opcode: #{ @frame.opcode }") 299 | end 300 | 301 | unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.final 302 | return fail(:protocol_error, "Received fragmented control frame: opcode = #{ @frame.opcode }") 303 | end 304 | 305 | if @message and OPENING_OPCODES.include?(@frame.opcode) 306 | return fail(:protocol_error, 'Received new data frame but previous continuous frame is unfinished') 307 | end 308 | end 309 | 310 | def parse_length(octet) 311 | @frame.masked = (octet & MASK) == MASK 312 | @frame.length = (octet & LENGTH) 313 | 314 | if @frame.length >= 0 and @frame.length <= 125 315 | @stage = @frame.masked ? 3 : 4 316 | return unless check_frame_length 317 | else 318 | @stage = 2 319 | @frame.length_bytes = (@frame.length == 126) ? 2 : 8 320 | end 321 | 322 | if @require_masking and not @frame.masked 323 | return fail(:unacceptable, 'Received unmasked frame but masking is required') 324 | end 325 | end 326 | 327 | def parse_extended_length(buffer) 328 | @frame.length = buffer.unpack(PACK_FORMATS[buffer.bytesize]).first 329 | @stage = @frame.masked ? 3 : 4 330 | 331 | unless MESSAGE_OPCODES.include?(@frame.opcode) or @frame.length <= 125 332 | return fail(:protocol_error, "Received control frame having too long payload: #{ @frame.length }") 333 | end 334 | 335 | return unless check_frame_length 336 | end 337 | 338 | def check_frame_length 339 | length = @message ? @message.data.bytesize : 0 340 | 341 | if length + @frame.length > @max_length 342 | fail(:too_large, 'WebSocket frame length too large') 343 | false 344 | else 345 | true 346 | end 347 | end 348 | 349 | def emit_frame(buffer) 350 | frame = @frame 351 | opcode = frame.opcode 352 | payload = frame.payload = Mask.mask(buffer, @frame.masking_key) 353 | bytesize = payload.bytesize 354 | 355 | @frame = nil 356 | 357 | case opcode 358 | when OPCODES[:continuation] then 359 | return fail(:protocol_error, 'Received unexpected continuation frame') unless @message 360 | @message << frame 361 | 362 | when OPCODES[:text], OPCODES[:binary] then 363 | @message = Message.new 364 | @message << frame 365 | 366 | when OPCODES[:close] then 367 | code, reason = payload.unpack('S>a*') if bytesize >= 2 368 | reason = Driver.encode(reason || '', Encoding::UTF_8) 369 | 370 | unless (bytesize == 0) or 371 | (code && code >= MIN_RESERVED_ERROR && code <= MAX_RESERVED_ERROR) or 372 | ERROR_CODES.include?(code) 373 | code = ERRORS[:protocol_error] 374 | end 375 | 376 | if bytesize > 125 or !reason.valid_encoding? 377 | code = ERRORS[:protocol_error] 378 | end 379 | 380 | shutdown(code || DEFAULT_ERROR_CODE, reason || '') 381 | 382 | when OPCODES[:ping] then 383 | frame(payload, :pong) 384 | emit(:ping, PingEvent.new(payload)) 385 | 386 | when OPCODES[:pong] then 387 | message = Driver.encode(payload, Encoding::UTF_8) 388 | callback = @ping_callbacks[message] 389 | @ping_callbacks.delete(message) 390 | callback.call if callback 391 | emit(:pong, PongEvent.new(payload)) 392 | end 393 | 394 | emit_message if frame.final and MESSAGE_OPCODES.include?(opcode) 395 | end 396 | 397 | def emit_message 398 | message = @extensions.process_incoming_message(@message) 399 | @message = nil 400 | 401 | payload = message.data 402 | 403 | case message.opcode 404 | when OPCODES[:text] then 405 | payload = Driver.encode(payload, Encoding::UTF_8) 406 | payload = nil unless payload.valid_encoding? 407 | when OPCODES[:binary] 408 | if @binary_data_format == :array 409 | payload = payload.bytes.to_a 410 | else 411 | payload = Driver.encode(payload, Encoding::BINARY) 412 | end 413 | end 414 | 415 | if payload 416 | emit(:message, MessageEvent.new(payload)) 417 | else 418 | fail(:encoding_error, 'Could not decode a text frame as UTF-8') 419 | end 420 | rescue ::WebSocket::Extensions::ExtensionError => error 421 | fail(:extension_error, error.message) 422 | end 423 | end 424 | 425 | end 426 | end 427 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websocket-driver 2 | 3 | This module provides a complete implementation of the WebSocket protocols that 4 | can be hooked up to any TCP library. It aims to simplify things by decoupling 5 | the protocol details from the I/O layer, such that users only need to implement 6 | code to stream data in and out of it without needing to know anything about how 7 | the protocol actually works. Think of it as a complete WebSocket system with 8 | pluggable I/O. 9 | 10 | Due to this design, you get a lot of things for free. In particular, if you hook 11 | this module up to some I/O object, it will do all of this for you: 12 | 13 | - Select the correct server-side driver to talk to the client 14 | - Generate and send both server- and client-side handshakes 15 | - Recognize when the handshake phase completes and the WS protocol begins 16 | - Negotiate subprotocol selection based on `Sec-WebSocket-Protocol` 17 | - Negotiate and use extensions via the 18 | [websocket-extensions](https://github.com/faye/websocket-extensions-ruby) 19 | module 20 | - Buffer sent messages until the handshake process is finished 21 | - Deal with proxies that defer delivery of the draft-76 handshake body 22 | - Notify you when the socket is open and closed and when messages arrive 23 | - Recombine fragmented messages 24 | - Dispatch text, binary, ping, pong and close frames 25 | - Manage the socket-closing handshake process 26 | - Automatically reply to ping frames with a matching pong 27 | - Apply masking to messages sent by the client 28 | 29 | This library was originally extracted from the [Faye](http://faye.jcoglan.com) 30 | project but now aims to provide simple WebSocket support for any Ruby server or 31 | I/O system. 32 | 33 | 34 | ## Installation 35 | 36 | ``` 37 | $ gem install websocket-driver 38 | ``` 39 | 40 | 41 | ## Usage 42 | 43 | To build either a server-side or client-side socket, the only requirement is 44 | that you supply a `socket` object with these methods: 45 | 46 | - `socket.url` - returns the full URL of the socket as a string. 47 | - `socket.write(string)` - writes the given string to a TCP stream. 48 | 49 | Server-side sockets require one additional method: 50 | 51 | - `socket.env` - returns a Rack-style env hash that will contain some of the 52 | following fields. Their values are strings containing the value of the named 53 | header, unless stated otherwise. 54 | * `HTTP_CONNECTION` 55 | * `HTTP_HOST` 56 | * `HTTP_ORIGIN` 57 | * `HTTP_SEC_WEBSOCKET_EXTENSIONS` 58 | * `HTTP_SEC_WEBSOCKET_KEY` 59 | * `HTTP_SEC_WEBSOCKET_KEY1` 60 | * `HTTP_SEC_WEBSOCKET_KEY2` 61 | * `HTTP_SEC_WEBSOCKET_PROTOCOL` 62 | * `HTTP_SEC_WEBSOCKET_VERSION` 63 | * `HTTP_UPGRADE` 64 | * `rack.input`, an `IO` object representing the request body 65 | * `REQUEST_METHOD`, the request's HTTP verb 66 | 67 | 68 | ### Server-side with Rack 69 | 70 | To handle a server-side WebSocket connection, you need to check whether the 71 | request is a WebSocket handshake, and if so create a protocol driver for it. 72 | You must give the driver an object with the `env`, `url` and `write` methods. A 73 | simple example might be: 74 | 75 | ```ruby 76 | require 'websocket/driver' 77 | require 'eventmachine' 78 | 79 | class WS 80 | attr_reader :env, :url 81 | 82 | def initialize(env) 83 | @env = env 84 | 85 | secure = Rack::Request.new(env).ssl? 86 | scheme = secure ? 'wss:' : 'ws:' 87 | @url = scheme + '//' + env['HTTP_HOST'] + env['REQUEST_URI'] 88 | 89 | @driver = WebSocket::Driver.rack(self) 90 | 91 | env['rack.hijack'].call 92 | @io = env['rack.hijack_io'] 93 | 94 | EM.attach(@io, Reader) { |conn| conn.driver = @driver } 95 | 96 | @driver.start 97 | end 98 | 99 | def write(string) 100 | @io.write(string) 101 | end 102 | 103 | module Reader 104 | attr_writer :driver 105 | 106 | def receive_data(string) 107 | @driver.parse(string) 108 | end 109 | end 110 | end 111 | ``` 112 | 113 | To explain what's going on here: the `WS` class implements the `env`, `url` and 114 | `write(string)` methods as required. When instantiated with a Rack environment, 115 | it stores the environment and infers the complete URL from it. Having set up 116 | the `env` and `url`, it asks `WebSocket::Driver` for a server-side driver for 117 | the socket. Then it uses the Rack hijack API to gain access to the TCP stream, 118 | and uses EventMachine to stream in incoming data from the client, handing 119 | incoming data off to the driver for parsing. Finally, we tell the driver to 120 | `start`, which will begin sending the handshake response. This will invoke the 121 | `WS#write` method, which will send the response out over the TCP socket. 122 | 123 | Having defined this class we could use it like this when handling a request: 124 | 125 | ```ruby 126 | if WebSocket::Driver.websocket?(env) 127 | socket = WS.new(env) 128 | end 129 | ``` 130 | 131 | The driver API is described in full below. 132 | 133 | 134 | ### Server-side with TCP 135 | 136 | You can also handle WebSocket connections in a bare TCP server, if you're not 137 | using Rack and don't want to implement HTTP parsing yourself. For this, your 138 | socket object only needs a `write` method. 139 | 140 | The driver will emit a `:connect` event when a request is received, and at this 141 | point you can detect whether it's a WebSocket and handle it as such. Here's an 142 | example using an EventMachine TCP server. 143 | 144 | ```ruby 145 | module Connection 146 | def initialize 147 | @driver = WebSocket::Driver.server(self) 148 | 149 | @driver.on :connect, -> (event) do 150 | if WebSocket::Driver.websocket?(@driver.env) 151 | @driver.start 152 | else 153 | # handle other HTTP requests, for example 154 | body = '

hello

' 155 | response = [ 156 | 'HTTP/1.1 200 OK', 157 | 'Content-Type: text/plain', 158 | "Content-Length: #{body.bytesize}", 159 | '', 160 | body 161 | ] 162 | send_data response.join("\r\n") 163 | end 164 | end 165 | 166 | @driver.on :message, -> (e) { @driver.text(e.data) } 167 | @driver.on :close, -> (e) { close_connection_after_writing } 168 | end 169 | 170 | def receive_data(data) 171 | @driver.parse(data) 172 | end 173 | 174 | def write(data) 175 | send_data(data) 176 | end 177 | end 178 | 179 | EM.run { 180 | EM.start_server('127.0.0.1', 4180, Connection) 181 | } 182 | ``` 183 | 184 | In the `:connect` event, `@driver.env` is a Rack env representing the request. 185 | If the request has a body, it will be in the `@driver.env['rack.input']` stream, 186 | but only as much of the body as you have so far routed to it using the `parse` 187 | method. 188 | 189 | 190 | ### Client-side 191 | 192 | Similarly, to implement a WebSocket client you need an object with `url` and 193 | `write` methods. Once you have one such object, you ask for a driver for it: 194 | 195 | ```ruby 196 | driver = WebSocket::Driver.client(socket) 197 | ``` 198 | 199 | After this you use the driver API as described below to process incoming data 200 | and send outgoing data. 201 | 202 | Client drivers have two additional methods for reading the HTTP data that was 203 | sent back by the server: 204 | 205 | - `driver.status` - the integer value of the HTTP status code 206 | - `driver.headers` - a hash-like object containing the response headers 207 | 208 | 209 | ### HTTP Proxies 210 | 211 | The client driver supports connections via HTTP proxies using the `CONNECT` 212 | method. Instead of sending the WebSocket handshake immediately, it will send a 213 | `CONNECT` request, wait for a `200` response, and then proceed as normal. 214 | 215 | To use this feature, call `proxy = driver.proxy(url)` where `url` is the origin 216 | of the proxy, including a username and password if required. This produces an 217 | object that manages the process of connecting via the proxy. You should call 218 | `proxy.start` to begin the connection process, and pass data you receive via the 219 | socket to `proxy.parse(data)`. When the proxy emits `:connect`, you should then 220 | start sending incoming data to `driver.parse(data)` as normal, and call 221 | `driver.start`. 222 | 223 | ```rb 224 | proxy = driver.proxy('http://username:password@proxy.example.com') 225 | 226 | proxy.on :connect, -> (event) do 227 | driver.start 228 | end 229 | ``` 230 | 231 | The proxy's `:connect` event is also where you should perform a TLS handshake on 232 | your TCP stream, if you are connecting to a `wss:` endpoint. 233 | 234 | In the event that proxy connection fails, `proxy` will emit an `:error`. You can 235 | inspect the proxy's response via `proxy.status` and `proxy.headers`. 236 | 237 | ```rb 238 | proxy.on :error, -> (error) do 239 | puts error.message 240 | puts proxy.status 241 | puts proxy.headers.inspect 242 | end 243 | ``` 244 | 245 | Before calling `proxy.start` you can set custom headers using 246 | `proxy.set_header`: 247 | 248 | ```rb 249 | proxy.set_header('User-Agent', 'ruby') 250 | proxy.start 251 | ``` 252 | 253 | 254 | ### Driver API 255 | 256 | Drivers are created using one of the following methods: 257 | 258 | ```ruby 259 | driver = WebSocket::Driver.rack(socket, options) 260 | driver = WebSocket::Driver.server(socket, options) 261 | driver = WebSocket::Driver.client(socket, options) 262 | ``` 263 | 264 | The `rack` method returns a driver chosen using the socket's `env`. The `server` 265 | method returns a driver that will parse an HTTP request and then decide which 266 | driver to use for it using the `rack` method. The `client` method always returns 267 | a driver for the RFC version of the protocol with masking enabled on outgoing 268 | frames. 269 | 270 | The `options` argument is optional, and is a hash. It may contain the following 271 | keys: 272 | 273 | - `:max_length` - the maximum allowed size of incoming message frames, in bytes. 274 | The default value is `2^26 - 1`, or 1 byte short of 64 MiB. 275 | - `:protocols` - an array of strings representing acceptable subprotocols for 276 | use over the socket. The driver will negotiate one of these to use via the 277 | `Sec-WebSocket-Protocol` header if supported by the other peer. 278 | - `:binary_data_format` - in older versions of this library, binary messages 279 | were represented as arrays of bytes, whereas they're now represented as 280 | strings with `Encoding::BINARY` for performance reasons. Set this option to 281 | `:array` to restore the old behaviour. 282 | 283 | All drivers respond to the following API methods, but some of them are no-ops 284 | depending on whether the client supports the behaviour. 285 | 286 | Note that most of these methods are commands: if they produce data that should 287 | be sent over the socket, they will give this to you by calling 288 | `socket.write(string)`. 289 | 290 | #### `driver.on :open, -> (event) {}` 291 | 292 | Adds a callback block to execute when the socket becomes open. 293 | 294 | #### `driver.on :message, -> (event) {}` 295 | 296 | Adds a callback block to execute when a message is received. `event` will have a 297 | `data` attribute whose value is a string with the encoding `Encoding::UTF_8` for 298 | text message, and `Encoding::BINARY` for binary message. 299 | 300 | #### `driver.on :error, -> (event) {}` 301 | 302 | Adds a callback to execute when a protocol error occurs due to the other peer 303 | sending an invalid byte sequence. `event` will have a `message` attribute 304 | describing the error. 305 | 306 | #### `driver.on :close, -> (event) {}` 307 | 308 | Adds a callback block to execute when the socket becomes closed. The `event` 309 | object has `code` and `reason` attributes. 310 | 311 | #### `driver.on :ping, -> (event) {}` 312 | 313 | Adds a callback block to execute when a ping is received. You do not need to 314 | handle this by sending a pong frame yourself; the driver handles this for you. 315 | 316 | #### `driver.on :pong, -> (event) {}` 317 | 318 | Adds a callback block to execute when a pong is received. If this was in 319 | response to a ping you sent, you can also handle this event via the 320 | `driver.ping(message) { ... }` callback. 321 | 322 | #### `driver.add_extension(extension)` 323 | 324 | Registers a protocol extension whose operation will be negotiated via the 325 | `Sec-WebSocket-Extensions` header. `extension` is any extension compatible with 326 | the [websocket-extensions](https://github.com/faye/websocket-extensions-ruby) 327 | framework. 328 | 329 | #### `driver.set_header(name, value)` 330 | 331 | Sets a custom header to be sent as part of the handshake response, either from 332 | the server or from the client. Must be called before `start`, since this is when 333 | the headers are serialized and sent. 334 | 335 | #### `driver.start` 336 | 337 | Initiates the protocol by sending the handshake - either the response for a 338 | server-side driver or the request for a client-side one. This should be the 339 | first method you invoke. Returns `true` if and only if a handshake was sent. 340 | 341 | #### `driver.parse(string)` 342 | 343 | Takes a string and parses it, potentially resulting in message events being 344 | emitted (see `on('message')` above) or in data being sent to `socket.write`. 345 | You should send all data you receive via I/O to this method. 346 | 347 | #### `driver.text(string)` 348 | 349 | Sends a text message over the socket. If the socket handshake is not yet 350 | complete, the message will be queued until it is. Returns `true` if the message 351 | was sent or queued, and `false` if the socket can no longer send messages. 352 | 353 | #### `driver.binary(buffer)` 354 | 355 | Takes either a string with encoding `Encoding::BINARY`, or an array of 356 | byte-sized integers, and sends it as a binary message. Will queue and return 357 | `true` or `false` the same way as the `text` method. It will also return `false` 358 | if the driver does not support binary messages. 359 | 360 | #### `driver.ping(string = '', &callback)` 361 | 362 | Sends a ping frame over the socket, queueing it if necessary. `string` and the 363 | `callback` block are both optional. If a callback is given, it will be invoked 364 | when the socket receives a pong frame whose content matches `string`. Returns 365 | `false` if frames can no longer be sent, or if the driver does not support 366 | ping/pong. 367 | 368 | #### `driver.pong(string = '')` 369 | 370 | Sends a pong frame over the socket, queueing it if necessary. `string` is 371 | optional. Returns `false` if frames can no longer be sent, or if the driver does 372 | not support ping/pong. 373 | 374 | You don't need to call this when a ping frame is received; pings are replied to 375 | automatically by the driver. This method is for sending unsolicited pongs. 376 | 377 | #### `driver.close` 378 | 379 | Initiates the closing handshake if the socket is still open. For drivers with no 380 | closing handshake, this will result in the immediate execution of the 381 | `on('close')` callback. For drivers with a closing handshake, this sends a 382 | closing frame and `emit('close')` will execute when a response is received or a 383 | protocol error occurs. 384 | 385 | #### `driver.version` 386 | 387 | Returns the WebSocket version in use as a string. Will either be `hixie-75`, 388 | `hixie-76` or `hybi-$version`. 389 | 390 | #### `driver.protocol` 391 | 392 | Returns a string containing the selected subprotocol, if any was agreed upon 393 | using the `Sec-WebSocket-Protocol` mechanism. This value becomes available after 394 | `emit('open')` has fired. 395 | -------------------------------------------------------------------------------- /spec/websocket/driver/hybi_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | require "spec_helper" 4 | 5 | describe WebSocket::Driver::Hybi do 6 | include EncodingHelper 7 | 8 | let :env do 9 | { 10 | "REQUEST_METHOD" => "GET", 11 | "HTTP_CONNECTION" => "Upgrade", 12 | "HTTP_UPGRADE" => "websocket", 13 | "HTTP_ORIGIN" => "http://www.example.com", 14 | # "HTTP_SEC_WEBSOCKET_EXTENSIONS" => "x-webkit-deflate-frame", 15 | "HTTP_SEC_WEBSOCKET_KEY" => "JFBCWHksyIpXV+6Wlq/9pw==", 16 | "HTTP_SEC_WEBSOCKET_VERSION" => "13" 17 | } 18 | end 19 | 20 | let :options do 21 | { :masking => false } 22 | end 23 | 24 | let :socket do 25 | socket = double(WebSocket) 26 | allow(socket).to receive(:env).and_return(env) 27 | allow(socket).to receive(:write) { |message| @bytes = bytes(message) } 28 | socket 29 | end 30 | 31 | let :driver do 32 | driver = WebSocket::Driver::Hybi.new(socket, options) 33 | driver.on :open, -> e { @open = true } 34 | driver.on(:message) { |e| extend_message(e.data) } 35 | driver.on(:error) { |e| @error = e } 36 | driver.on(:close) { |e| @close = [e.code, e.reason] } 37 | driver 38 | end 39 | 40 | def extend_message(data) 41 | if Array === data 42 | @message = @message.bytes.to_a unless Array === @message 43 | else 44 | @message = @message.to_s 45 | @message.force_encoding(data.encoding) 46 | end 47 | @message += data 48 | end 49 | 50 | before do 51 | @open = @error = @close = false 52 | @message = "" 53 | end 54 | 55 | describe "in the :connecting state" do 56 | it "starts in the :connecting state" do 57 | expect(driver.state).to eq :connecting 58 | end 59 | 60 | describe :start do 61 | it "writes the handshake response to the socket" do 62 | expect(socket).to receive(:write).with( 63 | "HTTP/1.1 101 Switching Protocols\r\n" + 64 | "Upgrade: websocket\r\n" + 65 | "Connection: Upgrade\r\n" + 66 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 67 | "\r\n") 68 | driver.start 69 | end 70 | 71 | it "returns true" do 72 | expect(driver.start).to eq true 73 | end 74 | 75 | describe "with subprotocols" do 76 | before do 77 | env["HTTP_SEC_WEBSOCKET_PROTOCOL"] = "foo, bar, xmpp" 78 | options[:protocols] = ["xmpp"] 79 | end 80 | 81 | it "writes the handshake with Sec-WebSocket-Protocol" do 82 | expect(socket).to receive(:write).with( 83 | "HTTP/1.1 101 Switching Protocols\r\n" + 84 | "Upgrade: websocket\r\n" + 85 | "Connection: Upgrade\r\n" + 86 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 87 | "Sec-WebSocket-Protocol: xmpp\r\n" + 88 | "\r\n") 89 | driver.start 90 | end 91 | 92 | it "sets the subprotocol" do 93 | driver.start 94 | expect(driver.protocol).to eq "xmpp" 95 | end 96 | end 97 | 98 | describe "with invalid extensions" do 99 | before do 100 | env["HTTP_SEC_WEBSOCKET_EXTENSIONS"] = "x-webkit- -frame" 101 | end 102 | 103 | it "writes a handshake error response" do 104 | expect(socket).to receive(:write).with( 105 | "HTTP/1.1 400 Bad Request\r\n" + 106 | "Content-Type: text/plain\r\n" + 107 | "Content-Length: 57\r\n" + 108 | "\r\n" + 109 | "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame") 110 | driver.start 111 | end 112 | 113 | it "does not trigger the onopen event" do 114 | driver.start 115 | expect(@open).to eq false 116 | end 117 | 118 | it "triggers the onerror event" do 119 | driver.start 120 | expect(@error.message).to eq "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame" 121 | end 122 | 123 | it "triggers the onclose event" do 124 | driver.start 125 | expect(@close).to eq [1002, "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame"] 126 | end 127 | 128 | it "changes the state to :closed" do 129 | driver.start 130 | expect(driver.state).to eq :closed 131 | end 132 | end 133 | 134 | describe "with custom headers" do 135 | before do 136 | driver.set_header "Authorization", "Bearer WAT" 137 | end 138 | 139 | it "writes the handshake with the custom headers" do 140 | expect(socket).to receive(:write).with( 141 | "HTTP/1.1 101 Switching Protocols\r\n" + 142 | "Authorization: Bearer WAT\r\n" + 143 | "Upgrade: websocket\r\n" + 144 | "Connection: Upgrade\r\n" + 145 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 146 | "\r\n") 147 | driver.start 148 | end 149 | end 150 | 151 | it "triggers the onopen event" do 152 | driver.start 153 | expect(@open).to eq true 154 | end 155 | 156 | it "changes the state to :open" do 157 | driver.start 158 | expect(driver.state).to eq :open 159 | end 160 | 161 | it "sets the protocol version" do 162 | driver.start 163 | expect(driver.version).to eq "hybi-13" 164 | end 165 | end 166 | 167 | describe :frame do 168 | it "does not write to the socket" do 169 | expect(socket).not_to receive(:write) 170 | driver.frame("Hello, world") 171 | end 172 | 173 | it "returns true" do 174 | expect(driver.frame("whatever")).to eq true 175 | end 176 | 177 | it "queues the frames until the handshake has been sent" do 178 | expect(socket).to receive(:write).with( 179 | "HTTP/1.1 101 Switching Protocols\r\n" + 180 | "Upgrade: websocket\r\n" + 181 | "Connection: Upgrade\r\n" + 182 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 183 | "\r\n") 184 | expect(socket).to receive(:write).with(encode [0x81, 0x02, 72, 105]) 185 | 186 | driver.frame("Hi") 187 | driver.start 188 | end 189 | end 190 | 191 | describe :ping do 192 | it "does not write to the socket" do 193 | expect(socket).not_to receive(:write) 194 | driver.ping 195 | end 196 | 197 | it "returns true" do 198 | expect(driver.ping).to eq true 199 | end 200 | 201 | it "queues the ping until the handshake has been sent" do 202 | expect(socket).to receive(:write).with( 203 | "HTTP/1.1 101 Switching Protocols\r\n" + 204 | "Upgrade: websocket\r\n" + 205 | "Connection: Upgrade\r\n" + 206 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 207 | "\r\n") 208 | expect(socket).to receive(:write).with(encode [0x89, 0]) 209 | 210 | driver.ping 211 | driver.start 212 | end 213 | end 214 | 215 | describe :pong do 216 | it "does not write to the socket" do 217 | expect(socket).not_to receive(:write) 218 | driver.pong 219 | end 220 | 221 | it "returns true" do 222 | expect(driver.pong).to eq true 223 | end 224 | 225 | it "queues the pong until the handshake has been sent" do 226 | expect(socket).to receive(:write).with( 227 | "HTTP/1.1 101 Switching Protocols\r\n" + 228 | "Upgrade: websocket\r\n" + 229 | "Connection: Upgrade\r\n" + 230 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 231 | "\r\n") 232 | expect(socket).to receive(:write).with(encode [0x8a, 0]) 233 | 234 | driver.pong 235 | driver.start 236 | end 237 | end 238 | 239 | describe :close do 240 | it "does not write anything to the socket" do 241 | expect(socket).not_to receive(:write) 242 | driver.close 243 | end 244 | 245 | it "returns true" do 246 | expect(driver.close).to eq true 247 | end 248 | 249 | it "triggers the onclose event" do 250 | driver.close 251 | expect(@close).to eq [1000, ""] 252 | end 253 | 254 | it "changes the state to :closed" do 255 | driver.close 256 | expect(driver.state).to eq :closed 257 | end 258 | end 259 | end 260 | 261 | describe "in the :open state" do 262 | before { driver.start } 263 | 264 | describe :parse do 265 | let(:mask) { (1..4).map { rand 255 } } 266 | 267 | def mask_message(*bytes) 268 | output = [] 269 | bytes.each_with_index do |byte, i| 270 | output[i] = byte ^ mask[i % 4] 271 | end 272 | output 273 | end 274 | 275 | it "parses unmasked text frames" do 276 | driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 277 | expect(@message).to eq "Hello" 278 | expect(@message.encoding).to eq Encoding::UTF_8 279 | end 280 | 281 | it "parses unmasked binary frames" do 282 | driver.parse [0x82, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 283 | expect(@message).to eq "Hello" 284 | expect(@message.encoding).to eq Encoding::BINARY 285 | end 286 | 287 | it "parses multiple frames from the same packet" do 288 | driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 289 | expect(@message).to eq "HelloHello" 290 | end 291 | 292 | it "parses empty text frames" do 293 | driver.parse [0x81, 0x00].pack("C*") 294 | expect(@message).to eq "" 295 | end 296 | 297 | it "parses fragmented text frames" do 298 | driver.parse [0x01, 0x03, 0x48, 0x65, 0x6c].pack("C*") 299 | driver.parse [0x80, 0x02, 0x6c, 0x6f].pack("C*") 300 | expect(@message).to eq "Hello" 301 | end 302 | 303 | it "parses masked text frames" do 304 | driver.parse ([0x81, 0x85] + mask + mask_message(0x48, 0x65, 0x6c, 0x6c, 0x6f)).pack("C*") 305 | expect(@message).to eq "Hello" 306 | end 307 | 308 | it "parses masked empty text frames" do 309 | driver.parse ([0x81, 0x80] + mask + mask_message()).pack("C*") 310 | expect(@message).to eq "" 311 | end 312 | 313 | it "parses masked fragmented text frames" do 314 | driver.parse ([0x01, 0x81] + mask + mask_message(0x48)).pack("C*") 315 | driver.parse ([0x80, 0x84] + mask + mask_message(0x65, 0x6c, 0x6c, 0x6f)).pack("C*") 316 | expect(@message).to eq "Hello" 317 | end 318 | 319 | it "closes the socket if the frame has an unrecognized opcode" do 320 | driver.parse [0x83, 0x00].pack("C*") 321 | expect(@bytes[0..3]).to eq [0x88, 0x1e, 0x03, 0xea] 322 | expect(@error.message).to eq "Unrecognized frame opcode: 3" 323 | expect(@close).to eq [1002, "Unrecognized frame opcode: 3"] 324 | expect(driver.state).to eq :closed 325 | end 326 | 327 | it "closes the socket if a close frame is received" do 328 | driver.parse [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 329 | expect(@bytes).to eq [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f] 330 | expect(@close).to eq [1000, "Hello"] 331 | expect(driver.state).to eq :closed 332 | end 333 | 334 | it "parses unmasked multibyte text frames" do 335 | driver.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf].pack("C*") 336 | expect(@message).to eq encode("Apple = ") 337 | end 338 | 339 | it "parses frames received in several packets" do 340 | driver.parse [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c].pack("C*") 341 | driver.parse [0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf].pack("C*") 342 | expect(@message).to eq encode("Apple = ") 343 | end 344 | 345 | it "parses fragmented multibyte text frames" do 346 | driver.parse [0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3].pack("C*") 347 | driver.parse [0x80, 0x01, 0xbf].pack("C*") 348 | expect(@message).to eq encode("Apple = ") 349 | end 350 | 351 | it "parses masked multibyte text frames" do 352 | driver.parse ([0x81, 0x8b] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf)).pack("C*") 353 | expect(@message).to eq encode("Apple = ") 354 | end 355 | 356 | it "parses masked fragmented multibyte text frames" do 357 | driver.parse ([0x01, 0x8a] + mask + mask_message(0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3)).pack("C*") 358 | driver.parse ([0x80, 0x81] + mask + mask_message(0xbf)).pack("C*") 359 | expect(@message).to eq encode("Apple = ") 360 | end 361 | 362 | it "parses unmasked medium-length text frames" do 363 | driver.parse ([0x81, 0x7e, 0x00, 0xc8] + [0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40).pack("C*") 364 | expect(@message).to eq "Hello" * 40 365 | end 366 | 367 | it "returns an error for too-large frames" do 368 | driver.parse [0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00].pack("C*") 369 | expect(@error.message).to eq "WebSocket frame length too large" 370 | expect(@close).to eq [1009, "WebSocket frame length too large"] 371 | expect(driver.state).to eq :closed 372 | end 373 | 374 | it "parses masked medium-length text frames" do 375 | driver.parse ([0x81, 0xfe, 0x00, 0xc8] + mask + mask_message(*([0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40))).pack("C*") 376 | expect(@message).to eq "Hello" * 40 377 | end 378 | 379 | it "replies to pings with a pong" do 380 | driver.parse [0x89, 0x04, 0x4f, 0x48, 0x41, 0x49].pack("C*") 381 | expect(@bytes).to eq [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49] 382 | end 383 | 384 | it "triggers the onping event when a ping arrives" do 385 | ping, pong = nil 386 | driver.on(:ping) { |event| ping = event } 387 | driver.on(:pong) { |event| pong = event } 388 | 389 | driver.parse [0x89, 0x04, 0x4f, 0x48, 0x41, 0x49].pack("C*") 390 | 391 | expect(ping.data).to eq("OHAI") 392 | expect(pong).to be_nil 393 | end 394 | 395 | describe "when a message listener raises an error" do 396 | before do 397 | @messages = [] 398 | 399 | driver.on :message do |msg| 400 | @messages << msg.data 401 | raise "an error" 402 | end 403 | end 404 | 405 | it "is not trapped by the parser" do 406 | buffer = [0x81, 0x02, 0x48, 0x65].pack('C*') 407 | expect { driver.parse buffer }.to raise_error(RuntimeError, "an error") 408 | end 409 | 410 | it "parses unmasked text frames without dropping input" do 411 | driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05].pack("C*") rescue nil 412 | driver.parse [0x57, 0x6f, 0x72, 0x6c, 0x64].pack("C*") rescue nil 413 | expect(@messages).to eq(["Hello", "World"]) 414 | end 415 | end 416 | end 417 | 418 | describe :frame do 419 | it "formats the given string as a WebSocket frame" do 420 | driver.frame "Hello" 421 | expect(@bytes).to eq [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f] 422 | end 423 | 424 | it "formats a byte array as a binary WebSocket frame" do 425 | driver.frame [0x48, 0x65, 0x6c] 426 | expect(@bytes).to eq [0x82, 0x03, 0x48, 0x65, 0x6c] 427 | end 428 | 429 | it "formats a binary string as a binary WebSocket frame" do 430 | driver.frame [0x48, 0x65, 0x6c].pack("C*") 431 | expect(@bytes).to eq [0x82, 0x03, 0x48, 0x65, 0x6c] 432 | end 433 | 434 | it "encodes multibyte characters correctly" do 435 | message = encode("Apple = ") 436 | driver.frame message 437 | expect(@bytes).to eq [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf] 438 | end 439 | 440 | it "encodes medium-length strings using extra length bytes" do 441 | message = "Hello" * 40 442 | driver.frame message 443 | expect(@bytes).to eq [0x81, 0x7e, 0x00, 0xc8] + [0x48, 0x65, 0x6c, 0x6c, 0x6f] * 40 444 | end 445 | 446 | it "encodes close frames with an error code" do 447 | driver.frame "Hello", :close, 1002 448 | expect(@bytes).to eq [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f] 449 | end 450 | 451 | it "encodes pong frames" do 452 | driver.frame "", :pong 453 | expect(@bytes).to eq [0x8a, 0x00] 454 | end 455 | end 456 | 457 | describe :ping do 458 | before do 459 | @reply = nil 460 | end 461 | 462 | it "writes a ping frame to the socket" do 463 | driver.ping("mic check") 464 | expect(@bytes).to eq [0x89, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b] 465 | end 466 | 467 | it "returns true" do 468 | expect(driver.ping).to eq true 469 | end 470 | 471 | it "runs the given callback on matching pong" do 472 | driver.ping("Hi") { @reply = true } 473 | driver.parse [0x8a, 0x02, 72, 105].pack("C*") 474 | expect(@reply).to eq true 475 | end 476 | 477 | it "triggers the onpong event when a pong arrives" do 478 | ping, pong = nil 479 | driver.on(:ping) { |event| ping = event } 480 | driver.on(:pong) { |event| pong = event } 481 | 482 | driver.parse [0x8a, 0x02, 72, 105].pack("C*") 483 | 484 | expect(ping).to be_nil 485 | expect(pong.data).to eq("Hi") 486 | end 487 | 488 | 489 | it "does not run the callback on non-matching pong" do 490 | driver.ping("Hi") { @reply = true } 491 | driver.parse [0x8a, 0x03, 119, 97, 116].pack("C*") 492 | expect(@reply).to eq nil 493 | end 494 | end 495 | 496 | describe :pong do 497 | it "writes a pong frame to the socket" do 498 | driver.pong("mic check") 499 | expect(@bytes).to eq [0x8a, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b] 500 | end 501 | 502 | it "returns true" do 503 | expect(driver.pong).to eq true 504 | end 505 | end 506 | 507 | describe :close do 508 | it "writes a close frame to the socket" do 509 | driver.close("<%= reasons %>", 1003) 510 | expect(@bytes).to eq [0x88, 0x10, 0x03, 0xeb, 0x3c, 0x25, 0x3d, 0x20, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x25, 0x3e] 511 | end 512 | 513 | it "returns true" do 514 | expect(driver.close).to eq true 515 | end 516 | 517 | it "does not trigger the onclose event" do 518 | driver.close 519 | expect(@close).to eq false 520 | end 521 | 522 | it "does not trigger the onerror event" do 523 | driver.close 524 | expect(@error).to eq false 525 | end 526 | 527 | it "changes the state to :closing" do 528 | driver.close 529 | expect(driver.state).to eq :closing 530 | end 531 | end 532 | end 533 | 534 | describe "when masking is required" do 535 | before do 536 | options[:require_masking] = true 537 | driver.start 538 | end 539 | 540 | it "does not emit a message" do 541 | driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 542 | expect(@message).to eq "" 543 | end 544 | 545 | it "returns an error" do 546 | driver.parse [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f].pack("C*") 547 | expect(@close).to eq [1003, "Received unmasked frame but masking is required"] 548 | end 549 | end 550 | 551 | describe "in the :closing state" do 552 | before do 553 | driver.start 554 | driver.close 555 | end 556 | 557 | describe :frame do 558 | it "does not write to the socket" do 559 | expect(socket).not_to receive(:write) 560 | driver.frame("dropped") 561 | end 562 | 563 | it "returns false" do 564 | expect(driver.frame("wut")).to eq false 565 | end 566 | end 567 | 568 | describe :ping do 569 | it "does not write to the socket" do 570 | expect(socket).not_to receive(:write) 571 | driver.ping 572 | end 573 | 574 | it "returns false" do 575 | expect(driver.ping).to eq false 576 | end 577 | end 578 | 579 | describe :pong do 580 | it "does not write to the socket" do 581 | expect(socket).not_to receive(:write) 582 | driver.pong 583 | end 584 | 585 | it "returns false" do 586 | expect(driver.pong).to eq false 587 | end 588 | end 589 | 590 | describe :close do 591 | it "does not write to the socket" do 592 | expect(socket).not_to receive(:write) 593 | driver.close 594 | end 595 | 596 | it "returns false" do 597 | expect(driver.close).to eq false 598 | end 599 | end 600 | 601 | describe "receiving a close frame" do 602 | before do 603 | driver.parse [0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b].pack("C*") 604 | end 605 | 606 | it "triggers the onclose event" do 607 | expect(@close).to eq [1001, "OK"] 608 | end 609 | 610 | it "changes the state to :closed" do 611 | expect(driver.state).to eq :closed 612 | end 613 | 614 | it "does not write another close frame" do 615 | expect(socket).not_to receive(:write) 616 | driver.parse [0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b].pack("C*") 617 | end 618 | end 619 | 620 | describe "receiving a close frame with a too-short payload" do 621 | before do 622 | driver.parse [0x88, 0x01, 0x03].pack("C*") 623 | end 624 | 625 | it "triggers the onclose event with a protocol error" do 626 | expect(@close).to eq [1002, ""] 627 | end 628 | 629 | it "changes the state to :closed" do 630 | expect(driver.state).to eq :closed 631 | end 632 | end 633 | 634 | describe "receiving a close frame with no code" do 635 | before do 636 | driver.parse [0x88, 0x00].pack("C*") 637 | end 638 | 639 | it "triggers the onclose event with code 1000" do 640 | expect(@close).to eq [1000, ""] 641 | end 642 | 643 | it "changes the state to :closed" do 644 | expect(driver.state).to eq :closed 645 | end 646 | end 647 | end 648 | 649 | describe "in the :closed state" do 650 | before do 651 | driver.start 652 | driver.close 653 | driver.parse [0x88, 0x02, 0x03, 0xea].pack("C*") 654 | end 655 | 656 | describe :frame do 657 | it "does not write to the socket" do 658 | expect(socket).not_to receive(:write) 659 | driver.frame("dropped") 660 | end 661 | 662 | it "returns false" do 663 | expect(driver.frame("wut")).to eq false 664 | end 665 | end 666 | 667 | describe :ping do 668 | it "does not write to the socket" do 669 | expect(socket).not_to receive(:write) 670 | driver.ping 671 | end 672 | 673 | it "returns false" do 674 | expect(driver.ping).to eq false 675 | end 676 | end 677 | 678 | describe :pong do 679 | it "does not write to the socket" do 680 | expect(socket).not_to receive(:write) 681 | driver.pong 682 | end 683 | 684 | it "returns false" do 685 | expect(driver.pong).to eq false 686 | end 687 | end 688 | 689 | describe :close do 690 | it "does not write to the socket" do 691 | expect(socket).not_to receive(:write) 692 | driver.close 693 | end 694 | 695 | it "returns false" do 696 | expect(driver.close).to eq false 697 | end 698 | 699 | it "leaves the state as :closed" do 700 | driver.close 701 | expect(driver.state).to eq :closed 702 | end 703 | end 704 | end 705 | end 706 | --------------------------------------------------------------------------------