websocket-driver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +6 -0
- data/README.md +266 -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/driver.rb +158 -0
- data/lib/websocket/driver/client.rb +117 -0
- data/lib/websocket/driver/draft75.rb +93 -0
- data/lib/websocket/driver/draft76.rb +97 -0
- data/lib/websocket/driver/event_emitter.rb +43 -0
- data/lib/websocket/driver/hybi.rb +368 -0
- data/lib/websocket/driver/hybi/stream_reader.rb +30 -0
- data/lib/websocket/driver/utf8_match.rb +7 -0
- metadata +95 -0
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,266 @@
|
|
1
|
+
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-ruby.png)](https://travis-ci.org/faye/websocket-driver-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 driver 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 `Sec-WebSocket-Protocol`
|
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
|
+
## Installation
|
32
|
+
|
33
|
+
```
|
34
|
+
$ gem install websocket-driver
|
35
|
+
```
|
36
|
+
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
To build either a server-side or client-side socket, the only requirement is
|
41
|
+
that you supply a `socket` object with these methods:
|
42
|
+
|
43
|
+
* `socket.url` - returns the full URL of the socket as a string.
|
44
|
+
* `socket.write(string)` - writes the given string to a TCP stream.
|
45
|
+
|
46
|
+
Server-side sockets require one additional method:
|
47
|
+
|
48
|
+
* `socket.env` - returns a Rack-style env hash that will contain some of the
|
49
|
+
following fields. Their values are strings containing the value of the named
|
50
|
+
header, unless stated otherwise.
|
51
|
+
* `HTTP_CONNECTION`
|
52
|
+
* `HTTP_HOST`
|
53
|
+
* `HTTP_ORIGIN`
|
54
|
+
* `HTTP_SEC_WEBSOCKET_KEY`
|
55
|
+
* `HTTP_SEC_WEBSOCKET_KEY1`
|
56
|
+
* `HTTP_SEC_WEBSOCKET_KEY2`
|
57
|
+
* `HTTP_SEC_WEBSOCKET_PROTOCOL`
|
58
|
+
* `HTTP_SEC_WEBSOCKET_VERSION`
|
59
|
+
* `HTTP_UPGRADE`
|
60
|
+
* `rack.input`, an `IO` object representing the request body
|
61
|
+
* `REQUEST_METHOD`, the request's HTTP verb
|
62
|
+
|
63
|
+
|
64
|
+
### Server-side
|
65
|
+
|
66
|
+
To handle a server-side WebSocket connection, you need to check whether the
|
67
|
+
request is a WebSocket handshake, and if so create a protocol driver for it.
|
68
|
+
You must give the driver an object with the `env`, `url` and `write` methods.
|
69
|
+
A simple example might be:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
require 'websocket/driver'
|
73
|
+
require 'eventmachine'
|
74
|
+
|
75
|
+
class WS
|
76
|
+
attr_reader :env, :url
|
77
|
+
|
78
|
+
def initialize(env)
|
79
|
+
@env = env
|
80
|
+
|
81
|
+
secure = Rack::Request.new(env).ssl?
|
82
|
+
scheme = secure ? 'wss:' : 'ws:'
|
83
|
+
@url = scheme + '//' + env['HTTP_HOST'] + env['REQUEST_URI']
|
84
|
+
|
85
|
+
@driver = WebSocket::Driver.rack(self)
|
86
|
+
|
87
|
+
env['rack.hijack'].call
|
88
|
+
@io = env['rack.hijack_io']
|
89
|
+
|
90
|
+
EM.attach(@io, Reader) { |conn| conn.driver = @driver }
|
91
|
+
|
92
|
+
@driver.start
|
93
|
+
end
|
94
|
+
|
95
|
+
def write(string)
|
96
|
+
@io.write(string)
|
97
|
+
end
|
98
|
+
|
99
|
+
module Reader
|
100
|
+
attr_writer :driver
|
101
|
+
|
102
|
+
def receive_data(string)
|
103
|
+
@driver.parse(string)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
To explain what's going on here: the `WS` class implements the `env`, `url` and
|
110
|
+
`write(string)` methods as required. When instantiated with a Rack environment,
|
111
|
+
it stores the environment and infers the complete URL from it. Having set up
|
112
|
+
the `env` and `url`, it asks `WebSocket::Driver` for a server-side driver for
|
113
|
+
the socket. Then it uses the Rack hijack API to gain access to the TCP stream,
|
114
|
+
and uses EventMachine to stream in incoming data from the client, handing
|
115
|
+
incoming data off to the driver for parsing. Finally, we tell the driver to
|
116
|
+
`start`, which will begin sending the handshake response. This will invoke the
|
117
|
+
`WS#write` method, which will send the response out over the TCP socket.
|
118
|
+
|
119
|
+
Having defined this class we could use it like this when handling a request:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
if WebSocket::Driver.websocket?(env)
|
123
|
+
socket = WS.new(env)
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
The driver API is described in full below.
|
128
|
+
|
129
|
+
|
130
|
+
### Client-side
|
131
|
+
|
132
|
+
Similarly, to implement a WebSocket client you need an object with `url` and
|
133
|
+
`write` methods. Once you have one such object, you ask for a driver for it:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
driver = WebSocket::Driver.client(socket)
|
137
|
+
```
|
138
|
+
|
139
|
+
After this you use the driver API as described below to process incoming data
|
140
|
+
and send outgoing data.
|
141
|
+
|
142
|
+
|
143
|
+
### Driver API
|
144
|
+
|
145
|
+
Drivers are created using one of the following methods:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
driver = WebSocket::Driver.rack(socket, options)
|
149
|
+
driver = WebSocket::Driver.client(socket, options)
|
150
|
+
```
|
151
|
+
|
152
|
+
The `rack` method returns a driver chosen using the socket's `env`. The
|
153
|
+
`client` method always returns a driver for the RFC version of the protocol
|
154
|
+
with masking enabled on outgoing frames.
|
155
|
+
|
156
|
+
The `options` argument is optional, and is a hash. It may contain the following
|
157
|
+
keys:
|
158
|
+
|
159
|
+
* `:protocols` - an array of strings representing acceptable subprotocols for
|
160
|
+
use over the socket. The driver will negotiate one of these to use via the
|
161
|
+
`Sec-WebSocket-Protocol` header if supported by the other peer.
|
162
|
+
|
163
|
+
All drivers respond to the following API methods, but some of them are no-ops
|
164
|
+
depending on whether the client supports the behaviour.
|
165
|
+
|
166
|
+
Note that most of these methods are commands: if they produce data that should
|
167
|
+
be sent over the socket, they will give this to you by calling
|
168
|
+
`socket.write(string)`.
|
169
|
+
|
170
|
+
#### `driver.on('open') { |event| }`
|
171
|
+
|
172
|
+
Sets the callback block to execute when the socket becomes open.
|
173
|
+
|
174
|
+
#### `driver.on('message') { |event| }`
|
175
|
+
|
176
|
+
Sets the callback block to execute when a message is received. `event` will
|
177
|
+
have a `data` attribute containing either a string in the case of a text
|
178
|
+
message or an array of integers in the case of a binary message.
|
179
|
+
|
180
|
+
#### `driver.on('error') { |event| }`
|
181
|
+
|
182
|
+
Sets the callback to execute when a protocol error occurs due to the other peer
|
183
|
+
sending an invalid byte sequence. `event` will have a `message` attribute
|
184
|
+
describing the error.
|
185
|
+
|
186
|
+
#### `driver.on('close') { |event| }`
|
187
|
+
|
188
|
+
Sets the callback block to execute when the socket becomes closed. The `event`
|
189
|
+
object has `code` and `reason` attributes.
|
190
|
+
|
191
|
+
#### `driver.start`
|
192
|
+
|
193
|
+
Initiates the protocol by sending the handshake - either the response for a
|
194
|
+
server-side driver or the request for a client-side one. This should be the
|
195
|
+
first method you invoke. Returns `true` iff a handshake was sent.
|
196
|
+
|
197
|
+
#### `driver.parse(string)`
|
198
|
+
|
199
|
+
Takes a string and parses it, potentially resulting in message events being
|
200
|
+
emitted (see `on('message')` above) or in data being sent to `socket.write`.
|
201
|
+
You should send all data you receive via I/O to this method.
|
202
|
+
|
203
|
+
#### `driver.text(string)`
|
204
|
+
|
205
|
+
Sends a text message over the socket. If the socket handshake is not yet
|
206
|
+
complete, the message will be queued until it is. Returns `true` if the message
|
207
|
+
was sent or queued, and `false` if the socket can no longer send messages.
|
208
|
+
|
209
|
+
#### `driver.binary(array)`
|
210
|
+
|
211
|
+
Takes an array of byte-sized integers and sends them as a binary message. Will
|
212
|
+
queue and return `true` or `false` the same way as the `text` method. It will
|
213
|
+
also return `false` if the driver does not support binary messages.
|
214
|
+
|
215
|
+
#### `driver.ping(string = '', &callback)`
|
216
|
+
|
217
|
+
Sends a ping frame over the socket, queueing it if necessary. `string` and the
|
218
|
+
`callback` block are both optional. If a callback is given, it will be invoked
|
219
|
+
when the socket receives a pong frame whose content matches `string`. Returns
|
220
|
+
`false` if frames can no longer be sent, or if the driver does not support
|
221
|
+
ping/pong.
|
222
|
+
|
223
|
+
#### `driver.close`
|
224
|
+
|
225
|
+
Initiates the closing handshake if the socket is still open. For drivers with
|
226
|
+
no closing handshake, this will result in the immediate execution of the
|
227
|
+
`on('close')` callback. For drivers with a closing handshake, this sends a
|
228
|
+
closing frame and `emit('close')` will execute when a response is received or a
|
229
|
+
protocol error occurs.
|
230
|
+
|
231
|
+
#### `driver.version`
|
232
|
+
|
233
|
+
Returns the WebSocket version in use as a string. Will either be `hixie-75`,
|
234
|
+
`hixie-76` or `hybi-$version`.
|
235
|
+
|
236
|
+
#### `driver.protocol`
|
237
|
+
|
238
|
+
Returns a string containing the selected subprotocol, if any was agreed upon
|
239
|
+
using the `Sec-WebSocket-Protocol` mechanism. This value becomes available
|
240
|
+
after `emit('open')` has fired.
|
241
|
+
|
242
|
+
|
243
|
+
## License
|
244
|
+
|
245
|
+
(The MIT License)
|
246
|
+
|
247
|
+
Copyright (c) 2010-2013 James Coglan
|
248
|
+
|
249
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
250
|
+
this software and associated documentation files (the 'Software'), to deal in
|
251
|
+
the Software without restriction, including without limitation the rights to
|
252
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
253
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
254
|
+
so, subject to the following conditions:
|
255
|
+
|
256
|
+
The above copyright notice and this permission notice shall be included in all
|
257
|
+
copies or substantial portions of the Software.
|
258
|
+
|
259
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
260
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
261
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
262
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
263
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
264
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
265
|
+
SOFTWARE.
|
266
|
+
|
@@ -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,158 @@
|
|
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 Driver
|
16
|
+
|
17
|
+
root = File.expand_path('../driver', __FILE__)
|
18
|
+
require root + '/../../websocket_mask'
|
19
|
+
|
20
|
+
if RUBY_PLATFORM =~ /java/
|
21
|
+
require 'jruby'
|
22
|
+
com.jcoglan.websocket.WebsocketMaskService.new.basicLoad(JRuby.runtime)
|
23
|
+
end
|
24
|
+
|
25
|
+
unless Mask.respond_to?(:mask)
|
26
|
+
def Mask.mask(payload, mask)
|
27
|
+
@instance ||= new
|
28
|
+
@instance.mask(payload, mask)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
unless String.instance_methods.include?(:force_encoding)
|
33
|
+
require root + '/utf8_match'
|
34
|
+
end
|
35
|
+
|
36
|
+
STATES = [:connecting, :open, :closing, :closed]
|
37
|
+
|
38
|
+
class OpenEvent < Struct.new(nil) ; end
|
39
|
+
class MessageEvent < Struct.new(:data) ; end
|
40
|
+
class CloseEvent < Struct.new(:code, :reason) ; end
|
41
|
+
|
42
|
+
class ProtocolError < StandardError ; end
|
43
|
+
|
44
|
+
autoload :EventEmitter, root + '/event_emitter'
|
45
|
+
autoload :Draft75, root + '/draft75'
|
46
|
+
autoload :Draft76, root + '/draft76'
|
47
|
+
autoload :Hybi, root + '/hybi'
|
48
|
+
autoload :Client, root + '/client'
|
49
|
+
|
50
|
+
include EventEmitter
|
51
|
+
attr_reader :protocol, :ready_state
|
52
|
+
|
53
|
+
def initialize(socket, options = {})
|
54
|
+
super()
|
55
|
+
|
56
|
+
@socket = socket
|
57
|
+
@options = options
|
58
|
+
@queue = []
|
59
|
+
@ready_state = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def state
|
63
|
+
return nil unless @ready_state >= 0
|
64
|
+
STATES[@ready_state]
|
65
|
+
end
|
66
|
+
|
67
|
+
def start
|
68
|
+
return false unless @ready_state == 0
|
69
|
+
@socket.write(handshake_response)
|
70
|
+
open unless @stage == -1
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def text(message)
|
75
|
+
frame(message)
|
76
|
+
end
|
77
|
+
|
78
|
+
def binary(message)
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def ping(*args)
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def close(reason = nil, code = nil)
|
87
|
+
return false unless @ready_state == 1
|
88
|
+
@ready_state = 3
|
89
|
+
emit(:close, CloseEvent.new(nil, nil))
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def open
|
96
|
+
@ready_state = 1
|
97
|
+
@queue.each { |message| frame(*message) }
|
98
|
+
@queue = []
|
99
|
+
emit(:open, OpenEvent.new)
|
100
|
+
end
|
101
|
+
|
102
|
+
def queue(message)
|
103
|
+
@queue << message
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.encode(string, validate_encoding = false)
|
108
|
+
if Array === string
|
109
|
+
string = utf8_string(string)
|
110
|
+
return nil if validate_encoding and !valid_utf8?(string)
|
111
|
+
end
|
112
|
+
utf8_string(string)
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.client(socket, options = {})
|
116
|
+
Client.new(socket, options.merge(:masking => true))
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.rack(socket, options = {})
|
120
|
+
env = socket.env
|
121
|
+
if env['HTTP_SEC_WEBSOCKET_VERSION']
|
122
|
+
Hybi.new(socket, options.merge(:require_masking => true))
|
123
|
+
elsif env['HTTP_SEC_WEBSOCKET_KEY1']
|
124
|
+
Draft76.new(socket, options)
|
125
|
+
else
|
126
|
+
Draft75.new(socket, options)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.utf8_string(string)
|
131
|
+
string = string.pack('C*') if Array === string
|
132
|
+
string.respond_to?(:force_encoding) ?
|
133
|
+
string.force_encoding('UTF-8') :
|
134
|
+
string
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.valid_utf8?(byte_array)
|
138
|
+
string = utf8_string(byte_array)
|
139
|
+
if defined?(UTF8_MATCH)
|
140
|
+
UTF8_MATCH =~ string ? true : false
|
141
|
+
else
|
142
|
+
string.valid_encoding?
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.websocket?(env)
|
147
|
+
return false unless env['REQUEST_METHOD'] == 'GET'
|
148
|
+
connection = env['HTTP_CONNECTION'] || ''
|
149
|
+
upgrade = env['HTTP_UPGRADE'] || ''
|
150
|
+
|
151
|
+
env['REQUEST_METHOD'] == 'GET' and
|
152
|
+
connection.downcase.split(/\s*,\s*/).include?('upgrade') and
|
153
|
+
upgrade.downcase == 'websocket'
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|