em-websocket-request 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,8 +8,10 @@ Gem::Specification.new do |gem|
8
8
  gem.summary = %q{EventMachine WebSocket client}
9
9
  gem.homepage = ""
10
10
 
11
+ gem.files = `git ls-files`.split("\n") +
12
+ %w(lib/em-ws-request/vendor/web-socket-ruby/lib/web_socket.rb)
13
+
11
14
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
- gem.files = `git ls-files`.split("\n")
13
15
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
16
  gem.name = "em-websocket-request"
15
17
  gem.require_paths = ["lib"]
@@ -60,11 +60,12 @@ module EventMachine
60
60
  def build_request
61
61
  head = super
62
62
  if websocket?
63
+ @sec_websocket_key = @wswrapper.generate_key
63
64
  head.delete_if { |k, v| !%w(host).include?(k) }
64
65
  head['upgrade'] = 'websocket'
65
66
  head['connection'] = 'Upgrade'
66
67
  head['origin'] = @req.uri.host
67
- head['Sec-WebSocket-Key'] = 'dGhlIHNhbXBsZSBub25jZQ=='
68
+ head['Sec-WebSocket-Key'] = @sec_websocket_key
68
69
  head['Sec-WebSocket-Version'] = PROTOCOL_VERSION
69
70
  end
70
71
  head
@@ -73,15 +74,34 @@ module EventMachine
73
74
  def parse_response_header(header, version, status)
74
75
  super
75
76
  if websocket?
76
- p [:parse_response_header, :WEBSOCKET]
77
- if @response_header.status == 101
77
+ # p [:parse_response_header, :WEBSOCKET]
78
+ if @response_header.status != 101
79
+
80
+ fail "websocket handshake failed (not status 101)"
81
+ elsif @wswrapper.security_digest(@sec_websocket_key) !=
82
+ @response_header['SEC_WEBSOCKET_ACCEPT']
83
+
84
+ fail "websocket handshake failed (mismatched key)"
85
+ else
78
86
  @state = :websocket
79
87
  succeed
80
- else
81
- fail "websocket handshake failed"
82
88
  end
83
89
  end
84
90
  end
91
+
92
+ #### DISCONNECT ####
93
+ def disconnect(&blk)
94
+ @disconnect = blk
95
+ end
96
+
97
+ def unbind(reason = nil)
98
+ if state == :stream || state == :websocket # FIXME
99
+ @disconnect.call(self) if @disconnect
100
+ on_error(reason) # TODO: Should we really barf on unbind?
101
+ else
102
+ super(reason)
103
+ end
104
+ end
85
105
  end
86
106
  end
87
107
 
