websocket-driver-kontena 0.6.5
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 +7 -0
- data/CHANGELOG.md +123 -0
- data/LICENSE.md +22 -0
- data/README.md +369 -0
- data/examples/tcp_server.rb +28 -0
- data/ext/websocket-driver/WebsocketMaskService.java +55 -0
- data/ext/websocket-driver/extconf.rb +4 -0
- data/ext/websocket-driver/websocket_mask.c +41 -0
- data/lib/websocket/driver.rb +199 -0
- data/lib/websocket/driver/client.rb +140 -0
- data/lib/websocket/driver/draft75.rb +102 -0
- data/lib/websocket/driver/draft76.rb +96 -0
- data/lib/websocket/driver/event_emitter.rb +54 -0
- data/lib/websocket/driver/headers.rb +45 -0
- data/lib/websocket/driver/hybi.rb +406 -0
- data/lib/websocket/driver/hybi/frame.rb +20 -0
- data/lib/websocket/driver/hybi/message.rb +31 -0
- data/lib/websocket/driver/proxy.rb +68 -0
- data/lib/websocket/driver/server.rb +80 -0
- data/lib/websocket/driver/stream_reader.rb +55 -0
- data/lib/websocket/http.rb +15 -0
- data/lib/websocket/http/headers.rb +112 -0
- data/lib/websocket/http/request.rb +45 -0
- data/lib/websocket/http/response.rb +29 -0
- data/lib/websocket/mask.rb +14 -0
- data/lib/websocket/websocket_mask.rb +2 -0
- metadata +143 -0
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'bundler/setup'
         | 
| 3 | 
            +
            require 'eventmachine'
         | 
| 4 | 
            +
            require 'websocket/driver'
         | 
| 5 | 
            +
            require 'permessage_deflate'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Connection
         | 
| 8 | 
            +
              def initialize
         | 
| 9 | 
            +
                @driver = WebSocket::Driver.server(self)
         | 
| 10 | 
            +
                @driver.add_extension(PermessageDeflate)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                @driver.on(:connect) { |e| @driver.start if WebSocket::Driver.websocket? @driver.env }
         | 
| 13 | 
            +
                @driver.on(:message) { |e| @driver.frame(e.data) }
         | 
| 14 | 
            +
                @driver.on(:close)   { |e| close_connection_after_writing }
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              def receive_data(data)
         | 
| 18 | 
            +
                @driver.parse(data)
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              def write(data)
         | 
| 22 | 
            +
                send_data(data)
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            EM.run {
         | 
| 27 | 
            +
              EM.start_server('127.0.0.1', ARGV[0], Connection)
         | 
| 28 | 
            +
            }
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            package com.jcoglan.websocket;
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import java.lang.Long;
         | 
| 4 | 
            +
            import java.io.IOException;
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            import org.jruby.Ruby;
         | 
| 7 | 
            +
            import org.jruby.RubyClass;
         | 
| 8 | 
            +
            import org.jruby.RubyModule;
         | 
| 9 | 
            +
            import org.jruby.RubyObject;
         | 
| 10 | 
            +
            import org.jruby.RubyString;
         | 
| 11 | 
            +
            import org.jruby.anno.JRubyMethod;
         | 
| 12 | 
            +
            import org.jruby.runtime.ObjectAllocator;
         | 
| 13 | 
            +
            import org.jruby.runtime.ThreadContext;
         | 
| 14 | 
            +
            import org.jruby.runtime.builtin.IRubyObject;
         | 
