Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 402222fdd9 | |||
| aa156a439d | |||
| 8174a4a0f9 | |||
| 96059802a6 | |||
| bd6d0acc01 | |||
| a8c847876b | |||
| 242f007cb9 | |||
| 249bcc106f | |||
| 738a0af1dd | |||
| 68c1b21129 | |||
| 8ee09b2924 | |||
| 7e64cfb6ca | |||
| 1a441fac80 | |||
| 60318127e2 | |||
| 392c672dd8 | |||
| 7ef1451923 | |||
| 76dbd000d4 | |||
| 4e7ae0b1f2 | |||
| ff9af49c92 | |||
| c44d436241 | |||
| 7b2ecb164c | |||
| 8592fb0288 | |||
| 7fe88150b2 | |||
| 14967f5e89 | |||
| afe3c6c6aa | |||
| 125807a973 | |||
| 92cead666f | |||
| 5b54b66cc1 | |||
| 3d143d2c3e | |||
| bb1179bc8c | |||
| 033020e78a | |||
| cc1d6196fe | |||
| b9da251798 | |||
| dd6cff6c09 | |||
| c73f432841 | |||
| 88abae45d2 | |||
| 7e29a5c63e | |||
| 3a3b0c537c | |||
| fb23226189 |
+16
-3
@@ -1,10 +1,23 @@
|
||||
sudo: false
|
||||
language: ruby
|
||||
|
||||
rvm:
|
||||
- 1.9.3
|
||||
- 2.0.0
|
||||
- 2.1.3
|
||||
- 2.1.10
|
||||
- 2.2.10
|
||||
- 2.3.8
|
||||
- 2.4.10
|
||||
- 2.5.8
|
||||
- 2.6.6
|
||||
- 2.7.1
|
||||
- jruby-19mode
|
||||
- rbx-2.2
|
||||
- jruby-9.0
|
||||
- jruby-9.1
|
||||
- jruby-9.2
|
||||
|
||||
script: bundle exec rspec -c spec/
|
||||
before_install:
|
||||
- '[[ "$(ruby --version)" != *"1.9.3"* ]] || gem update --system 2.4.8'
|
||||
|
||||
script:
|
||||
- bundle exec rspec
|
||||
|
||||
@@ -0,0 +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
|
||||
|
||||
### 0.1.2 / 2015-02-19
|
||||
|
||||
- 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
|
||||
|
||||
### 0.1.0 / 2014-12-13
|
||||
|
||||
- Initial release
|
||||
@@ -0,0 +1,4 @@
|
||||
# Code of Conduct
|
||||
|
||||
All projects under the [Faye](https://github.com/faye) umbrella are covered by
|
||||
the [Code of Conduct](https://github.com/faye/code-of-conduct).
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Copyright 2014-2020 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
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
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.
|
||||
@@ -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,29 +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)
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 James Coglan
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
- Consumer: [websocket-driver](https://github.com/faye/websocket-driver-ruby)
|
||||
- Provider: [permessage-deflate](https://github.com/faye/permessage-deflate-ruby)
|
||||
|
||||
@@ -38,7 +38,7 @@ module WebSocket
|
||||
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 @@ module WebSocket
|
||||
|
||||
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)
|
||||
@@ -98,9 +98,9 @@ module WebSocket
|
||||
end
|
||||
|
||||
def generate_response(header)
|
||||
offers = Parser.parse_header(header)
|
||||
sessions = []
|
||||
response = []
|
||||
offers = Parser.parse_header(header)
|
||||
|
||||
@in_order.each do |ext|
|
||||
offer = offers.by_name(ext.name)
|
||||
@@ -118,7 +118,7 @@ module WebSocket
|
||||
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|
|
||||
@@ -155,6 +155,8 @@ module WebSocket
|
||||
end
|
||||
|
||||
def close
|
||||
return unless @sessions
|
||||
|
||||
@sessions.each do |ext, session|
|
||||
session.close rescue nil
|
||||
end
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
require 'strscan'
|
||||
|
||||
module WebSocket
|
||||
class Extensions
|
||||
|
||||
class Parser
|
||||
TOKEN = /([!#\$%&'\*\+\-\.\^_`\|~0-9a-z]+)/
|
||||
NOTOKEN = /([^!#\$%&'\*\+\-\.\^_`\|~0-9a-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})*$}
|
||||
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 })*$}
|
||||
NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/
|
||||
|
||||
ParseError = Class.new(ArgumentError)
|
||||
@@ -17,7 +19,7 @@ module WebSocket
|
||||
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)
|
||||
@@ -36,7 +38,7 @@ module WebSocket
|
||||
else
|
||||
data = true
|
||||
end
|
||||
if data =~ NUMBER
|
||||
if data != true and data =~ NUMBER
|
||||
data = data =~ /\./ ? data.to_f : data.to_i(10)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,62 +20,68 @@ describe WebSocket::Extensions::Parser do
|
||||
|
||||
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
|
||||
|
||||
@@ -39,7 +39,7 @@ describe WebSocket::Extensions do
|
||||
|
||||
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 WebSocket::Extensions do
|
||||
|
||||
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 WebSocket::Extensions do
|
||||
|
||||
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
|
||||
|
||||
@@ -325,11 +325,14 @@ describe WebSocket::Extensions do
|
||||
expect(@extensions.generate_response("deflate, tar")).to eq "deflate; mode=compress"
|
||||
end
|
||||
|
||||
it "returns a response for potentially conflicting extensions if their preceeding extensions don't build a session" do
|
||||
it "raises an error if the header is invalid" do
|
||||
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
|
||||
allow(@ext).to receive(:create_server_session).and_return(nil)
|
||||
expect(@extensions.generate_response("deflate, tar")).to eq "tar; gzip"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'websocket-extensions'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'Generic extension manager for WebSocket connections'
|
||||
s.author = 'James Coglan'
|
||||
s.email = 'jcoglan@gmail.com'
|
||||
s.homepage = 'http://github.com/faye/websocket-extensions-ruby'
|
||||
s.license = 'MIT'
|
||||
s.name = 'websocket-extensions'
|
||||
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 = 'Apache-2.0'
|
||||
|
||||
s.extra_rdoc_files = %w[README.md]
|
||||
s.rdoc_options = %w[--main README.md --markup markdown]
|
||||
s.require_paths = %w[lib]
|
||||
s.extra_rdoc_files = %w[README.md]
|
||||
s.rdoc_options = %w[--main README.md --markup markdown]
|
||||
s.require_paths = %w[lib]
|
||||
|
||||
s.files = %w[README.md] + Dir.glob('lib/**/*.rb')
|
||||
s.files = %w[CHANGELOG.md LICENSE.md README.md] + Dir.glob('lib/**/*.rb')
|
||||
|
||||
s.add_development_dependency 'rspec'
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user