diff --git a/.travis.yml b/.travis.yml index 64b16e7..e45546d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,14 +5,19 @@ rvm: - 1.9.3 - 2.0.0 - 2.1.10 - - 2.2.8 - - 2.3.5 - - 2.4.2 + - 2.2.10 + - 2.3.8 + - 2.4.10 + - 2.5.8 + - 2.6.6 + - 2.7.1 - jruby-19mode - - jruby-head + - jruby-9.0 + - jruby-9.1 + - jruby-9.2 before_install: - - '[[ "$(ruby --version)" == *"1.9.3"* ]] && gem update --system 2.4.8 || true' + - '[[ "$(ruby --version)" != *"1.9.3"* ]] || gem update --system 2.4.8' script: - - bundle exec rspec -c spec + - bundle exec rspec diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8b2ee..6aa5d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,24 @@ +### 0.1.5 / 2020-06-02 + +- Remove a ReDoS vulnerability in the header parser (CVE-2020-7663) + +### 0.1.4 / 2019-06-10 + +- Fix a deprecation warning for using the `=~` operator on `true` +- Change license from MIT to Apache 2.0 + ### 0.1.3 / 2017-11-11 -* Accept extension names and parameters including uppercase letters +- Accept extension names and parameters including uppercase letters ### 0.1.2 / 2015-02-19 -* Make it safe to call `Extensions#close` if the handshake is not complete +- Make it safe to call `Extensions#close` if the handshake is not complete ### 0.1.1 / 2014-12-14 -* Explicitly require `strscan` which is not loaded in a vanilla Ruby environment +- Explicitly require `strscan` which is not loaded in a vanilla Ruby environment ### 0.1.0 / 2014-12-13 -* Initial release +- Initial release diff --git a/LICENSE.md b/LICENSE.md index 1b6b930..3a88e51 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,20 +1,12 @@ -# The MIT license +Copyright 2014-2020 James Coglan -Copyright (c) 2014-2017 James Coglan +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md index c85f8ea..187021a 100644 --- a/README.md +++ b/README.md @@ -227,8 +227,8 @@ then the `permessage-deflate` extension will receive the call: ```rb ext.create_server_session([ - {'server_no_context_takeover' => true, 'server_max_window_bits' => 8}, - {'server_max_window_bits' => 15} + { 'server_no_context_takeover' => true, 'server_max_window_bits' => 8 }, + { 'server_max_window_bits' => 15 } ]) ``` @@ -244,8 +244,8 @@ implement the following methods, as well as the *Session* API listed below. ```rb client_session.generate_offer # e.g. -> [ -# {'server_no_context_takeover' => true, 'server_max_window_bits' => 8}, -# {'server_max_window_bits' => 15} +# { 'server_no_context_takeover' => true, 'server_max_window_bits' => 8 }, +# { 'server_max_window_bits' => 15 } # ] ``` @@ -270,7 +270,7 @@ must implement the following methods, as well as the *Session* API listed below. ```rb server_session.generate_response -# e.g. -> {'server_max_window_bits' => 8} +# e.g. -> { 'server_max_window_bits' => 8 } ``` This returns the set of parameters the server session wants to send in its @@ -309,5 +309,5 @@ the session to release any resources it's using. ## Examples -* Consumer: [websocket-driver](https://github.com/faye/websocket-driver-ruby) -* Provider: [permessage-deflate](https://github.com/faye/permessage-deflate-ruby) +- Consumer: [websocket-driver](https://github.com/faye/websocket-driver-ruby) +- Provider: [permessage-deflate](https://github.com/faye/permessage-deflate-ruby) diff --git a/lib/websocket/extensions.rb b/lib/websocket/extensions.rb index 2912ff9..14ca7ab 100644 --- a/lib/websocket/extensions.rb +++ b/lib/websocket/extensions.rb @@ -38,7 +38,7 @@ def add(ext) end if @by_name.has_key?(ext.name) - raise TypeError, %Q{An extension with name "#{ext.name}" is already registered} + raise TypeError, %Q{An extension with name "#{ ext.name }" is already registered} end @by_name[ext.name] = ext @@ -78,18 +78,18 @@ def activate(header) responses.each_offer do |name, params| unless record = @index[name] - raise ExtensionError, %Q{Server sent am extension response for unknown extension "#{name}"} + raise ExtensionError, %Q{Server sent am extension response for unknown extension "#{ name } } end ext, session = *record if reserved = reserved?(ext) - raise ExtensionError, %Q{Server sent two extension responses that use the RSV#{reserved[0]} } + - %Q{ bit: "#{reserved[1]}" and "#{ext.name}"} + raise ExtensionError, %Q{Server sent two extension responses that use the RSV#{ reserved[0] }} + + %Q{bit: "#{ reserved[1] }" and "#{ ext.name }"} end unless session.activate(params) == true - raise ExtensionError, %Q{Server send unacceptable extension parameters: #{Parser.serialize_params(name, params)}} + raise ExtensionError, %Q{Server send unacceptable extension parameters: #{ Parser.serialize_params(name, params) }} end reserve(ext) @@ -118,7 +118,7 @@ def generate_response(header) end def valid_frame_rsv(frame) - allowed = {:rsv1 => false, :rsv2 => false, :rsv3 => false} + allowed = { :rsv1 => false, :rsv2 => false, :rsv3 => false } if MESSAGE_OPCODES.include?(frame.opcode) @sessions.each do |ext, session| diff --git a/lib/websocket/extensions/parser.rb b/lib/websocket/extensions/parser.rb index d6d3462..f8d8891 100644 --- a/lib/websocket/extensions/parser.rb +++ b/lib/websocket/extensions/parser.rb @@ -6,10 +6,10 @@ class Extensions class Parser TOKEN = /([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)/ NOTOKEN = /([^!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z])/ - QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"/ - PARAM = %r{#{TOKEN.source}(?:=(?:#{TOKEN.source}|#{QUOTED.source}))?} - EXT = %r{#{TOKEN.source}(?: *; *#{PARAM.source})*} - EXT_LIST = %r{^#{EXT.source}(?: *, *#{EXT.source})*$} + QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)"/ + PARAM = %r{#{ TOKEN.source }(?:=(?:#{ TOKEN.source }|#{ QUOTED.source }))?} + EXT = %r{#{ TOKEN.source }(?: *; *#{ PARAM.source })*} + EXT_LIST = %r{^#{ EXT.source }(?: *, *#{ EXT.source })*$} NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/ ParseError = Class.new(ArgumentError) @@ -19,7 +19,7 @@ def self.parse_header(header) return offers if header == '' or header.nil? unless header =~ EXT_LIST - raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{header}" + raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{ header }" end scanner = StringScanner.new(header) @@ -38,7 +38,7 @@ def self.parse_header(header) else data = true end - if data =~ NUMBER + if data != true and data =~ NUMBER data = data =~ /\./ ? data.to_f : data.to_i(10) end diff --git a/spec/websocket/extensions/parser_spec.rb b/spec/websocket/extensions/parser_spec.rb index 4e98e77..7458210 100644 --- a/spec/websocket/extensions/parser_spec.rb +++ b/spec/websocket/extensions/parser_spec.rb @@ -20,62 +20,68 @@ def parse(string) it "parses one offer with no params" do expect(parse 'a').to eq [ - {:name => "a", :params => {}} + { :name => "a", :params => {} } ] end it "parses two offers with no params" do expect(parse 'a, b').to eq [ - {:name => "a", :params => {}}, {:name => "b", :params => {}} + { :name => "a", :params => {} }, { :name => "b", :params => {} } ] end it "parses a duplicate offer name" do expect(parse 'a, a').to eq [ - {:name => "a", :params => {}}, - {:name => "a", :params => {}} + { :name => "a", :params => {} }, + { :name => "a", :params => {} } ] end it "parses a flag" do expect(parse 'a; b').to eq [ - {:name => "a", :params => {"b" => true}} + { :name => "a", :params => { "b" => true } } ] end it "parses an unquoted param" do expect(parse 'a; b=1').to eq [ - {:name => "a", :params => {"b" => 1}} + { :name => "a", :params => { "b" => 1 } } ] end it "parses a quoted param" do expect(parse 'a; b="hi, \"there"').to eq [ - {:name => "a", :params => {"b" => 'hi, "there'}} + { :name => "a", :params => { "b" => 'hi, "there' } } ] end it "parses multiple params" do expect(parse 'a; b; c=1; d="hi"').to eq [ - {:name => "a", :params => {"b" => true, "c" => 1, "d" => "hi"}} + { :name => "a", :params => { "b" => true, "c" => 1, "d" => "hi" } } ] end it "parses duplicate params" do expect(parse 'a; b; c=1; b="hi"').to eq [ - {:name => "a", :params => {"b" => [true, "hi"], "c" => 1}} + { :name => "a", :params => { "b" => [true, "hi"], "c" => 1 } } ] end it "parses multiple complex offers" do expect(parse 'a; b=1, c, b; d, c; e="hi, there"; e, a; b').to eq [ - {:name => "a", :params => {"b" => 1}}, - {:name => "c", :params => {}}, - {:name => "b", :params => {"d" => true}}, - {:name => "c", :params => {"e" => ['hi, there', true]}}, - {:name => "a", :params => {"b" => true}} + { :name => "a", :params => { "b" => 1 } }, + { :name => "c", :params => {} }, + { :name => "b", :params => { "d" => true } }, + { :name => "c", :params => { "e" => ['hi, there', true] } }, + { :name => "a", :params => { "b" => true } } ] end + + it "rejects a string missing its closing quote" do + expect { + parse "foo; bar=\"fooa\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a\\a" + }.to raise_error(WebSocket::Extensions::Parser::ParseError) + end end describe :serialize_params do diff --git a/spec/websocket/extensions_spec.rb b/spec/websocket/extensions_spec.rb index aa161b2..7c1e4f1 100644 --- a/spec/websocket/extensions_spec.rb +++ b/spec/websocket/extensions_spec.rb @@ -39,7 +39,7 @@ describe "client sessions" do before do - @offer = {"mode" => "compress"} + @offer = { "mode" => "compress" } allow(@ext).to receive(:create_client_session).and_return(@session) allow(@session).to receive(:generate_offer).and_return(@offer) @extensions.add(@ext) @@ -248,7 +248,7 @@ describe "server sessions" do before do - @response = {"mode" => "compress"} + @response = { "mode" => "compress" } allow(@ext).to receive(:create_server_session).and_return(@session) allow(@session).to receive(:generate_response).and_return(@response) @@ -269,12 +269,12 @@ describe :generate_response do it "asks the extension for a server session with the offer" do - expect(@ext).to receive(:create_server_session).with([{"flag" => true}]).exactly(1).and_return(@session) + expect(@ext).to receive(:create_server_session).with([{ "flag" => true }]).exactly(1).and_return(@session) @extensions.generate_response("deflate; flag") end it "asks the extension for a server session with multiple offers" do - expect(@ext).to receive(:create_server_session).with([{"a" => true}, {"b" => true}]).exactly(1).and_return(@session) + expect(@ext).to receive(:create_server_session).with([{ "a" => true }, { "b" => true }]).exactly(1).and_return(@session) @extensions.generate_response("deflate; a, deflate; b") end @@ -326,7 +326,7 @@ end it "raises an error if the header is invalid" do - expect { @extensions.generate_response("x-webkit- -frame") }.to raise_error + expect { @extensions.generate_response("x-webkit- -frame") }.to raise_error(WebSocket::Extensions::Parser::ParseError) end it "returns a response for potentially conflicting extensions if their preceding extensions don't build a session" do diff --git a/websocket-extensions.gemspec b/websocket-extensions.gemspec index 3d8ea87..a1dab05 100644 --- a/websocket-extensions.gemspec +++ b/websocket-extensions.gemspec @@ -1,11 +1,11 @@ Gem::Specification.new do |s| s.name = 'websocket-extensions' - s.version = '0.1.3' + s.version = '0.1.5' s.summary = 'Generic extension manager for WebSocket connections' s.author = 'James Coglan' s.email = 'jcoglan@gmail.com' s.homepage = 'https://github.com/faye/websocket-extensions-ruby' - s.license = 'MIT' + s.license = 'Apache-2.0' s.extra_rdoc_files = %w[README.md] s.rdoc_options = %w[--main README.md --markup markdown]