| 15 | 
            +
            import org.jruby.runtime.load.BasicLibraryService;
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            public class WebsocketMaskService implements BasicLibraryService {
         | 
| 18 | 
            +
              private Ruby runtime;
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              public boolean basicLoad(Ruby runtime) throws IOException {
         | 
| 21 | 
            +
                this.runtime = runtime;
         | 
| 22 | 
            +
                RubyModule websocket = runtime.defineModule("WebSocket");
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                RubyClass webSocketMask = websocket.defineClassUnder("Mask", runtime.getObject(), new ObjectAllocator() {
         | 
| 25 | 
            +
                  public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) {
         | 
| 26 | 
            +
                    return new WebsocketMask(runtime, rubyClass);
         | 
| 27 | 
            +
                  }
         | 
| 28 | 
            +
                });
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                webSocketMask.defineAnnotatedMethods(WebsocketMask.class);
         | 
| 31 | 
            +
                return true;
         | 
| 32 | 
            +
              }
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              public class WebsocketMask extends RubyObject {
         | 
| 35 | 
            +
                public WebsocketMask(final Ruby runtime, RubyClass rubyClass) {
         | 
| 36 | 
            +
                  super(runtime, rubyClass);
         | 
| 37 | 
            +
                }
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                @JRubyMethod
         | 
| 40 | 
            +
                public IRubyObject mask(ThreadContext context, IRubyObject payload, IRubyObject mask) {
         | 
| 41 | 
            +
                  if (mask.isNil()) return payload;
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  byte[] payload_a = ((RubyString)payload).getBytes();
         | 
| 44 | 
            +
                  byte[] mask_a    = ((RubyString)mask).getBytes();
         | 
| 45 | 
            +
                  int i, n         = payload_a.length;
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  if (n == 0) return payload;
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  for (i = 0; i < n; i++) {
         | 
| 50 | 
            +
                    payload_a[i] ^= mask_a[i % 4];
         | 
| 51 | 
            +
                  }
         | 
| 52 | 
            +
                  return RubyString.newStringNoCopy(runtime, payload_a);
         | 
| 53 | 
            +
                }
         | 
| 54 | 
            +
              }
         | 
| 55 | 
            +
            }
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            #include <ruby.h>
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            VALUE WebSocket = Qnil;
         | 
| 4 | 
            +
            VALUE WebSocketMask = Qnil;
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            void Init_websocket_mask();
         | 
| 7 | 
            +
            VALUE method_websocket_mask(VALUE self, VALUE payload, VALUE mask);
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            void
         | 
| 10 | 
            +
            Init_websocket_mask()
         | 
| 11 | 
            +
            {
         | 
| 12 | 
            +
              WebSocket = rb_define_module("WebSocket");
         | 
| 13 | 
            +
              WebSocketMask = rb_define_module_under(WebSocket, "Mask");
         | 
| 14 | 
            +
              rb_define_singleton_method(WebSocketMask, "mask", method_websocket_mask, 2);
         | 
| 15 | 
            +
            }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            VALUE
         | 
| 18 | 
            +
            method_websocket_mask(VALUE self,
         | 
| 19 | 
            +
                                  VALUE payload,
         | 
| 20 | 
            +
                                  VALUE mask)
         | 
| 21 | 
            +
            {
         | 
| 22 | 
            +
              char *payload_s, *mask_s, *unmasked_s;
         | 
| 23 | 
            +
              long i, n;
         | 
| 24 | 
            +
              VALUE unmasked;
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              if (mask == Qnil || RSTRING_LEN(mask) != 4) {
         | 
| 27 | 
            +
                return payload;
         | 
| 28 | 
            +
              }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              payload_s = RSTRING_PTR(payload);
         | 
| 31 | 
            +
              mask_s    = RSTRING_PTR(mask);
         | 
| 32 | 
            +
              n         = RSTRING_LEN(payload);
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              unmasked   = rb_str_new(0, n);
         | 
| 35 | 
            +
              unmasked_s = RSTRING_PTR(unmasked);
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              for (i = 0; i < n; i++) {
         | 
| 38 | 
            +
                unmasked_s[i] = payload_s[i] ^ mask_s[i % 4];
         | 
| 39 | 
            +
              }
         | 
| 40 | 
            +
              return unmasked;
         | 
| 41 | 
            +
            }
         | 
| @@ -0,0 +1,199 @@ | |
| 1 | 
            +
            # Protocol references:
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
         | 
| 4 | 
            +
            # * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
         | 
| 5 | 
            +
            # * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require 'base64'
         | 
| 8 | 
            +
            require 'digest/md5'
         | 
| 9 | 
            +
            require 'digest/sha1'
         | 
| 10 | 
            +
            require 'securerandom'
         | 
| 11 | 
            +
            require 'set'
         | 
| 12 | 
            +
            require 'stringio'
         | 
| 13 | 
            +
            require 'uri'
         | 
| 14 | 
            +
            require 'websocket/extensions'
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            module WebSocket
         | 
| 17 | 
            +
              autoload :HTTP, File.expand_path('../http', __FILE__)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              class Driver
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                root = File.expand_path('../driver', __FILE__)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                begin
         | 
| 24 | 
            +
                  # Load C native extension
         | 
| 25 | 
            +
                  require 'websocket_mask'
         | 
| 26 | 
            +
                rescue LoadError
         | 
| 27 | 
            +
                  # Fall back to pure-Ruby implementation
         | 
