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