protocol-websocket 0.20.2 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 359985418f2c1304957337e8d10ca42c7dec2225fa60e5db0e030e40ccdfbc30
4
- data.tar.gz: fc29187e933f95505987ff87616c61877809fe3a0a94600b782d4d1728edef87
3
+ metadata.gz: 9d235a3a271493c223dda297cb508d9be00b2abf75a425cd0dada77f5eec94e2
4
+ data.tar.gz: 348ba1d7a04ebb63a5d521a1605d7503bdb8ab727992e6657106444928231201
5
5
  SHA512:
6
- metadata.gz: ef23fec7204ea0b9d965c3972b94fa39e615b5581470703f16564e5031d9819acd720618027274fd900b77864bf662c8adb44e895ce74b505c95c4cb3a7393ed
7
- data.tar.gz: 5fb3eefc7a49e405fa7c8cd729812cf79095e8b8addf8af27a8aa35b4d3f759901de6c41a7233aceb2a76df43166dba43440e54d5bda5b765225480b21fd4057
6
+ metadata.gz: ed8567becfa23f00a414801b263d3aa28db0ce790ab4823160efba567bd4efeab86beb3b77c8729e9df523ae39f8cbe9afb53b5c313afdb45be63640e0e5f339
7
+ data.tar.gz: d61ec56857439b1fd58469c28b28132948036b7d4033cf431786dba5b05931863674e285d5fabd012106e44839fe47bbe2273c2b4fda4b3566a52fadb8204723
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,91 @@
1
+ # Extensions
2
+
3
+ This guide explains how to use `protocol-websocket` for implementing a websocket client and server using extensions.
4
+
5
+ ## Per-message Deflate
6
+
7
+ WebSockets have a mechanism for implementing extensions. At the time of writing, the only published extension is `permessage-deflate` for per-message compression. It operates on complete messages rather than individual frames.
8
+
9
+ Clients and servers can negotiate a set of extensions to use. The server can accept or reject these extensions. The client can then instantiate the extensions and apply them to the connection. More specifically, clients need to define a set of extensions they want to support:
10
+
11
+ ~~~ ruby
12
+ require 'protocol/websocket'
13
+ require 'protocol/websocket/extensions'
14
+
15
+ client_extensions = Protocol::WebSocket::Extensions::Client.new([
16
+ [Protocol::WebSocket::Extension::Compression, {}]
17
+ ])
18
+
19
+ offer_headers = []
20
+
21
+ client_extensions.offer do |header|
22
+ offer_headers << header.join(';')
23
+ end
24
+
25
+ offer_headers # => ["permessage-deflate;client_max_window_bits"]
26
+ ~~~
27
+
28
+ This is transmitted to the server via the `Sec-WebSocket-Extensions` header. The server processes this and returns a subset of accepted extensions. The client receives a list of accepted extensions and instantiates them:
29
+
30
+ ~~~ ruby
31
+ server_extensions = Protocol::WebSocket::Extensions::Server.new([
32
+ [Protocol::WebSocket::Extension::Compression, {}]
33
+ ])
34
+
35
+ accepted_headers = []
36
+
37
+ server_extensions.accept(offer_headers) do |header|
38
+ accepted_headers << header.join(';')
39
+ end
40
+
41
+ accepted_headers # => ["permessage-deflate;client_max_window_bits=15"]
42
+
43
+ client_extensions.accept(accepted_headers)
44
+ ~~~
45
+
46
+ We can check the extensions are accepted:
47
+
48
+ ~~~ ruby
49
+ server_extensions.accepted
50
+ # => [[Protocol::WebSocket::Extension::Compression, {:client_max_window_bits=>15}]]
51
+
52
+ client_extensions.accepted
53
+ # => [[Protocol::WebSocket::Extension::Compression, {:client_max_window_bits=>15}]]
54
+ ~~~
55
+
56
+ Once the extensions are negotiated, they can be applied to the connection:
57
+
58
+ ~~~ ruby
59
+ require 'protocol/websocket/connection'
60
+ require 'socket'
61
+
62
+ sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)
63
+
64
+ client = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.first))
65
+ server = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.last))
66
+
67
+ client_extensions.apply(client)
68
+ server_extensions.apply(server)
69
+
70
+ # We can see that the appropriate wrappers have been added to the connections:
71
+ client.reader.class # => Protocol::WebSocket::Extension::Compression::Inflate
72
+ client.writer.class # => Protocol::WebSocket::Extension::Compression::Deflate
73
+ server.reader.class # => Protocol::WebSocket::Extension::Compression::Inflate
74
+ server.writer.class # => Protocol::WebSocket::Extension::Compression::Deflate
75
+
76
+ client.send_text("Hello World")
77
+ # => #<Protocol::WebSocket::TextFrame:0x000000011d555460 @finished=true, @flags=4, @length=13, @mask=nil, @opcode=1, @payload="\xF2H\xCD\xC9\xC9W\b\xCF/\xCAI\x01\x00">
78
+
79
+ server.read
80
+ # => #<Protocol::WebSocket::TextMessage:0x000000011e1e5248 @buffer="Hello World">
81
+ ~~~
82
+
83
+ It's possible to disable compression on a per-message basis:
84
+
85
+ ~~~ ruby
86
+ client.send_text("Hello World", compress: false)
87
+ # => #<Protocol::WebSocket::TextFrame:0x00000001028945b0 @finished=true, @flags=0, @length=11, @mask=nil, @opcode=1, @payload="Hello World">
88
+
89
+ server.read
90
+ # => #<Protocol::WebSocket::TextMessage:0x000000011e77eb50 @buffer="Hello World">
91
+ ~~~
@@ -0,0 +1,73 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `protocol-websocket` for implementing a websocket client and server.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add protocol-websocket
11
+ ~~~
12
+
13
+ ## Core Concepts
14
+
15
+ `protocol-websocket` has several core concepts:
16
+
17
+ - A {ruby Protocol::WebSocket::Frame} is the base class which is used to represent protocol-specific structured frames.
18
+ - A {ruby Protocol::WebSocket::Framer} wraps an underlying {ruby Async::IO::Stream} for reading and writing binary data into structured frames.
19
+ - A {ruby Protocol::WebSocket::Connection} wraps a framer and implements for implementing connection specific interactions like reading and writing text.
20
+ - A {ruby Protocol::WebSocket::Message} is a higher-level abstraction for reading and writing messages.
21
+
22
+ ## Bi-directional Communication
23
+
24
+ We can create a small bi-directional WebSocket client server:
25
+
26
+ ~~~ ruby
27
+ require 'protocol/websocket'
28
+ require 'protocol/websocket/connection'
29
+ require 'socket'
30
+
31
+ sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)
32
+
33
+ client = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.first))
34
+ server = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.last))
35
+
36
+ client.send_text("Hello World")
37
+ server.read
38
+ # #<Protocol::WebSocket::TextMessage:0x000000011d2338e0 @buffer="Hello World">
39
+
40
+ client.send_binary("Hello World")
41
+ server.read
42
+ #<Protocol::WebSocket::BinaryMessage:0x000000011d371db0 @buffer="Hello World">
43
+ ~~~
44
+
45
+ ## Messages
46
+
47
+ We can also use the {ruby Protocol::WebSocket::Message} class to read and write messages:
48
+
49
+ ~~~ ruby
50
+ require 'protocol/websocket'
51
+ require 'protocol/websocket/connection'
52
+ require 'socket'
53
+
54
+ sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)
55
+
56
+ client = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.first))
57
+ server = Protocol::WebSocket::Connection.new(Protocol::WebSocket::Framer.new(sockets.last))
58
+
59
+ # Encode a value using JSON:
60
+ message = Protocol::WebSocket::TextMessage.generate({hello: "world"})
61
+
62
+ client.write(message)
63
+ server.read.to_h
64
+ # {:hello=>"world"}
65
+ ~~~
66
+
67
+ ### Text Messages
68
+
69
+ Text messages contain UTF-8 encoded text. Invalid UTF-8 sequences will result in errors. Text messages are useful for sending structured data like JSON.
70
+
71
+ ### Binary Messages
72
+
73
+ Binary messages contain arbitrary binary data. They can be used to send any kind of data. Binary messages are useful for sending files or other binary data, like images or video.
@@ -0,0 +1,16 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A low level implementation of the WebSocket protocol.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/protocol-websocket/
7
+ source_code_uri: https://github.com/socketry/protocol-websocket.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide explains how to use `protocol-websocket` for implementing
12
+ a websocket client and server.
13
+ - path: extensions.md
14
+ title: Extensions
15
+ description: This guide explains how to use `protocol-websocket` for implementing
16
+ a websocket client and server using extensions.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require "json"
7
7
 