| 28 | 
            +
                  require 'websocket/mask'
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
             | 
| 32 | 
            +
                if RUBY_PLATFORM =~ /java/
         | 
| 33 | 
            +
                  require 'jruby'
         | 
| 34 | 
            +
                  com.jcoglan.websocket.WebsocketMaskService.new.basicLoad(JRuby.runtime)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                unless Mask.respond_to?(:mask)
         | 
| 38 | 
            +
                  def Mask.mask(payload, mask)
         | 
| 39 | 
            +
                    @instance ||= new
         | 
| 40 | 
            +
                    @instance.mask(payload, mask)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                MAX_LENGTH = 0x3ffffff
         | 
| 45 | 
            +
                STATES     = [:connecting, :open, :closing, :closed]
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                BINARY  = 'ASCII-8BIT'
         | 
| 48 | 
            +
                UNICODE = 'UTF-8'
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                ConnectEvent = Struct.new(nil)
         | 
| 51 | 
            +
                OpenEvent    = Struct.new(nil)
         | 
| 52 | 
            +
                MessageEvent = Struct.new(:data)
         | 
| 53 | 
            +
                CloseEvent   = Struct.new(:code, :reason)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                ProtocolError      = Class.new(StandardError)
         | 
| 56 | 
            +
                URIError           = Class.new(ArgumentError)
         | 
| 57 | 
            +
                ConfigurationError = Class.new(ArgumentError)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                autoload :Client,       root + '/client'
         | 
| 60 | 
            +
                autoload :Draft75,      root + '/draft75'
         | 
| 61 | 
            +
                autoload :Draft76,      root + '/draft76'
         | 
| 62 | 
            +
                autoload :EventEmitter, root + '/event_emitter'
         | 
| 63 | 
            +
                autoload :Headers,      root + '/headers'
         | 
| 64 | 
            +
                autoload :Hybi,         root + '/hybi'
         | 
| 65 | 
            +
                autoload :Proxy,        root + '/proxy'
         | 
| 66 | 
            +
                autoload :Server,       root + '/server'
         | 
| 67 | 
            +
                autoload :StreamReader, root + '/stream_reader'
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                include EventEmitter
         | 
| 70 | 
            +
                attr_reader :protocol, :ready_state
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def initialize(socket, options = {})
         | 
| 73 | 
            +
                  super()
         | 
| 74 | 
            +
                  Driver.validate_options(options, [:max_length, :masking, :require_masking, :protocols])
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  @socket      = socket
         | 
| 77 | 
            +
                  @reader      = StreamReader.new
         | 
| 78 | 
            +
                  @options     = options
         | 
| 79 | 
            +
                  @max_length  = options[:max_length] || MAX_LENGTH
         | 
| 80 | 
            +
                  @headers     = Headers.new
         | 
| 81 | 
            +
                  @queue       = []
         | 
| 82 | 
            +
                  @ready_state = 0
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def state
         | 
| 86 | 
            +
                  return nil unless @ready_state >= 0
         | 
| 87 | 
            +
                  STATES[@ready_state]
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def add_extension(extension)
         | 
| 91 | 
            +
                  false
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                def set_header(name, value)
         | 
| 95 | 
            +
                  return false unless @ready_state <= 0
         | 
| 96 | 
            +
                  @headers[name] = value
         | 
| 97 | 
            +
                  true
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def start
         | 
| 101 | 
            +
                  return false unless @ready_state == 0
         | 
| 102 | 
            +
                  response = handshake_response
         | 
| 103 | 
            +
                  return false unless response
         | 
| 104 | 
            +
                  @socket.write(response)
         | 
| 105 | 
            +
                  open unless @stage == -1
         | 
| 106 | 
            +
                  true
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def text(message)
         | 
| 110 | 
            +
                  message = message.encode(UNICODE) unless message.encoding.name == UNICODE
         | 
| 111 | 
            +
                  frame(message, :text)
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def binary(message)
         | 
| 115 | 
            +
                  false
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                def ping(*args)
         | 
| 119 | 
            +
                  false
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def pong(*args)
         | 
| 123 | 
            +
                  false
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def close(reason = nil, code = nil)
         | 
| 127 | 
            +
                  return false unless @ready_state == 1
         | 
| 128 | 
            +
                  @ready_state = 3
         | 
| 129 | 
            +
                  emit(:close, CloseEvent.new(nil, nil))
         | 
| 130 | 
            +
                  true
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
              private
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def open
         | 
