websocket-protocol 0.0.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +6 -0
- data/README.rdoc +247 -0
- data/ext/websocket_mask/WebsocketMaskService.java +61 -0
- data/ext/websocket_mask/extconf.rb +5 -0
- data/ext/websocket_mask/websocket_mask.c +33 -0
- data/lib/websocket/protocol/client.rb +94 -0
- data/lib/websocket/protocol/draft75.rb +91 -0
- data/lib/websocket/protocol/draft76.rb +96 -0
- data/lib/websocket/protocol/hybi/stream_reader.rb +30 -0
- data/lib/websocket/protocol/hybi.rb +348 -0
- data/lib/websocket/protocol/utf8_match.rb +7 -0
- data/lib/websocket/protocol.rb +184 -0
- data/lib/websocket_mask.jar +0 -0
- metadata +98 -0
data/CHANGELOG.txt
ADDED
data/README.rdoc
ADDED
@@ -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,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,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,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,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,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
|
+
|
@@ -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
|
+
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: websocket-protocol
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: java
|
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
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: !binary |-
|
21
|
+
MA==
|
22
|
+
none: false
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: !binary |-
|
28
|
+
MA==
|
29
|
+
none: false
|
30
|
+
prerelease: false
|
31
|
+
type: :development
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rspec
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: !binary |-
|
39
|
+
MA==
|
40
|
+
none: false
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: !binary |-
|
46
|
+
MA==
|
47
|
+
none: false
|
48
|
+
prerelease: false
|
49
|
+
type: :development
|
50
|
+
description:
|
51
|
+
email: jcoglan@gmail.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files:
|
55
|
+
- README.rdoc
|
56
|
+
files:
|
57
|
+
- README.rdoc
|
58
|
+
- CHANGELOG.txt
|
59
|
+
- ext/websocket_mask/websocket_mask.c
|
60
|
+
- ext/websocket_mask/WebsocketMaskService.java
|
61
|
+
- ext/websocket_mask/extconf.rb
|
62
|
+
- lib/websocket/protocol.rb
|
63
|
+
- lib/websocket/protocol/client.rb
|
64
|
+
- lib/websocket/protocol/draft75.rb
|
65
|
+
- lib/websocket/protocol/draft76.rb
|
66
|
+
- lib/websocket/protocol/hybi.rb
|
67
|
+
- lib/websocket/protocol/utf8_match.rb
|
68
|
+
- lib/websocket/protocol/hybi/stream_reader.rb
|
69
|
+
- lib/websocket_mask.jar
|
70
|
+
homepage: http://github.com/faye/websocket-protocol-ruby
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options:
|
74
|
+
- "--main"
|
75
|
+
- README.rdoc
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: !binary |-
|
83
|
+
MA==
|
84
|
+
none: false
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: !binary |-
|
90
|
+
MA==
|
91
|
+
none: false
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.8.24
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: WebSocket protocol handler with pluggable I/O
|
98
|
+
test_files: []
|