websocket-protocol 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ === 0.0.0 / 2013-04-22
2
+
3
+ * First release
4
+ * Proof of concept for people to try out
5
+ * Might be unstable
6
+
@@ -0,0 +1,247 @@
1
+ = websocket-protocol {<img src="https://secure.travis-ci.org/faye/websocket-protocol-ruby.png" />}[http://travis-ci.org/faye/websocket-protocol-ruby]
2
+
3
+ This module provides a complete implementation of the WebSocket protocols that
4
+ can be hooked up to any TCP library. It aims to simplify things by decoupling
5
+ the protocol details from the I/O layer, such that users only need to implement
6
+ code to stream data in and out of it without needing to know anything about how
7
+ the protocol actually works. Think of it as a complete WebSocket system with
8
+ pluggable I/O.
9
+
10
+ Due to this design, you get a lot of things for free. In particular, if you
11
+ hook this module up to some I/O object, it will do all of this for you:
12
+
13
+ * Select the correct server-side protocol handler to talk to the client
14
+ * Generate and send both server- and client-side handshakes
15
+ * Recognize when the handshake phase completes and the WS protocol begins
16
+ * Negotiate subprotocol selection based on <tt>Sec-WebSocket-Protocol</tt>
17
+ * Buffer sent messages until the handshake process is finished
18
+ * Deal with proxies that defer delivery of the draft-76 handshake body
19
+ * Notify you when the socket is open and closed and when messages arrive
20
+ * Recombine fragmented messages
21
+ * Dispatch text, binary, ping and close frames
22
+ * Manage the socket-closing handshake process
23
+ * Automatically reply to ping frames with a matching pong
24
+ * Apply masking to messages sent by the client
25
+
26
+ This library was originally extracted from the {Faye}[http://faye.jcoglan.com]
27
+ project but now aims to provide simple WebSocket support for any Ruby server or
28
+ I/O system.
29
+
30
+
31
+ == Usage
32
+
33
+ To build either a server-side or client-side socket, the only requirement is
34
+ that you supply a +socket+ object with these methods:
35
+
36
+ * <tt>socket.url</tt> - returns the full URL of the socket as a string.
37
+ * <tt>socket.write(string)</tt> - writes the given string to a TCP stream.
38
+
39
+ Server-side sockets require one additional method:
40
+
41
+ * <tt>socket.env</tt> - returns a Rack-style env hash that will contain some of
42
+ the following fields. Their values are strings containing the value of the
43
+ named header, unless stated otherwise.
44
+ * +HTTP_CONNECTION+
45
+ * +HTTP_HOST+
46
+ * +HTTP_ORIGIN+
47
+ * +HTTP_SEC_WEBSOCKET_KEY+
48
+ * +HTTP_SEC_WEBSOCKET_KEY1+
49
+ * +HTTP_SEC_WEBSOCKET_KEY2+
50
+ * +HTTP_SEC_WEBSOCKET_PROTOCOL+
51
+ * +HTTP_SEC_WEBSOCKET_VERSION+
52
+ * +HTTP_UPGRADE+
53
+ * <tt>rack.input</tt>, an +IO+ object representing the request body
54
+ * +REQUEST_METHOD+, the request's HTTP verb
55
+
56
+
57
+ === Server-side
58
+
59
+ To handle a server-side WebSocket connection, you need to check whether the
60
+ request is a WebSocket handshake, and if so create a protocol handler for it.
61
+ You must give the handler an object with the +env+, +url+ and +write+ methods.
62
+ A simple example might be:
63
+
64
+ require 'websocket/protocol'
65
+ require 'eventmachine'
66
+
67
+ class WS
68
+ attr_reader :env, :url
69
+
70
+ def initialize(env)
71
+ @env = env
72
+
73
+ secure = Rack::Request.new(env).ssl?
74
+ scheme = secure ? 'wss:' : 'ws:'
75
+ @url = scheme + '//' + env['HTTP_HOST'] + env['REQUEST_URI']
76
+
77
+ @handler = WebSocket::Protocol.rack(self)
78
+
79
+ env['rack.hijack'].call
80
+ @io = env['rack.hijack_io']
81
+
82
+ EM.attach(@io, Reader) { |conn| conn.handler = @handler }
83
+
84
+ @handler.start
85
+ end
86
+
87
+ def write(string)
88
+ @io.write(string)
89
+ end
90
+
91
+ module Reader
92
+ attr_writer :handler
93
+
94
+ def receive_data(string)
95
+ @handler.parse(string)
96
+ end
97
+ end
98
+ end
99
+
100
+ To explain what's going on here: the +WS+ class implements the +env+, +url+ and
101
+ <tt>write(string)</tt> methods as required. When instantiated with a Rack
102
+ environment, it stores the environment and infers the complete URL from it.
103
+ Having set up the +env+ and +url+, it asks <tt>WebSocket::Protocol</tt> for a
104
+ server-side handler for the socket. Then it uses the Rack hijack API to gain
105
+ access to the TCP stream, and uses EventMachine to stream in incoming data from
106
+ the client, handing incoming data off to the handler for parsing. Finally, we
107
+ tell the handler to +start+, which will begin sending the handshake response.
108
+ This will invoke the <tt>WS#write</tt> method, which will send the response out
109
+ over the TCP socket.
110
+
111
+ Having defined this class we could use it like this when handling a request:
112
+
113
+ if WebSocket::Protocol.websocket?(env)
114
+ socket = WS.new(env)
115
+ end
116
+
117
+ The handler API is described in full below.
118
+
119
+
120
+ === Client-side
121
+
122
+ Similarly, to implement a WebSocket client you just need an object with +url+
123
+ and +write+ methods. Once you have one such object, you ask for a handler for
124
+ it:
125
+
126
+ handler = WebSocket::Protocol.client(socket)
127
+
128
+ After this you use the handler API as described below to process incoming data
129
+ and send outgoing data.
130
+
131
+
132
+ === Handler API
133
+
134
+ Handlers are created using one of the following methods:
135
+
136
+ handler = WebSocket::Protocol.rack(socket, options)
137
+ handler = WebSocket::Protocol.client(socket, options)
138
+
139
+ The +rack+ method returns a handler chosen using the socket's +env+. The
140
+ +client+ method always returns a handler for the RFC version of the protocol
141
+ with masking enabled on outgoing frames.
142
+
143
+ The +options+ argument is optional, and is a hash. It may contain the following
144
+ keys:
145
+
146
+ * <tt>:protocols</tt> - an array of strings representing acceptable
147
+ subprotocols for use over the socket. The handler will negotiate one of these
148
+ to use via the <tt>Sec-WebSocket-Protocol</tt> header if supported by the
149
+ other peer.
150
+
151
+ All handlers respond to the following API methods, but some of them are no-ops
152
+ depending on whether the client supports the behaviour.
153
+
154
+ Note that most of these methods are commands: if they produce data that should
155
+ be sent over the socket, they will give this to you by calling
156
+ <tt>socket.write(string)</tt>.
157
+
158
+ ==== <tt>handler.onopen { |event| }</tt>
159
+
160
+ Sets the handler block to execute when the socket becomes open.
161
+
162
+ ==== <tt>handler.onmessage { |event| }</tt>
163
+
164
+ Sets the handler block to execute when a message is received. +event+ will have
165
+ a +data+ attribute containing either a string in the case of a text message or
166
+ an array of integers in the case of a binary message.
167
+
168
+ ==== <tt>handler.onclose { |event| }</tt>
169
+
170
+ Sets the handler block to execute when the socket becomes closed. The +event+
171
+ object has +code+ and +reason+ attributes.
172
+
173
+ ==== <tt>handler.start</tt>
174
+
175
+ Initiates the protocol by sending the handshake - either the response for a
176
+ server-side handler or the request for a client-side one. This should be the
177
+ first method you invoke. Returns +true+ iff a handshake was sent.
178
+
179
+ ==== <tt>handler.parse(string)</tt>
180
+
181
+ Takes a string and parses it, potentially resulting in message events being
182
+ emitted (see +onmessage+ above) or in data being sent to <tt>socket.write</tt>.
183
+ You should send all data you receive via I/O to this method.
184
+
185
+ ==== <tt>handler.text(string)</tt>
186
+
187
+ Sends a text message over the socket. If the socket handshake is not yet
188
+ complete, the message will be queued until it is. Returns +true+ if the message
189
+ was sent or queued, and +false+ if the socket can no longer send messages.
190
+
191
+ ==== <tt>handler.binary(array)</tt>
192
+
193
+ Takes an array of byte-sized integers and sends them as a binary message. Will
194
+ queue and return +true+ or +false+ the same way as the +text+ method. It will
195
+ also return +false+ if the handler does not support binary messages.
196
+
197
+ ==== <tt>handler.ping(string = '', &callback)</tt>
198
+
199
+ Sends a ping frame over the socket, queueing it if necessary. +string+ and the
200
+ +callback+ block are both optional. If a callback is given, it will be invoked
201
+ when the socket receives a pong frame whose content matches +string+. Returns
202
+ +false+ if frames can no longer be sent, or if the handler does not support
203
+ ping/pong.
204
+
205
+ ==== <tt>handler.close</tt>
206
+
207
+ Initiates the closing handshake if the socket is still open. For handlers with
208
+ no closing handshake, this will result in the immediate execution of the
209
+ +onclose+ handler. For handlers with a closing handshake, this sends a closing
210
+ frame and +onclose+ will execute when a response is received or a protocol
211
+ error occurs.
212
+
213
+ ==== <tt>handler.version</tt>
214
+
215
+ Returns the WebSocket version in use as a string. Will either be
216
+ <tt>hixie-75</tt>, <tt>hixie-76</tt> or <tt>hybi-$version</tt>.
217
+
218
+ ==== <tt>handler.protocol</tt>
219
+
220
+ Returns a string containing the selected subprotocol, if any was agreed upon
221
+ using the <tt>Sec-WebSocket-Protocol</tt> mechanism. This value becomes
222
+ available after +onopen+ has fired.
223
+
224
+
225
+ == License
226
+
227
+ (The MIT License)
228
+
229
+ Copyright (c) 2009-2013 James Coglan
230
+
231
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
232
+ this software and associated documentation files (the 'Software'), to deal in
233
+ the Software without restriction, including without limitation the rights to use,
234
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
235
+ Software, and to permit persons to whom the Software is furnished to do so,
236
+ subject to the following conditions:
237
+
238
+ The above copyright notice and this permission notice shall be included in all
239
+ copies or substantial portions of the Software.
240
+
241
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
242
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
243
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
244
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
245
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
246
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
247
+
@@ -0,0 +1,61 @@
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.RubyArray;
8
+ import org.jruby.RubyClass;
9
+ import org.jruby.RubyFixnum;
10
+ import org.jruby.RubyModule;
11
+ import org.jruby.RubyObject;
12
+ import org.jruby.anno.JRubyMethod;
13
+ import org.jruby.runtime.ObjectAllocator;
14
+ import org.jruby.runtime.ThreadContext;
15
+ import org.jruby.runtime.builtin.IRubyObject;
16
+ import org.jruby.runtime.load.BasicLibraryService;
17
+
18
+ public class WebsocketMaskService implements BasicLibraryService {
19
+ private Ruby runtime;
20
+
21
+ public boolean basicLoad(Ruby runtime) throws IOException {
22
+ this.runtime = runtime;
23
+ RubyModule websocket = runtime.defineModule("WebSocket");
24
+
25
+ RubyClass webSocketMask = websocket.defineClassUnder("Mask", runtime.getObject(), new ObjectAllocator() {
26
+ public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) {
27
+ return new WebsocketMask(runtime, rubyClass);
28
+ }
29
+ });
30
+
31
+ webSocketMask.defineAnnotatedMethods(WebsocketMask.class);
32
+ return true;
33
+ }
34
+
35
+ public class WebsocketMask extends RubyObject {
36
+ public WebsocketMask(final Ruby runtime, RubyClass rubyClass) {
37
+ super(runtime, rubyClass);
38
+ }
39
+
40
+ @JRubyMethod
41
+ public IRubyObject mask(ThreadContext context, IRubyObject payload, IRubyObject mask) {
42
+ int n = ((RubyArray)payload).getLength(), i;
43
+ long p, m;
44
+ RubyArray unmasked = RubyArray.newArray(runtime, n);
45
+
46
+ long[] maskArray = {
47
+ (Long)((RubyArray)mask).get(0),
48
+ (Long)((RubyArray)mask).get(1),
49
+ (Long)((RubyArray)mask).get(2),
50
+ (Long)((RubyArray)mask).get(3)
51
+ };
52
+
53
+ for (i = 0; i < n; i++) {
54
+ p = (Long)((RubyArray)payload).get(i);
55
+ m = maskArray[i % 4];
56
+ unmasked.set(i, p ^ m);
57
+ }
58
+ return unmasked;
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ require 'mkmf'
2
+ extension_name = 'websocket_mask'
3
+ dir_config(extension_name)
4
+ create_makefile(extension_name)
5
+
@@ -0,0 +1,33 @@
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 Init_websocket_mask() {
10
+ WebSocket = rb_define_module("WebSocket");
11
+ WebSocketMask = rb_define_module_under(WebSocket, "Mask");
12
+ rb_define_singleton_method(WebSocketMask, "mask", method_websocket_mask, 2);
13
+ }
14
+
15
+ VALUE method_websocket_mask(VALUE self, VALUE payload, VALUE mask) {
16
+ int n = RARRAY_LEN(payload), i, p, m;
17
+ VALUE unmasked = rb_ary_new2(n);
18
+
19
+ int mask_array[] = {
20
+ NUM2INT(rb_ary_entry(mask, 0)),
21
+ NUM2INT(rb_ary_entry(mask, 1)),
22
+ NUM2INT(rb_ary_entry(mask, 2)),
23
+ NUM2INT(rb_ary_entry(mask, 3))
24
+ };
25
+
26
+ for (i = 0; i < n; i++) {
27
+ p = NUM2INT(rb_ary_entry(payload, i));
28
+ m = mask_array[i % 4];
29
+ rb_ary_store(unmasked, i, INT2NUM(p ^ m));
30
+ }
31
+ return unmasked;
32
+ }
33
+
@@ -0,0 +1,184 @@
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 'net/http'
11
+ require 'stringio'
12
+ require 'uri'
13
+
14
+ module WebSocket
15
+ class Protocol
16
+
17
+ root = File.expand_path('../protocol', __FILE__)
18
+ require root + '/../../websocket_mask'
19
+
20
+ def self.jruby?
21
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
22
+ end
23
+
24
+ def self.rbx?
25
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
26
+ end
27
+
28
+ if jruby?
29
+ require 'jruby'
30
+ com.jcoglan.websocket.WebsocketMaskService.new.basicLoad(JRuby.runtime)
31
+ end
32
+
33
+ unless Mask.respond_to?(:mask)
34
+ def Mask.mask(payload, mask)
35
+ @instance ||= new
36
+ @instance.mask(payload, mask)
37
+ end
38
+ end
39
+
40
+ unless String.instance_methods.include?(:force_encoding)
41
+ require root + '/utf8_match'
42
+ end
43
+
44
+ STATES = [:connecting, :open, :closing, :closed]
45
+
46
+ class OpenEvent < Struct.new(nil) ; end
47
+ class MessageEvent < Struct.new(:data) ; end
48
+ class CloseEvent < Struct.new(:code, :reason) ; end
49
+
50
+ autoload :Draft75, root + '/draft75'
51
+ autoload :Draft76, root + '/draft76'
52
+ autoload :Hybi, root + '/hybi'
53
+ autoload :Client, root + '/client'
54
+
55
+ attr_reader :protocol, :ready_state
56
+
57
+ def initialize(socket, options = {})
58
+ @socket = socket
59
+ @options = options
60
+ @queue = []
61
+ @ready_state = 0
62
+ end
63
+
64
+ def state
65
+ return nil unless @ready_state >= 0
66
+ STATES[@ready_state]
67
+ end
68
+
69
+ def start
70
+ return false unless @ready_state == 0
71
+ @socket.write(handshake_response)
72
+ open unless @stage == -1
73
+ true
74
+ end
75
+
76
+ def text(message)
77
+ frame(message)
78
+ end
79
+
80
+ def binary(message)
81
+ false
82
+ end
83
+
84
+ def ping(*args)
85
+ false
86
+ end
87
+
88
+ def close(reason = nil, code = nil)
89
+ return false unless @ready_state == 1
90
+ @ready_state = 3
91
+ dispatch(:onclose, CloseEvent.new(nil, nil))
92
+ true
93
+ end
94
+
95
+ def onopen(&block)
96
+ @onopen = block if block_given?
97
+ @onopen
98
+ end
99
+
100
+ def onmessage(&block)
101
+ @onmessage = block if block_given?
102
+ @onmessage
103
+ end
104
+
105
+ def onerror(&block)
106
+ @onerror = block if block_given?
107
+ @onerror
108
+ end
109
+
110
+ def onclose(&block)
111
+ @onclose = block if block_given?
112
+ @onclose
113
+ end
114
+
115
+ private
116
+
117
+ def open
118
+ @ready_state = 1
119
+ @queue.each { |message| frame(*message) }
120
+ @queue = []
121
+ dispatch(:onopen, OpenEvent.new)
122
+ end
123
+
124
+ def dispatch(name, event)
125
+ handler = __send__(name)
126
+ handler.call(event) if handler
127
+ end
128
+
129
+ def queue(message)
130
+ @queue << message
131
+ true
132
+ end
133
+
134
+ def self.encode(string, validate_encoding = false)
135
+ if Array === string
136
+ string = utf8_string(string)
137
+ return nil if validate_encoding and !valid_utf8?(string)
138
+ end
139
+ utf8_string(string)
140
+ end
141
+
142
+ def self.client(socket, options = {})
143
+ Client.new(socket, options.merge(:masking => true))
144
+ end
145
+
146
+ def self.rack(socket, options = {})
147
+ env = socket.env
148
+ if env['HTTP_SEC_WEBSOCKET_VERSION']
149
+ Hybi.new(socket, options.merge(:require_masking => true))
150
+ elsif env['HTTP_SEC_WEBSOCKET_KEY1']
151
+ Draft76.new(socket, options)
152
+ else
153
+ Draft75.new(socket, options)
154
+ end
155
+ end
156
+
157
+ def self.utf8_string(string)
158
+ string = string.pack('C*') if Array === string
159
+ string.respond_to?(:force_encoding) ?
160
+ string.force_encoding('UTF-8') :
161
+ string
162
+ end
163
+
164
+ def self.valid_utf8?(byte_array)
165
+ string = utf8_string(byte_array)
166
+ if defined?(UTF8_MATCH)
167
+ UTF8_MATCH =~ string ? true : false
168
+ else
169
+ string.valid_encoding?
170
+ end
171
+ end
172
+
173
+ def self.websocket?(env)
174
+ connection = env['HTTP_CONNECTION'] || ''
175
+ upgrade = env['HTTP_UPGRADE'] || ''
176
+
177
+ env['REQUEST_METHOD'] == 'GET' and
178
+ connection.downcase.split(/\s*,\s*/).include?('upgrade') and
179
+ upgrade.downcase == 'websocket'
180
+ end
181
+
182
+ end
183
+ end
184
+
@@ -0,0 +1,94 @@
1
+ module WebSocket
2
+ class Protocol
3
+
4
+ class Client < Hybi
5
+ def self.generate_key
6
+ Base64.encode64((1..16).map { rand(255).chr } * '').strip
7
+ end
8
+
9
+ def initialize(socket, options = {})
10
+ super
11
+
12
+ @ready_state = -1
13
+ @key = Client.generate_key
14
+ @accept = Base64.encode64(Digest::SHA1.digest(@key + GUID)).strip
15
+ end
16
+
17
+ def start
18
+ return false unless @ready_state == -1
19
+ @socket.write(handshake_request)
20
+ @ready_state = 0
21
+ true
22
+ end
23
+
24
+ def parse(buffer)
25
+ return super if @ready_state > 0
26
+ message = []
27
+ buffer.each_byte do |data|
28
+ case @ready_state
29
+ when 0 then
30
+ @buffer << data
31
+ if @buffer[-4..-1] == [0x0D, 0x0A, 0x0D, 0x0A]
32
+ if valid?
33
+ open
34
+ else
35
+ @ready_state = 3
36
+ dispatch(:onclose, CloseEvent.new(ERRORS[:protocol_error], ''))
37
+ end
38
+ end
39
+ when 1 then
40
+ message << data
41
+ end
42
+ end
43
+ parse(message) if @ready_state == 1
44
+ end
45
+
46
+ private
47
+
48
+ def handshake_request
49
+ uri = URI.parse(@socket.url)
50
+ host = uri.host + (uri.port ? ":#{uri.port}" : '')
51
+ path = (uri.path == '') ? '/' : uri.path
52
+ query = uri.query ? "?#{uri.query}" : ''
53
+
54
+ headers = [ "GET #{path}#{query} HTTP/1.1",
55
+ "Host: #{host}",
56
+ "Upgrade: websocket",
57
+ "Connection: Upgrade",
58
+ "Sec-WebSocket-Key: #{@key}",
59
+ "Sec-WebSocket-Version: 13"
60
+ ]
61
+
62
+ if @protocols
63
+ headers << "Sec-WebSocket-Protocol: #{@protocols * ', '}"
64
+ end
65
+
66
+ (headers + ['', '']).join("\r\n")
67
+ end
68
+
69
+ def valid?
70
+ data = Protocol.encode(@buffer)
71
+ @buffer = []
72
+
73
+ response = Net::HTTPResponse.read_new(Net::BufferedIO.new(StringIO.new(data)))
74
+ return false unless response.code.to_i == 101
75
+
76
+ connection = response['Connection'] || ''
77
+ upgrade = response['Upgrade'] || ''
78
+ accept = response['Sec-WebSocket-Accept']
79
+ protocol = response['Sec-WebSocket-Protocol']
80
+
81
+ @protocol = @protocols && @protocols.include?(protocol) ?
82
+ protocol :
83
+ nil
84
+
85
+ connection.downcase.split(/\s*,\s*/).include?('upgrade') and
86
+ upgrade.downcase == 'websocket' and
87
+ ((!@protocols and !protocol) or @protocol) and
88
+ accept == @accept
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+
@@ -0,0 +1,91 @@
1
+ module WebSocket
2
+ class Protocol
3
+
4
+ class Draft75 < Protocol
5
+ def initialize(socket, options = {})
6
+ super
7
+ @stage = 0
8
+ end
9
+
10
+ def version
11
+ 'hixie-75'
12
+ end
13
+
14
+ def parse(buffer)
15
+ buffer = buffer.bytes if buffer.respond_to?(:bytes)
16
+
17
+ buffer.each do |data|
18
+ case @stage
19
+ when -1 then
20
+ @head << data
21
+ send_handshake_body
22
+
23
+ when 0 then
24
+ parse_leading_byte(data)
25
+
26
+ when 1 then
27
+ value = (data & 0x7F)
28
+ @length = value + 128 * @length
29
+
30
+ if @closing and @length.zero?
31
+ @ready_state = 3
32
+ dispatch(:onclose, CloseEvent.new(nil, nil))
33
+ elsif (0x80 & data) != 0x80
34
+ if @length.zero?
35
+ dispatch(:onmessage, MessageEvent.new(''))
36
+ @stage = 0
37
+ else
38
+ @buffer = []
39
+ @stage = 2
40
+ end
41
+ end
42
+
43
+ when 2 then
44
+ if data == 0xFF
45
+ dispatch(:onmessage, MessageEvent.new(Protocol.encode(@buffer)))
46
+ @stage = 0
47
+ else
48
+ @buffer << data
49
+ if @length and @buffer.size == @length
50
+ @stage = 0
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def frame(data, type = nil, error_type = nil)
58
+ return queue([data, type, error_type]) if @ready_state == 0
59
+ data = Protocol.encode(data)
60
+ frame = ["\x00", data, "\xFF"].map(&Protocol.method(:encode)) * ''
61
+ @socket.write(frame)
62
+ true
63
+ end
64
+
65
+ private
66
+
67
+ def handshake_response
68
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
69
+ upgrade << "Upgrade: WebSocket\r\n"
70
+ upgrade << "Connection: Upgrade\r\n"
71
+ upgrade << "WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
72
+ upgrade << "WebSocket-Location: #{@socket.url}\r\n"
73
+ upgrade << "\r\n"
74
+ upgrade
75
+ end
76
+
77
+ def parse_leading_byte(data)
78
+ if (0x80 & data) == 0x80
79
+ @length = 0
80
+ @stage = 1
81
+ else
82
+ @length = nil
83
+ @buffer = []
84
+ @stage = 2
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+ end
91
+
@@ -0,0 +1,96 @@
1
+ module WebSocket
2
+ class Protocol
3
+
4
+ class Draft76 < Draft75
5
+ HEAD_SIZE = 8
6
+
7
+ def initialize(socket, options = {})
8
+ super
9
+ input = @socket.env['rack.input']
10
+ @stage = -1
11
+ @head = input ? input.read.bytes.to_a : []
12
+ end
13
+
14
+ def version
15
+ 'hixie-76'
16
+ end
17
+
18
+ def start
19
+ return false unless super
20
+ send_handshake_body
21
+ true
22
+ end
23
+
24
+ def close(reason = nil, code = nil)
25
+ return false if @ready_state == 3
26
+ @socket.write("\xFF\x00")
27
+ @ready_state = 3
28
+ dispatch(:onclose, CloseEvent.new(nil, nil))
29
+ end
30
+
31
+ private
32
+
33
+ def handshake_response
34
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
35
+ upgrade << "Upgrade: WebSocket\r\n"
36
+ upgrade << "Connection: Upgrade\r\n"
37
+ upgrade << "Sec-WebSocket-Origin: #{@socket.env['HTTP_ORIGIN']}\r\n"
38
+ upgrade << "Sec-WebSocket-Location: #{@socket.url}\r\n"
39
+ upgrade << "\r\n"
40
+ upgrade
41
+ end
42
+
43
+ def handshake_signature
44
+ return nil unless @head.size >= HEAD_SIZE
45
+
46
+ head = @head[0...HEAD_SIZE].pack('C*')
47
+ head.force_encoding('ASCII-8BIT') if head.respond_to?(:force_encoding)
48
+
49
+ env = @socket.env
50
+
51
+ key1 = env['HTTP_SEC_WEBSOCKET_KEY1']
52
+ value1 = number_from_key(key1) / spaces_in_key(key1)
53
+
54
+ key2 = env['HTTP_SEC_WEBSOCKET_KEY2']
55
+ value2 = number_from_key(key2) / spaces_in_key(key2)
56
+
57
+ Digest::MD5.digest(big_endian(value1) +
58
+ big_endian(value2) +
59
+ head)
60
+ end
61
+
62
+ def send_handshake_body
63
+ return unless signature = handshake_signature
64
+ @socket.write(signature)
65
+ @stage = 0
66
+ open
67
+ parse(@head[HEAD_SIZE..-1]) if @head.size > HEAD_SIZE
68
+ end
69
+
70
+ def parse_leading_byte(data)
71
+ return super unless data == 0xFF
72
+ @closing = true
73
+ @length = 0
74
+ @stage = 1
75
+ end
76
+
77
+ def number_from_key(key)
78
+ key.scan(/[0-9]/).join('').to_i(10)
79
+ end
80
+
81
+ def spaces_in_key(key)
82
+ key.scan(/ /).size
83
+ end
84
+
85
+ def big_endian(number)
86
+ string = ''
87
+ [24,16,8,0].each do |offset|
88
+ string << (number >> offset & 0xFF).chr
89
+ end
90
+ string
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+
@@ -0,0 +1,348 @@
1
+ module WebSocket
2
+ class Protocol
3
+
4
+ class Hybi < Protocol
5
+ root = File.expand_path('../hybi', __FILE__)
6
+ autoload :StreamReader, root + '/stream_reader'
7
+
8
+ GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
9
+
10
+ BYTE = 0b11111111
11
+ FIN = MASK = 0b10000000
12
+ RSV1 = 0b01000000
13
+ RSV2 = 0b00100000
14
+ RSV3 = 0b00010000
15
+ OPCODE = 0b00001111
16
+ LENGTH = 0b01111111
17
+
18
+ OPCODES = {
19
+ :continuation => 0,
20
+ :text => 1,
21
+ :binary => 2,
22
+ :close => 8,
23
+ :ping => 9,
24
+ :pong => 10
25
+ }
26
+
27
+ FRAGMENTED_OPCODES = OPCODES.values_at(:continuation, :text, :binary)
28
+ OPENING_OPCODES = OPCODES.values_at(:text, :binary)
29
+
30
+ ERRORS = {
31
+ :normal_closure => 1000,
32
+ :going_away => 1001,
33
+ :protocol_error => 1002,
34
+ :unacceptable => 1003,
35
+ :encoding_error => 1007,
36
+ :policy_violation => 1008,
37
+ :too_large => 1009,
38
+ :extension_error => 1010,
39
+ :unexpected_condition => 1011
40
+ }
41
+
42
+ ERROR_CODES = ERRORS.values
43
+
44
+ def initialize(socket, options = {})
45
+ super
46
+ reset
47
+
48
+ @reader = StreamReader.new
49
+ @stage = 0
50
+ @masking = options[:masking]
51
+ @protocols = options[:protocols]
52
+ @protocols = @protocols.strip.split(/\s*,\s*/) if String === @protocols
53
+
54
+ @require_masking = options[:require_masking]
55
+ @ping_callbacks = {}
56
+ end
57
+
58
+ def version
59
+ "hybi-#{@socket.env['HTTP_SEC_WEBSOCKET_VERSION']}"
60
+ end
61
+
62
+ def parse(data)
63
+ data = data.bytes.to_a if data.respond_to?(:bytes)
64
+ @reader.put(data)
65
+ buffer = true
66
+ while buffer
67
+ case @stage
68
+ when 0 then
69
+ buffer = @reader.read(1)
70
+ parse_opcode(buffer[0]) if buffer
71
+
72
+ when 1 then
73
+ buffer = @reader.read(1)
74
+ parse_length(buffer[0]) if buffer
75
+
76
+ when 2 then
77
+ buffer = @reader.read(@length_size)
78
+ parse_extended_length(buffer) if buffer
79
+
80
+ when 3 then
81
+ buffer = @reader.read(4)
82
+ if buffer
83
+ @mask = buffer
84
+ @stage = 4
85
+ end
86
+
87
+ when 4 then
88
+ buffer = @reader.read(@length)
89
+ if buffer
90
+ @payload = buffer
91
+ emit_frame
92
+ @stage = 0
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def frame(data, type = nil, code = nil)
99
+ return queue([data, type, code]) if @ready_state == 0
100
+ return false unless @ready_state == 1
101
+
102
+ data = data.to_s unless Array === data
103
+ data = Protocol.encode(data) if String === data
104
+
105
+ is_text = (String === data)
106
+ opcode = OPCODES[type || (is_text ? :text : :binary)]
107
+ buffer = data.respond_to?(:bytes) ? data.bytes.to_a : data
108
+ insert = code ? 2 : 0
109
+ length = buffer.size + insert
110
+ header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10)
111
+ offset = header + (@masking ? 4 : 0)
112
+ masked = @masking ? MASK : 0
113
+ frame = Array.new(offset)
114
+
115
+ frame[0] = FIN | opcode
116
+
117
+ if length <= 125
118
+ frame[1] = masked | length
119
+ elsif length <= 65535
120
+ frame[1] = masked | 126
121
+ frame[2] = (length >> 8) & BYTE
122
+ frame[3] = length & BYTE
123
+ else
124
+ frame[1] = masked | 127
125
+ frame[2] = (length >> 56) & BYTE
126
+ frame[3] = (length >> 48) & BYTE
127
+ frame[4] = (length >> 40) & BYTE
128
+ frame[5] = (length >> 32) & BYTE
129
+ frame[6] = (length >> 24) & BYTE
130
+ frame[7] = (length >> 16) & BYTE
131
+ frame[8] = (length >> 8) & BYTE
132
+ frame[9] = length & BYTE
133
+ end
134
+
135
+ if code
136
+ buffer = [(code >> 8) & BYTE, code & BYTE] + buffer
137
+ end
138
+
139
+ if @masking
140
+ mask = [rand(256), rand(256), rand(256), rand(256)]
141
+ frame[header...offset] = mask
142
+ buffer = Mask.mask(buffer, mask)
143
+ end
144
+
145
+ frame.concat(buffer)
146
+
147
+ @socket.write(Protocol.encode(frame))
148
+ true
149
+ end
150
+
151
+ def text(message)
152
+ frame(message, :text)
153
+ end
154
+
155
+ def binary(message)
156
+ frame(message, :binary)
157
+ end
158
+
159
+ def ping(message = '', &callback)
160
+ @ping_callbacks[message] = callback if callback
161
+ frame(message, :ping)
162
+ end
163
+
164
+ def close(reason = nil, code = nil)
165
+ reason ||= ''
166
+ code ||= ERRORS[:normal_closure]
167
+
168
+ case @ready_state
169
+ when 0 then
170
+ @ready_state = 3
171
+ dispatch(:onclose, CloseEvent.new(code, reason))
172
+ true
173
+ when 1 then
174
+ frame(reason, :close, code)
175
+ @ready_state = 2
176
+ true
177
+ else
178
+ false
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def handshake_response
185
+ sec_key = @socket.env['HTTP_SEC_WEBSOCKET_KEY']
186
+ return '' unless String === sec_key
187
+
188
+ accept = Base64.encode64(Digest::SHA1.digest(sec_key + GUID)).strip
189
+ protos = @socket.env['HTTP_SEC_WEBSOCKET_PROTOCOL']
190
+ supported = @protocols
191
+ proto = nil
192
+
193
+ headers = [
194
+ "HTTP/1.1 101 Switching Protocols",
195
+ "Upgrade: websocket",
196
+ "Connection: Upgrade",
197
+ "Sec-WebSocket-Accept: #{accept}"
198
+ ]
199
+
200
+ if protos and supported
201
+ protos = protos.split(/\s*,\s*/) if String === protos
202
+ proto = protos.find { |p| supported.include?(p) }
203
+ if proto
204
+ @protocol = proto
205
+ headers << "Sec-WebSocket-Protocol: #{proto}"
206
+ end
207
+ end
208
+
209
+ (headers + ['','']).join("\r\n")
210
+ end
211
+
212
+ def shutdown(code, reason)
213
+ code ||= ERRORS[:normal_closure]
214
+ reason ||= ''
215
+
216
+ frame(reason, :close, code)
217
+ @ready_state = 3
218
+ dispatch(:onclose, CloseEvent.new(code, reason))
219
+ end
220
+
221
+ def parse_opcode(data)
222
+ if [RSV1, RSV2, RSV3].any? { |rsv| (data & rsv) == rsv }
223
+ return shutdown(ERRORS[:protocol_error], nil)
224
+ end
225
+
226
+ @final = (data & FIN) == FIN
227
+ @opcode = (data & OPCODE)
228
+ @mask = []
229
+ @payload = []
230
+
231
+ unless OPCODES.values.include?(@opcode)
232
+ return shutdown(ERRORS[:protocol_error], nil)
233
+ end
234
+
235
+ unless FRAGMENTED_OPCODES.include?(@opcode) or @final
236
+ return shutdown(ERRORS[:protocol_error], nil)
237
+ end
238
+
239
+ if @mode and OPENING_OPCODES.include?(@opcode)
240
+ return shutdown(ERRORS[:protocol_error], nil)
241
+ end
242
+
243
+ @stage = 1
244
+ end
245
+
246
+ def parse_length(data)
247
+ @masked = (data & MASK) == MASK
248
+ return shutdown(ERRORS[:unacceptable], nil) if @require_masking and not @masked
249
+
250
+ @length = (data & LENGTH)
251
+
252
+ if @length <= 125
253
+ @stage = @masked ? 3 : 4
254
+ else
255
+ @length_size = (@length == 126) ? 2 : 8
256
+ @stage = 2
257
+ end
258
+ end
259
+
260
+ def parse_extended_length(buffer)
261
+ @length = integer(buffer)
262
+ @stage = @masked ? 3 : 4
263
+ end
264
+
265
+ def emit_frame
266
+ payload = @masked ? Mask.mask(@payload, @mask) : @payload
267
+
268
+ case @opcode
269
+ when OPCODES[:continuation] then
270
+ return shutdown(ERRORS[:protocol_error], nil) unless @mode
271
+ @buffer.concat(payload)
272
+ if @final
273
+ message = @buffer
274
+ message = Protocol.encode(message, true) if @mode == :text
275
+ reset
276
+ if message
277
+ dispatch(:onmessage, MessageEvent.new(message))
278
+ else
279
+ shutdown(ERRORS[:encoding_error], nil)
280
+ end
281
+ end
282
+
283
+ when OPCODES[:text] then
284
+ if @final
285
+ message = Protocol.encode(payload, true)
286
+ if message
287
+ dispatch(:onmessage, MessageEvent.new(message))
288
+ else
289
+ shutdown(ERRORS[:encoding_error], nil)
290
+ end
291
+ else
292
+ @mode = :text
293
+ @buffer.concat(payload)
294
+ end
295
+
296
+ when OPCODES[:binary] then
297
+ if @final
298
+ dispatch(:onmessage, MessageEvent.new(payload))
299
+ else
300
+ @mode = :binary
301
+ @buffer.concat(payload)
302
+ end
303
+
304
+ when OPCODES[:close] then
305
+ code = (payload.size >= 2) ? 256 * payload[0] + payload[1] : nil
306
+
307
+ unless (payload.size == 0) or
308
+ (code && code >= 3000 && code < 5000) or
309
+ ERROR_CODES.include?(code)
310
+ code = ERRORS[:protocol_error]
311
+ end
312
+
313
+ if payload.size > 125 or not Protocol.valid_utf8?(payload[2..-1] || [])
314
+ code = ERRORS[:protocol_error]
315
+ end
316
+
317
+ reason = (payload.size > 2) ? Protocol.encode(payload[2..-1], true) : ''
318
+ shutdown(code, reason)
319
+
320
+ when OPCODES[:ping] then
321
+ return shutdown(ERRORS[:protocol_error], nil) if payload.size > 125
322
+ frame(payload, :pong)
323
+
324
+ when OPCODES[:pong] then
325
+ message = Protocol.encode(payload, true)
326
+ callback = @ping_callbacks[message]
327
+ @ping_callbacks.delete(message)
328
+ callback.call if callback
329
+ end
330
+ end
331
+
332
+ def reset
333
+ @buffer = []
334
+ @mode = nil
335
+ end
336
+
337
+ def integer(bytes)
338
+ number = 0
339
+ bytes.each_with_index do |data, i|
340
+ number += data << (8 * (bytes.size - 1 - i))
341
+ end
342
+ number
343
+ end
344
+ end
345
+
346
+ end
347
+ end
348
+
@@ -0,0 +1,30 @@
1
+ module WebSocket
2
+ class Protocol
3
+
4
+ class Hybi
5
+ class StreamReader
6
+ def initialize
7
+ @queue = []
8
+ end
9
+
10
+ def read(length)
11
+ read_bytes(length)
12
+ end
13
+
14
+ def put(bytes)
15
+ return unless bytes and bytes.size > 0
16
+ @queue.concat(bytes)
17
+ end
18
+
19
+ private
20
+
21
+ def read_bytes(length)
22
+ return nil if length > @queue.size
23
+ @queue.shift(length)
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,7 @@
1
+ module WebSocket
2
+ class Protocol
3
+ # http://www.w3.org/International/questions/qa-forms-utf-8.en.php
4
+ UTF8_MATCH = /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/
5
+ end
6
+ end
7
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: websocket-protocol
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Coglan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake-compiler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description:
47
+ email: jcoglan@gmail.com
48
+ executables: []
49
+ extensions:
50
+ - ext/websocket_mask/extconf.rb
51
+ extra_rdoc_files:
52
+ - README.rdoc
53
+ files:
54
+ - README.rdoc
55
+ - CHANGELOG.txt
56
+ - ext/websocket_mask/websocket_mask.c
57
+ - ext/websocket_mask/WebsocketMaskService.java
58
+ - ext/websocket_mask/extconf.rb
59
+ - lib/websocket/protocol/client.rb
60
+ - lib/websocket/protocol/draft75.rb
61
+ - lib/websocket/protocol/draft76.rb
62
+ - lib/websocket/protocol/hybi/stream_reader.rb
63
+ - lib/websocket/protocol/hybi.rb
64
+ - lib/websocket/protocol/utf8_match.rb
65
+ - lib/websocket/protocol.rb
66
+ homepage: http://github.com/faye/websocket-protocol-ruby
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options:
70
+ - --main
71
+ - README.rdoc
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 1.8.23
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: WebSocket protocol handler with pluggable I/O
92
+ test_files: []
93
+ has_rdoc: