websocket-driver-kontena 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +123 -0
- data/LICENSE.md +22 -0
- data/README.md +369 -0
- data/examples/tcp_server.rb +28 -0
- data/ext/websocket-driver/WebsocketMaskService.java +55 -0
- data/ext/websocket-driver/extconf.rb +4 -0
- data/ext/websocket-driver/websocket_mask.c +41 -0
- data/lib/websocket/driver.rb +199 -0
- data/lib/websocket/driver/client.rb +140 -0
- data/lib/websocket/driver/draft75.rb +102 -0
- data/lib/websocket/driver/draft76.rb +96 -0
- data/lib/websocket/driver/event_emitter.rb +54 -0
- data/lib/websocket/driver/headers.rb +45 -0
- data/lib/websocket/driver/hybi.rb +406 -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 +143 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'websocket/driver'
|
5
|
+
require 'permessage_deflate'
|
6
|
+
|
7
|
+
module Connection
|
8
|
+
def initialize
|
9
|
+
@driver = WebSocket::Driver.server(self)
|
10
|
+
@driver.add_extension(PermessageDeflate)
|
11
|
+
|
12
|
+
@driver.on(:connect) { |e| @driver.start if WebSocket::Driver.websocket? @driver.env }
|
13
|
+
@driver.on(:message) { |e| @driver.frame(e.data) }
|
14
|
+
@driver.on(:close) { |e| close_connection_after_writing }
|
15
|
+
end
|
16
|
+
|
17
|
+
def receive_data(data)
|
18
|
+
@driver.parse(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(data)
|
22
|
+
send_data(data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
EM.run {
|
27
|
+
EM.start_server('127.0.0.1', ARGV[0], Connection)
|
28
|
+
}
|
@@ -0,0 +1,55 @@
|
|
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.RubyClass;
|
8
|
+
import org.jruby.RubyModule;
|
9
|
+
import org.jruby.RubyObject;
|
10
|
+
import org.jruby.RubyString;
|
11
|
+
import org.jruby.anno.JRubyMethod;
|
12
|
+
import org.jruby.runtime.ObjectAllocator;
|
13
|
+
import org.jruby.runtime.ThreadContext;
|
14
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
15
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
16
|
+
|
17
|
+
public class WebsocketMaskService implements BasicLibraryService {
|
18
|
+
private Ruby runtime;
|
19
|
+
|
20
|
+
public boolean basicLoad(Ruby runtime) throws IOException {
|
21
|
+
this.runtime = runtime;
|
22
|
+
RubyModule websocket = runtime.defineModule("WebSocket");
|
23
|
+
|
24
|
+
RubyClass webSocketMask = websocket.defineClassUnder("Mask", runtime.getObject(), new ObjectAllocator() {
|
25
|
+
public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) {
|
26
|
+
return new WebsocketMask(runtime, rubyClass);
|
27
|
+
}
|
28
|
+
});
|
29
|
+
|
30
|
+
webSocketMask.defineAnnotatedMethods(WebsocketMask.class);
|
31
|
+
return true;
|
32
|
+
}
|
33
|
+
|
34
|
+
public class WebsocketMask extends RubyObject {
|
35
|
+
public WebsocketMask(final Ruby runtime, RubyClass rubyClass) {
|
36
|
+
super(runtime, rubyClass);
|
37
|
+
}
|
38
|
+
|
39
|
+
@JRubyMethod
|
40
|
+
public IRubyObject mask(ThreadContext context, IRubyObject payload, IRubyObject mask) {
|
41
|
+
if (mask.isNil()) return payload;
|
42
|
+
|
43
|
+
byte[] payload_a = ((RubyString)payload).getBytes();
|
44
|
+
byte[] mask_a = ((RubyString)mask).getBytes();
|
45
|
+
int i, n = payload_a.length;
|
46
|
+
|
47
|
+
if (n == 0) return payload;
|
48
|
+
|
49
|
+
for (i = 0; i < n; i++) {
|
50
|
+
payload_a[i] ^= mask_a[i % 4];
|
51
|
+
}
|
52
|
+
return RubyString.newStringNoCopy(runtime, payload_a);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
@@ -0,0 +1,41 @@
|
|
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
|
10
|
+
Init_websocket_mask()
|
11
|
+
{
|
12
|
+
WebSocket = rb_define_module("WebSocket");
|
13
|
+
WebSocketMask = rb_define_module_under(WebSocket, "Mask");
|
14
|
+
rb_define_singleton_method(WebSocketMask, "mask", method_websocket_mask, 2);
|
15
|
+
}
|
16
|
+
|
17
|
+
VALUE
|
18
|
+
method_websocket_mask(VALUE self,
|
19
|
+
VALUE payload,
|
20
|
+
VALUE mask)
|
21
|
+
{
|
22
|
+
char *payload_s, *mask_s, *unmasked_s;
|
23
|
+
long i, n;
|
24
|
+
VALUE unmasked;
|
25
|
+
|
26
|
+
if (mask == Qnil || RSTRING_LEN(mask) != 4) {
|
27
|
+
return payload;
|
28
|
+
}
|
29
|
+
|
30
|
+
payload_s = RSTRING_PTR(payload);
|
31
|
+
mask_s = RSTRING_PTR(mask);
|
32
|
+
n = RSTRING_LEN(payload);
|
33
|
+
|
34
|
+
unmasked = rb_str_new(0, n);
|
35
|
+
unmasked_s = RSTRING_PTR(unmasked);
|
36
|
+
|
37
|
+
for (i = 0; i < n; i++) {
|
38
|
+
unmasked_s[i] = payload_s[i] ^ mask_s[i % 4];
|
39
|
+
}
|
40
|
+
return unmasked;
|
41
|
+
}
|
@@ -0,0 +1,199 @@
|
|
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
|
+
CloseEvent = Struct.new(:code, :reason)
|
54
|
+
|
55
|
+
ProtocolError = Class.new(StandardError)
|
56
|
+
URIError = Class.new(ArgumentError)
|
57
|
+
ConfigurationError = Class.new(ArgumentError)
|
58
|
+
|
59
|
+
autoload :Client, root + '/client'
|
60
|
+
autoload :Draft75, root + '/draft75'
|
61
|
+
autoload :Draft76, root + '/draft76'
|
62
|
+
autoload :EventEmitter, root + '/event_emitter'
|
63
|
+
autoload :Headers, root + '/headers'
|
64
|
+
autoload :Hybi, root + '/hybi'
|
65
|
+
autoload :Proxy, root + '/proxy'
|
66
|
+
autoload :Server, root + '/server'
|
67
|
+
autoload :StreamReader, root + '/stream_reader'
|
68
|
+
|
69
|
+
include EventEmitter
|
70
|
+
attr_reader :protocol, :ready_state
|
71
|
+
|
72
|
+
def initialize(socket, options = {})
|
73
|
+
super()
|
74
|
+
Driver.validate_options(options, [:max_length, :masking, :require_masking, :protocols])
|
75
|
+
|
76
|
+
@socket = socket
|
77
|
+
@reader = StreamReader.new
|
78
|
+
@options = options
|
79
|
+
@max_length = options[:max_length] || MAX_LENGTH
|
80
|
+
@headers = Headers.new
|
81
|
+
@queue = []
|
82
|
+
@ready_state = 0
|
83
|
+
end
|
84
|
+
|
85
|
+
def state
|
86
|
+
return nil unless @ready_state >= 0
|
87
|
+
STATES[@ready_state]
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_extension(extension)
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_header(name, value)
|
95
|
+
return false unless @ready_state <= 0
|
96
|
+
@headers[name] = value
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def start
|
101
|
+
return false unless @ready_state == 0
|
102
|
+
response = handshake_response
|
103
|
+
return false unless response
|
104
|
+
@socket.write(response)
|
105
|
+
open unless @stage == -1
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
def text(message)
|
110
|
+
message = message.encode(UNICODE) unless message.encoding.name == UNICODE
|
111
|
+
frame(message, :text)
|
112
|
+
end
|
113
|
+
|
114
|
+
def binary(message)
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def ping(*args)
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
def pong(*args)
|
123
|
+
false
|
124
|
+
end
|
125
|
+
|
126
|
+
def close(reason = nil, code = nil)
|
127
|
+
return false unless @ready_state == 1
|
128
|
+
@ready_state = 3
|
129
|
+
emit(:close, CloseEvent.new(nil, nil))
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def open
|
136
|
+
@ready_state = 1
|
137
|
+
@queue.each { |message| frame(*message) }
|
138
|
+
@queue = []
|
139
|
+
emit(:open, OpenEvent.new)
|
140
|
+
end
|
141
|
+
|
142
|
+
def queue(message)
|
143
|
+
@queue << message
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.client(socket, options = {})
|
148
|
+
Client.new(socket, options.merge(:masking => true))
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.server(socket, options = {})
|
152
|
+
Server.new(socket, options.merge(:require_masking => true))
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.rack(socket, options = {})
|
156
|
+
env = socket.env
|
157
|
+
if env['HTTP_SEC_WEBSOCKET_VERSION']
|
158
|
+
Hybi.new(socket, options.merge(:require_masking => true))
|
159
|
+
elsif env['HTTP_SEC_WEBSOCKET_KEY1']
|
160
|
+
Draft76.new(socket, options)
|
161
|
+
else
|
162
|
+
Draft75.new(socket, options)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.encode(string, encoding = nil)
|
167
|
+
case string
|
168
|
+
when Array then
|
169
|
+
string = string.pack('C*')
|
170
|
+
encoding ||= BINARY
|
171
|
+
when String then
|
172
|
+
encoding ||= UNICODE
|
173
|
+
end
|
174
|
+
unless string.encoding.name == encoding
|
175
|
+
string = string.dup if string.frozen?
|
176
|
+
string.force_encoding(encoding)
|
177
|
+
end
|
178
|
+
string.valid_encoding? ? string : nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.validate_options(options, valid_keys)
|
182
|
+
options.keys.each do |key|
|
183
|
+
unless valid_keys.include?(key)
|
184
|
+
raise ConfigurationError, "Unrecognized option: #{key.inspect}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.websocket?(env)
|
190
|
+
connection = env['HTTP_CONNECTION'] || ''
|
191
|
+
upgrade = env['HTTP_UPGRADE'] || ''
|
192
|
+
|
193
|
+
env['REQUEST_METHOD'] == 'GET' and
|
194
|
+
connection.downcase.split(/ *, */).include?('upgrade') and
|
195
|
+
upgrade.downcase == 'websocket'
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
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'] = '13'
|
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-13'
|
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
|