[DRE-commits] [ruby-websocket-extensions] 01/04: Imported Upstream version 0.1.2

Hleb Valoshka tsfgnu-guest at moszumanska.debian.org
Sun Mar 6 08:47:50 UTC 2016


This is an automated email from the git hooks/post-receive script.

tsfgnu-guest pushed a commit to branch master
in repository ruby-websocket-extensions.

commit 848796374a812151bb830d9de1ac8f2db22dc373
Author: Hleb Valoshka <375gnu at gmail.com>
Date:   Sun Mar 6 11:37:00 2016 +0300

    Imported Upstream version 0.1.2
---
 CHANGELOG.md                       |  11 ++
 README.md                          | 336 +++++++++++++++++++++++++++++++++++++
 lib/websocket/extensions.rb        | 181 ++++++++++++++++++++
 lib/websocket/extensions/parser.rb | 111 ++++++++++++
 metadata.yml                       |  66 ++++++++
 5 files changed, 705 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ce407e4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+### 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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..23dc39c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,336 @@
+# websocket-extensions [![Build status](https://secure.travis-ci.org/faye/websocket-extensions-ruby.svg)](http://travis-ci.org/faye/websocket-extensions-ruby)
+
+A minimal framework that supports the implementation of WebSocket extensions in
+a way that's decoupled from the main protocol. This library aims to allow a
+WebSocket extension to be written and used with any protocol library, by
+defining abstract representations of frames and messages that allow modules to
+co-operate.
+
+`websocket-extensions` provides a container for registering extension plugins,
+and provides all the functions required to negotiate which extensions to use
+during a session via the `Sec-WebSocket-Extensions` header. By implementing the
+APIs defined in this document, an extension may be used by any WebSocket library
+based on this framework.
+
+## Installation
+
+```
+$ gem install websocket-extensions
+```
+
+## Usage
+
+There are two main audiences for this library: authors implementing the
+WebSocket protocol, and authors implementing extensions. End users of a
+WebSocket library or an extension should be able to use any extension by passing
+it as an argument to their chosen protocol library, without needing to know how
+either of them work, or how the `websocket-extensions` framework operates.
+
+The library is designed with the aim that any protocol implementation and any
+extension can be used together, so long as they support the same abstract
+representation of frames and messages.
+
+### Data types
+
+The APIs provided by the framework rely on two data types; extensions will
+expect to be given data and to be able to return data in these formats:
+
+#### *Frame*
+
+*Frame* is a structure representing a single WebSocket frame of any type. Frames
+are simple objects that must have at least the following properties, which
+represent the data encoded in the frame:
+
+| property      | description                                                        |
+| ------------  | ------------------------------------------------------------------ |
+| `final`       | `true` if the `FIN` bit is set, `false` otherwise                  |
+| `rsv1`        | `true` if the `RSV1` bit is set, `false` otherwise                 |
+| `rsv2`        | `true` if the `RSV2` bit is set, `false` otherwise                 |
+| `rsv3`        | `true` if the `RSV3` bit is set, `false` otherwise                 |
+| `opcode`      | the numeric opcode (`0`, `1`, `2`, `8`, `9`, or `10`) of the frame |
+| `masked`      | `true` if the `MASK` bit is set, `false` otherwise                 |
+| `masking_key` | a 4-byte string if `masked` is `true`, otherwise `nil`             |
+| `payload`     | a string containing the (unmasked) application data                |
+
+#### *Message*
+
+A *Message* represents a complete application message, which can be formed from
+text, binary and continuation frames. It has the following properties:
+
+| property | description                                                       |
+| -------- | ----------------------------------------------------------------- |
+| `rsv1`   | `true` if the first frame of the message has the `RSV1` bit set   |
+| `rsv2`   | `true` if the first frame of the message has the `RSV2` bit set   |
+| `rsv3`   | `true` if the first frame of the message has the `RSV3` bit set   |
+| `opcode` | the numeric opcode (`1` or `2`) of the first frame of the message |
+| `data`   | the concatenation of all the frame payloads in the message        |
+
+### For driver authors
+
+A driver author is someone implementing the WebSocket protocol proper, and who
+wishes end users to be able to use WebSocket extensions with their library.
+
+At the start of a WebSocket session, on both the client and the server side,
+they should begin by creating an extension container and adding whichever
+extensions they want to use.
+
+```rb
+require 'websocket/extensions'
+require 'permessage_deflate'
+
+exts = WebSocket::Extensions.new
+exts.add(PermessageDeflate)
+```
+
+In the following examples, `exts` refers to this `Extensions` instance.
+
+#### Client sessions
+
+Clients will use the methods `generate_offer` and `activate(header)`.
+
+As part of the handshake process, the client must send a
+`Sec-WebSocket-Extensions` header to advertise that it supports the registered
+extensions. This header should be generated using:
+
+```rb
+request_headers['Sec-WebSocket-Extensions'] = exts.generate_offer
+```
+
+This returns a string, for example `"permessage-deflate;
+client_max_window_bits"`, that represents all the extensions the client is
+offering to use, and their parameters. This string may contain multiple offers
+for the same extension.
+
+When the client receives the handshake response from the server, it should pass
+the incoming `Sec-WebSocket-Extensions` header in to `exts` to activate the
+extensions the server has accepted:
+
+```rb
+exts.activate(response_headers['Sec-WebSocket-Extensions'])
+```
+
+If the server has sent any extension responses that the client does not
+recognize, or are in conflict with one another for use of RSV bits, or that use
+invalid parameters for the named extensions, then `exts.activate` will `raise`.
+In this event, the client driver should fail the connection with closing code
+`1010`.
+
+#### Server sessions
+
+Servers will use the method `generate_response(header)`.
+
+A server session needs to generate a `Sec-WebSocket-Extensions` header to send
+in its handshake response:
+
+```rb
+client_offer = request_env['HTTP_SEC_WEBSOCKET_EXTENSIONS']
+ext_response = exts.generate_response(client_offer)
+
+response_headers['Sec-WebSocket-Extensions'] = ext_response
+```
+
+Calling `exts.generate_response(header)` activates those extensions the client
+has asked to use, if they are registered, asks each extension for a set of
+response parameters, and returns a string containing the response parameters for
+all accepted extensions.
+
+#### In both directions
+
+Both clients and servers will use the methods `valid_frame_rsv(frame)`,
+`process_incoming_message(message)` and `process_outgoing_message(message)`.
+
+The WebSocket protocol requires that frames do not have any of the `RSV` bits
+set unless there is an extension in use that allows otherwise. When processing
+an incoming frame, sessions should pass a *Frame* object to:
+
+```rb
+exts.valid_frame_rsv(frame)
+```
+
+If this method returns `false`, the session should fail the WebSocket connection
+with closing code `1002`.
+
+To pass incoming messages through the extension stack, a session should
+construct a *Message* object according to the above datatype definitions, and
+call:
+
+```rb
+message = exts.process_incoming_message(message)
+```
+
+If any extensions fail to process the message, then this call will `raise` an
+error and the session should fail the WebSocket connection with closing code
+`1010`. Otherwise, `message` should be passed on to the application.
+
+To pass outgoing messages through the extension stack, a session should
+construct a *Message* as before, and call:
+
+```rb
+message = exts.process_outgoing_message(message)
+```
+
+If any extensions fail to process the message, then this call will `raise` an
+error and the session should fail the WebSocket connection with closing code
+`1010`. Otherwise, `message` should be converted into frames (with the message's
+`rsv1`, `rsv2`, `rsv3` and `opcode` set on the first frame) and written to the
+transport.
+
+At the end of the WebSocket session (either when the protocol is explicitly
+ended or the transport connection disconnects), the driver should call:
+
+```rb
+exts.close
+```
+
+### For extension authors
+
+An extension author is someone implementing an extension that transforms
+WebSocket messages passing between the client and server. They would like to
+implement their extension once and have it work with any protocol library.
+
+Extension authors will not install `websocket-extensions` or call it directly.
+Instead, they should implement the following API to allow their extension to
+plug into the `websocket-extensions` framework.
+
+An `Extension` is any object that has the following properties:
+
+| property | description                                                                  |
+| -------- | ---------------------------------------------------------------------------- |
+| `name`   | a string containing the name of the extension as used in negotiation headers |
+| `type`   | a string, must be `"permessage"`                                             |
+| `rsv1`   | either `true` if the extension uses the RSV1 bit, `false` otherwise          |
+| `rsv2`   | either `true` if the extension uses the RSV2 bit, `false` otherwise          |
+| `rsv3`   | either `true` if the extension uses the RSV3 bit, `false` otherwise          |
+
+It must also implement the following methods:
+
+```rb
+ext.create_client_session
+```
+
+This returns a *ClientSession*, whose interface is defined below.
+
+```rb
+ext.create_server_session(offers)
+```
+
+This takes an array of offer params and returns a *ServerSession*, whose
+interface is defined below. For example, if the client handshake contains the
+offer header:
+
+```
+Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; server_max_window_bits=8, \
+                          permessage-deflate; server_max_window_bits=15
+```
+
+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}
+])
+```
+
+The extension must decide which set of parameters it wants to accept, if any,
+and return a *ServerSession* if it wants to accept the parameters and `nil`
+otherwise.
+
+#### *ClientSession*
+
+A *ClientSession* is the type returned by `ext.create_client_session`. It must
+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}
+#          ]
+```
+
+This must return a set of parameters to include in the client's
+`Sec-WebSocket-Extensions` offer header. If the session wants to offer multiple
+configurations, it can return an array of sets of parameters as shown above.
+
+```rb
+client_session.activate(params) # -> true
+```
+
+This must take a single set of parameters from the server's handshake response
+and use them to configure the client session. If the client accepts the given
+parameters, then this method must return `true`. If it returns any other value,
+the framework will interpret this as the client rejecting the response, and will
+`raise`.
+
+#### *ServerSession*
+
+A *ServerSession* is the type returned by `ext.create_server_session(offers)`. It
+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}
+```
+
+This returns the set of parameters the server session wants to send in its
+`Sec-WebSocket-Extensions` response header. Only one set of parameters is
+returned to the client per extension. Server sessions that would confict on
+their use of RSV bits are not activated.
+
+#### *Session*
+
+The *Session* API must be implemented by both client and server sessions. It
+contains three methods: `process_incoming_message(message)` and
+`process_outgoing_message(message)`.
+
+```rb
+message = session.process_incoming_message(message)
+```
+
+The session must implement this method to take an incoming *Message* as defined
+above, transform it in any way it needs, then return it. If there is an error
+processing the message, this method should `raise` an error.
+
+```rb
+message = session.process_outgoing_message(message)
+```
+
+The session must implement this method to take an outgoing *Message* as defined
+above, transform it in any way it needs, then return it. If there is an error
+processing the message, this method should `raise` an error.
+
+```rb
+session.close
+```
+
+The framework will call this method when the WebSocket session ends, allowing
+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-2015 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.
diff --git a/lib/websocket/extensions.rb b/lib/websocket/extensions.rb
new file mode 100644
index 0000000..1fb2a0d
--- /dev/null
+++ b/lib/websocket/extensions.rb
@@ -0,0 +1,181 @@
+module WebSocket
+  class Extensions
+
+    autoload :Parser, File.expand_path('../extensions/parser', __FILE__)
+
+    ExtensionError = Class.new(ArgumentError)
+
+    MESSAGE_OPCODES = [1, 2]
+
+    def initialize
+      @rsv1 = @rsv2 = @rsv3 = nil
+
+      @by_name  = {}
+      @in_order = []
+      @sessions = []
+      @index    = {}
+    end
+
+    def add(ext)
+      unless ext.respond_to?(:name) and ext.name.is_a?(String)
+        raise TypeError, 'extension.name must be a string'
+      end
+
+      unless ext.respond_to?(:type) and ext.type == 'permessage'
+        raise TypeError, 'extension.type must be "permessage"'
+      end
+
+      unless ext.respond_to?(:rsv1) and [true, false].include?(ext.rsv1)
+        raise TypeError, 'extension.rsv1 must be true or false'
+      end
+
+      unless ext.respond_to?(:rsv2) and [true, false].include?(ext.rsv2)
+        raise TypeError, 'extension.rsv2 must be true or false'
+      end
+
+      unless ext.respond_to?(:rsv3) and [true, false].include?(ext.rsv3)
+        raise TypeError, 'extension.rsv3 must be true or false'
+      end
+
+      if @by_name.has_key?(ext.name)
+        raise TypeError, %Q{An extension with name "#{ext.name}" is already registered}
+      end
+
+      @by_name[ext.name] = ext
+      @in_order.push(ext)
+    end
+
+    def generate_offer
+      sessions = []
+      offer    = []
+      index    = {}
+
+      @in_order.each do |ext|
+        session = ext.create_client_session
+        next unless session
+
+        record = [ext, session]
+        sessions.push(record)
+        index[ext.name] = record
+
+        offers = session.generate_offer
+        offers = offers ? [offers].flatten : []
+
+        offers.each do |off|
+          offer.push(Parser.serialize_params(ext.name, off))
+        end
+      end
+
+      @sessions = sessions
+      @index    = index
+
+      offer.size > 0 ? offer.join(', ') : nil
+    end
+
+    def activate(header)
+      responses = Parser.parse_header(header)
+      @sessions = []
+
+      responses.each_offer do |name, params|
+        unless record = @index[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}"}
+        end
+
+        unless session.activate(params) == true
+          raise ExtensionError, %Q{Server send unacceptable extension parameters: #{Parser.serialize_params(name, params)}}
+        end
+
+        reserve(ext)
+        @sessions.push(record)
+      end
+    end
+
+    def generate_response(header)
+      offers   = Parser.parse_header(header)
+      sessions = []
+      response = []
+
+      @in_order.each do |ext|
+        offer = offers.by_name(ext.name)
+        next if offer.empty? or reserved?(ext)
+
+        next unless session = ext.create_server_session(offer)
+
+        reserve(ext)
+        sessions.push([ext, session])
+        response.push(Parser.serialize_params(ext.name, session.generate_response))
+      end
+
+      @sessions = sessions
+      response.size > 0 ? response.join(', ') : nil
+    end
+
+    def valid_frame_rsv(frame)
+      allowed = {:rsv1 => false, :rsv2 => false, :rsv3 => false}
+
+      if MESSAGE_OPCODES.include?(frame.opcode)
+        @sessions.each do |ext, session|
+          allowed[:rsv1] ||= ext.rsv1
+          allowed[:rsv2] ||= ext.rsv2
+          allowed[:rsv3] ||= ext.rsv3
+        end
+      end
+
+      (allowed[:rsv1] || !frame.rsv1) &&
+      (allowed[:rsv2] || !frame.rsv2) &&
+      (allowed[:rsv3] || !frame.rsv3)
+    end
+    alias :valid_frame_rsv? :valid_frame_rsv
+
+    def process_incoming_message(message)
+      @sessions.reverse.inject(message) do |msg, (ext, session)|
+        begin
+          session.process_incoming_message(msg)
+        rescue => error
+          raise ExtensionError, [ext.name, error.message].join(': ')
+        end
+      end
+    end
+
+    def process_outgoing_message(message)
+      @sessions.inject(message) do |msg, (ext, session)|
+        begin
+          session.process_outgoing_message(msg)
+        rescue => error
+          raise ExtensionError, [ext.name, error.message].join(': ')
+        end
+      end
+    end
+
+    def close
+      return unless @sessions
+
+      @sessions.each do |ext, session|
+        session.close rescue nil
+      end
+    end
+
+  private
+
+    def reserve(ext)
+      @rsv1 ||= ext.rsv1 && ext.name
+      @rsv2 ||= ext.rsv2 && ext.name
+      @rsv3 ||= ext.rsv3 && ext.name
+    end
+
+    def reserved?(ext)
+      return [1, @rsv1] if @rsv1 and ext.rsv1
+      return [2, @rsv2] if @rsv2 and ext.rsv2
+      return [3, @rsv3] if @rsv3 and ext.rsv3
+      false
+    end
+
+  end
+end
diff --git a/lib/websocket/extensions/parser.rb b/lib/websocket/extensions/parser.rb
new file mode 100644
index 0000000..06db917
--- /dev/null
+++ b/lib/websocket/extensions/parser.rb
@@ -0,0 +1,111 @@
+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})*$}
+      NUMBER   = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/
+
+      ParseError = Class.new(ArgumentError)
+
+      def self.parse_header(header)
+        offers = Offers.new
+        return offers if header == '' or header.nil?
+
+        unless header =~ EXT_LIST
+          raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{header}"
+        end
+
+        scanner = StringScanner.new(header)
+        value   = scanner.scan(EXT)
+
+        until value.nil?
+          params = value.scan(PARAM)
+          name   = params.shift.first
+          offer  = {}
+
+          params.each do |key, unquoted, quoted|
+            if unquoted
+              data = unquoted
+            elsif quoted
+              data = quoted.gsub(/\\/, '')
+            else
+              data = true
+            end
+            if data =~ NUMBER
+              data = data =~ /\./ ? data.to_f : data.to_i(10)
+            end
+
+            if offer.has_key?(key)
+              offer[key] = [*offer[key]] + [data]
+            else
+              offer[key] = data
+            end
+          end
+
+          offers.push(name, offer)
+
+          scanner.scan(/ *, */)
+          value = scanner.scan(EXT)
+        end
+        offers
+      end
+
+      def self.serialize_params(name, params)
+        values = []
+
+        print = lambda do |key, value|
+          case value
+          when Array   then value.each { |v| print[key, v] }
+          when true    then values.push(key)
+          when Numeric then values.push(key + '=' + value.to_s)
+          else
+            if value =~ NOTOKEN
+              values.push(key + '="' + value.gsub(/"/, '\"') + '"')
+            else
+              values.push(key + '=' + value)
+            end
+          end
+        end
+
+        params.keys.sort.each { |key| print[key, params[key]] }
+
+        ([name] + values).join('; ')
+      end
+    end
+
+    class Offers
+      def initialize
+        @by_name  = {}
+        @in_order = []
+      end
+
+      def push(name, params)
+        @by_name[name] ||= []
+        @by_name[name].push(params)
+        @in_order.push(:name => name, :params => params)
+      end
+
+      def each_offer(&block)
+        @in_order.each do |offer|
+          block.call(offer[:name], offer[:params])
+        end
+      end
+
+      def by_name(name)
+        @by_name[name] || []
+      end
+
+      def to_a
+        @in_order.dup
+      end
+    end
+
+  end
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..420cce1
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,66 @@
+--- !ruby/object:Gem::Specification
+name: websocket-extensions
+version: !ruby/object:Gem::Version
+  version: 0.1.2
+platform: ruby
+authors:
+- James Coglan
+autorequire: 
+bindir: bin
+cert_chain: []
+date: 2015-02-19 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+  name: rspec
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ">="
+      - !ruby/object:Gem::Version
+        version: '0'
+description: 
+email: jcoglan at gmail.com
+executables: []
+extensions: []
+extra_rdoc_files:
+- README.md
+files:
+- CHANGELOG.md
+- README.md
+- lib/websocket/extensions.rb
+- lib/websocket/extensions/parser.rb
+homepage: http://github.com/faye/websocket-extensions-ruby
+licenses:
+- MIT
+metadata: {}
+post_install_message: 
+rdoc_options:
+- "--main"
+- README.md
+- "--markup"
+- markdown
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
+requirements: []
+rubyforge_project: 
+rubygems_version: 2.4.5
+signing_key: 
+specification_version: 4
+summary: Generic extension manager for WebSocket connections
+test_files: []

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-websocket-extensions.git



More information about the Pkg-ruby-extras-commits mailing list