@@ -10,6 +10,8 @@ module Protocol
10
10
  module Coder
11
11
  # A JSON coder that uses the standard JSON library.
12
12
  class JSON
13
+ # Initialize a new JSON coder.
14
+ # @parameter options [Hash] Options to pass to the JSON library when parsing or generating.
13
15
  def initialize(**options)
14
16
  @options = options
15
17
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "coder/json"
7
7
 
8
8
  module Protocol
9
9
  module WebSocket
10
+ # @namespace
10
11
  module Coder
11
12
  # The default coder for WebSocket messages.
12
13
  DEFAULT = JSON::DEFAULT
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
5
- # Copyright, 2019, by William T. Nelson.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
+ # Copyright, 2019-2026, by William T. Nelson.
6
6
  # Copyright, 2021, by Aurora Nockert.
7
7
 
8
8
  require_relative "framer"
@@ -104,6 +104,8 @@ module Protocol
104
104
  else
105
105
  send_close
106
106
  end
107
+ rescue
108
+ @state = :closed
107
109
  end
108
110
 
109
111
  # Close the connection gracefully. This will send a close frame and wait for the remote end to respond with a close frame. Any data received after the close frame is sent will be ignored. If you want to process this data, use {#close_write} instead, and read the data before calling {#close}.
@@ -281,7 +283,11 @@ module Protocol
281
283
 
282
284
  # The default implementation for reading a message buffer. This is used by the {#reader} interface.
283
285
  def unpack_frames(frames)
284
- frames.map(&:unpack).join("")
286
+ if frames.size == 1
287
+ frames[0].unpack
288
+ else
289
+ frames.map(&:unpack).join("")
290
+ end
285
291
  end
286
292
 
287
293
  # Read a message from the connection. If an error occurs while reading the message, the connection will be closed.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/error"
7
7
 
@@ -41,6 +41,9 @@ module Protocol
41
41
 
42
42
  # Raised by stream or connection handlers, results in GOAWAY frame which signals termination of the current connection. You *cannot* recover from this exception, or any exceptions subclassed from it.
43
43
  class ProtocolError < Error
44
+ # Initialize a protocol error with an optional status code.
45
+ # @parameter message [String] The error message.
46
+ # @parameter code [Integer] The WebSocket status code associated with the error.
44
47
  def initialize(message, code = PROTOCOL_ERROR)
45
48
  super(message)
46
49
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
5
 
6
6
  require "zlib"
7
7
 
@@ -10,7 +10,7 @@ module Protocol
10
10
  module Extension
11
11
  module Compression
12
12
  NAME = "permessage-deflate"
