socky-client 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ Changelog
2
+ =========
3
+
4
+ ## 0.4.2 / 2010-10-29
5
+
6
+ - new features:
7
+ - change config_path and config from constant to method
8
+ - bugfixes:
9
+ - none
10
+
11
+ ## 0.4.1 / 2010-10-28
12
+
13
+ - new features:
14
+ - none
15
+ - bugfixes:
16
+ - require by 'socky-client' to stop interfering with socky-ruby-server
17
+ - return 'true' after successful sending message
18
+
19
+ ## 0.4.0 / 2010-10-28
20
+
21
+ - new features:
22
+ - split project to 3 parts - socky-client-ruby, socky-client-rails and socky-js
23
+ - release as gem
24
+ - bugfixes:
25
+ - none
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ Socky - client in Ruby
2
+ ===========
3
+
4
+ Socky is push server for Ruby based on WebSockets. It allows you to break border between your application and client browser by letting the server initialize a connection and push data to the client.
5
+
6
+ ## Getting Started
7
+
8
+ - [Install](http://github.com/socky/socky-client-ruby/wiki/install) the gem
9
+ - Read up about its [Usage](http://github.com/socky/socky-client-ruby/wiki/usage) and [Configuration](http://github.com/socky/socky-client-ruby/wiki/configuration)
10
+ - Try [Example Application](http://github.com/socky/socky-example) or [demo page](http://sockydemo.imanel.org)
11
+ - Fork and Contribute your own modifications
12
+ - See [sites using socky](http://github.com/socky/socky-server-ruby/wiki/sites)
13
+ - Discuss on [google group](http://groups.google.com/group/socky-framework)
14
+
15
+ ## License
16
+
17
+ (The MIT License)
18
+
19
+ Copyright (c) 2010 Bernard Potocki
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ CLEAN.include %w(**/*.{log,rbc})
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ task :default => :spec
8
+
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ end
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |gemspec|
15
+ gemspec.name = "socky-client"
16
+ gemspec.summary = "Socky is a WebSocket server and client for Ruby"
17
+ gemspec.description = "Socky is a WebSocket server and client for Ruby"
18
+ gemspec.email = "bernard.potocki@imanel.org"
19
+ gemspec.homepage = "http://imanel.org/projects/socky"
20
+ gemspec.authors = ["Bernard Potocki"]
21
+ gemspec.add_dependency 'json'
22
+ gemspec.add_development_dependency 'rspec', '~> 2.0'
23
+ gemspec.files.exclude ".gitignore"
24
+ end
25
+ rescue LoadError
26
+ puts "Jeweler not available. Install it with: gem install jeweler"
27
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.2
@@ -0,0 +1,116 @@
1
+ require 'yaml'
2
+ require 'json'
3
+
4
+ require File.dirname(__FILE__) + '/socky-client/websocket'
5
+
6
+ module Socky
7
+
8
+ class << self
9
+
10
+ attr_accessor :config_path
11
+ def config_path
12
+ @config_path ||= 'socky_hosts.yml'
13
+ end
14
+
15
+ def config
16
+ @config ||= YAML.load_file(config_path).freeze
17
+ end
18
+
19
+ def send(*args)
20
+ options = normalize_options(*args)
21
+ send_message(options.delete(:data), options)
22
+ end
23
+
24
+ def show_connections
25
+ send_query(:show_connections)
26
+ end
27
+
28
+ def hosts
29
+ config[:hosts]
30
+ end
31
+
32
+ private
33
+
34
+ def normalize_options(data, options = {})
35
+ case data
36
+ when Hash
37
+ options, data = data, nil
38
+ when String, Symbol
39
+ options[:data] = data
40
+ else
41
+ options.merge!(:data => data)
42
+ end
43
+
44
+ options[:data] = options[:data].to_s
45
+ options
46
+ end
47
+
48
+ def send_message(data, opts = {})
49
+ to = opts[:to] || {}
50
+ except = opts[:except] || {}
51
+
52
+ unless to.is_a?(Hash) && except.is_a?(Hash)
53
+ raise "recipiend data should be in hash format"
54
+ end
55
+
56
+ to_clients = to[:client] || to[:clients]
57
+ to_channels = to[:channel] || to[:channels]
58
+ except_clients = except[:client] || except[:clients]
59
+ except_channels = except[:channel] || except[:channels]
60
+
61
+ # If clients or channels are non-nil but empty then there's no users to target message
62
+ return if (to_clients.is_a?(Array) && to_clients.empty?) || (to_channels.is_a?(Array) && to_channels.empty?)
63
+
64
+ hash = {
65
+ :command => :broadcast,
66
+ :body => data,
67
+ :to => {
68
+ :clients => to_clients,
69
+ :channels => to_channels,
70
+ },
71
+ :except => {
72
+ :clients => except_clients,
73
+ :channels => except_channels,
74
+ }
75
+ }
76
+
77
+ [:to, :except].each do |type|
78
+ hash[type].reject! { |key,val| val.nil? || (type == :except && val.empty?)}
79
+ hash.delete(type) if hash[type].empty?
80
+ end
81
+
82
+ send_data(hash)
83
+ end
84
+
85
+ def send_query(type)
86
+ hash = {
87
+ :command => :query,
88
+ :type => type
89
+ }
90
+ send_data(hash, true)
91
+ end
92
+
93
+ def send_data(hash, response = false)
94
+ res = []
95
+ hosts.each do |address|
96
+ begin
97
+ scheme = (address[:secure] ? "wss" : "ws")
98
+ @socket = WebSocket.new("#{scheme}://#{address[:host]}:#{address[:port]}/?admin=1&client_secret=#{address[:secret]}")
99
+ @socket.send(hash.to_json)
100
+ res << @socket.receive if response
101
+ rescue
102
+ puts "ERROR: Connection to server at '#{scheme}://#{address[:host]}:#{address[:port]}' failed"
103
+ ensure
104
+ @socket.close if @socket && !@socket.tcp_socket.closed?
105
+ end
106
+ end
107
+ if response
108
+ res.collect {|r| JSON.parse(r)["body"] }
109
+ else
110
+ true
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,230 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
4
+
5
+ require "socket"
6
+ require "uri"
7
+ require "digest/md5"
8
+ require "openssl"
9
+
10
+
11
+ class WebSocket
12
+
13
+ class << self
14
+
15
+ attr_accessor(:debug)
16
+
17
+ end
18
+
19
+ class Error < RuntimeError
20
+
21
+ end
22
+
23
+ def initialize(arg, params = {})
24
+
25
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
26
+
27
+ if uri.scheme == "ws"
28
+ default_port = 80
29
+ elsif uri.scheme = "wss"
30
+ default_port = 443
31
+ else
32
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
33
+ end
34
+
35
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
36
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
37
+ origin = params[:origin] || "http://#{uri.host}"
38
+ key1 = generate_key()
39
+ key2 = generate_key()
40
+ key3 = generate_key3()
41
+
42
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
43
+
44
+ if uri.scheme == "ws"
45
+ @socket = socket
46
+ else
47
+ @socket = ssl_handshake(socket)
48
+ end
49
+
50
+ write(
51
+ "GET #{@path} HTTP/1.1\r\n" +
52
+ "Upgrade: WebSocket\r\n" +
53
+ "Connection: Upgrade\r\n" +
54
+ "Host: #{host}\r\n" +
55
+ "Origin: #{origin}\r\n" +
56
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
57
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
58
+ "\r\n" +
59
+ "#{key3}")
60
+ flush()
61
+
62
+ line = gets().chomp()
63
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
64
+ read_header()
65
+ if @header["Sec-WebSocket-Origin"] != origin
66
+ raise(WebSocket::Error,
67
+ "origin doesn't match: '#{@header["WebSocket-Origin"]}' != '#{origin}'")
68
+ end
69
+ reply_digest = read(16)
70
+ expected_digest = security_digest(key1, key2, key3)
71
+ if reply_digest != expected_digest
72
+ raise(WebSocket::Error,
73
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
74
+ end
75
+ @handshaked = true
76
+
77
+ @closing_started = false
78
+ end
79
+
80
+ attr_reader(:header, :path)
81
+
82
+ def send(data)
83
+ if !@handshaked
84
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
85
+ end
86
+ data = force_encoding(data.dup(), "ASCII-8BIT")
87
+ write("\x00#{data}\xff")
88
+ flush()
89
+ end
90
+
91
+ def receive()
92
+ if !@handshaked
93
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
94
+ end
95
+ packet = gets("\xff")
96
+ return nil if !packet
97
+ if packet =~ /\A\x00(.*)\xff\z/nm
98
+ return force_encoding($1, "UTF-8")
99
+ elsif packet == "\xff" && read(1) == "\x00" # closing
100
+ if @server
101
+ @socket.close()
102
+ else
103
+ close()
104
+ end
105
+ return nil
106
+ else
107
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
108
+ end
109
+ end
110
+
111
+ def tcp_socket
112
+ return @socket
113
+ end
114
+
115
+ def host
116
+ return @header["Host"]
117
+ end
118
+
119
+ def origin
120
+ return @header["Origin"]
121
+ end
122
+
123
+ # Does closing handshake.
124
+ def close()
125
+ return if @closing_started
126
+ write("\xff\x00")
127
+ @socket.close() if !@server
128
+ @closing_started = true
129
+ end
130
+
131
+ def close_socket()
132
+ @socket.close()
133
+ end
134
+
135
+ private
136
+
137
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
138
+
139
+ def read_header()
140
+ @header = {}
141
+ while line = gets()
142
+ line = line.chomp()
143
+ break if line.empty?
144
+ if !(line =~ /\A(\S+): (.*)\z/n)
145
+ raise(WebSocket::Error, "invalid request: #{line}")
146
+ end
147
+ @header[$1] = $2
148
+ end
149
+ if @header["Upgrade"] != "WebSocket"
150
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["Upgrade"])
151
+ end
152
+ if @header["Connection"] != "Upgrade"
153
+ raise(WebSocket::Error, "invalid Connection: " + @header["Connection"])
154
+ end
155
+ end
156
+
157
+ def gets(rs = $/)
158
+ line = @socket.gets(rs)
159
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
160
+ return line
161
+ end
162
+
163
+ def read(num_bytes)
164
+ str = @socket.read(num_bytes)
165
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
166
+ return str
167
+ end
168
+
169
+ def write(data)
170
+ if WebSocket.debug
171
+ data.scan(/\G(.*?(\n|\z))/n) do
172
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
173
+ end
174
+ end
175
+ @socket.write(data)
176
+ end
177
+
178
+ def flush()
179
+ @socket.flush()
180
+ end
181
+
182
+ def security_digest(key1, key2, key3)
183
+ bytes1 = websocket_key_to_bytes(key1)
184
+ bytes2 = websocket_key_to_bytes(key2)
185
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
186
+ end
187
+
188
+ def generate_key()
189
+ spaces = 1 + rand(12)
190
+ max = 0xffffffff / spaces
191
+ number = rand(max + 1)
192
+ key = (number * spaces).to_s()
193
+ (1 + rand(12)).times() do
194
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
195
+ pos = rand(key.size + 1)
196
+ key[pos...pos] = char
197
+ end
198
+ spaces.times() do
199
+ pos = 1 + rand(key.size - 1)
200
+ key[pos...pos] = " "
201
+ end
202
+ return key
203
+ end
204
+
205
+ def generate_key3()
206
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
207
+ end
208
+
209
+ def websocket_key_to_bytes(key)
210
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
211
+ return [num].pack("N")
212
+ end
213
+
214
+ def force_encoding(str, encoding)
215
+ if str.respond_to?(:force_encoding)
216
+ return str.force_encoding(encoding)
217
+ else
218
+ return str
219
+ end
220
+ end
221
+
222
+ def ssl_handshake(socket)
223
+ ssl_context = OpenSSL::SSL::SSLContext.new()
224
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
225
+ ssl_socket.sync_close = true
226
+ ssl_socket.connect()
227
+ return ssl_socket
228
+ end
229
+
230
+ end
data/socky_hosts.yml ADDED
@@ -0,0 +1,5 @@
1
+ :hosts:
2
+ - :host: 127.0.0.1
3
+ :port: 8080
4
+ :secret: my_secret_key
5
+ :secure: false
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ describe Socky do
4
+ it "should have config in hash form" do
5
+ Socky.config.should_not be_nil
6
+ Socky.config.class.should eql(Hash)
7
+ end
8
+
9
+ it "should have host list taken from config" do
10
+ Socky.hosts.should eql(Socky.config[:hosts])
11
+ end
12
+
13
+ context "#send" do
14
+ before(:each) do
15
+ Socky.stub!(:send_data)
16
+ end
17
+ it "should send broadcast with data" do
18
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
19
+ Socky.send("test")
20
+ end
21
+ context "should normalize options" do
22
+ it "when nil given" do
23
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => ""})
24
+ Socky.send(nil)
25
+ end
26
+ it "when string given" do
27
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
28
+ Socky.send("test")
29
+ end
30
+ it "when hash given" do
31
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
32
+ Socky.send({:data => "test"})
33
+ end
34
+ it "when hash without body given" do
35
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => ""})
36
+ Socky.send({})
37
+ end
38
+ end
39
+ context "should handle recipient conditions for" do
40
+ it ":to => :client" do
41
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :to => { :clients => "first" }})
42
+ Socky.send("test", :to => { :client => "first" })
43
+ end
44
+ it ":to => :clients" do
45
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :to => { :clients => ["first","second"] }})
46
+ Socky.send("test", :to => { :clients => ["first","second"] })
47
+ end
48
+ it ":to => :channel" do
49
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :to => { :channels => "first" }})
50
+ Socky.send("test", :to => { :channel => "first" })
51
+ end
52
+ it ":to => :channels" do
53
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :to => { :channels => ["first","second"] }})
54
+ Socky.send("test", :to => { :channels => ["first","second"] })
55
+ end
56
+ it ":except => :client" do
57
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :except => { :clients => "first" }})
58
+ Socky.send("test", :except => { :client => "first" })
59
+ end
60
+ it ":except => :clients" do
61
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :except => { :clients => ["first","second"] }})
62
+ Socky.send("test", :except => { :clients => ["first","second"] })
63
+ end
64
+ it ":except => :channel" do
65
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :except => { :channels => "first" }})
66
+ Socky.send("test", :except => { :channel => "first" })
67
+ end
68
+ it ":except => :channels" do
69
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test", :except => { :channels => ["first","second"] }})
70
+ Socky.send("test", :except => { :channels => ["first","second"] })
71
+ end
72
+ it "combination" do
73
+ Socky.should_receive(:send_data).with({
74
+ :command => :broadcast,
75
+ :body => "test",
76
+ :to => {
77
+ :clients => "allowed_user",
78
+ :channels => "allowed_channel"
79
+ },
80
+ :except => {
81
+ :clients => "disallowed_user",
82
+ :channels => "disallowed_channel"
83
+ }
84
+ })
85
+ Socky.send("test", :to => {
86
+ :clients => "allowed_user",
87
+ :channels => "allowed_channel"
88
+ },
89
+ :except => {
90
+ :clients => "disallowed_user",
91
+ :channels => "disallowed_channel"
92
+ })
93
+ end
94
+ end
95
+ context "should ignore nil value for" do
96
+ it ":to => :clients" do
97
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
98
+ Socky.send("test", :to => { :clients => nil })
99
+ end
100
+ it ":to => :channels" do
101
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
102
+ Socky.send("test", :to => { :channels => nil })
103
+ end
104
+ it ":except => :clients" do
105
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
106
+ Socky.send("test", :except => { :clients => nil })
107
+ end
108
+ it ":except => :channels" do
109
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
110
+ Socky.send("test", :except => { :channels => nil })
111
+ end
112
+ end
113
+ context "should handle empty array for" do
114
+ it ":to => :clients by not sending message" do
115
+ Socky.should_not_receive(:send_data)
116
+ Socky.send("test", :to => { :clients => [] })
117
+ end
118
+ it ":to => :channels by not sending message" do
119
+ Socky.should_not_receive(:send_data)
120
+ Socky.send("test", :to => { :channels => [] })
121
+ end
122
+ it ":except => :clients by ignoring it" do
123
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
124
+ Socky.send("test", :except => { :clients => [] })
125
+ end
126
+ it ":except => :channels by ignoring it" do
127
+ Socky.should_receive(:send_data).with({:command => :broadcast, :body => "test"})
128
+ Socky.send("test", :except => { :channels => [] })
129
+ end
130
+ end
131
+ end
132
+
133
+ context "#show_connections" do
134
+ before(:each) do
135
+ Socky.stub!(:send_data)
136
+ end
137
+ it "should send query :show_connections" do
138
+ Socky.should_receive(:send_data).with({:command => :query, :type => :show_connections}, true)
139
+ Socky.show_connections
140
+ end
141
+ end
142
+
143
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+
4
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
5
+ require 'socky-client'
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: socky-client
3
+ version: !ruby/object:Gem::Version
4
+ hash: 11
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 2
10
+ version: 0.4.2
11
+ platform: ruby
12
+ authors:
13
+ - Bernard Potocki
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-29 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 2
46
+ - 0
47
+ version: "2.0"
48
+ type: :development
49
+ version_requirements: *id002
50
+ description: Socky is a WebSocket server and client for Ruby
51
+ email: bernard.potocki@imanel.org
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files:
57
+ - README.md
58
+ files:
59
+ - CHANGELOG.md
60
+ - README.md
61
+ - Rakefile
62
+ - VERSION
63
+ - lib/socky-client.rb
64
+ - lib/socky-client/websocket.rb
65
+ - socky_hosts.yml
66
+ - spec/socky-client_spec.rb
67
+ - spec/spec_helper.rb
68
+ has_rdoc: true
69
+ homepage: http://imanel.org/projects/socky
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --charset=UTF-8
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.3.7
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Socky is a WebSocket server and client for Ruby
102
+ test_files:
103
+ - spec/socky-client_spec.rb
104
+ - spec/spec_helper.rb