em-websocket-request 0.0.1 → 0.0.3

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.
@@ -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