├── .gitignore ├── .rubocop.yml ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── ddp-server.gemspec ├── examples └── rack_example.ru ├── lib └── ddp │ ├── ejson.rb │ ├── server.rb │ └── server │ ├── api.rb │ ├── protocol.rb │ ├── protocol │ ├── data.rb │ ├── heartbeat.rb │ └── rpc.rb │ └── version.rb └── spec ├── ejson_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/Tab: 2 | Enabled: false 3 | Style/IndentationWidth: 4 | Width: 1 5 | Metrics/LineLength: 6 | Max: 120 7 | Style/SignalException: 8 | Enabled: false 9 | Style/MultilineOperationIndentation: 10 | Enabled: false 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem 'rspec' 5 | gem 'rspec-mocks' 6 | gem 'webmock' 7 | gem 'guard' 8 | gem 'guard-rspec' 9 | gem 'cucumber', require: false 10 | gem 'guard-cucumber' 11 | gem 'rubocop', require: false 12 | gem 'guard-rubocop' 13 | gem 'simplecov', require: false 14 | end 15 | 16 | # Specify your gem's dependencies in ddp-server.gemspec 17 | gemspec 18 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | # Note: The cmd option is now required due to the increasing number of ways 5 | # rspec may be run, below are examples of the most common uses. 6 | # * bundler: 'bundle exec rspec' 7 | # * bundler binstubs: 'bin/rspec' 8 | # * spring: 'bin/rsspec' (This will use spring if running and you have 9 | # installed the spring binstubs per the docs) 10 | # * zeus: 'zeus rspec' (requires the server to be started separetly) 11 | # * 'just' rspec: 'rspec' 12 | guard :rspec, cmd: 'bundle exec rspec -c' do 13 | watch(/^spec\/.+_spec\.rb$/) { 'spec' } 14 | watch(/^lib\/.+\.rb$/) { 'spec' } 15 | watch('spec/spec_helper.rb') { 'spec' } 16 | end 17 | 18 | guard :rubocop do 19 | watch(/.+\.rb$/) 20 | watch(/(?:.+\/)?\.rubocop\.yml$/) { |m| File.dirname(m[0]) } 21 | end 22 | 23 | guard 'cucumber' do 24 | watch(/^features\/.+\.feature$/) 25 | watch(%r{^features\/support\/.+$}) { 'features' } 26 | 27 | watch(%r{^features/step_definitions/(.+)_steps\.rb$}) do |m| 28 | Dir[File.join("**/#{m[1]}.feature")][0] || 'features' 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tinco Andringa 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDP::Server 2 | 3 | Ruby implementation of the DDP protocol for Meteor style servers. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'ddp-server' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install ddp-server 18 | 19 | ## Usage 20 | 21 | For usage check out the `ddp-server-rethinkdb` gem which should be a good example of an implementation. This gem only covers the bare basics of the protocol. 22 | 23 | ## Contributing 24 | 25 | 1. Fork it 26 | 2. Create your feature branch (`git checkout -b my-new-feature`) 27 | 3. Commit your changes (`git commit -am 'Add some feature'`) 28 | 4. Push to the branch (`git push origin my-new-feature`) 29 | 5. Create new Pull Request 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /ddp-server.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ddp/server/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'ddp-server' 8 | spec.version = DDP::Server::VERSION 9 | spec.authors = ['Tinco Andringa'] 10 | spec.email = ['mail@tinco.nl'] 11 | spec.description = 'DDP Protocol server for implementing Ruby DDP backends' 12 | spec.summary = 'DDP Protocol server for implementing Ruby DDP backends' 13 | spec.homepage = 'https://github.com/d-snp/ruby-ddp-server' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($RS) 17 | spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(/^(test|spec|features)\//) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.3' 22 | spec.add_development_dependency 'rake' 23 | 24 | spec.add_dependency 'celluloid-websocket', '~> 0.0.10' 25 | end 26 | -------------------------------------------------------------------------------- /examples/rack_example.ru: -------------------------------------------------------------------------------- 1 | require 'ddp/server' 2 | 3 | class APIClass 4 | def invoke_rpc(method, *args) 5 | case method 6 | when 'hello_world' then 'Hello world!' 7 | else 8 | raise 'Don\'t know that method' 9 | end 10 | end 11 | 12 | def collection_query(name, *args) 13 | lambda do |&on_update| 14 | 5.times do |i| 15 | sleep 5 16 | on_update.({}, id: 1, message: "Message #{name}: #{i}") 17 | end 18 | end 19 | end 20 | end 21 | 22 | run DDP::Server::WebSocket.rack(APIClass) -------------------------------------------------------------------------------- /lib/ddp/ejson.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'base64' 3 | 4 | module DDP 5 | # EJSON is a way of embedding more than the built-in JSON types in JSON. 6 | # It supports all types built into JSON as plain JSON, plus some custom 7 | # types identified by a key prefixed with '$'. 8 | class EJSON 9 | def self.parse(string) 10 | parsed = JSON.parse string 11 | 12 | deserialize(parsed) 13 | end 14 | 15 | def self.generate(object) 16 | JSON.generate as_ejson(object) 17 | end 18 | 19 | def self.deserialize(object) 20 | if object.is_a? Hash 21 | deserialize_hash(object) 22 | elsif object.is_a? Array 23 | object.map { |e| deserialize(e) } 24 | else 25 | object 26 | end 27 | end 28 | 29 | def self.as_ejson(object) 30 | if object.respond_to? :as_ejson 31 | object.as_ejson 32 | elsif object.is_a? Hash 33 | hash_as_ejson(object) 34 | elsif object.is_a? Array 35 | object.map { |i| as_ejson(i) } 36 | else 37 | object 38 | end 39 | end 40 | 41 | # Hashes can contain keys that need to be escaped 42 | def self.hash_as_ejson(hash) 43 | result = hash.map do |k, v| 44 | if k.is_a?(String) && k[0] == '$' 45 | ['$escape', { k => as_ejson(v) }] 46 | else 47 | [k, as_ejson(v)] 48 | end 49 | end 50 | Hash[result] 51 | end 52 | 53 | def self.deserialize_hash(hash) 54 | deserialize_operation(hash) || hash.map do |k, v| 55 | [k, deserialize(v)] 56 | end.to_h 57 | end 58 | 59 | def self.deserialize_operation(hash) 60 | if hash['$escape'] 61 | return deserialize_escape(hash['$escape']) 62 | elsif hash['$date'] 63 | return Time.at(hash['$date'] / 1000.0) 64 | elsif hash['$binary'] 65 | return Base64.decode64(hash['$binary']) 66 | elsif hash['$type'] 67 | return deserialize_type(hash) 68 | end 69 | false 70 | end 71 | 72 | def self.deserialize_escape(hash) 73 | hash.map do |k, v| 74 | [k, deserialize(v)] 75 | end.to_h 76 | end 77 | 78 | def self.deserialize_type(hash) 79 | klass = @classes[hash['$type']] 80 | if klass 81 | klass.from_ejson(hash['$value']) 82 | else 83 | raise UnknownTypeError, "Don't know how to deserialize #{hash['$type']}" 84 | end 85 | end 86 | 87 | def self.add_serializable_class(klass) 88 | @classes ||= {} 89 | @classes[klass.name] = klass 90 | end 91 | 92 | def self.rename_serializable_class(klass, name) 93 | @classes.delete(klass.name) 94 | @classes[name] = klass 95 | end 96 | 97 | # Classes can include this module to be picked up by the EJSON parser 98 | module Serializable 99 | def self.extended(klass) 100 | EJSON.add_serializable_class(klass) 101 | end 102 | 103 | def ejson_type_name(name) 104 | EJSON.rename_serializable_class(self, name) 105 | end 106 | 107 | def from_ejson(_object) 108 | raise InvalidSerializableClassError, "Class #{name} must override from_ejson." 109 | end 110 | end 111 | 112 | # Raised when parsing an unknown type 113 | class UnknownTypeError < StandardError 114 | end 115 | 116 | # Raised when serializable class does not implement from_ejson 117 | class InvalidSerializableClassError < StandardError 118 | end 119 | end 120 | end 121 | 122 | # Builtin EJSON types: 123 | class Time 124 | def as_ejson 125 | # milliseconds since epoch 126 | { '$date' => (to_f * 1000).to_i } 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/ddp/server.rb: -------------------------------------------------------------------------------- 1 | require 'ddp/server/version' 2 | require 'ddp/server/protocol' 3 | require 'celluloid/websocket/rack' 4 | require 'ddp/ejson' 5 | require 'securerandom' 6 | 7 | module DDP 8 | module Server 9 | # Server on top of a Celluloid::WebSocket 10 | class WebSocket < Celluloid::WebSocket 11 | include DDP::Server::Protocol 12 | 13 | attr_accessor :api, :subscriptions 14 | 15 | def initialize(api_class, *args) 16 | @api = api_class.new(*args) 17 | @subscriptions = {} 18 | end 19 | 20 | def on_open 21 | handle_connect 22 | end 23 | 24 | def read_message 25 | EJSON.parse read 26 | end 27 | 28 | def write_message(message) 29 | write EJSON.generate(message) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ddp/server/api.rb: -------------------------------------------------------------------------------- 1 | module DDP 2 | module Server 3 | # Helper class that users can extend to implement an API that can be passed 4 | # as the RPC API parameter to the RethinkDB DDP protocol 5 | class API 6 | def initialize 7 | setup_rpc 8 | setup_collections 9 | end 10 | 11 | def invoke_rpc(method, *params) 12 | raise 'No such method' unless @rpc_methods.include? method 13 | send(method, *params) 14 | end 15 | 16 | def collection_query(name, *params) 17 | raise 'No such collection' unless @collections.include? name 18 | wrap_query(send(name, *params)) 19 | end 20 | 21 | # Implementors must override wrap_query. The argument is a query that is to be executed 22 | # the result should be a proc that yields data values to its block parameter. 23 | def wrap_query(query) 24 | raise 'implement wrap query' 25 | end 26 | 27 | private 28 | 29 | def setup_rpc 30 | rpc_module = self.class.const_get :RPC 31 | @rpc_methods = rpc_module.instance_methods.map(&:to_s) 32 | singleton_class.include rpc_module 33 | end 34 | 35 | def setup_collections 36 | collections_module = self.class.const_get :Collections 37 | @collections = collections_module.instance_methods.map(&:to_s) 38 | singleton_class.include collections_module 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/ddp/server/protocol.rb: -------------------------------------------------------------------------------- 1 | require 'ddp/server/protocol/heartbeat' 2 | require 'ddp/server/protocol/data' 3 | require 'ddp/server/protocol/rpc' 4 | 5 | module DDP 6 | module Server 7 | # Implementation of the DDP protocol 8 | # Can be included into any class that has 9 | # an on_open, a read_message and a write_message method 10 | module Protocol 11 | include Heartbeat 12 | include Data 13 | include RPC 14 | 15 | DDP_VERSION = '1' 16 | 17 | attr_accessor :session_id 18 | 19 | def new_session_id 20 | SecureRandom.hex 21 | end 22 | 23 | def handle_connect 24 | message = read_message 25 | 26 | if message['msg'] == 'connect' && message['version'] == DDP_VERSION 27 | handle_session(message) 28 | else 29 | write_message('msg' => 'failed', 'version' => DDP_VERSION) 30 | close 31 | end 32 | end 33 | 34 | def handle_session(message) 35 | @session_id = message['session'] || new_session_id 36 | 37 | write_message('msg' => 'connected', 'session' => session_id) 38 | 39 | handle_established 40 | end 41 | 42 | def handle_established 43 | loop do 44 | @message = read_message 45 | 46 | next if handle_heartbeat 47 | next if handle_data 48 | next if handle_rpc 49 | break 50 | end 51 | 52 | close 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ddp/server/protocol/data.rb: -------------------------------------------------------------------------------- 1 | require 'celluloid' 2 | 3 | module DDP 4 | module Server 5 | module Protocol 6 | # Protocol regarding handling data subscriptions 7 | module Data 8 | def handle_data 9 | case @message['msg'] 10 | when 'sub' 11 | handle_sub(@message['id'], @message['name'], @message['params']) 12 | true 13 | when 'unsub' 14 | handle_unsub(@message['id']) 15 | true 16 | else 17 | false 18 | end 19 | end 20 | 21 | def handle_sub(id, name, params) 22 | params ||= [] 23 | query = api.collection_query(name, *params) 24 | subscription = subscriptions[id] = Subscription.new(self, id, name, query) 25 | subscription.async.start 26 | send_ready([id]) 27 | rescue => e 28 | send_error_result(id, e) 29 | end 30 | 31 | def subscription_update(id, old_value, new_value) 32 | subscription_name = @subscriptions[id].name 33 | 34 | return send_added(subscription_name, new_value['id'], new_value) if old_value.nil? 35 | return send_removed(subscription_name, old_value['id']) if new_value.nil? 36 | 37 | send_changed(subscription_name, old_value['id'], new_value, old_value.keys - new_value.keys) 38 | end 39 | 40 | def handle_unsub(id) 41 | subscription = @subscriptions.delete(id) 42 | subscription.stop unless subscription.nil? 43 | send_nosub(id) 44 | end 45 | 46 | def send_nosub(id, error = nil) 47 | message = { msg: 'nosub', id: id } 48 | message.merge!(error: error) if error 49 | write_message message 50 | end 51 | 52 | def send_added(collection, id, fields = nil) 53 | message = { msg: 'added', id: id, collection: collection } 54 | message.merge!(fields: fields) if fields 55 | write_message message 56 | end 57 | 58 | def send_changed(collection, id, fields = nil, cleared = nil) 59 | message = { msg: 'changed', id: id, collection: collection } 60 | message.merge!(fields: fields) if fields 61 | message.merge!(cleared: cleared) if cleared 62 | write_message message 63 | end 64 | 65 | def send_removed(collection, id) 66 | write_message msg: 'removed', collection: collection, id: id 67 | end 68 | 69 | def send_ready(subs) 70 | write_message msg: 'ready', subs: subs 71 | end 72 | 73 | def send_added_before(collection, id, fields = nil, before = nil) 74 | message = { msg: 'addedBefore', id: id, collection: collection, before: before } 75 | message.merge!(fields: fields) if fields 76 | write_message message 77 | end 78 | 79 | def send_moved_before(collection, id, before = nil) 80 | write_message msg: 'movedBefore', id: id, collection: collection, before: before 81 | end 82 | 83 | # Actor that asynchronously monitors a collection 84 | class Subscription 85 | include Celluloid 86 | 87 | attr_reader :name, :stopped, :listener, :query, :id 88 | alias_method :stopped?, :stopped 89 | 90 | def initialize(listener, id, name, query) 91 | @stopped = false 92 | @name = name 93 | @listener = listener 94 | @id = id 95 | @query = query 96 | end 97 | 98 | def start 99 | query.call do |old_value, new_value| 100 | listener.subscription_update(id, old_value, new_value) 101 | break if stopped? 102 | end 103 | end 104 | 105 | def stop 106 | @stopped = true 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/ddp/server/protocol/heartbeat.rb: -------------------------------------------------------------------------------- 1 | module DDP 2 | module Server 3 | module Protocol 4 | # Protocol regarding heartbeat messages 5 | module Heartbeat 6 | def handle_heartbeat 7 | case @message['msg'] 8 | when 'ping' 9 | write_message msg: 'pong', id: @message['id'] 10 | true 11 | when 'pong' 12 | true 13 | else 14 | false 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ddp/server/protocol/rpc.rb: -------------------------------------------------------------------------------- 1 | module DDP 2 | module Server 3 | module Protocol 4 | # Protocol regarding remote procedure calls 5 | module RPC 6 | NO_RESULT = Object.new 7 | 8 | def handle_rpc 9 | case @message['msg'] 10 | when 'method' 11 | handle_method(@message['id'], @message['method'], @message['params']) 12 | true 13 | else 14 | false 15 | end 16 | end 17 | 18 | def handle_method(id, method, params) 19 | params ||= [] 20 | result = api.invoke_rpc(method, *params) 21 | send_result(id, result) 22 | rescue => e 23 | send_error_result(id, e) 24 | end 25 | 26 | def send_result(id, result = NO_RESULT) 27 | message = { msg: 'result', id: id } 28 | message['result'] = result unless result == NO_RESULT 29 | write_message(message) 30 | end 31 | 32 | def send_error_result(id, error) 33 | message = { msg: 'result', id: id } 34 | message['error'] = { 35 | error: error.class.name, 36 | reason: error.message, 37 | details: "Backtrace: \n#{error.backtrace.join("\n")}" 38 | } 39 | write_message(message) 40 | end 41 | 42 | def send_updated(methods) 43 | write_message msg: 'updated', methods: methods 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ddp/server/version.rb: -------------------------------------------------------------------------------- 1 | module DDP 2 | # We use semantic versioning 3 | module Server 4 | VERSION = '0.1.2' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/ejson_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ddp/ejson' 3 | require 'json' 4 | require 'base64' 5 | 6 | # Tests in DDP Module 7 | module DDP 8 | describe EJSON do 9 | describe 'generate' do 10 | it 'generates normal json for simple structures' do 11 | [ 12 | { 'a' => 'b' }, 13 | ['a'], 14 | { 'a' => [1] } 15 | ].each do |example| 16 | expect(EJSON.generate(example)).to eq(JSON.generate(example)) 17 | end 18 | end 19 | 20 | it 'escapes keys that start with a dollar sign' do 21 | example = { '$bla' => 'value' } 22 | ejson = EJSON.generate(example) 23 | expect(JSON.parse(ejson)['$escape']).to eq('$bla' => 'value') 24 | end 25 | 26 | it 'generates a special type for dates' do 27 | time = Time.now 28 | ms_since_epoch = (time.to_f * 1000).to_i 29 | ejson = EJSON.generate('date' => time) 30 | expect(JSON.parse(ejson)['date']).to eq('$date' => ms_since_epoch) 31 | end 32 | end 33 | 34 | describe 'parse' do 35 | it 'parses generic json' do 36 | [ 37 | { 'a' => 'b' }, 38 | ['a'], 39 | { 'a' => [1] } 40 | ].each do |example| 41 | json = JSON.generate(example) 42 | expect(EJSON.parse(json)).to eq(example) 43 | end 44 | end 45 | 46 | it 'parses an escaped ejson' do 47 | example = { '$escape' => { '$date' => 'a' } } 48 | ejson = JSON.generate(example) 49 | expect(EJSON.parse(ejson)).to eq('$date' => 'a') 50 | end 51 | 52 | it 'parses a date ejson' do 53 | time = Time.now 54 | ms_since_epoch = (time.to_f * 1000).to_i 55 | example = { 'date' => { '$date' => ms_since_epoch } } 56 | ejson = JSON.generate(example) 57 | expect(EJSON.parse(ejson)['date'].to_s).to eq(time.to_s) 58 | end 59 | 60 | it 'parses a binary ejson' do 61 | example = { '$binary' => Base64.encode64('Hello World') } 62 | ejson = JSON.generate(example) 63 | expect(EJSON.parse(ejson)).to eq('Hello World') 64 | end 65 | 66 | it 'parses custom types' do 67 | # Test class 68 | class A 69 | extend EJSON::Serializable 70 | 71 | ejson_type_name 'A' 72 | 73 | def as_ejson 74 | { '$type' => 'A', '$value' => '1234' } 75 | end 76 | 77 | def self.from_ejson(object) 78 | object.to_i 79 | end 80 | end 81 | 82 | ejson = EJSON.generate(A.new) 83 | expect(EJSON.parse(ejson)).to eq(1234) 84 | end 85 | 86 | it 'raises an exception when parsing an unknown type' do 87 | ejson = JSON.generate('$type' => 'B', '$value' => '1234') 88 | expect do 89 | EJSON.parse(ejson) 90 | end.to raise_error(EJSON::UnknownTypeError) 91 | end 92 | 93 | it 'raises an exception when a serializable class does not override from_ejson' do 94 | expect do 95 | # Test class 96 | class C 97 | extend EJSON::Serializable 98 | end 99 | 100 | C.from_ejson(false) 101 | end.to raise_error(EJSON::InvalidSerializableClassError) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'bundler/setup' 5 | 6 | def make_sockets 7 | host = '127.0.0.1' 8 | port = 10_151 9 | 10 | server = TCPServer.new(host, port) 11 | client = TCPSocket.new(host, port) 12 | peer = server.accept 13 | 14 | [server, client, peer] 15 | end 16 | 17 | # def with_socket_pair 18 | # server, client, peer = make_sockets 19 | 20 | # begin 21 | # yield client, peer 22 | # ensure 23 | # server.close rescue nil 24 | # client.close rescue nil 25 | # peer.close rescue nil 26 | # end 27 | # end 28 | 29 | def example_host 30 | 'www.example.com' 31 | end 32 | 33 | def example_path 34 | '/example' 35 | end 36 | 37 | def example_url 38 | "ws://#{example_host}#{example_path}" 39 | end 40 | 41 | def handshake_headers 42 | { 43 | 'Host' => example_host, 44 | 'Upgrade' => 'websocket', 45 | 'Connection' => 'Upgrade', 46 | 'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==', 47 | 'Origin' => 'http://example.com', 48 | 'Sec-WebSocket-Protocol' => 'chat, superchat', 49 | 'Sec-WebSocket-Version' => '13' 50 | } 51 | end 52 | 53 | def handshake 54 | WebSocket::ClientHandshake.new(:get, example_url, handshake_headers) 55 | end 56 | 57 | def with_websocket_pair 58 | with_socket_pair do |client, peer| 59 | connection = Reel::Connection.new(peer) 60 | client << handshake.to_data 61 | request = connection.request 62 | 63 | websocket = request.websocket 64 | 65 | # Discard handshake 66 | client.readpartial(4096) 67 | 68 | yield client, websocket 69 | end 70 | end 71 | 72 | RSpec.configure do |config| 73 | config.before(:all) do 74 | # nothing yet 75 | end 76 | config.before(:each) do 77 | # nothing yet 78 | end 79 | config.after(:all) {} 80 | config.after(:each) {} 81 | end 82 | --------------------------------------------------------------------------------