| 136 | 
            +
                  @ready_state = 1
         | 
| 137 | 
            +
                  @queue.each { |message| frame(*message) }
         | 
| 138 | 
            +
                  @queue = []
         | 
| 139 | 
            +
                  emit(:open, OpenEvent.new)
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def queue(message)
         | 
| 143 | 
            +
                  @queue << message
         | 
| 144 | 
            +
                  true
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                def self.client(socket, options = {})
         | 
| 148 | 
            +
                  Client.new(socket, options.merge(:masking => true))
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def self.server(socket, options = {})
         | 
| 152 | 
            +
                  Server.new(socket, options.merge(:require_masking => true))
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def self.rack(socket, options = {})
         | 
| 156 | 
            +
                  env = socket.env
         | 
| 157 | 
            +
                  if env['HTTP_SEC_WEBSOCKET_VERSION']
         | 
| 158 | 
            +
                    Hybi.new(socket, options.merge(:require_masking => true))
         | 
| 159 | 
            +
                  elsif env['HTTP_SEC_WEBSOCKET_KEY1']
         | 
| 160 | 
            +
                    Draft76.new(socket, options)
         | 
| 161 | 
            +
                  else
         | 
| 162 | 
            +
                    Draft75.new(socket, options)
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                def self.encode(string, encoding = nil)
         | 
| 167 | 
            +
                  case string
         | 
| 168 | 
            +
                    when Array then
         | 
| 169 | 
            +
                      string = string.pack('C*')
         | 
| 170 | 
            +
                      encoding ||= BINARY
         | 
| 171 | 
            +
                    when String then
         | 
| 172 | 
            +
                      encoding ||= UNICODE
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
                  unless string.encoding.name == encoding
         | 
| 175 | 
            +
                    string = string.dup if string.frozen?
         | 
| 176 | 
            +
                    string.force_encoding(encoding)
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
                  string.valid_encoding? ? string : nil
         | 
| 179 | 
            +
                end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                def self.validate_options(options, valid_keys)
         | 
| 182 | 
            +
                  options.keys.each do |key|
         | 
| 183 | 
            +
                    unless valid_keys.include?(key)
         | 
| 184 | 
            +
                      raise ConfigurationError, "Unrecognized option: #{key.inspect}"
         | 
| 185 | 
            +
                    end
         | 
| 186 | 
            +
                  end
         | 
| 187 | 
            +
                end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                def self.websocket?(env)
         | 
| 190 | 
            +
                  connection = env['HTTP_CONNECTION'] || ''
         | 
| 191 | 
            +
                  upgrade    = env['HTTP_UPGRADE']    || ''
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  env['REQUEST_METHOD'] == 'GET' and
         | 
| 194 | 
            +
                  connection.downcase.split(/ *, */).include?('upgrade') and
         | 
| 195 | 
            +
                  upgrade.downcase == 'websocket'
         | 
| 196 | 
            +
                end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
              end
         | 
| 199 | 
            +
            end
         | 
| @@ -0,0 +1,140 @@ | |
| 1 | 
            +
            module WebSocket
         | 
| 2 | 
            +
              class Driver
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                class Client < Hybi
         | 
| 5 | 
            +
                  VALID_SCHEMES = %w[ws wss]
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def self.generate_key
         | 
| 8 | 
            +
                    Base64.strict_encode64(SecureRandom.random_bytes(16))
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_reader :status, :headers
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(socket, options = {})
         | 
| 14 | 
            +
                    super
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    @ready_state = -1
         | 
| 17 | 
            +
                    @key         = Client.generate_key
         | 
| 18 | 
            +
                    @accept      = Hybi.generate_accept(@key)
         | 
| 19 | 
            +
                    @http        = HTTP::Response.new
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    uri = URI.parse(@socket.url)
         | 
| 22 | 
            +
                    unless VALID_SCHEMES.include?(uri.scheme)
         | 
| 23 | 
            +
                      raise URIError, "#{socket.url} is not a valid WebSocket URL"
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    host      = uri.host + (uri.port ? ":#{uri.port}" : '')
         | 
| 27 | 
            +
                    path      = (uri.path == '') ? '/' : uri.path
         | 
| 28 | 
            +
                    @pathname = path + (uri.query ? '?' + uri.query : '')
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    @headers['Host']                  = host
         | 
| 31 | 
            +
                    @headers['Upgrade']               = 'websocket'
         | 
| 32 | 
            +
                    @headers['Connection']            = 'Upgrade'
         | 
| 33 | 
            +
                    @headers['Sec-WebSocket-Key']     = @key
         | 
| 34 | 
            +
                    @headers['Sec-WebSocket-Version'] = '13'
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    if @protocols.size > 0
         | 
| 37 | 
            +
                      @headers['Sec-WebSocket-Protocol'] = @protocols * ', '
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    if uri.user
         | 
| 41 | 
            +
                      auth = Base64.strict_encode64([uri.user, uri.password] * ':')
         | 
| 42 | 
            +
                      @headers['Authorization'] = 'Basic ' + auth
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def version
         | 
| 47 | 
            +
                    'hybi-13'
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def proxy(origin, options = {})
         | 
| 51 | 
            +
                    Proxy.new(self, origin, options)
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def start
         | 
| 55 | 
            +
                    return false unless @ready_state == -1
         | 
| 56 | 
            +
                    @socket.write(handshake_request)
         | 
| 57 | 
            +
                    @ready_state = 0
         | 
| 58 | 
            +
                    true
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def parse(chunk)
         | 
| 62 | 
            +
                    return if @ready_state == 3
         | 
| 63 | 
            +
                    return super if @ready_state > 0
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    @http.parse(chunk)
         | 
| 66 | 
            +
                    return fail_handshake('Invalid HTTP response') if @http.error?
         | 
| 67 | 
            +
                    return unless @http.complete?
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    validate_handshake
         | 
| 70 | 
            +
                    return if @ready_state == 3
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    open
         | 
| 73 | 
            +
                    parse(@http.body)
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                private 
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def handshake_request
         | 
| 79 | 
            +
                    extensions = @extensions.generate_offer
         | 
| 80 | 
            +
                    @headers['Sec-WebSocket-Extensions'] = extensions if extensions
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    start   = "GET #{@pathname} HTTP/1.1"
         | 
| 83 | 
            +
                    headers = [start, @headers.to_s, '']
         | 
| 84 | 
            +
                    headers.join("\r\n")
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  def fail_handshake(message)
         | 
| 88 | 
            +
                    message = "Error during WebSocket handshake: #{message}"
         | 
| 89 | 
            +
                    @ready_state = 3
         | 
| 90 | 
            +
                    emit(:error, ProtocolError.new(message))
         | 
| 91 | 
            +
                    emit(:close, CloseEvent.new(ERRORS[:protocol_error], message))
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def validate_handshake
         | 
| 95 | 
            +
                    @status  = @http.code
         | 
| 96 | 
            +
                    @headers = Headers.new(@http.headers)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                    unless @http.code == 101
         | 
| 99 | 
            +
                      return fail_handshake("Unexpected response code: #{@http.code}")
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    upgrade    = @http['Upgrade'] || ''
         | 
| 103 | 
            +
                    connection = @http['Connection'] || ''
         | 
| 104 | 
            +
                    accept     = @http['Sec-WebSocket-Accept'] || ''
         | 
| 105 | 
            +
                    protocol   = @http['Sec-WebSocket-Protocol'] || ''
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    if upgrade == ''
         | 
| 108 | 
            +
                      return fail_handshake("'Upgrade' header is missing")
         | 
| 109 | 
            +
                    elsif upgrade.downcase != 'websocket'
         | 
| 110 | 
            +
                      return fail_handshake("'Upgrade' header value is not 'WebSocket'")
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    if connection == ''
         | 
| 114 | 
            +
                      return fail_handshake("'Connection' header is missing")
         | 
| 115 | 
            +
                    elsif connection.downcase != 'upgrade'
         | 
| 116 | 
            +
                      return fail_handshake("'Connection' header value is not 'Upgrade'")
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    unless accept == @accept
         | 
| 120 | 
            +
                      return fail_handshake('Sec-WebSocket-Accept mismatch')
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    unless protocol == ''
         | 
| 124 | 
            +
                      if @protocols.include?(protocol)
         | 
| 125 | 
            +
                        @protocol = protocol
         | 
| 126 | 
            +
                      else
         | 
| 127 | 
            +
                        return fail_handshake('Sec-WebSocket-Protocol mismatch')
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    begin
         | 
| 132 | 
            +
                      @extensions.activate(@headers['Sec-WebSocket-Extensions'])
         | 
| 133 | 
            +
                    rescue ::WebSocket::Extensions::ExtensionError => error
         | 
| 134 | 
            +
                      return fail_handshake(error.message)
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
            end
         |