13
-
13
+
14
14
  # Zlib is not capable of handling < 9 window bits.
15
15
  MINIMUM_WINDOW_BITS = 9
16
16
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "constants"
7
7
 
@@ -9,6 +9,7 @@ module Protocol
9
9
  module WebSocket
10
10
  module Extension
11
11
  module Compression
12
+ # Compresses outgoing WebSocket frames using the DEFLATE algorithm.
12
13
  class Deflate
13
14
  # Client writing to server.
14
15
  def self.client(parent, client_max_window_bits: 15, client_no_context_takeover: false, **options)
@@ -28,6 +29,13 @@ module Protocol
28
29
  )
29
30
  end
30
31
 
32
+ # Initialize a new deflate compressor.
33
+ # @parameter parent [Object] The parent framer to wrap.
34
+ # @parameter level [Integer] The compression level. Defaults to `Zlib::DEFAULT_COMPRESSION`.
35
+ # @parameter memory_level [Integer] The memory level for compression. Defaults to `Zlib::DEF_MEM_LEVEL`.
36
+ # @parameter strategy [Integer] The compression strategy. Defaults to `Zlib::DEFAULT_STRATEGY`.
37
+ # @parameter window_bits [Integer] The window size in bits for the DEFLATE algorithm.
38
+ # @parameter context_takeover [Boolean] Whether to reuse the compression context across messages.
31
39
  def initialize(parent, level: Zlib::DEFAULT_COMPRESSION, memory_level: Zlib::DEF_MEM_LEVEL, strategy: Zlib::DEFAULT_STRATEGY, window_bits: 15, context_takeover: true, **options)
32
40
  @parent = parent
33
41
 
@@ -36,7 +44,7 @@ module Protocol
36
44
  @level = level
37
45
  @memory_level = memory_level
38
46
  @strategy = strategy
39
-
47
+
40
48
  # This is handled during negotiation:
41
49
  # if window_bits < MINIMUM_WINDOW_BITS
42
50
  # window_bits = MINIMUM_WINDOW_BITS
@@ -46,13 +54,20 @@ module Protocol
46
54
  @context_takeover = context_takeover
47
55
  end
48
56
 
57
+ # @returns [String] A string representation including window bits and context takeover settings.
49
58
  def to_s
50
59
  "#<#{self.class} window_bits=#{@window_bits} context_takeover=#{@context_takeover}>"
51
60
  end
52
61
 
62
+ # @attribute [Integer] The window size in bits used for compression.
53
63
  attr :window_bits
64
+ # @attribute [Boolean] Whether the compression context is reused across messages.
54
65
  attr :context_takeover
55
66
 
67
+ # Pack a text frame, optionally compressing the buffer.
68
+ # @parameter buffer [String] The text payload to pack.
69
+ # @parameter compress [Boolean] Whether to compress the buffer. Defaults to `true`.
70
+ # @returns [Frame] The packed (and optionally compressed) text frame.
56
71
  def pack_text_frame(buffer, compress: true, **options)
57
72
  if compress
58
73
  buffer = self.deflate(buffer)
@@ -67,6 +82,10 @@ module Protocol
67
82
  return frame
68
83
  end
69
84
 
85
+ # Pack a binary frame, optionally compressing the buffer.
86
+ # @parameter buffer [String] The binary payload to pack.
87
+ # @parameter compress [Boolean] Whether to compress the buffer. Defaults to `false`.
88
+ # @returns [Frame] The packed (and optionally compressed) binary frame.
70
89
  def pack_binary_frame(buffer, compress: false, **options)
71
90
  if compress
72
91
  buffer = self.deflate(buffer)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "constants"
7
7
 
@@ -9,6 +9,7 @@ module Protocol
9
9
  module WebSocket
10
10
  module Extension
11
11
  module Compression
12
+ # Decompresses incoming WebSocket frames using the DEFLATE algorithm.
12
13
  class Inflate
13
14
  # Client reading from server.
14
15
  def self.client(parent, server_max_window_bits: 15, server_no_context_takeover: false, **options)
@@ -28,6 +29,10 @@ module Protocol
28
29
 
29
30
  TRAILER = [0x00, 0x00, 0xff, 0xff].pack("C*")
30
31
 
32
+ # Initialize a new inflate decompressor.
33
+ # @parameter parent [Object] The parent framer to wrap.
34
+ # @parameter context_takeover [Boolean] Whether to reuse the decompression context across messages.
35
+ # @parameter window_bits [Integer] The window size in bits for the DEFLATE algorithm.
31
36
  def initialize(parent, context_takeover: true, window_bits: 15)
32
37
  @parent = parent
33
38
 
@@ -42,13 +47,19 @@ module Protocol
42
47
  @context_takeover = context_takeover
43
48
  end
44
49
 
50
+ # @returns [String] A string representation including window bits and context takeover settings.
45
51
  def to_s
46
52
  "#<#{self.class} window_bits=#{@window_bits} context_takeover=#{@context_takeover}>"
47
53
  end
48
54
 
55
+ # @attribute [Integer] The window size in bits used for decompression.
49
56
  attr :window_bits
57
+ # @attribute [Boolean] Whether the decompression context is reused across messages.
50
58
  attr :context_takeover
51
59
 
60
+ # Unpack and decompress frames into a buffer.
61
+ # @parameter frames [Array(Frame)] The frames to unpack.
62
+ # @returns [String] The decompressed payload buffer.
52
63
  def unpack_frames(frames, **options)
53
64
  buffer = @parent.unpack_frames(frames, **options)
54
65
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "compression/constants"
7
7
  require_relative "compression/inflate"
@@ -9,6 +9,7 @@ require_relative "compression/deflate"
9
9
 
