├── .gitignore ├── Gemfile ├── spec ├── spec_helper.rb └── permessage_deflate │ ├── client_session_spec.rb │ └── server_session_spec.rb ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── LICENSE.md ├── permessage_deflate.gemspec ├── .github └── workflows │ └── test.yml ├── lib ├── permessage_deflate.rb └── permessage_deflate │ ├── server_session.rb │ ├── client_session.rb │ └── session.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../lib/permessage_deflate", __FILE__) 2 | 3 | class Message < Struct.new(:data, :rsv1) 4 | end 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.1.4 / 2017-09-10 2 | 3 | - Use `9` instead of `8` as the `windowBits` parameter to zlib, to deal with 4 | restrictions introduced in zlib v1.2.9 5 | 6 | ### 0.1.3 / 2016-05-20 7 | 8 | - Amend all warnings issued when running with -W2 9 | 10 | ### 0.1.2 / 2015-11-06 11 | 12 | - The server does not send `server_max_window_bits` if the client does not ask 13 | for it; this works around an issue in Firefox. 14 | 15 | ### 0.1.1 / 2014-12-18 16 | 17 | - Don't allow configure() to be called with unrecognized options 18 | 19 | ### 0.1.0 / 2014-12-13 20 | 21 | - Initial release 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014-2017 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 | -------------------------------------------------------------------------------- /permessage_deflate.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'permessage_deflate' 3 | s.version = '0.1.4' 4 | s.summary = 'Per-message DEFLATE compression extension for WebSocket connections' 5 | s.author = 'James Coglan' 6 | s.email = 'jcoglan@gmail.com' 7 | s.homepage = 'https://github.com/faye/permessage-deflate-ruby' 8 | s.license = 'Apache-2.0' 9 | 10 | s.extra_rdoc_files = %w[README.md] 11 | s.rdoc_options = %w[--main README.md --markup markdown] 12 | s.require_paths = %w[lib] 13 | 14 | s.files = %w[CHANGELOG.md LICENSE.md README.md] + Dir.glob('lib/**/*.rb') 15 | 16 | s.add_development_dependency 'rspec' 17 | end 18 | -------------------------------------------------------------------------------- /.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.0 12 | - ruby-2.1 13 | # - ruby-2.2 14 | - ruby-2.3 15 | - ruby-2.4 16 | - ruby-2.5 17 | - ruby-2.6 18 | - ruby-2.7 19 | - ruby-3.0 20 | - ruby-3.1 21 | - ruby-3.2 22 | - jruby-9.1 23 | - jruby-9.2 24 | - jruby-9.3 25 | - jruby-9.4 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - run: ruby --version 34 | - run: bundle exec rspec 35 | -------------------------------------------------------------------------------- /lib/permessage_deflate.rb: -------------------------------------------------------------------------------- 1 | require 'zlib' 2 | 3 | class PermessageDeflate 4 | root = File.expand_path('..', __FILE__) 5 | require root + '/permessage_deflate/session' 6 | require root + '/permessage_deflate/client_session' 7 | require root + '/permessage_deflate/server_session' 8 | 9 | ConfigurationError = Class.new(ArgumentError) 10 | 11 | VALID_OPTIONS = [ 12 | :level, 13 | :mem_level, 14 | :strategy, 15 | :no_context_takeover, 16 | :max_window_bits, 17 | :request_no_context_takeover, 18 | :request_max_window_bits 19 | ] 20 | 21 | def self.validate_options(options, valid_keys) 22 | options.keys.each do |key| 23 | unless valid_keys.include?(key) 24 | raise ConfigurationError, "Unrecognized option: #{ key.inspect }" 25 | end 26 | end 27 | end 28 | 29 | module Extension 30 | define_method(:name) { 'permessage-deflate' } 31 | define_method(:type) { 'permessage' } 32 | define_method(:rsv1) { true } 33 | define_method(:rsv2) { false } 34 | define_method(:rsv3) { false } 35 | 36 | def configure(options) 37 | @options ||= nil 38 | 39 | PermessageDeflate.validate_options(options, VALID_OPTIONS) 40 | options = (@options || {}).merge(options) 41 | PermessageDeflate.new(options) 42 | end 43 | 44 | def create_client_session 45 | ClientSession.new(@options || {}) 46 | end 47 | 48 | def create_server_session(offers) 49 | offers.each do |offer| 50 | return ServerSession.new(@options || {}, offer) if ServerSession.valid_params?(offer) 51 | end 52 | nil 53 | end 54 | end 55 | 56 | include Extension 57 | extend Extension 58 | 59 | def initialize(options) 60 | @options = options 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/permessage_deflate/server_session.rb: -------------------------------------------------------------------------------- 1 | class PermessageDeflate 2 | class ServerSession < Session 3 | 4 | def self.valid_params?(params) 5 | return false unless super 6 | 7 | if params.has_key?('client_max_window_bits') 8 | return false unless ([true] + VALID_WINDOW_BITS).include?(params['client_max_window_bits']) 9 | end 10 | 11 | true 12 | end 13 | 14 | def initialize(options, params) 15 | super(options) 16 | @params = params 17 | end 18 | 19 | def generate_response 20 | response = {} 21 | 22 | # https://tools.ietf.org/html/rfc7692#section-7.1.1.1 23 | 24 | @own_context_takeover = !@accept_no_context_takeover && 25 | !@params['server_no_context_takeover'] 26 | 27 | response['server_no_context_takeover'] = true unless @own_context_takeover 28 | 29 | # https://tools.ietf.org/html/rfc7692#section-7.1.1.2 30 | 31 | @peer_context_takeover = !@request_no_context_takeover && 32 | !@params['client_no_context_takeover'] 33 | 34 | response['client_no_context_takeover'] = true unless @peer_context_takeover 35 | 36 | # https://tools.ietf.org/html/rfc7692#section-7.1.2.1 37 | 38 | @own_window_bits = [ @accept_max_window_bits || MAX_WINDOW_BITS, 39 | @params['server_max_window_bits'] || MAX_WINDOW_BITS 40 | ].min 41 | 42 | # In violation of the spec, Firefox closes the connection if it does not 43 | # send server_max_window_bits but the server includes this in its response 44 | if @own_window_bits < MAX_WINDOW_BITS and @params['server_max_window_bits'] 45 | response['server_max_window_bits'] = @own_window_bits 46 | end 47 | 48 | # https://tools.ietf.org/html/rfc7692#section-7.1.2.2 49 | 50 | if client_max = @params['client_max_window_bits'] 51 | client_max = MAX_WINDOW_BITS if client_max == true 52 | @peer_window_bits = [@request_max_window_bits || MAX_WINDOW_BITS, client_max].min 53 | else 54 | @peer_window_bits = MAX_WINDOW_BITS 55 | end 56 | 57 | if @peer_window_bits < MAX_WINDOW_BITS 58 | response['client_max_window_bits'] = @peer_window_bits 59 | end 60 | 61 | response 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/permessage_deflate/client_session.rb: -------------------------------------------------------------------------------- 1 | class PermessageDeflate 2 | class ClientSession < Session 3 | 4 | def self.valid_params?(params) 5 | return false unless super 6 | 7 | if params.has_key?('client_max_window_bits') 8 | return false unless VALID_WINDOW_BITS.include?(params['client_max_window_bits']) 9 | end 10 | 11 | true 12 | end 13 | 14 | def generate_offer 15 | offer = {} 16 | 17 | if @accept_no_context_takeover 18 | offer['client_no_context_takeover'] = true 19 | end 20 | 21 | if @accept_max_window_bits 22 | unless VALID_WINDOW_BITS.include?(@accept_max_window_bits) 23 | raise ConfigurationError, 'Invalid value for max_window_bits' 24 | end 25 | offer['client_max_window_bits'] = @accept_max_window_bits 26 | else 27 | offer['client_max_window_bits'] = true 28 | end 29 | 30 | if @request_no_context_takeover 31 | offer['server_no_context_takeover'] = true 32 | end 33 | 34 | if @request_max_window_bits 35 | unless VALID_WINDOW_BITS.include?(@request_max_window_bits) 36 | raise ConfigurationError, 'Invalid value for request_max_window_bits' 37 | end 38 | offer['server_max_window_bits'] = @request_max_window_bits 39 | end 40 | 41 | offer 42 | end 43 | 44 | def activate(params) 45 | return false unless ClientSession.valid_params?(params) 46 | 47 | if @accept_max_window_bits and params['client_max_window_bits'] 48 | return false if params['client_max_window_bits'] > @accept_max_window_bits 49 | end 50 | 51 | if @request_no_context_takeover and !params['server_no_context_takeover'] 52 | return false 53 | end 54 | 55 | if @request_max_window_bits 56 | return false unless params['server_max_window_bits'] 57 | return false if params['server_max_window_bits'] > @request_max_window_bits 58 | end 59 | 60 | @own_context_takeover = !(@accept_no_context_takeover || params['client_no_context_takeover']) 61 | @own_window_bits = [ 62 | @accept_max_window_bits || MAX_WINDOW_BITS, 63 | params['client_max_window_bits'] || MAX_WINDOW_BITS 64 | ].min 65 | 66 | @peer_context_takeover = !params['server_no_context_takeover'] 67 | @peer_window_bits = params['server_max_window_bits'] || MAX_WINDOW_BITS 68 | 69 | true 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # permessage_deflate 2 | 3 | Implements the 4 | [permessage-deflate](https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression) 5 | WebSocket protocol extension as a plugin for 6 | [websocket-extensions](https://github.com/faye/websocket-extensions-ruby). 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ gem install permessage_deflate 12 | ``` 13 | 14 | ## Usage 15 | 16 | Add the plugin to your extensions: 17 | 18 | ```rb 19 | require 'websocket/extensions' 20 | require 'permessage_deflate' 21 | 22 | exts = WebSocket::Extensions.new 23 | exts.add(PermessageDeflate) 24 | ``` 25 | 26 | The extension can be configured, for example: 27 | 28 | ```rb 29 | require 'websocket/extensions' 30 | require 'permessage_deflate' 31 | 32 | deflate = PermessageDeflate.configure( 33 | :level => Zlib::BEST_COMPRESSION, 34 | :max_window_bits => 13 35 | ) 36 | 37 | exts = WebSocket::Extensions.new 38 | exts.add(deflate) 39 | ``` 40 | 41 | The set of available options can be split into two sets: those that control the 42 | session's compressor for outgoing messages and do not need to be communicated to 43 | the peer, and those that are negotiated as part of the protocol. The settings 44 | only affecting the compressor are described fully in the [Zlib 45 | documentation](http://ruby-doc.org/stdlib-2.1.0/libdoc/zlib/rdoc/Zlib/Deflate.html#method-c-new): 46 | 47 | - `:level`: sets the compression level, can be an integer from `0` to `9`, or 48 | one of the constants `Zlib::NO_COMPRESSION`, `Zlib::BEST_SPEED`, 49 | `Zlib::BEST_COMPRESSION`, or `Zlib::DEFAULT_COMPRESSION` 50 | - `:mem_level`: sets how much memory the compressor allocates, can be an integer 51 | from `1` to `9`, or one of the constants `Zlib::MAX_MEM_LEVEL`, or 52 | `Zlib::DEF_MEM_LEVEL` 53 | - `:strategy`: can be one of the constants `Zlib::FILTERED`, 54 | `Zlib::HUFFMAN_ONLY`, `Zlib::RLE`, `Zlib::FIXED`, or `Zlib::DEFAULT_STRATEGY` 55 | 56 | The other options relate to settings that are negotiated via the protocol and 57 | can be used to set the local session's behaviour and control that of the peer: 58 | 59 | - `:no_context_takeover`: if `true`, stops the session reusing a deflate context 60 | between messages 61 | - `:request_no_context_takeover`: if `true`, makes the session tell the other 62 | peer not to reuse a deflate context between messages 63 | - `:max_window_bits`: an integer from `8` to `15` inclusive that sets the 64 | maximum size of the session's sliding window; a lower window size will be used 65 | if requested by the peer 66 | - `:request_max_window_bits`: an integer from `8` to `15` inclusive to ask the 67 | other peer to use to set its maximum sliding window size, if supported 68 | -------------------------------------------------------------------------------- /lib/permessage_deflate/session.rb: -------------------------------------------------------------------------------- 1 | class PermessageDeflate 2 | class Session 3 | 4 | VALID_PARAMS = [ 5 | 'server_no_context_takeover', 6 | 'client_no_context_takeover', 7 | 'server_max_window_bits', 8 | 'client_max_window_bits' 9 | ] 10 | 11 | MIN_WINDOW_BITS = 9 12 | MAX_WINDOW_BITS = 15 13 | VALID_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15] 14 | 15 | def self.valid_params?(params) 16 | return false unless params.keys.all? { |k| VALID_PARAMS.include?(k) } 17 | return false if params.values.grep(Array).any? 18 | 19 | if params.has_key?('server_no_context_takeover') 20 | return false unless params['server_no_context_takeover'] == true 21 | end 22 | 23 | if params.has_key?('client_no_context_takeover') 24 | return false unless params['client_no_context_takeover'] == true 25 | end 26 | 27 | if params.has_key?('server_max_window_bits') 28 | return false unless VALID_WINDOW_BITS.include?(params['server_max_window_bits']) 29 | end 30 | 31 | true 32 | end 33 | 34 | def initialize(options) 35 | @level = options.fetch(:level, Zlib::DEFAULT_COMPRESSION) 36 | @mem_level = options.fetch(:mem_level, Zlib::DEF_MEM_LEVEL) 37 | @strategy = options.fetch(:strategy, Zlib::DEFAULT_STRATEGY) 38 | 39 | @accept_no_context_takeover = options.fetch(:no_context_takeover, false) 40 | @accept_max_window_bits = options.fetch(:max_window_bits, nil) 41 | @request_no_context_takeover = options.fetch(:request_no_context_takeover, false) 42 | @request_max_window_bits = options.fetch(:request_max_window_bits, nil) 43 | 44 | @deflate = @inflate = nil 45 | end 46 | 47 | def process_incoming_message(message) 48 | return message unless message.rsv1 49 | 50 | inflate = get_inflate 51 | 52 | message.data = inflate.inflate(message.data) + 53 | inflate.inflate([0x00, 0x00, 0xff, 0xff].pack('C*')) 54 | 55 | free(inflate) unless @inflate 56 | message 57 | end 58 | 59 | def process_outgoing_message(message) 60 | deflate = get_deflate 61 | 62 | message.data = deflate.deflate(message.data, Zlib::SYNC_FLUSH)[0...-4] 63 | message.rsv1 = true 64 | 65 | free(deflate) unless @deflate 66 | message 67 | end 68 | 69 | def close 70 | free(@inflate) 71 | @inflate = nil 72 | 73 | free(@deflate) 74 | @deflate = nil 75 | end 76 | 77 | private 78 | 79 | def free(codec) 80 | return if codec.nil? 81 | codec.finish rescue nil 82 | codec.close 83 | end 84 | 85 | def get_inflate 86 | return @inflate if @inflate 87 | 88 | window_bits = [@peer_window_bits, MIN_WINDOW_BITS].max 89 | inflate = Zlib::Inflate.new(-window_bits) 90 | 91 | @inflate = inflate if @peer_context_takeover 92 | inflate 93 | end 94 | 95 | def get_deflate 96 | return @deflate if @deflate 97 | 98 | window_bits = [@own_window_bits, MIN_WINDOW_BITS].max 99 | deflate = Zlib::Deflate.new(@level, -window_bits, @mem_level, @strategy) 100 | 101 | @deflate = deflate if @own_context_takeover 102 | deflate 103 | end 104 | 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/permessage_deflate/client_session_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe PermessageDeflate::ClientSession do 4 | let(:ext) { PermessageDeflate.configure(options) } 5 | let(:session) { ext.create_client_session } 6 | let(:options) { {} } 7 | let(:offer) { session.generate_offer } 8 | let(:response) { {} } 9 | let(:activate) { session.activate(response) } 10 | 11 | let(:deflate) { double(:deflate, :deflate => [0x00, 0x00, 0xff, 0xff].pack("C*")) } 12 | let(:inflate) { double(:inflate, :inflate => [0x00, 0x00, 0xff, 0xff].pack("C*")) } 13 | let(:level) { Zlib::DEFAULT_COMPRESSION } 14 | let(:mem_level) { Zlib::DEF_MEM_LEVEL } 15 | let(:strategy) { Zlib::DEFAULT_STRATEGY } 16 | 17 | let(:message) { Message.new("hello", true) } 18 | 19 | def process_incoming_message 20 | session.process_incoming_message(message) 21 | end 22 | 23 | def process_outgoing_message 24 | session.process_outgoing_message(message) 25 | end 26 | 27 | describe "with default options" do 28 | it "indicates support for client_max_window_bits" do 29 | expect(offer).to eq("client_max_window_bits" => true) 30 | end 31 | 32 | describe "with an empty response" do 33 | it "accepts the response" do 34 | expect(activate).to be true 35 | end 36 | 37 | it "uses context takeover and 15 window bits for inflating incoming messages" do 38 | activate 39 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(1).and_return(inflate) 40 | process_incoming_message 41 | process_incoming_message 42 | end 43 | 44 | it "uses context takeover and 15 window bits for deflating outgoing messages" do 45 | activate 46 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(1).and_return(deflate) 47 | process_outgoing_message 48 | process_outgoing_message 49 | end 50 | end 51 | 52 | describe "when the response includes server_no_context_takeover" do 53 | before { response["server_no_context_takeover"] = true } 54 | 55 | it "accepts the response" do 56 | expect(activate).to be true 57 | end 58 | 59 | it "uses no context takeover and 15 window bits for inflating incoming messages" do 60 | activate 61 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) 62 | expect(inflate).to receive(:finish).exactly(2) 63 | expect(inflate).to receive(:close).exactly(2) 64 | process_incoming_message 65 | process_incoming_message 66 | end 67 | end 68 | 69 | describe "when the response includes client_no_context_takeover" do 70 | before { response["client_no_context_takeover"] = true } 71 | 72 | it "accepts the response" do 73 | expect(activate).to be true 74 | end 75 | 76 | it "uses no context takeover and 15 window bits for deflating outgoing messages" do 77 | activate 78 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) 79 | expect(deflate).to receive(:finish).exactly(2) 80 | expect(deflate).to receive(:close).exactly(2) 81 | process_outgoing_message 82 | process_outgoing_message 83 | end 84 | end 85 | 86 | describe "when the response includes server_max_window_bits" do 87 | before { response["server_max_window_bits"] = 8 } 88 | 89 | it "accepts the response" do 90 | expect(activate).to be true 91 | end 92 | 93 | it "uses context takeover and 9 window bits for inflating incoming messages" do 94 | activate 95 | expect(Zlib::Inflate).to receive(:new).with(-9).exactly(1).and_return(inflate) 96 | process_incoming_message 97 | process_incoming_message 98 | end 99 | end 100 | 101 | describe "when the response includes invalid server_max_window_bits" do 102 | before { response["server_max_window_bits"] = 20 } 103 | 104 | it "rejects the response" do 105 | expect(activate).to be false 106 | end 107 | end 108 | 109 | describe "when the response includes client_max_window_bits" do 110 | before { response["client_max_window_bits"] = 8 } 111 | 112 | it "accepts the response" do 113 | expect(activate).to be true 114 | end 115 | 116 | it "uses context takeover and 9 window bits for deflating outgoing messages" do 117 | activate 118 | expect(Zlib::Deflate).to receive(:new).with(level, -9, mem_level, strategy).exactly(1).and_return(deflate) 119 | process_outgoing_message 120 | process_outgoing_message 121 | end 122 | end 123 | 124 | describe "when the response includes invalid client_max_window_bits" do 125 | before { response["client_max_window_bits"] = 20 } 126 | 127 | it "rejects the response" do 128 | expect(activate).to be false 129 | end 130 | end 131 | end 132 | 133 | describe "with no_context_takeover" do 134 | before { options[:no_context_takeover] = true } 135 | 136 | it "sends client_no_context_takeover" do 137 | expect(offer).to eq( 138 | "client_no_context_takeover" => true, 139 | "client_max_window_bits" => true 140 | ) 141 | end 142 | 143 | describe "with an empty response" do 144 | it "accepts the response" do 145 | expect(activate).to be true 146 | end 147 | 148 | it "uses no context takeover and 15 window bits for deflating outgoing messages" do 149 | activate 150 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) 151 | expect(deflate).to receive(:finish).exactly(2) 152 | expect(deflate).to receive(:close).exactly(2) 153 | process_outgoing_message 154 | process_outgoing_message 155 | end 156 | end 157 | end 158 | 159 | describe "with max_window_bits" do 160 | before { options[:max_window_bits] = 9 } 161 | 162 | it "sends client_max_window_bits" do 163 | expect(offer).to eq("client_max_window_bits" => 9) 164 | end 165 | 166 | describe "with an empty response" do 167 | it "accepts the response" do 168 | expect(activate).to be true 169 | end 170 | 171 | it "uses context takeover and 9 window bits for deflating outgoing messages" do 172 | activate 173 | expect(Zlib::Deflate).to receive(:new).with(level, -9, mem_level, strategy).exactly(1).and_return(deflate) 174 | process_outgoing_message 175 | process_outgoing_message 176 | end 177 | end 178 | 179 | describe "when the response has higher client_max_window_bits" do 180 | before { response["client_max_window_bits"] = 10 } 181 | 182 | it "rejects the response" do 183 | expect(activate).to be false 184 | end 185 | end 186 | 187 | describe "when the response has lower client_max_window_bits" do 188 | before { response["client_max_window_bits"] = 8 } 189 | 190 | it "accepts the response" do 191 | expect(activate).to be true 192 | end 193 | 194 | it "uses context takeover and 9 window bits for deflating outgoing messages" do 195 | activate 196 | expect(Zlib::Deflate).to receive(:new).with(level, -9, mem_level, strategy).exactly(1).and_return(deflate) 197 | process_outgoing_message 198 | process_outgoing_message 199 | end 200 | end 201 | end 202 | 203 | describe "with invalid max_window_bits" do 204 | before { options[:max_window_bits] = 20 } 205 | 206 | it "raises when generating the offer" do 207 | expect { offer }.to raise_error(PermessageDeflate::ConfigurationError) 208 | end 209 | end 210 | 211 | describe "with request_no_context_takeover" do 212 | before { options[:request_no_context_takeover] = true } 213 | 214 | it "sends server_no_context_takeover" do 215 | expect(offer).to eq( 216 | "client_max_window_bits" => true, 217 | "server_no_context_takeover" => true 218 | ) 219 | end 220 | 221 | describe "with an empty response" do 222 | it "rejects the response" do 223 | expect(activate).to be false 224 | end 225 | end 226 | 227 | describe "when the response includes server_no_context_takeover" do 228 | before { response["server_no_context_takeover"] = true } 229 | 230 | it "accepts the response" do 231 | expect(activate).to be true 232 | end 233 | 234 | it "uses no context takeover and 15 window bits for inflating incoming messages" do 235 | activate 236 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) 237 | expect(inflate).to receive(:finish).exactly(2) 238 | expect(inflate).to receive(:close).exactly(2) 239 | process_incoming_message 240 | process_incoming_message 241 | end 242 | end 243 | end 244 | 245 | describe "with request_max_window_bits" do 246 | before { options[:request_max_window_bits] = 12 } 247 | 248 | it "sends server_max_window_bits" do 249 | expect(offer).to eq( 250 | "client_max_window_bits" => true, 251 | "server_max_window_bits" => 12 252 | ) 253 | end 254 | 255 | describe "with an empty response" do 256 | it "rejects the response" do 257 | expect(activate).to be false 258 | end 259 | end 260 | 261 | describe "when the response has higher server_max_window_bits" do 262 | before { response["server_max_window_bits"] = 13 } 263 | 264 | it "rejects the response" do 265 | expect(activate).to be false 266 | end 267 | end 268 | 269 | describe "when the response has lower server_max_window_bits" do 270 | before { response["server_max_window_bits"] = 11 } 271 | 272 | it "accepts the response" do 273 | expect(activate).to be true 274 | end 275 | 276 | it "uses context takeover and 11 window bits for inflating incoming messages" do 277 | activate 278 | expect(Zlib::Inflate).to receive(:new).with(-11).exactly(1).and_return(inflate) 279 | process_incoming_message 280 | process_incoming_message 281 | end 282 | end 283 | end 284 | 285 | describe "with invalid request_max_window_bits" do 286 | before { options[:request_max_window_bits] = 20 } 287 | 288 | it "raises when generating an offer" do 289 | expect { offer }.to raise_error(PermessageDeflate::ConfigurationError) 290 | end 291 | end 292 | 293 | describe "with level" do 294 | before { options[:level] = Zlib::BEST_SPEED } 295 | 296 | it "sets the level of the deflate stream" do 297 | activate 298 | expect(Zlib::Deflate).to receive(:new).with(Zlib::BEST_SPEED, -15, mem_level, strategy).and_return(deflate) 299 | process_outgoing_message 300 | end 301 | end 302 | 303 | describe "with mem_level" do 304 | before { options[:mem_level] = 5 } 305 | 306 | it "sets the mem_level of the deflate stream" do 307 | activate 308 | expect(Zlib::Deflate).to receive(:new).with(level, -15, 5, strategy).and_return(deflate) 309 | process_outgoing_message 310 | end 311 | end 312 | 313 | describe "with strategy" do 314 | before { options[:strategy] = Zlib::FILTERED } 315 | 316 | it "sets the strategy of the deflate stream" do 317 | activate 318 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, Zlib::FILTERED).and_return(deflate) 319 | process_outgoing_message 320 | end 321 | end 322 | end 323 | -------------------------------------------------------------------------------- /spec/permessage_deflate/server_session_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe PermessageDeflate::ServerSession do 4 | let(:ext) { PermessageDeflate.configure(options) } 5 | let(:offer) { {} } 6 | let(:session) { ext.create_server_session([offer]) } 7 | let(:options) { {} } 8 | let(:response) { session.generate_response } 9 | 10 | let(:deflate) { double(:deflate, :deflate => [0x00, 0x00, 0xff, 0xff].pack("C*")) } 11 | let(:inflate) { double(:inflate, :inflate => [0x00, 0x00, 0xff, 0xff].pack("C*")) } 12 | let(:level) { Zlib::DEFAULT_COMPRESSION } 13 | let(:mem_level) { Zlib::DEF_MEM_LEVEL } 14 | let(:strategy) { Zlib::DEFAULT_STRATEGY } 15 | 16 | let(:message) { Message.new("hello", true) } 17 | 18 | def process_incoming_message 19 | session.process_incoming_message(message) 20 | end 21 | 22 | def process_outgoing_message 23 | session.process_outgoing_message(message) 24 | end 25 | 26 | describe "with default options" do 27 | describe "with an empty offer" do 28 | it "generates an empty response" do 29 | expect(response).to eq({}) 30 | end 31 | 32 | it "uses context takeover and 15 window bits for inflating incoming messages" do 33 | response 34 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(1).and_return(inflate) 35 | process_incoming_message 36 | process_incoming_message 37 | end 38 | 39 | it "uses context takeover and 15 window bits for deflating outgoing messages" do 40 | response 41 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(1).and_return(deflate) 42 | process_outgoing_message 43 | process_outgoing_message 44 | end 45 | end 46 | 47 | describe "when the offer includes server_no_context_takeover" do 48 | before { offer["server_no_context_takeover"] = true } 49 | 50 | it "includes server_no_context_takeover in the response" do 51 | expect(response).to eq("server_no_context_takeover" => true) 52 | end 53 | 54 | it "uses no context takeover and 15 window bits for deflating outgoing messages" do 55 | response 56 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) 57 | expect(deflate).to receive(:finish).exactly(2) 58 | expect(deflate).to receive(:close).exactly(2) 59 | process_outgoing_message 60 | process_outgoing_message 61 | end 62 | end 63 | 64 | describe "when the offer includes client_no_context_takeover" do 65 | before { offer["client_no_context_takeover"] = true } 66 | 67 | it "includes client_no_context_takeover in the response" do 68 | expect(response).to eq("client_no_context_takeover" => true) 69 | end 70 | 71 | it "uses no context takeover and 15 window bits for inflating incoming messages" do 72 | response 73 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) 74 | expect(inflate).to receive(:finish).exactly(2) 75 | expect(inflate).to receive(:close).exactly(2) 76 | process_incoming_message 77 | process_incoming_message 78 | end 79 | end 80 | 81 | describe "when the offer includes server_max_window_bits" do 82 | before { offer["server_max_window_bits"] = 13 } 83 | 84 | it "includes server_max_window_bits in the response" do 85 | expect(response).to eq("server_max_window_bits" => 13) 86 | end 87 | 88 | it "uses context takeover and 13 window bits for deflating outgoing messages" do 89 | response 90 | expect(Zlib::Deflate).to receive(:new).with(level, -13, mem_level, strategy).exactly(1).and_return(deflate) 91 | process_outgoing_message 92 | process_outgoing_message 93 | end 94 | end 95 | 96 | describe "when the offer includes invalid server_max_window_bits" do 97 | before { offer["server_max_window_bits"] = 20 } 98 | 99 | it "does not create a session" do 100 | expect(session).to be_nil 101 | end 102 | end 103 | 104 | describe "when the offer includes client_max_window_bits" do 105 | before { offer["client_max_window_bits"] = true } 106 | 107 | it "does not include a client_max_window_bits hint in the response" do 108 | expect(response).to eq({}) 109 | end 110 | 111 | it "uses context takeover and 15 window bits for inflating incoming messages" do 112 | response 113 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(1).and_return(inflate) 114 | process_incoming_message 115 | process_incoming_message 116 | end 117 | end 118 | 119 | describe "when the offer includes a client_max_window_bits hint" do 120 | before { offer["client_max_window_bits"] = 13 } 121 | 122 | it "includes a client_max_window_bits hint in the response" do 123 | expect(response).to eq("client_max_window_bits" => 13) 124 | end 125 | 126 | it "uses context takeover and 13 window bits for inflating incoming messages" do 127 | response 128 | expect(Zlib::Inflate).to receive(:new).with(-13).exactly(1).and_return(inflate) 129 | process_incoming_message 130 | process_incoming_message 131 | end 132 | end 133 | 134 | describe "when the offer includes invalid client_max_window_bits" do 135 | before { offer["client_max_window_bits"] = 20 } 136 | 137 | it "does not create a session" do 138 | expect(session).to be_nil 139 | end 140 | end 141 | end 142 | 143 | describe "with no_context_takeover" do 144 | before { options[:no_context_takeover] = true } 145 | 146 | describe "with an empty offer" do 147 | it "includes server_no_context_takeover in the response" do 148 | expect(response).to eq("server_no_context_takeover" => true) 149 | end 150 | 151 | it "uses no context takeover and 15 window bits for deflating outgoing messages" do 152 | response 153 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, strategy).exactly(2).and_return(deflate) 154 | expect(deflate).to receive(:finish).exactly(2) 155 | expect(deflate).to receive(:close).exactly(2) 156 | process_outgoing_message 157 | process_outgoing_message 158 | end 159 | end 160 | end 161 | 162 | describe "with max_window_bits" do 163 | before { options[:max_window_bits] = 12 } 164 | 165 | describe "with an empty offer" do 166 | it "does not include server_max_window_bits in the response" do 167 | expect(response).to eq({}) 168 | end 169 | 170 | it "uses context takeover and 12 window bits for deflating outgoing messages" do 171 | response 172 | expect(Zlib::Deflate).to receive(:new).with(level, -12, mem_level, strategy).exactly(1).and_return(deflate) 173 | process_outgoing_message 174 | process_outgoing_message 175 | end 176 | end 177 | 178 | describe "when the offer has higher server_max_window_bits" do 179 | before { offer["server_max_window_bits"] = 13 } 180 | 181 | it "includes server_max_window_bits in the response" do 182 | expect(response).to eq("server_max_window_bits" => 12) 183 | end 184 | 185 | it "uses context takeover and 12 window bits for deflating outgoing messages" do 186 | response 187 | expect(Zlib::Deflate).to receive(:new).with(level, -12, mem_level, strategy).exactly(1).and_return(deflate) 188 | process_outgoing_message 189 | process_outgoing_message 190 | end 191 | end 192 | 193 | describe "when the offer has lower server_max_window_bits" do 194 | before { offer["server_max_window_bits"] = 11 } 195 | 196 | it "includes server_max_window_bits in the response" do 197 | expect(response).to eq("server_max_window_bits" => 11) 198 | end 199 | 200 | it "uses context takeover and 11 window bits for deflating outgoing messages" do 201 | response 202 | expect(Zlib::Deflate).to receive(:new).with(level, -11, mem_level, strategy).exactly(1).and_return(deflate) 203 | process_outgoing_message 204 | process_outgoing_message 205 | end 206 | end 207 | end 208 | 209 | describe "with request_no_context_takeover" do 210 | before { options[:request_no_context_takeover] = true } 211 | 212 | describe "with an empty offer" do 213 | it "includes client_no_context_takeover in the response" do 214 | expect(response).to eq("client_no_context_takeover" => true) 215 | end 216 | 217 | it "uses no context takeover and 15 window bits for inflating incoming messages" do 218 | response 219 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(2).and_return(inflate) 220 | expect(inflate).to receive(:finish).exactly(2) 221 | expect(inflate).to receive(:close).exactly(2) 222 | process_incoming_message 223 | process_incoming_message 224 | end 225 | end 226 | end 227 | 228 | describe "with request_max_window_bits" do 229 | before { options[:request_max_window_bits] = 11 } 230 | 231 | describe "with an empty offer" do 232 | it "does not include client_max_window_bits in the response" do 233 | expect(response).to eq({}) 234 | end 235 | 236 | it "uses context takeover and 15 window bits for inflating incoming messages" do 237 | response 238 | expect(Zlib::Inflate).to receive(:new).with(-15).exactly(1).and_return(inflate) 239 | process_incoming_message 240 | process_incoming_message 241 | end 242 | end 243 | 244 | describe "when the offer includes client_max_window_bits" do 245 | before { offer["client_max_window_bits"] = true } 246 | 247 | it "includes client_max_window_bits in the response" do 248 | expect(response).to eq("client_max_window_bits" => 11) 249 | end 250 | 251 | it "uses context takeover and 11 window bits for inflating incoming messages" do 252 | response 253 | expect(Zlib::Inflate).to receive(:new).with(-11).exactly(1).and_return(inflate) 254 | process_incoming_message 255 | process_incoming_message 256 | end 257 | end 258 | 259 | describe "when the offer has higher client_max_window_bits" do 260 | before { offer["client_max_window_bits"] = 12 } 261 | 262 | it "includes client_max_window_bits in the response" do 263 | expect(response).to eq("client_max_window_bits" => 11) 264 | end 265 | 266 | it "uses context takeover and 11 window bits for inflating incoming messages" do 267 | response 268 | expect(Zlib::Inflate).to receive(:new).with(-11).exactly(1).and_return(inflate) 269 | process_incoming_message 270 | process_incoming_message 271 | end 272 | end 273 | 274 | describe "when the offer has lower client_max_window_bits" do 275 | before { offer["client_max_window_bits"] = 10 } 276 | 277 | it "includes client_max_window_bits in the response" do 278 | expect(response).to eq("client_max_window_bits" => 10) 279 | end 280 | 281 | it "uses context takeover and 10 window bits for inflating incoming messages" do 282 | response 283 | expect(Zlib::Inflate).to receive(:new).with(-10).exactly(1).and_return(inflate) 284 | process_incoming_message 285 | process_incoming_message 286 | end 287 | end 288 | end 289 | 290 | describe "with level" do 291 | before { options[:level] = Zlib::BEST_SPEED } 292 | 293 | it "sets the level of the deflate stream" do 294 | response 295 | expect(Zlib::Deflate).to receive(:new).with(Zlib::BEST_SPEED, -15, mem_level, strategy).and_return(deflate) 296 | process_outgoing_message 297 | end 298 | end 299 | 300 | describe "with mem_level" do 301 | before { options[:mem_level] = 5 } 302 | 303 | it "sets the mem_level of the deflate stream" do 304 | response 305 | expect(Zlib::Deflate).to receive(:new).with(level, -15, 5, strategy).and_return(deflate) 306 | process_outgoing_message 307 | end 308 | end 309 | 310 | describe "with strategy" do 311 | before { options[:strategy] = Zlib::FILTERED } 312 | 313 | it "sets the strategy of the deflate stream" do 314 | response 315 | expect(Zlib::Deflate).to receive(:new).with(level, -15, mem_level, Zlib::FILTERED).and_return(deflate) 316 | process_outgoing_message 317 | end 318 | end 319 | end 320 | --------------------------------------------------------------------------------