websocket-driver 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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