websocket-driver 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +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
|