From 4fe6dcc3efc27bcfbede55a339302a98bc80ee19 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Tue, 9 Dec 2014 22:04:34 +0000 Subject: [PATCH] Unit tests for the ClientSession compression options and offer negotiation. --- .travis.yml | 11 + lib/permessage_deflate.rb | 18 +- lib/permessage_deflate/client_session.rb | 40 ++- lib/permessage_deflate/server_session.rb | 4 +- lib/permessage_deflate/session.rb | 13 +- .../permessage_deflate/client_session_spec.rb | 322 ++++++++++++++++++ spec/spec_helper.rb | 4 + 7 files changed, 403 insertions(+), 9 deletions(-) create mode 100644 .travis.yml create mode 100644 spec/permessage_deflate/client_session_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..834d832 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: ruby + +rvm: + - 1.9.3 + - 2.0.0 + - 2.1.3 + - jruby-18mode + - jruby-19mode + - rbx-2.2 + +script: bundle exec rspec -c spec/ diff --git a/lib/permessage_deflate.rb b/lib/permessage_deflate.rb index 8901629..619bfcd 100644 --- a/lib/permessage_deflate.rb +++ b/lib/permessage_deflate.rb @@ -6,6 +6,8 @@ class PermessageDeflate require root + '/permessage_deflate/client_session' require root + '/permessage_deflate/server_session' + ConfigurationError = Class.new(ArgumentError) + module Extension define_method(:name) { 'permessage-deflate' } define_method(:type) { 'permessage' } @@ -13,16 +15,26 @@ class PermessageDeflate define_method(:rsv2) { false } define_method(:rsv3) { false } + def configure(options) + options = (@options || {}).merge(options) + PermessageDeflate.new(options) + end + def create_client_session - ClientSession.new + ClientSession.new(@options || {}) end def create_server_session(offers) offers.each do |offer| - return ServerSession.new(offer) if ServerSession.valid_params?(offer) + return ServerSession.new(@options || {}, offer) if ServerSession.valid_params?(offer) end end end - extend Extension + include Extension + extend Extension + + def initialize(options) + @options = options + end end diff --git a/lib/permessage_deflate/client_session.rb b/lib/permessage_deflate/client_session.rb index b501946..302253b 100644 --- a/lib/permessage_deflate/client_session.rb +++ b/lib/permessage_deflate/client_session.rb @@ -12,14 +12,48 @@ class PermessageDeflate end def generate_offer - [{'client_max_window_bits' => true}] + offer = {} + + if @accept_no_context_takeover + offer['client_no_context_takeover'] = true + end + + if @accept_max_window_bits + raise ConfigurationError unless VALID_WINDOW_BITS.include?(@accept_max_window_bits) + offer['client_max_window_bits'] = @accept_max_window_bits + else + offer['client_max_window_bits'] = true + end + + if @request_no_context_takeover + offer['server_no_context_takeover'] = true + end + + if @request_max_window_bits + raise ConfigurationError unless VALID_WINDOW_BITS.include?(@request_max_window_bits) + offer['server_max_window_bits'] = @request_max_window_bits + end + + offer end def activate(params) return false unless ClientSession.valid_params?(params) - @own_context_takeover = !params['client_no_context_takeover'] - @own_window_bits = params['client_max_window_bits'] || DEFAULT_MAX_WINDOW_BITS + if @accept_max_window_bits and params['client_max_window_bits'] + return false if params['client_max_window_bits'] > @accept_max_window_bits + end + + if @request_no_context_takeover and !params['server_no_context_takeover'] + return false + end + + if @request_max_window_bits + return false unless params['server_max_window_bits'] and params['server_max_window_bits'] <= @request_max_window_bits + end + + @own_context_takeover = !(@accept_no_context_takeover || params['client_no_context_takeover']) + @own_window_bits = [@accept_max_window_bits, params['client_max_window_bits']].map { |x| x || DEFAULT_MAX_WINDOW_BITS }.min @peer_context_takeover = !params['server_no_context_takeover'] @peer_window_bits = params['server_max_window_bits'] || DEFAULT_MAX_WINDOW_BITS diff --git a/lib/permessage_deflate/server_session.rb b/lib/permessage_deflate/server_session.rb index c745bd3..624fdb2 100644 --- a/lib/permessage_deflate/server_session.rb +++ b/lib/permessage_deflate/server_session.rb @@ -11,8 +11,8 @@ class PermessageDeflate true end - def initialize(params) - super() + def initialize(options, params) + super(options) @params = params end diff --git a/lib/permessage_deflate/session.rb b/lib/permessage_deflate/session.rb index 120ba39..3f2c11a 100644 --- a/lib/permessage_deflate/session.rb +++ b/lib/permessage_deflate/session.rb @@ -31,6 +31,17 @@ class PermessageDeflate true end + def initialize(options) + @level = options[:level] || Zlib::DEFAULT_COMPRESSION + @mem_level = options[:mem_level] || Zlib::DEF_MEM_LEVEL + @strategy = options[:strategy] || Zlib::DEFAULT_STRATEGY + + @accept_no_context_takeover = options[:no_context_takeover] + @accept_max_window_bits = options[:max_window_bits] + @request_no_context_takeover = options[:request_no_context_takeover] + @request_max_window_bits = options[:request_max_window_bits] + end + def valid_frame_rsv(frame) if MESSAGE_OPCODES.include?(frame.opcode) {:rsv1 => true, :rsv2 => false, :rsv3 => false} @@ -86,7 +97,7 @@ class PermessageDeflate def get_deflate return @deflate if @deflate - deflate = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -@own_window_bits) + deflate = Zlib::Deflate.new(@level, -@own_window_bits, @mem_level, @strategy) @deflate = deflate if @own_context_takeover deflate end diff --git a/spec/permessage_deflate/client_session_spec.rb b/spec/permessage_deflate/client_session_spec.rb new file mode 100644 index 0000000..d51ab17 --- /dev/null +++ b/spec/permessage_deflate/client_session_spec.rb @@ -0,0 +1,322 @@ +require "spec_helper" + +describe PermessageDeflate::ClientSession do + let(:ext) { PermessageDeflate.configure(options) } + let(:session) { ext.create_client_session } + let(:options) { {} } + let(:offer) { session.generate_offer } + let(:response) { {} } + let(:activate) { session.activate(response) } + + let(:deflate) { double(:deflate, :deflate => "") } + let(:inflate) { double(:inflate, :inflate => "") } + let(:level) { Zlib::DEFAULT_COMPRESSION } + let(:mem_level) { Zlib::DEF_MEM_LEVEL } + let(:strategy) { Zlib::DEFAULT_STRATEGY } + + let(:message) { Message.new("hello", true) } + + def process_incoming_message + session.process_incoming_message(message) + end + + def process_outgoing_message + session.process_outgoing_message(message) + end + + describe "with default options" do + it "indicates support for client_max_window_bits" do + expect(offer).to eq("client_max_window_bits" => true) + end + + describe "with an empty response" do + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 15 window bits for inflating incoming messages" do + activate + expect(Zlib::Inflate).to receive(:new).with(-15).exactly(1).and_return(inflate) + process_incoming_message + process_incoming_message + end + + it "uses context takeover and 15 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(1).and_return(deflate) + process_outgoing_message + process_outgoing_message + end + end + + describe "when the response includes server_no_context_takeover" do + before { response["server_no_context_takeover"] = true } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses no context takeover and 15 window bits for inflating incoming messages" do + activate + expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) + expect(inflate).to receive(:finish).exactly(2) + expect(inflate).to receive(:close).exactly(2) + process_incoming_message + process_incoming_message + end + end + + describe "when the response includes client_no_context_takeover" do + before { response["client_no_context_takeover"] = true } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses no context takeover and 15 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) + expect(deflate).to receive(:finish).exactly(2) + expect(deflate).to receive(:close).exactly(2) + process_outgoing_message + process_outgoing_message + end + end + + describe "when the response includes server_max_window_bits" do + before { response["server_max_window_bits"] = 8 } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 8 window bits for inflating incoming messages" do + activate + expect(Zlib::Inflate).to receive(:new).with(-8).exactly(1).and_return(inflate) + process_incoming_message + process_incoming_message + end + end + + describe "when the response includes invalid server_max_window_bits" do + before { response["server_max_window_bits"] = 20 } + + it "rejects the response" do + expect(activate).to be false + end + end + + describe "when the response includes client_max_window_bits" do + before { response["client_max_window_bits"] = 8 } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 8 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -8, mem_level, strategy).exactly(1).and_return(deflate) + process_outgoing_message + process_outgoing_message + end + end + + describe "when the response includes invalid client_max_window_bits" do + before { response["client_max_window_bits"] = 20 } + + it "rejects the response" do + expect(activate).to be false + end + end + end + + describe "with no_context_takeover" do + before { options[:no_context_takeover] = true } + + it "sends client_no_context_takeover with no_context_takeover" do + expect(offer).to eq( + "client_no_context_takeover" => true, + "client_max_window_bits" => true + ) + end + + describe "with an empty response" do + it "accepts the response" do + expect(activate).to be true + end + + it "uses no context takeover and 15 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) + expect(deflate).to receive(:finish).exactly(2) + expect(deflate).to receive(:close).exactly(2) + process_outgoing_message + process_outgoing_message + end + end + end + + describe "with max_window_bits" do + before { options[:max_window_bits] = 9 } + + it "sends client_max_window_bits with max_window_bits" do + expect(offer).to eq("client_max_window_bits" => 9) + end + + describe "with an empty response" do + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 9 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -9, mem_level, strategy).exactly(1).and_return(deflate) + process_outgoing_message + process_outgoing_message + end + end + + describe "when the response has higher client_max_window_bits" do + before { response["client_max_window_bits"] = 10 } + + it "does not accept the response" do + expect(activate).to be false + end + end + + describe "when the response has lower client_max_window_bits" do + before { response["client_max_window_bits"] = 8 } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 8 window bits for deflating outgoing messages" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -8, mem_level, strategy).exactly(1).and_return(deflate) + process_outgoing_message + process_outgoing_message + end + end + end + + describe "with invalid max_window_bits" do + before { options[:max_window_bits] = 20 } + + it "raises when generating the offer with invalid max_window_bits" do + expect { offer }.to raise_error + end + end + + describe "with request_no_context_takeover" do + before { options[:request_no_context_takeover] = true } + + it "sends server_no_context_takeover with request_no_context_takeover" do + expect(offer).to eq( + "client_max_window_bits" => true, + "server_no_context_takeover" => true + ) + end + + describe "with an empty response" do + it "rejects the response" do + expect(activate).to be false + end + end + + describe "when the response includes server_no_context_takeover" do + before { response["server_no_context_takeover"] = true } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses no no context takeover and 15 window bits for inflating incoming messages" do + activate + expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) + expect(inflate).to receive(:finish).exactly(2) + expect(inflate).to receive(:close).exactly(2) + process_incoming_message + process_incoming_message + end + end + end + + describe "with request_max_window_bits" do + before { options[:request_max_window_bits] = 12 } + + it "sends server_max_window_bits with request_max_window_bits" do + expect(offer).to eq( + "client_max_window_bits" => true, + "server_max_window_bits" => 12 + ) + end + + describe "with an empty response" do + it "rejects the response" do + expect(activate).to be false + end + end + + describe "when the response has higher server_max_window_bits" do + before { response["server_max_window_bits"] = 13 } + + it "rejects the response" do + expect(activate).to be false + end + end + + describe "when the response has lower server_max_window_bits" do + before { response["server_max_window_bits"] = 11 } + + it "accepts the response" do + expect(activate).to be true + end + + it "uses context takeover and 11 window bits for inflating incoming messages" do + activate + expect(Zlib::Inflate).to receive(:new).with(-11).exactly(1).and_return(inflate) + process_incoming_message + process_incoming_message + end + end + end + + describe "with invalid request_max_window_bits" do + before { options[:request_max_window_bits] = 20 } + + it "raises when generating an offer with invalid request_max_window_bits" do + expect { offer }.to raise_error + end + end + + describe "with level" do + before { options[:level] = Zlib::BEST_SPEED } + + it "sets the level of the deflate stream" do + activate + expect(Zlib::Deflate).to receive(:new).with(Zlib::BEST_SPEED, -15, mem_level, strategy).and_return(deflate) + process_outgoing_message + end + end + + describe "with mem_level" do + before { options[:mem_level] = 5 } + + it "sets the mem_level of the deflate stream" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -15, 5, strategy).and_return(deflate) + process_outgoing_message + end + end + + describe "with strategy" do + before { options[:strategy] = Zlib::RLE } + + it "sets the strategy of the deflate stream" do + activate + expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, Zlib::RLE).and_return(deflate) + process_outgoing_message + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..5557983 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,4 @@ +require File.expand_path("../../lib/permessage_deflate", __FILE__) + +class Message < Struct.new(:data, :rsv1) +end