10
10
  module Protocol
11
11
  module WebSocket
12
+ # @namespace
12
13
  module Extension
13
14
  # Provides support for the permessage-deflate extension.
14
15
  module Compression
@@ -46,7 +47,7 @@ module Protocol
46
47
 
47
48
  return header
48
49
  end
49
-
50
+
50
51
  # Negotiate on the server a response to client based on the incoming client offer.
51
52
  # @parameter options [Hash] a hash of options which are accepted by the server.
52
53
  # @returns [Array(String)] a list of compression parameters suitable to send back to the client.
@@ -1,14 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
+ # Copyright, 2026, by William T. Nelson.
5
6
 
6
7
  require_relative "extension/compression"
7
8
  require_relative "headers"
8
9
 
9
10
  module Protocol
10
11
  module WebSocket
12
+ # Manages WebSocket extensions negotiated during the handshake.
11
13
  module Extensions
14
+ # Parse a list of extension header values into name and argument pairs.
15
+ # @parameter headers [Array(String)] The raw extension header values.
16
+ # @yields {|name, arguments| ...} Each parsed extension.
17
+ # @parameter name [String] The name of the extension.
18
+ # @parameter arguments [Array] The key-value argument pairs.
12
19
  def self.parse(headers)
13
20
  return to_enum(:parse, headers) unless block_given?
14
21
 
@@ -23,27 +30,39 @@ module Protocol
23
30
  end
24
31
  end
25
32
 
33
+ # Manages extensions on the client side, offering and accepting server responses.
26
34
  class Client
35
+ # Create a default client with permessage-deflate compression enabled.
36
+ # @returns [Client] A new client with the default compression extension.
27
37
  def self.default
28
38
  self.new([
29
39
  [Extension::Compression, {}]
30
40
  ])
31
41
  end
32
42
 
43
+ # Initialize a new client extension manager.
44
+ # @parameter extensions [Array] The list of extensions to offer, each as `[klass, options]`.
33
45
  def initialize(extensions = [])
34
46
  @extensions = extensions
35
47
  @accepted = []
36
48
  end
37
49
 
50
+ # @attribute [Array] The list of extensions to offer.
38
51
  attr :extensions
52
+ # @attribute [Array] The extensions accepted after negotiation.
39
53
  attr :accepted
40
54
 
55
+ # Build a lookup table of extensions keyed by their name.
56
+ # @returns [Hash] A hash mapping extension names to their `[klass, options]` pairs.
41
57
  def named
42
58
  @extensions.map do |extension|
43
59
  [extension.first::NAME, extension]
44
60
  end.to_h
45
61
  end
46
62
 
63
+ # Yield extension offer headers for each registered extension.
64
+ # @yields {|header| ...} Each offer header string.
65
+ # @parameter header [Array(String)] The extension offer header tokens.
47
66
  def offer
48
67
  @extensions.each do |extension, options|
49
68
  if header = extension.offer(**options)
@@ -52,6 +71,9 @@ module Protocol
52
71
  end
53
72
  end
54
73
 
74
+ # Accept server extension responses and record the negotiated extensions.
75
+ # @parameter headers [Array(String)] The `Sec-WebSocket-Extensions` response header values.
76
+ # @returns [Array] The accepted extensions as `[klass, options]` pairs.
55
77
  def accept(headers)
56
78
  named = self.named
57
79
 
@@ -69,6 +91,8 @@ module Protocol
69
91
  return @accepted
70
92
  end
71
93
 
94
+ # Apply all accepted extensions to the given connection as a client.
95
+ # @parameter connection [Connection] The WebSocket connection to configure.
72
96
  def apply(connection)
73
97
  @accepted.each do |(klass, options)|
74
98
  klass.client(connection, **options)
@@ -76,32 +100,43 @@ module Protocol
76
100
  end
77
101
  end
78
102
 
103
+ # Manages extensions on the server side, negotiating client offers and applying the agreed extensions.
79
104
  class Server
105
+ # Create a default server with permessage-deflate compression enabled.
106
+ # @returns [Server] A new server with the default compression extension.
80
107
  def self.default
81
108
  self.new([
82
109
  [Extension::Compression, {}]
83
110
  ])
84
111
  end
85
112
 
113
+ # Initialize a new server extension manager.
114
+ # @parameter extensions [Array] The list of supported extensions, each as `[klass, options]`.
86
115
  def initialize(extensions)
87
116
  @extensions = extensions
88
117
  @accepted = []
89
118
  end
90
119
 
120
+ # @attribute [Array] The list of supported extensions.
91
121
  attr :extensions
122
+ # @attribute [Array] The extensions accepted after negotiation.
92
123
  attr :accepted
93
124
 
125
+ # Build a lookup table of extensions keyed by their name.
126
+ # @returns [Hash] A hash mapping extension names to their `[klass, options]` pairs.
94
127
  def named
95
128
  @extensions.map do |extension|
96
129
  [extension.first::NAME, extension]
97
130
  end.to_h
98
131
  end
99
132
 
133
+ # Negotiate client extension offers and yield accepted response headers.
134
+ # @parameter headers [Array(String)] The `Sec-WebSocket-Extensions` request header values.
135
+ # @yields {|header| ...} Each accepted extension header to include in the response.
136
+ # @parameter header [Array(String)] The negotiated extension header tokens.
137
+ # @returns [Array] The accepted extensions as `[klass, options]` pairs.
100
138
  def accept(headers)
101
- extensions = []
102
-
103
139
  named = self.named
104
- response = []
105
140
 
106
141
  # Each response header should map to at least one extension.
107
142
  Extensions.parse(headers) do |name, arguments|
@@ -124,6 +159,8 @@ module Protocol
124
159
  return @accepted
