Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ engines:
enabled: true
rubocop:
enabled: true
channel: rubocop-0-52
ratings:
paths:
- "**.rb"
Expand Down
20 changes: 15 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Release Gem
uses: cadwallion/publish-rubygems-action@master
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'

- name: Build and publish gem
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
run: |
mkdir -p ~/.gem
printf -- "---\n:rubygems_api_key: %s\n" "$RUBYGEMS_API_KEY" > ~/.gem/credentials
chmod 0600 ~/.gem/credentials
gem build websocket.gemspec
gem push websocket-*.gem
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
name: Rubocop
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
ruby-version: '3.3'

- name: Install gems
run: bundle install
Expand All @@ -42,13 +42,14 @@ jobs:
- 3.2
- 3.3
- 3.4
- "4.0"
- jruby

runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Gemfile.lock
autobahn
pkg/*.gem
coverage/
40 changes: 35 additions & 5 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
require: rubocop-rspec
plugins:
- rubocop-rspec

AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.1
NewCops: disable
SuggestExtensions: false

# New version of Rubocop does not support 2.0
Gemspec/RequiredRubyVersion:
Enabled: false

Layout/IndentHeredoc:
Enabled: false

# Target: 15
Metrics/AbcSize:
Max: 24
Expand All @@ -30,8 +30,9 @@ Metrics/CyclomaticComplexity:
Max: 11
Exclude:
- spec/support/handshake_requests.rb
- lib/websocket/handshake/client.rb

Metrics/LineLength:
Layout/LineLength:
Enabled: false

# Target: 10
Expand All @@ -46,6 +47,13 @@ Metrics/PerceivedComplexity:
Exclude:
- lib/websocket/frame/handler/handler75.rb
- spec/support/handshake_requests.rb
- lib/websocket/handshake/client.rb

# Draft numbers (75, 76, 04, 11...) are part of the protocol version they
# implement, not counters - renaming them would obscure their meaning.
Naming/VariableNumber:
Exclude:
- spec/support/handshake_requests.rb

RSpec/ContextWording:
Enabled: false
Expand All @@ -62,8 +70,30 @@ RSpec/InstanceVariable:
RSpec/MultipleExpectations:
Enabled: false

RSpec/MultipleMemoizedHelpers:
Enabled: false

RSpec/NamedSubject:
Enabled: false

# These examples assert via a shared `validate_request` helper method, which
# this cop cannot see into, producing a false positive.
RSpec/NoExpectationExample:
Exclude:
- spec/support/all_client_drafts.rb
- spec/support/all_server_drafts.rb

# This codebase names spec files/directories after what they describe (e.g.
# spec/frame/data_spec.rb for WebSocket::Frame::Data), not after the full
# `WebSocket::` constant path the cop expects.
RSpec/SpecFilePathFormat:
Enabled: false

# @frame_type is intentionally shared state between decode_continuation_frame
# and decode_finish_continuation_frame, not a per-method memoization.
Naming/MemoizedInstanceVariableName:
Exclude:
- lib/websocket/frame/handler/handler03.rb

Style/Documentation:
Enabled: false
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.0.6
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Edge

- fix `bundle install`/`rspec` on Ruby 2.1-3.1: gate `rubocop`, `rubocop-rspec` (require Ruby >= 2.7) and `simplecov` (requires Ruby >= 3.2) behind Ruby-version checks in the Gemfile, and make `spec_helper.rb` tolerate simplecov not being installed
- add Ruby 4.0 to the CI test matrix and verify the full suite passes on it
- upgrade development dependencies to current, maintained versions (rspec, rubocop, rubocop-rspec, rake, webrick)
- add bundler-audit and simplecov to the development toolchain
- raise `required_ruby_version` to match the Ruby versions actually covered by CI
- replace the abandoned, unpinned `cadwallion/publish-rubygems-action` in the release workflow with a plain `gem build`/`gem push` step
- reach 100% line and branch test coverage
- add YARD documentation to previously undocumented public classes and methods

## 1.2.11

- remove unused base64 require that would cause issues in Ruby 3.4
Expand Down
26 changes: 20 additions & 6 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
# frozen_string_literal: true

source 'http://rubygems.org'
source 'https://rubygems.org'

running_ruby_version = Gem::Version.new(RUBY_VERSION)

group :development do
gem 'bundler-audit', '~> 0.9', require: false
gem 'rake'
gem 'rspec', '~> 3.7'
gem 'rspec', '~> 3.13'
gem 'webrick'

# Use same version as Code Climate for consistency with CI
# https://github.com/codeclimate/codeclimate-rubocop/blob/master/Gemfile.lock
gem 'rubocop', '0.52.1', require: false
gem 'rubocop-rspec', '1.21.0', require: false
# rubocop/rubocop-rspec 1.88.x/3.10.x require Ruby >= 2.7; the test matrix
# goes back to Ruby 2.1, so these are dev-lint-only and gated to keep
# `bundle install` working on every Ruby the test suite still supports.
if running_ruby_version >= Gem::Version.new('2.7')
gem 'rubocop', '~> 1.88', require: false
gem 'rubocop-rspec', '~> 3.10', require: false
end

# simplecov 1.0.x requires Ruby >= 3.2. JRuby is excluded even when it
# reports a compatible RUBY_VERSION: JRuby's Coverage data is documented
# (by simplecov itself, and github.com/jruby/jruby#1196) to undercount
# unless JRuby's full-trace mode is explicitly enabled, which this CI
# config does not do -- so the 100% `minimum_coverage` gate below would
# spuriously fail there even though the suite itself passes.
gem 'simplecov', '~> 1.0', require: false if RUBY_ENGINE != 'jruby' && running_ruby_version >= Gem::Version.new('3.2')
end

gemspec
17 changes: 11 additions & 6 deletions lib/websocket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ def self.should_raise
def self.should_raise=(val)
@should_raise = val
end
end

# Try loading websocket-native if available
begin
require 'websocket-native'
rescue LoadError => e
raise unless e.message =~ /websocket-native/
# Attempt to load the optional `websocket-native` gem, which provides a native
# (C/Java) implementation of frame masking for extra speed. This is a no-op if
# the gem is not installed; any other LoadError is re-raised.
# @see https://github.com/imanel/websocket-ruby-native
def self.load_native_extension
require 'websocket-native'
rescue LoadError => e
raise unless e.message =~ /websocket-native/
end
end

WebSocket.load_native_extension
35 changes: 35 additions & 0 deletions lib/websocket/error.rb
Original file line number Diff line number Diff line change
@@ -1,124 +1,159 @@
# frozen_string_literal: true

module WebSocket
# Base class for all errors raised by this library. Every error's #message
# returns a Symbol (rather than a String) describing the failure, which is
# also the value exposed via `error` on frames and handshakes when
# {WebSocket.should_raise} is false.
class Error < RuntimeError
# Errors raised while encoding or decoding a {WebSocket::Frame}.
class Frame < ::WebSocket::Error
# A control frame (close/ping/pong) declared a payload larger than the
# 125-byte limit imposed by the spec.
class ControlFramePayloadTooLong < ::WebSocket::Error::Frame
def message
:control_frame_payload_too_long
end
end

# A text or binary frame was received while a previous fragmented
# message was still awaiting its continuation frame.
class DataFrameInsteadContinuation < ::WebSocket::Error::Frame
def message
:data_frame_instead_continuation
end
end

# A control frame (close/ping/pong) was sent as a fragment, which the
# spec forbids - control frames must never be fragmented.
class FragmentedControlFrame < ::WebSocket::Error::Frame
def message
:fragmented_control_frame
end
end

# A hixie-75 draft frame did not match the expected `\x00...\xff` framing.
class Invalid < ::WebSocket::Error::Frame
def message
:invalid_frame
end
end

# A text frame's payload (or the concatenated payload of a fragmented
# message) was not valid UTF-8.
class InvalidPayloadEncoding < ::WebSocket::Error::Frame
def message
:invalid_payload_encoding
end
end

# A masked frame's payload was shorter than the 4-byte masking key.
class MaskTooShort < ::WebSocket::Error::Frame
def message
:mask_is_too_short
end
end

# A frame used one of the reserved RSV bits, which is only permitted
# when negotiated via an extension this library does not implement.
class ReservedBitUsed < ::WebSocket::Error::Frame
def message
:reserved_bit_used
end
end

# A frame declared a payload length larger than {WebSocket.max_frame_size}.
class TooLong < ::WebSocket::Error::Frame
def message
:frame_too_long
end
end

# A continuation frame was received without a preceding fragmented
# (more-bit/fin-bit) frame to continue.
class UnexpectedContinuationFrame < ::WebSocket::Error::Frame
def message
:unexpected_continuation_frame
end
end

# A frame's opcode did not map to any frame type known by the handler.
class UnknownFrameType < ::WebSocket::Error::Frame
def message
:unknown_frame_type
end
end

# A frame's opcode byte did not match any opcode known by the handler.
class UnknownOpcode < ::WebSocket::Error::Frame
def message
:unknown_opcode
end
end

# A close frame carried a status code outside the ranges permitted by the spec.
class UnknownCloseCode < ::WebSocket::Error::Frame
def message
:unknown_close_code
end
end

# A frame was constructed with a protocol version not supported by this library.
class UnknownVersion < ::WebSocket::Error::Frame
def message
:unknown_protocol_version
end
end
end

# Errors raised while constructing or parsing a {WebSocket::Handshake}.
class Handshake < ::WebSocket::Error
# A server received a request whose method was not GET.
class GetRequestRequired < ::WebSocket::Error::Handshake
def message
:get_request_required
end
end

# The handshake's authentication challenge (Sec-WebSocket-Accept, hixie-76
# challenge response, etc.) did not match the expected value.
class InvalidAuthentication < ::WebSocket::Error::Handshake
def message
:invalid_handshake_authentication
end
end

# The request/response's first line (request line or status line) could not be parsed.
class InvalidHeader < ::WebSocket::Error::Handshake
def message
:invalid_header
end
end

# None of the client's requested sub-protocols were supported by the server, or
# vice versa.
class UnsupportedProtocol < ::WebSocket::Error::Handshake
def message
:unsupported_protocol
end
end

# A client received a response whose HTTP status code was not 101 (Switching Protocols).
class InvalidStatusCode < ::WebSocket::Error::Handshake
def message
:invalid_status_code
end
end

# A client handshake was constructed without a :host, :url or :uri option.
class NoHostProvided < ::WebSocket::Error::Handshake
def message
:no_host_provided
end
end

# A handshake was constructed, or detected, with a protocol version not
# supported by this library.
class UnknownVersion < ::WebSocket::Error::Handshake
def message
:unknown_protocol_version
Expand Down
2 changes: 2 additions & 0 deletions lib/websocket/frame/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def convert_args(args)
# Extract mask from 4 first bytes according to spec
def set_mask
raise WebSocket::Error::Frame::MaskTooShort if bytesize < 4

@masking_key = self[0..3].bytes.to_a
end

Expand All @@ -38,6 +39,7 @@ def getbytes(start_index, count)
# Mask whole payload using mask key
def mask(payload, mask)
return mask_native(payload, mask) if respond_to?(:mask_native)

result = []
payload.each_with_index do |byte, i|
result[i] = byte ^ mask[i % 4]
Expand Down
Loading
Loading