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.
@@ -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,4 @@
1
+ require 'mkmf'
2
+ extension_name = 'websocket_mask'
3
+ dir_config(extension_name)
4
+ create_makefile(extension_name)
@@ -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