125
160
  end
126
161
 
162
+ # Apply all accepted extensions to the given connection as a server.
163
+ # @parameter connection [Connection] The WebSocket connection to configure.
127
164
  def apply(connection)
128
165
  @accepted.reverse_each do |(klass, options)|
129
166
  klass.server(connection, **options)
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
  # Copyright, 2019, by Soumya.
6
6
  # Copyright, 2021, by Aurora Nockert.
7
7
  # Copyright, 2025, by Taleh Zaliyev.
8
+ # Copyright, 2026, by William T. Nelson.
8
9
 
9
10
  require_relative "error"
10
11
 
11
12
  module Protocol
12
13
  module WebSocket
14
+ # Represents a single WebSocket frame as defined by RFC 6455.
13
15
  class Frame
14
16
  include Comparable
15
17
 
@@ -19,8 +21,7 @@ module Protocol
19
21
  RESERVED = RSV1 | RSV2 | RSV3
20
22
 
21
23
  OPCODE = 0
22
-
23
- # @parameter length [Integer] The length of the payload, or nil if the header has not been read yet.
24
+
24
25
  # @parameter mask [Boolean | String] An optional 4-byte string which is used to mask the payload.
25
26
  def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false)
26
27
  if mask == true
@@ -31,22 +32,31 @@ module Protocol
31
32
  @flags = flags
32
33
  @opcode = opcode
33
34
  @mask = mask
34
- @length = payload&.bytesize
35
35
  @payload = payload
36
36
  end
37
37
 
38
+ # Check whether the specified RSV flag bit is set on this frame.
39
+ # @parameter value [Integer] The flag bitmask to test (e.g. `RSV1`).
40
+ # @returns [Boolean] `true` if the flag bit is set.
38
41
  def flag?(value)
39
42
  @flags & value != 0
40
43
  end
41
44
 
45
+ # Compare this frame to another frame by their array representation.
46
+ # @parameter other [Frame] The frame to compare against.
47
+ # @returns [Integer] A comparison result (-1, 0, or 1).
42
48
  def <=> other
43
49
  to_ary <=> other.to_ary
44
50
  end
45
51
 
52
+ # Convert this frame to an array of its fields for comparison or inspection.
53
+ # @returns [Array] An array of `[finished, flags, opcode, mask, payload]`.
46
54
  def to_ary
47
- [@finished, @flags, @opcode, @mask, @length, @payload]
55
+ [@finished, @flags, @opcode, @mask, @payload]
48
56
  end
49
57
 
58
+ # Check whether this is a control frame (opcode has bit 3 set).
59
+ # @returns [Boolean] `true` if this is a control frame.
50
60
  def control?
51
61
  @opcode & 0x8 != 0
52
62
  end
@@ -56,10 +66,14 @@ module Protocol
56
66
  false
57
67
  end
58
68
 
69
+ # Check whether this is the final frame in a message.
70
+ # @returns [Boolean] `true` if the FIN bit is set.
59
71
  def finished?
60
72
  @finished == true
61
73
  end
62
74
 
75
+ # Check whether this frame is a continuation fragment (FIN bit not set).
76
+ # @returns [Boolean] `true` if the FIN bit is not set.
63
77
  def continued?
64
78
  @finished == false
65
79
  end
@@ -89,9 +103,14 @@ module Protocol
89
103
  attr_accessor :flags
90
104
  attr_accessor :opcode
91
105
  attr_accessor :mask
92
- attr_accessor :length
93
106
  attr_accessor :payload
94
107
 
108
+ # The byte length of the payload.
109
+ # @returns [Integer | nil]
110
+ def length
111
+ @payload&.bytesize
112
+ end
113
+
95
114
  if IO.const_defined?(:Buffer) && IO::Buffer.respond_to?(:for) && IO::Buffer.method_defined?(:xor!)
96
115
  private def mask_xor(data, mask)
97
116
  buffer = data.dup
@@ -107,7 +126,7 @@ module Protocol
107
126
  warn "IO::Buffer not available, falling back to slow implementation of mask_xor!"
108
127
  private def mask_xor(data, mask)
109
128
  result = String.new(encoding: Encoding::BINARY)
110
-
129
+
111
130
  for i in 0...data.bytesize do
112
131
  result << (data.getbyte(i) ^ mask.getbyte(i % 4))
113
132
  end
@@ -116,24 +135,25 @@ module Protocol
116
135
  end
117
136
  end
118
137
 
138
+ # Pack the given data into this frame's payload, applying masking if configured.
139
+ # @parameter data [String] The payload data to pack.
140
+ # @returns [Frame] Returns `self`.
119
141
  def pack(data = "")
120
- length = data.bytesize
121
-
122
- if length.bit_length > 63
123
- raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
142
+ if data.bytesize.bit_length > 63
143
+ raise ProtocolError, "Frame length #{data.bytesize} bigger than allowed maximum!"
124
144
  end
125
145
 
126
146
  if @mask
127
- @payload = mask_xor(data, mask)
128
- @length = length
147
+ @payload = mask_xor(data, @mask)
129
148
  else
130
149
  @payload = data
131
- @length = length
132
150
  end
133
151
 
134
152
  return self
135
153
  end
136
154
 
155
+ # Unpack the raw payload, removing masking if present.
156
+ # @returns [String] The unmasked payload data.
137
157
  def unpack
138
158
  if @mask and !@payload.empty?
139
159
  return mask_xor(@payload, @mask)
@@ -142,101 +162,11 @@ module Protocol
142
162
  end
143
163
  end
144
164
 
165
+ # Apply this frame to the connection by dispatching it to the appropriate handler.
166
+ # @parameter connection [Connection] The WebSocket connection to receive this frame.
145
167
  def apply(connection)