@@ -0,0 +1,565 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
4
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
5
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
6
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7
+
8
+ require "base64"
9
+ require "socket"
10
+ require "uri"
11
+ require "digest/md5"
12
+ require "digest/sha1"
13
+ require "openssl"
14
+ require "stringio"
15
+
16
+
17
+ class WebSocket
18
+
19
+ class << self
20
+
21
+ attr_accessor(:debug)
22
+
23
+ end
24
+
25
+ class Error < RuntimeError
26
+
27
+ end
28
+
29
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
+ OPCODE_CONTINUATION = 0x00
31
+ OPCODE_TEXT = 0x01
32
+ OPCODE_BINARY = 0x02
33
+ OPCODE_CLOSE = 0x08
34
+ OPCODE_PING = 0x09
35
+ OPCODE_PONG = 0x0a
36
+
37
+ def initialize(arg, params = {})
38
+ if params[:server] # server
39
+
40
+ @server = params[:server]
41
+ @socket = arg
42
+ line = gets().chomp()
43
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
44
+ raise(WebSocket::Error, "invalid request: #{line}")
45
+ end
46
+ @path = $1
47
+ read_header()
48
+ if @header["sec-websocket-version"]
49
+ @web_socket_version = @header["sec-websocket-version"]
50
+ @key3 = nil
51
+ elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
52
+ @web_socket_version = "hixie-76"
53
+ @key3 = read(8)
54
+ else
55
+ @web_socket_version = "hixie-75"
56
+ @key3 = nil
57
+ end
58
+ if !@server.accepted_origin?(self.origin)
59
+ raise(WebSocket::Error,
60
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
61
+ "To accept this origin, write e.g. \n" +
62
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
63
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
64
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
65
+ end
66
+ @handshaked = false
67
+
68
+ else # client
69
+
70
+ @web_socket_version = "hixie-76"
71
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
72
+
73
+ if uri.scheme == "ws"
74
+ default_port = 80
75
+ elsif uri.scheme = "wss"
76
+ default_port = 443
77
+ else
78
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
79
+ end
80
+
81
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
82
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
83
+ origin = params[:origin] || "http://#{uri.host}"
84
+ key1 = generate_key()
85
+ key2 = generate_key()
86
+ key3 = generate_key3()
87
+
88
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
89
+
90
+ if uri.scheme == "ws"
91
+ @socket = socket
92
+ else
93
+ @socket = ssl_handshake(socket)
94
+ end
95
+
96
+ write(
97
+ "GET #{@path} HTTP/1.1\r\n" +
98
+ "Upgrade: WebSocket\r\n" +
99
+ "Connection: Upgrade\r\n" +
100
+ "Host: #{host}\r\n" +
101
+ "Origin: #{origin}\r\n" +
102
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
103
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
104
+ "\r\n" +
105
+ "#{key3}")
106
+ flush()
107
+
108
+ line = gets().chomp()
109
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
110
+ read_header()
111
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
112
+ raise(WebSocket::Error,
113
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
114
+ end
115
+ reply_digest = read(16)
116
+ expected_digest = hixie_76_security_digest(key1, key2, key3)
117
+ if reply_digest != expected_digest
118
+ raise(WebSocket::Error,
119
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
120
+ end
121
+ @handshaked = true
122
+
123
+ end
124
+ @received = []
125
+ @buffer = ""
126
+ @closing_started = false
127
+ end
128
+
129
+ attr_reader(:server, :header, :path)
130
+
131
+ def handshake(status = nil, header = {})
132
+ if @handshaked
133
+ raise(WebSocket::Error, "handshake has already been done")
134
+ end
135
+ status ||= "101 Switching Protocols"
136
+ def_header = {}
137
+ case @web_socket_version
138
+ when "hixie-75"
139
+ def_header["WebSocket-Origin"] = self.origin
140
+ def_header["WebSocket-Location"] = self.location
141
+ extra_bytes = ""
142
+ when "hixie-76"
143
+ def_header["Sec-WebSocket-Origin"] = self.origin
144
+ def_header["Sec-WebSocket-Location"] = self.location
145
+ extra_bytes = hixie_76_security_digest(
146
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
147
+ else
148
+ def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
149
+ extra_bytes = ""
150
+ end
151
+ header = def_header.merge(header)
152
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
153
+ # Note that Upgrade and Connection must appear in this order.
154
+ write(
155
+ "HTTP/1.1 #{status}\r\n" +
156
+ "Upgrade: websocket\r\n" +
157
+ "Connection: Upgrade\r\n" +
158
+ "#{header_str}\r\n#{extra_bytes}")
159
+ flush()
160
+ @handshaked = true
161
+ end
162
+
163
+ def send(data)
164
+ if !@handshaked
165
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
166
+ end
167
+ case @web_socket_version
168
+ when "hixie-75", "hixie-76"
169
+ data = force_encoding(data.dup(), "ASCII-8BIT")
170
+ write("\x00#{data}\xff")
171
+ flush()
172
+ else
173
+ send_frame(OPCODE_TEXT, data, !@server)
174
+ end
175
+ end
176
+
177
+ def receive()
178
+ if !@handshaked
179
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
180
+ end
181
+ case @web_socket_version
182
+
183
+ when "hixie-75", "hixie-76"
184
+ packet = gets("\xff")
185
+ return nil if !packet
186
+ if packet =~ /\A\x00(.*)\xff\z/nm
187
+ return force_encoding($1, "UTF-8")
188
+ elsif packet == "\xff" && read(1) == "\x00" # closing
189
+ close(true)
190
+ return nil
191
+ else
192
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
193
+ end
194
+
195
+ else
196
+ bytes = read(2).unpack("C*")
197
+ fin = (bytes[0] & 0x80) != 0
198
+ opcode = bytes[0] & 0x0f
199
+ mask = (bytes[1] & 0x80) != 0
200
+ plength = bytes[1] & 0x7f
201
+ if plength == 126
202
+ bytes = read(2)
203
+ plength = bytes.unpack("n")[0]
204
+ elsif plength == 127
205
+ bytes = read(8)
206
+ (high, low) = bytes.unpack("NN")
207
+ plength = high * (2 ** 32) + low
208
+ end
209
+ if @server && !mask
210
+ # Masking is required.
211
+ @socket.close()
212
+ raise(WebSocket::Error, "received unmasked data")
213
+ end
214
+ mask_key = mask ? read(4).unpack("C*") : nil
215
+ payload = read(plength)
216
+ payload = apply_mask(payload, mask_key) if mask
217
+ case opcode
218
+ when OPCODE_TEXT
219
+ return force_encoding(payload, "UTF-8")
220
+ when OPCODE_BINARY
221
+ raise(WebSocket::Error, "received binary data, which is not supported")
222
+ when OPCODE_CLOSE
223
+ close(true)
224
+ return nil
225
+ when OPCODE_PING
226
+ raise(WebSocket::Error, "received ping, which is not supported")
227
+ when OPCODE_PONG
228
+ else
229
+ raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
230
+ end
231
+
232
+ end
233
+ end
234
+
235
+ def tcp_socket
236
+ return @socket
237
+ end
238
+
239
+ def host
240
+ return @header["host"]
241
+ end
242
+
243
+ def origin
244
+ case @web_socket_version
245
+ when "7", "8"
246
+ name = "sec-websocket-origin"
247
+ else
248
+ name = "origin"
249
+ end
250
+ if @header[name]
251
+ return @header[name]
252
+ else
253
+ raise(WebSocket::Error, "%s header is missing" % name)
254
+ end
255
+ end
256
+
257
+ def location
258
+ return "ws://#{self.host}#{@path}"
259
+ end
260
+
261
+ # Does closing handshake.
262
+ def close(by_peer = false)
263
+ if !@closing_started
264
+ case @web_socket_version
265
+ when "hixie-75", "hixie-76"
266
+ write("\xff\x00")
267
+ else
268
+ send_frame(OPCODE_CLOSE, "", false)
269
+ end
270
+ end
271
+ @socket.close() if by_peer
272
+ @closing_started = true
273
+ end
274
+
275
+ def close_socket()
276
+ @socket.close()
277
+ end
278
+
279
+ private
280
+
281
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
282
+
283
+ def read_header()
284
+ @header = {}
285
+ while line = gets()
286
+ line = line.chomp()
287
+ break if line.empty?
288
+ if !(line =~ /\A(\S+): (.*)\z/n)
289
+ raise(WebSocket::Error, "invalid request: #{line}")
290
+ end
291
+ @header[$1] = $2
292
+ @header[$1.downcase()] = $2
293
+ end
294
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
295
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
296
+ end
297
+ if !(@header["connection"] =~ /\AUpgrade\z/i)
298
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
299
+ end
300
+ end
301
+
302
+ def send_frame(opcode, payload, mask)
303
+ payload = force_encoding(payload.dup(), "ASCII-8BIT")
304
+ # Setting StringIO's encoding to ASCII-8BIT.
305
+ buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
306
+ write_byte(buffer, 0x80 | opcode)
307
+ masked_byte = mask ? 0x80 : 0x00
308
+ if payload.bytesize <= 125
309
+ write_byte(buffer, masked_byte | payload.bytesize)
310
+ elsif payload.bytesize < 2 ** 16
311
+ write_byte(buffer, masked_byte | 126)
312
+ buffer.write([payload.bytesize].pack("n"))
313
+ else
314
+ write_byte(buffer, masked_byte | 127)
315
+ buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
316
+ end
317
+ if mask
318
+ mask_key = Array.new(4){ rand(256) }
319
+ buffer.write(mask_key.pack("C*"))
320
+ payload = apply_mask(payload, mask_key)
321
+ end
322
+ buffer.write(payload)
323
+ write(buffer.string)
324
+ end
325
+
326
+ def gets(rs = $/)
327
+ line = @socket.gets(rs)
328
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
329
+ return line
330
+ end
331
+
332
+ def read(num_bytes)
333
+ str = @socket.read(num_bytes)
334
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
335
+ return str
336
+ end
337
+
338
+ def write(data)
339
+ if WebSocket.debug
340
+ data.scan(/\G(.*?(\n|\z))/n) do
341
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
342
+ end
343
+ end
344
+ @socket.write(data)
345
+ end
346
+
347
+ def flush()
348
+ @socket.flush()
349
+ end
350
+
351
+ def write_byte(buffer, byte)
352
+ buffer.write([byte].pack("C"))
353
+ end
354
+
355
+ def security_digest(key)
356
+ return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
357
+ end
358
+
359
+ def hixie_76_security_digest(key1, key2, key3)
360
+ bytes1 = websocket_key_to_bytes(key1)
361
+ bytes2 = websocket_key_to_bytes(key2)
362
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
363
+ end
364
+
365
+ def apply_mask(payload, mask_key)
366
+ orig_bytes = payload.unpack("C*")
367
+ new_bytes = []
368
+ orig_bytes.each_with_index() do |b, i|
369
+ new_bytes.push(b ^ mask_key[i % 4])
370
+ end
371
+ return new_bytes.pack("C*")
372
+ end
373
+
374
+ def generate_key()
375
+ spaces = 1 + rand(12)
376
+ max = 0xffffffff / spaces
377
+ number = rand(max + 1)
378
+ key = (number * spaces).to_s()
379
+ (1 + rand(12)).times() do
380
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
381
+ pos = rand(key.size + 1)
382
+ key[pos...pos] = char
383
+ end
384
+ spaces.times() do
385
+ pos = 1 + rand(key.size - 1)
386
+ key[pos...pos] = " "
387
+ end
388
+ return key
389
+ end
390
+
391
+ def generate_key3()
392
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
393
+ end
394
+
395
+ def websocket_key_to_bytes(key)
396
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
397
+ return [num].pack("N")
398
+ end
399
+
400
+ def force_encoding(str, encoding)
401
+ if str.respond_to?(:force_encoding)
402
+ return str.force_encoding(encoding)
403
+ else
404
+ return str
405
+ end
406
+ end
407
+
408
+ def ssl_handshake(socket)
409
+ ssl_context = OpenSSL::SSL::SSLContext.new()
410
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
411
+ ssl_socket.sync_close = true
412
+ ssl_socket.connect()
413
+ return ssl_socket
414
+ end
415
+
416
+ end
417
+
418
+
419
+ class WebSocketServer
420
+
421
+ def initialize(params_or_uri, params = nil)
422
+ if params
423
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
424
+ params[:port] ||= uri.port
425
+ params[:accepted_domains] ||= [uri.host]
426
+ else
427
+ params = params_or_uri
428
+ end
429
+ @port = params[:port] || 80
430
+ @accepted_domains = params[:accepted_domains]
431
+ if !@accepted_domains
432
+ raise(ArgumentError, "params[:accepted_domains] is required")
433
+ end
434
+ if params[:host]
435
+ @tcp_server = TCPServer.open(params[:host], @port)
436
+ else
437
+ @tcp_server = TCPServer.open(@port)
438
+ end
439
+ end
440
+
441
+ attr_reader(:tcp_server, :port, :accepted_domains)
442
+
443
+ def run(&block)
444
+ while true
445
+ Thread.start(accept()) do |s|
446
+ begin
447
+ ws = create_web_socket(s)
448
+ yield(ws) if ws
449
+ rescue => ex
450
+ print_backtrace(ex)
451
+ ensure
452
+ begin
453
+ ws.close_socket() if ws
454
+ rescue
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ def accept()
462
+ return @tcp_server.accept()
463
+ end
464
+
465
+ def accepted_origin?(origin)
466
+ domain = origin_to_domain(origin)
467
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
468
+ end
469
+
470
+ def origin_to_domain(origin)
471
+ if origin == "null" || origin == "file://" # local file
472
+ return "null"
473
+ else
474
+ return URI.parse(origin).host
475
+ end
476
+ end
477
+
478
+ def create_web_socket(socket)
479
+ ch = socket.getc()
480
+ if ch == ?<
481
+ # This is Flash socket policy file request, not an actual Web Socket connection.
482
+ send_flash_socket_policy_file(socket)
483
+ return nil
484
+ else
485
+ socket.ungetc(ch)
486
+ return WebSocket.new(socket, :server => self)
487
+ end
488
+ end
489
+
490
+ private
491
+
492
+ def print_backtrace(ex)
493
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
494
+ for s in ex.backtrace[1..-1]
495
+ $stderr.printf(" %s\n", s)
496
+ end
497
+ end
498
+
499
+ # Handles Flash socket policy file request sent when web-socket-js is used:
500
+ # http://github.com/gimite/web-socket-js/tree/master
501
+ def send_flash_socket_policy_file(socket)
502
+ socket.puts('<?xml version="1.0"?>')
503
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
504
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
505
+ socket.puts('<cross-domain-policy>')
506
+ for domain in @accepted_domains
507
+ next if domain == "file://"
508
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
509
+ end
510
+ socket.puts('</cross-domain-policy>')
511
+ socket.close()
512
+ end
513
+
514
+ end
515
+
516
+
517
+ if __FILE__ == $0
518
+ Thread.abort_on_exception = true
519
+
520
+ if ARGV[0] == "server" && ARGV.size == 3
521
+
522
+ server = WebSocketServer.new(
523
+ :accepted_domains => [ARGV[1]],
524
+ :port => ARGV[2].to_i())
525
+ puts("Server is running at port %d" % server.port)
526
+ server.run() do |ws|
527
+ puts("Connection accepted")
528
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
529
+ if ws.path == "/"
530
+ ws.handshake()
531
+ while data = ws.receive()
532
+ printf("Received: %p\n", data)
533
+ ws.send(data)
534
+ printf("Sent: %p\n", data)
535
+ end
536
+ else
537
+ ws.handshake("404 Not Found")
538
+ end
539
+ puts("Connection closed")
540
+ end
541
+
542
+ elsif ARGV[0] == "client" && ARGV.size == 2
543
+
544
+ client = WebSocket.new(ARGV[1])
545
+ puts("Connected")
546
+ Thread.new() do
547
+ while data = client.receive()
548
+ printf("Received: %p\n", data)
549
+ end
550
+ end
551
+ $stdin.each_line() do |line|
552
+ data = line.chomp()
553
+ client.send(data)
554
+ printf("Sent: %p\n", data)
555
+ end
556
+
557
+ else
558
+
559
+ $stderr.puts("Usage:")
560
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
561
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
562
+ exit(1)
563
+
564
+ end
565
+ end
@@ -1,3 +1,3 @@
1
1
  module EventMachine
2
- WS_REQUEST_VERSION = "0.0.1"
2
+ WS_REQUEST_VERSION = "0.0.3"
3
3
  end
@@ -28,6 +28,15 @@ module EventMachine
28
28
  return str
29
29
  end
30
30
 
31
+ # Called on a CLOSE frame
32
+ def close(by_peer = false)
33
+ super(by_peer)
34
+ @client.unbind
35
+ end
36
+
37
+ # Used for Sec-WebSocket-Key and Sec-WebSocket-Accept auth
38
+ public :security_digest, :generate_key
39
+
31
40
  private
32
41
  FRAME_TYPES = {
33
42
  :continuation => 0,
metadata CHANGED
@@ -1,34 +1,38 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: em-websocket-request
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.1
3
+ version: !ruby/object:Gem::Version
5
4
  prerelease:
5
+ version: 0.0.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Michael Rykov
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-12 00:00:00.000000000Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
12
+
13
+ date: 2011-10-13 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
15
16
  name: em-http-request
16
- requirement: &70329779717660 !ruby/object:Gem::Requirement
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
17
19
  none: false
18
- requirements:
20
+ requirements:
19
21
  - - ~>
20
- - !ruby/object:Gem::Version
22
+ - !ruby/object:Gem::Version
21
23
  version: 1.0.0
22
24
  type: :runtime
23
- prerelease: false
24
- version_requirements: *70329779717660
25
+ version_requirements: *id001
25
26
  description: EventMachine WebSocket client
26
- email:
27
+ email:
27
28
  - mrykov@gmail.com
28
29
  executables: []
30
+
29
31
  extensions: []
32
+
30
33
  extra_rdoc_files: []
31
- files:
34
+
35
+ files:
32
36
  - .gitignore
33
37
  - .gitmodules
34
38
  - Gemfile
@@ -39,29 +43,33 @@ files:
39
43
  - lib/em-ws-request/version.rb
40
44
  - lib/em-ws-request/wrapper.rb
41
45
  - spec/websocket_spec.rb
42
- homepage: ''
46
+ - lib/em-ws-request/vendor/web-socket-ruby/lib/web_socket.rb
47
+ homepage: ""
43
48
  licenses: []
49
+
44
50
  post_install_message:
45
51
  rdoc_options: []
46
- require_paths:
52
+
53
+ require_paths:
47
54
  - lib
48
- required_ruby_version: !ruby/object:Gem::Requirement
55
+ required_ruby_version: !ruby/object:Gem::Requirement
49
56
  none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
62
  none: false
56
- requirements:
57
- - - ! '>='
58
- - !ruby/object:Gem::Version
59
- version: '0'
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
60
67
  requirements: []
68
+
61
69
  rubyforge_project:
62
70
  rubygems_version: 1.8.11
63
71
  signing_key:
64
72
  specification_version: 3
65
73
  summary: EventMachine WebSocket client
66
- test_files:
74
+ test_files:
67
75
  - spec/websocket_spec.rb