146
168
  connection.receive_frame(self)
147
169
  end
148
-
149
- def self.parse_header(buffer)
150
- byte = buffer.unpack("C").first
151
-
152
- finished = (byte & 0b1000_0000 != 0)
153
- flags = (byte & 0b0111_0000) >> 4
154
- opcode = byte & 0b0000_1111
155
-
156
- if (0x3 .. 0x7).include?(opcode)
157
- raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
158
- elsif (0xB .. 0xF).include?(opcode)
159
- raise ProtocolError, "Control opcode = #{opcode} is reserved!"
160
- end
161
-
162
- return finished, flags, opcode
163
- end
164
-
165
- def self.read(finished, flags, opcode, stream, maximum_frame_size)
166
- buffer = stream.read(1) or raise EOFError, "Could not read header!"
167
- byte = buffer.unpack("C").first
168
-
169
- mask = (byte & 0b1000_0000 != 0)
170
- length = byte & 0b0111_1111
171
-
172
- if opcode & 0x8 != 0
173
- if length > 125
174
- raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
175
- elsif !finished
176
- raise ProtocolError, "Fragmented control frame!"
177
- end
178
- end
179
-
180
- if length == 126
181
- buffer = stream.read(2) or raise EOFError, "Could not read length!"
182
- length = buffer.unpack("n").first
183
- elsif length == 127
184
- buffer = stream.read(8) or raise EOFError, "Could not read length!"
185
- length = buffer.unpack("Q>").first
186
- end
187
-
188
- if length > maximum_frame_size
189
- raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
190
- end
191
-
192
- if mask
193
- mask = stream.read(4) or raise EOFError, "Could not read mask!"
194
- end
195
-
196
- payload = stream.read(length) or raise EOFError, "Could not read payload!"
197
-
198
- if payload.bytesize != length
199
- raise EOFError, "Incorrect payload length: #{length} != #{payload.bytesize}!"
200
- end
201
-
202
- return self.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
203
- end
204
-
205
- def write(stream)
206
- buffer = String.new(encoding: Encoding::BINARY)
207
-
208
- if @payload&.bytesize != @length
209
- raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
210
- end
211
-
212
- if @mask and @mask.bytesize != 4
213
- raise ProtocolError, "Invalid mask length!"
214
- end
215
-
216
- if length <= 125
217
- short_length = length
218
- elsif length.bit_length <= 16
219
- short_length = 126
220
- else
221
- short_length = 127
222
- end
223
-
224
- buffer << [
225
- (@finished ? 0b1000_0000 : 0) | (@flags << 4) | @opcode,
226
- (@mask ? 0b1000_0000 : 0) | short_length,
227
- ].pack("CC")
228
-
229
- if short_length == 126
230
- buffer << [@length].pack("n")
231
- elsif short_length == 127
232
- buffer << [@length].pack("Q>")
233
- end
234
-
235
- buffer << @mask if @mask
236
-
237
- stream.write(buffer)
238
- stream.write(@payload)
239
- end
240
170
  end
241
171
  end
242
172
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
+ # Copyright, 2026, by William T. Nelson.
5
6
 
6
7
  require_relative "frame"
7
8
 
@@ -29,6 +30,9 @@ module Protocol
29
30
 
30
31
  # Wraps an underlying {Async::IO::Stream} for reading and writing binary data into structured frames.
31
32
  class Framer
33
+ # Initialize a new framer wrapping the given stream.
34
+ # @parameter stream [IO] The underlying stream to read from and write to.
35
+ # @parameter frames [Hash] A mapping of opcodes to frame classes.
32
36
  def initialize(stream, frames = FRAMES)
33
37
  @stream = stream
34
38
  @frames = frames
@@ -47,28 +51,105 @@ module Protocol
47
51
  # Read a frame from the underlying stream.
48
52
  # @returns [Frame] the frame read from the stream.
49
53
  def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
50
- # Read the header:
51
- finished, flags, opcode = read_header
54
+ buffer = @stream.read(2)
52
55
 
53
- # Read the frame:
54
- klass = @frames[opcode] || Frame
55
- frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size)
56
+ unless buffer and buffer.bytesize == 2
57
+ raise EOFError, "Could not read frame header!"
58
+ end
59
+
60
+ first_byte = buffer.getbyte(0)
61
+ second_byte = buffer.getbyte(1)
62
+
63
+ finished = (first_byte & 0b1000_0000 != 0)
64
+ flags = (first_byte & 0b0111_0000) >> 4
65
+ opcode = first_byte & 0b0000_1111
66
+
67
+ if opcode >= 0x3 && opcode <= 0x7
68
+ raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
69
+ elsif opcode >= 0xB
70
+ raise ProtocolError, "Control opcode = #{opcode} is reserved!"
71
+ end
72
+
73
+ mask = (second_byte & 0b1000_0000 != 0)
74
+ length = second_byte & 0b0111_1111
75
+
76
+ if opcode & 0x8 != 0
77
+ if length > 125
78
+ raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
79
+ elsif !finished
80
+ raise ProtocolError, "Fragmented control frame!"
81
+ end
82
+ end
83
+
84
+ if length == 126
85
+ if mask
86
+ buffer = @stream.read(6) or raise EOFError, "Could not read length and mask!"
87
+ length = buffer.unpack1("n")
88
+ mask = buffer.byteslice(2, 4)
89
+ else
90
+ buffer = @stream.read(2) or raise EOFError, "Could not read length!"
91
+ length = buffer.unpack1("n")
92
+ end
93
+ elsif length == 127
94
+ if mask
95
+ buffer = @stream.read(12) or raise EOFError, "Could not read length and mask!"
96
+ length = buffer.unpack1("Q>")
97
+ mask = buffer.byteslice(8, 4)
98
+ else
99
+ buffer = @stream.read(8) or raise EOFError, "Could not read length!"
100
+ length = buffer.unpack1("Q>")
101
+ end
102
+ elsif mask
103
+ mask = @stream.read(4) or raise EOFError, "Could not read mask!"
104
+ end
105
+
106
+ if length > maximum_frame_size
107
+ raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
108
+ end
56
109
 
57
- return frame
110
+ payload = @stream.read(length) or raise EOFError, "Could not read payload!"
111
+
112
+ if payload.bytesize != length
113
+ raise EOFError, "Incorrect payload length: #{length} != #{payload.bytesize}!"
114
+ end
115
+
116
+ klass = @frames[opcode] || Frame
117
+ return klass.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
58
118
  end
59
119
 
60
120
  # Write a frame to the underlying stream.
121
+ # @parameter frame [Frame] The frame to serialize and write.
122
+ # @raises [ProtocolError] If the frame has an invalid mask.
61
123
  def write_frame(frame)
62
- frame.write(@stream)
63
- end
64
-
65
- # Read the header of the frame.
66
- def read_header
67
- if buffer = @stream.read(1) and buffer.bytesize == 1
68
- return Frame.parse_header(buffer)
124
+ if frame.mask and frame.mask.bytesize != 4
125
+ raise ProtocolError, "Invalid mask length!"
69
126
  end
70
127
 
71
- raise EOFError, "Could not read frame header!"
128
+ length = frame.length
129
+
130
+ if length <= 125
131
+ short_length = length
132
+ elsif length.bit_length <= 16
133
+ short_length = 126
134
+ else
135
+ short_length = 127
136
+ end
137
+
138
+ buffer = [
139
+ (frame.finished ? 0b1000_0000 : 0) | (frame.flags << 4) | frame.opcode,
140
+ (frame.mask ? 0b1000_0000 : 0) | short_length,
141
+ ].pack("CC")
142
+
143
+ if short_length == 126
144
+ buffer << [length].pack("n")
145
+ elsif short_length == 127
146
+ buffer << [length].pack("Q>")
147
+ end
148
+
149
+ buffer << frame.mask if frame.mask
150
+
151
+ @stream.write(buffer)
152
+ @stream.write(frame.payload)
72
153
  end
73
154
  end
74
155
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require "digest/sha1"
7
7
  require "securerandom"
8
8
 
9
9
  module Protocol
10
10
  module WebSocket
11
+ # @namespace
11
12
  module Headers
12
13
  # The protocol string used for the `upgrade:` header (HTTP/1) and `:protocol` pseudo-header (HTTP/2).
13
14
  PROTOCOL = "websocket"
@@ -23,6 +24,7 @@ module Protocol
23
24
 
24
25
  SEC_WEBSOCKET_EXTENSIONS = "sec-websocket-extensions"
25
26
 
27
+ # Provides utilities for generating and verifying the WebSocket handshake nonce.
26
28
  module Nounce
27
29
  GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
28
30
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "frame"
7
7
  require_relative "coder"
@@ -57,6 +57,8 @@ module Protocol
57
57
  parse(...).to_h
58
58
  end
59
59
 
60
+ # Send this message as a text frame over the given connection.
61
+ # @parameter connection [Connection] The WebSocket connection to send through.
60
62
  def send(connection, **options)
61
63
  connection.send_text(@buffer, **options)
62
64
  end
@@ -68,6 +70,8 @@ module Protocol
68
70
 
69
71
  # Represents a binary message that can be sent or received over a WebSocket connection.
70
72
  class BinaryMessage < Message
73
+ # Send this message as a binary frame over the given connection.
74
+ # @parameter connection [Connection] The WebSocket connection to send through.
71
75
  def send(connection, **options)
72
76
  connection.send_binary(@buffer, **options)
73
77
  end
@@ -75,6 +79,8 @@ module Protocol
75
79
 
76
80
  # Represents a ping message that can be sent over a WebSocket connection.
77
81
  class PingMessage < Message
82
+ # Send this message as a ping frame over the given connection.
83
+ # @parameter connection [Connection] The WebSocket connection to send through.
78
84
  def send(connection)
79
85
  connection.send_ping(@buffer)
80
86
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Protocol
8
+ # @namespace
7
9
  module WebSocket
8
- VERSION = "0.20.2"
10
+ VERSION = "0.21.1"
9
11
  end
10
12
  end
data/license.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2019-2025, by Samuel Williams.
3
+ Copyright, 2019-2026, by Samuel Williams.
4
4
  Copyright, 2019, by Soumya.
5
- Copyright, 2019, by William T. Nelson.
5
+ Copyright, 2019-2026, by William T. Nelson.
6
6
  Copyright, 2020, by Olle Jonsson.
7
7
  Copyright, 2021, by Aurora Nockert.
8
8
  Copyright, 2025, by Taleh Zaliyev.
data/readme.md CHANGED
@@ -12,6 +12,52 @@ Please see the [project documentation](https://socketry.github.io/protocol-webso
12
12
 
13
13
  - [Extensions](https://socketry.github.io/protocol-websocket/guides/extensions/index) - This guide explains how to use `protocol-websocket` for implementing a websocket client and server using extensions.
14
14
 
15
+ ## Releases
16
+
17
+ Please see the [project releases](https://socketry.github.io/protocol-websocket/releases/index) for all releases.
18
+
19
+ ### v0.21.1
20
+
21
+ - If `Connection#close_write` fails, the connection will now be fully closed to prevent hanging connections.
22
+
23
+ ### v0.21.0
24
+
25
+ - All frame reading and writing logic has been consolidated into `Framer` to improve performance.
26
+
27
+ ### v0.20.2
28
+
29
+ - Fix error messages for `Frame` to be more descriptive.
30
+
31
+ ### v0.20.1
32
+
33
+ - Revert masking enforcement option introduced in v0.20.0 due to compatibility issues.
34
+
35
+ ### v0.20.0
36
+
37
+ - Introduce option `requires_masking` to `Framer` for enforcing masking on received frames.
38
+
39
+ ### v0.19.1
40
+
41
+ - Ensure ping reply payload is packed correctly.
42
+
43
+ ### v0.19.0
44
+
45
+ - Default to empty string for message buffer when no data is provided.
46
+
47
+ ### v0.18.0
48
+
49
+ - Add `PingMessage` alongside `TextMessage` and `BinaryMessage` for a consistent message interface.
50
+ - Remove `JSONMessage` (use application-level encoding instead).
51
+
52
+ ### v0.17.0
53
+
54
+ - Introduce `#close_write` and `#shutdown` methods on `Connection` for more precise connection lifecycle control.
55
+
56
+ ### v0.16.0
57
+
58
+ - Move `#send` logic into `Message` for better encapsulation.
59
+ - Improve error handling when a `nil` message is passed.
60
+
15
61
  ## Contributing
16
62
 
17
63
  We welcome contributions to this project.
@@ -22,6 +68,22 @@ We welcome contributions to this project.
22
68
  4. Push to the branch (`git push origin my-new-feature`).
23
69
  5. Create new Pull Request.
24
70
 
71
+ ### Running Tests
72
+
73
+ To run the test suite:
74
+
75
+ ``` shell
76
+ bundle exec sus
77
+ ```
78
+
79
+ ### Making Releases
80
+
81
+ To make a new release:
82
+
83
+ ``` shell
84
+ bundle exec bake gem:release:patch # or minor or major
85
+ ```
86
+
25
87
  ### Developer Certificate of Origin
26
88
 
27
89
  In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
data/releases.md ADDED
@@ -0,0 +1,47 @@
1
+ # Releases
2
+
3
+ ## v0.21.1
4
+
5
+ - If `Connection#close_write` fails, the connection will now be fully closed to prevent hanging connections.
6
+
7
+ ## v0.21.0
8
+
9
+ - All frame reading and writing logic has been consolidated into `Framer` to improve performance.
10
+
11
+ ## v0.20.2
12
+
13
+ - Fix error messages for `Frame` to be more descriptive.
14
+
15
+ ## v0.20.1
16
+
17
+ - Revert masking enforcement option introduced in v0.20.0 due to compatibility issues.
18
+
19
+ ## v0.20.0
20
+
21
+ - Introduce option `requires_masking` to `Framer` for enforcing masking on received frames.
22
+
23
+ ## v0.19.1
24
+
25
+ - Ensure ping reply payload is packed correctly.
26
+
27
+ ## v0.19.0
28
+
29
+ - Default to empty string for message buffer when no data is provided.
30
+
31
+ ## v0.18.0
32
+
33
+ - Add `PingMessage` alongside `TextMessage` and `BinaryMessage` for a consistent message interface.
34
+ - Remove `JSONMessage` (use application-level encoding instead).
35
+
36
+ ## v0.17.0
37
+
38
+ - Introduce `#close_write` and `#shutdown` methods on `Connection` for more precise connection lifecycle control.
39
+
40
+ ## v0.16.0
41
+
42
+ - Move `#send` logic into `Message` for better encapsulation.
43
+ - Improve error handling when a `nil` message is passed.
44
+
45
+ ## v0.15.0
46
+
47
+ - Require `Message` class by default.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-websocket
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.2
4
+ version: 0.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
+ - William T. Nelson
8
9
  - Aurora Nockert
9
10
  - Soumya
10
11
  - Olle Jonsson
11
12
  - Taleh Zaliyev
12
- - William T. Nelson
13
13
  bindir: bin
14
14
  cert_chain:
15
15
  - |
@@ -41,7 +41,7 @@ cert_chain:
41
41
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
42
42
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
43
43
  -----END CERTIFICATE-----
44
- date: 2025-04-12 00:00:00.000000000 Z
44
+ date: 1980-01-02 00:00:00.000000000 Z
45
45
  dependencies:
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: protocol-http
@@ -61,6 +61,9 @@ executables: []
61
61
  extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
+ - context/extensions.md
65
+ - context/getting-started.md
66
+ - context/index.yaml
64
67
  - lib/protocol/websocket.rb
65
68
  - lib/protocol/websocket/binary_frame.rb
66
69
  - lib/protocol/websocket/close_frame.rb
@@ -84,6 +87,7 @@ files:
84
87
  - lib/protocol/websocket/version.rb
85
88
  - license.md
86
89
  - readme.md
90
+ - releases.md
87
91
  homepage: https://github.com/socketry/protocol-websocket
88
92
  licenses:
89
93
  - MIT
@@ -97,14 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
97
101
  requirements:
98
102
  - - ">="
99
103
  - !ruby/object:Gem::Version
100
- version: '3.1'
104
+ version: '3.3'
101
105
  required_rubygems_version: !ruby/object:Gem::Requirement
102
106
  requirements:
103
107
  - - ">="
104
108
  - !ruby/object:Gem::Version
105
109
  version: '0'
106
110
  requirements: []
107
- rubygems_version: 3.6.2
111
+ rubygems_version: 4.0.10
108
112
  specification_version: 4
109
113
  summary: A low level implementation of the WebSocket protocol.
110
114
  test_files: []
metadata.gz.sig CHANGED
Binary file