garufa 0.0.1.alpha.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e75a892c17eda812e7d24f2eb34282c2cb34b992
4
+ data.tar.gz: c2a7430113bb000c5c2c061a70c8f1cbe24bbe8e
5
+ SHA512:
6
+ metadata.gz: 1bb546c89b9fb29bf16bfd7bcdf9c883aeedc9fef507cf33b2b6b353567c3cbadf67d073b639df3157bb412ddc1a383d8d51b2d02e6854a1e87c0edd6660a014
7
+ data.tar.gz: 40f0cdd7e0a2c2e6246de48332e082190be57ec66fd297806c5fc80cdddfc79cf34efe099d8b57954d731e53613fa94a0267e72e8d35951be22c40e20104ef1c
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Juan Manuel Cuello
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ Garufa
2
+ ====
3
+
4
+ A websocket server compatible with the Pusher protocol.
5
+
6
+ **IMPORTANT:** Garufa is currently in alpha version, which means it is not
7
+ production ready, but you are free to use it and test it. Any feedback is
8
+ welcome.
9
+
10
+ Intro
11
+ -----
12
+
13
+ Garufa is a websocket server which implements the [Pusher][pusher] protocol. It
14
+ was built on top of [Goliath][goliath], a high performance non-blocking web
15
+ server.
16
+
17
+ It was based on [Slanger][slanger], another server compatible with Pusher.
18
+
19
+ [pusher]: http://pusher.com
20
+ [goliath]: https://github.com/postrank-labs/goliath/
21
+ [slanger]: https://github.com/stevegraham/slanger
22
+
23
+ Install
24
+ -------
25
+
26
+ Make sure you have a ruby version >= 1.9.2
27
+
28
+ ``` console
29
+ $ gem install garufa --pre
30
+
31
+ $ garufa --help
32
+
33
+ $ garufa -sv -p 4567 --app_key my-application-key --secret my-secret-string
34
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.pattern = "test/*.rb"
5
+ end
6
+
7
+ task default: :test
data/bin/garufa ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'goliath/runner'
4
+ require 'garufa'
5
+
6
+ module Garufa
7
+ runner = Goliath::Runner.new(ARGV, GarufaApp.new)
8
+ runner.app = Goliath::Rack::Builder.build(GarufaApp, runner.api)
9
+
10
+ if runner.daemonize
11
+ runner.log_file ||= './garufa.log'
12
+ runner.pid_file ||= './garufa.pid'
13
+ end
14
+
15
+ runner.run
16
+ end
data/bin/garufa.log ADDED
@@ -0,0 +1 @@
1
+ [4707:INFO] 2013-12-21 18:43:01 :: Starting server on 0.0.0.0:9000 in development mode. Watch out for stones.
@@ -0,0 +1,2 @@
1
+ terminate called after throwing an instance of 'std::runtime_error'
2
+ what(): setuid_string failed: no setuid
data/bin/garufa.pid ADDED
@@ -0,0 +1 @@
1
+ 4707
data/garufa.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require 'garufa/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "garufa"
8
+ s.version = Garufa::VERSION
9
+ s.summary = "Websocket server compatible with Pusher."
10
+ s.description = "Garufa is a websocket server compatible with the Pusher service protocol."
11
+ s.authors = ["Juan Manuel Cuello"]
12
+ s.email = ["juanmacuello@gmail.com"]
13
+ s.homepage = "http://github.com/Juanmcuello/garufa"
14
+ s.bindir = 'bin'
15
+ s.executables << 'garufa'
16
+
17
+ s.files = Dir[
18
+ "LICENSE",
19
+ "README.md",
20
+ "Rakefile",
21
+ "lib/**/*.rb",
22
+ "bin/*",
23
+ "*.gemspec",
24
+ "test/*.*"
25
+ ]
26
+
27
+ s.add_dependency "goliath"
28
+ s.add_dependency "faye-websocket"
29
+ s.add_dependency "cuba"
30
+ s.add_dependency "signature"
31
+ end
data/lib/garufa.rb ADDED
@@ -0,0 +1 @@
1
+ require 'garufa/garufa_app'
@@ -0,0 +1,35 @@
1
+ require 'cuba'
2
+ require 'garufa/cuba/authentication'
3
+ require 'garufa/message'
4
+ require 'garufa/subscriptions'
5
+
6
+ module Garufa
7
+ Cuba.plugin Cuba::Authentication
8
+
9
+ ApiServer = Cuba.new do
10
+
11
+ on "apps/:app_id" do |app_id|
12
+
13
+ authenticate
14
+
15
+ # Events
16
+ on post, "events" do
17
+ message = Message.new(JSON.parse(req.body.read))
18
+ options = { data: message.data, socket_id: message.socket_id }
19
+ Subscriptions.notify message.channels, message.name, options
20
+ res.write "{}"
21
+ end
22
+
23
+ # Channels
24
+ on get, "channels" do
25
+ end
26
+
27
+ on get, "channels/:channel" do
28
+ end
29
+
30
+ # Users
31
+ on get, "channels/:channel/users" do
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module Garufa
2
+ module Config
3
+ extend self
4
+
5
+ def [](key)
6
+ settings[key]
7
+ end
8
+
9
+ def []=(key, value)
10
+ settings[key] = value
11
+ end
12
+
13
+ def settings
14
+ @settings ||= {}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ require 'uri'
2
+
3
+ module Garufa
4
+ class Connection
5
+
6
+ attr_reader :socket_id
7
+
8
+ def initialize(socket, logger)
9
+ @socket = socket
10
+ @logger = logger
11
+ @socket_id = SecureRandom.uuid
12
+ @subscriptions = {}
13
+ end
14
+
15
+ def establish
16
+ if valid_app_key?
17
+ send_message Message.connection_established(@socket_id)
18
+ else
19
+ error(4001, "Could not find app by key #{app_key}")
20
+ close
21
+ end
22
+ end
23
+
24
+ def handle_incomming_data(data)
25
+ @logger.debug "Incomming message. #{@socket_id}: #{data}"
26
+
27
+ message = Message.new(JSON.parse(data))
28
+ event, data = message.event, message.data
29
+
30
+ case event
31
+ when /^pusher:/
32
+ handle_pusher_event(event, data)
33
+ when /^client-/
34
+ handle_client_event(event, data)
35
+ end
36
+ end
37
+
38
+ def error(code, message)
39
+ send_message Message.error(code, message)
40
+ end
41
+
42
+ def send_message(message)
43
+ @logger.debug "Outgoing message: #{message.to_json}"
44
+
45
+ @socket.send message.to_json
46
+ end
47
+
48
+ def close
49
+ @socket.close
50
+ end
51
+
52
+ private
53
+
54
+ def handle_pusher_event(event, data)
55
+ accepted_events = %w{ping pong subscribe unsubscribe}
56
+ event_name = event.partition(':').last
57
+
58
+ if accepted_events.include?(event_name)
59
+ method("pusher_#{event_name}").call data
60
+ end
61
+ end
62
+
63
+ def handle_client_event(event, data)
64
+ # NOTE: not supported yet
65
+ error(nil, 'Client events are not supported yet')
66
+ end
67
+
68
+ def pusher_ping(data)
69
+ send_message Message.pong
70
+ end
71
+
72
+ def pusher_pong(data)
73
+ # There is nothing to do with a pong message
74
+ end
75
+
76
+ def pusher_subscribe(data)
77
+ subscription = Subscription.new(data, self)
78
+ subscription.subscribe
79
+
80
+ if subscription.success?
81
+ @subscriptions[subscription.channel] = subscription
82
+ send_subscription_succeeded(subscription) unless subscription.public_channel?
83
+ else
84
+ error(subscription.error.code, subscription.error.message)
85
+ end
86
+ end
87
+
88
+ def pusher_unsubscribe(data)
89
+ subscription = @subscriptions.delete data["channel"]
90
+ subscription.unsubscribe if subscription
91
+ end
92
+
93
+ def valid_app_key?
94
+ app_key && (app_key == Config[:app_key])
95
+ end
96
+
97
+ def app_key
98
+ @app_key ||= URI.parse(@socket.url).path.split('/').last
99
+ end
100
+
101
+ def send_subscription_succeeded(subscription)
102
+ send_message Message.subscription_succeeded(subscription.channel)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,14 @@
1
+ require 'signature'
2
+ require 'garufa/config'
3
+
4
+ class Cuba
5
+ module Authentication
6
+ def authenticate
7
+ request = Signature::Request.new(req.request_method, env['REQUEST_PATH'], req.params)
8
+ request.authenticate { |key| Signature::Token.new(key, Garufa::Config[:secret]) }
9
+
10
+ rescue Signature::AuthenticationError
11
+ halt([401, {}, ['401 Unauthorized']])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # This is included only to provide a fix when the 'env' was not set.
2
+ # This fix was already included in 'master' of faye-websocket-ruby,
3
+ # but not released as a gem yet.
4
+ #
5
+ # See https://github.com/faye/faye-websocket-ruby/issues/38
6
+ #
7
+ module Faye
8
+ class WebSocket
9
+ module Adapter
10
+ def websocket?
11
+ e = defined?(@env) ? @env : env
12
+ e && WebSocket.websocket?(e)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ require 'goliath/api'
2
+ require 'goliath/connection'
3
+ require 'faye/websocket'
4
+ require 'garufa/config'
5
+ require 'garufa/ws_server'
6
+ require 'garufa/api_server'
7
+
8
+ # Remove this require after next release of faye-websocket-ruby.
9
+ # See https://github.com/faye/faye-websocket-ruby/issues/38
10
+ require 'garufa/faye_websocket_patch'
11
+
12
+ module Garufa
13
+ Faye::WebSocket.load_adapter('goliath')
14
+
15
+ class GarufaApp < Goliath::API
16
+
17
+ # Extend goliath options with our own options.
18
+ def options_parser(opts, options)
19
+ opts.separator ""
20
+ opts.separator "Pusher options:"
21
+
22
+ new_options = {
23
+ app_key: ['--app_key APP_KEY', 'Pusher application key (required)'],
24
+ secret: ['--secret SECRET', 'Pusher application secret (required)']
25
+ }
26
+ new_options.each do |k, v|
27
+ opts.on(v.first, v.last) { |value| Garufa::Config[k] = value }
28
+ end
29
+ end
30
+
31
+ def response(env)
32
+ if Faye::WebSocket.websocket?(env)
33
+ WsServer.call(env)
34
+ else
35
+ ApiServer.call(env)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ require 'json'
2
+
3
+ module Garufa
4
+ class Message
5
+
6
+ ACTIVITY_TIMEOUT = 120
7
+
8
+ ATTRIBUTES = [:channels, :channel, :event, :data, :name, :socket_id]
9
+
10
+ def initialize(attributes)
11
+ @attributes = ATTRIBUTES.each_with_object({}) do |key, hash|
12
+ hash[key] = attributes[key] || attributes[key.to_s]
13
+ end
14
+
15
+ @attributes.each do |name, value|
16
+ instance_variable_set("@#{name}", value)
17
+ self.class.send(:attr_reader, name)
18
+ end
19
+ end
20
+
21
+ def to_json
22
+ @attributes.delete_if { |k, v| v.nil? }.to_json
23
+ end
24
+
25
+ def self.channel_event(channel, event, data)
26
+ new(channel: channel, event: event, data: data)
27
+ end
28
+
29
+ def self.connection_established(socket_id)
30
+ data = { socket_id: socket_id, activity_timeout: ACTIVITY_TIMEOUT }.to_json
31
+ new(event: 'pusher:connection_established', data: data)
32
+ end
33
+
34
+ def self.subscription_succeeded(channel)
35
+ new(event: 'pusher_internal:subscription_succeeded', channel: channel)
36
+ end
37
+
38
+ def self.pong
39
+ new(event: 'pusher:pong')
40
+ end
41
+
42
+ def self.error(code, message)
43
+ data = { code: code, message: message }.to_json
44
+ new(event: 'pusher:error', data: data)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,121 @@
1
+ require 'set'
2
+
3
+ module Garufa
4
+ module Subscriptions
5
+ extend self
6
+
7
+ def all
8
+ subscriptions
9
+ end
10
+
11
+ def add(subscription)
12
+ subscriptions[subscription.channel].add subscription
13
+ end
14
+
15
+ def remove(subscription)
16
+ subscriptions[subscription.channel].delete subscription
17
+ end
18
+
19
+ def notify(channels, event, options = {})
20
+ channels.each do |channel|
21
+ connections = subscriptions[channel].map { |s| s.connection }
22
+ next unless connections.any?
23
+
24
+ connections.each do |connection|
25
+ next if connection.socket_id == options[:socket_id]
26
+
27
+ message = Message.channel_event(channel, event, options[:data])
28
+ connection.send_message(message)
29
+ end
30
+ end
31
+ end
32
+
33
+ def include?(subscription)
34
+ subscriptions[subscription.channel].include? subscription
35
+ end
36
+
37
+ private
38
+
39
+ def subscriptions
40
+ @subscriptions ||= Hash.new(Set.new)
41
+ end
42
+ end
43
+
44
+ class Subscription
45
+ attr_reader :data, :connection, :error
46
+
47
+ def initialize(data, connection)
48
+ @data, @connection = data, connection
49
+ end
50
+
51
+ def subscribe
52
+ case true
53
+ when invalid_channel?
54
+ set_error(nil, 'Invalid channnel or not present')
55
+ when invalid_signature?
56
+ set_error(nil, 'Invalid signature')
57
+ when already_subscribed?
58
+ set_error(nil, "Already subscribed to channel: #{channel}")
59
+ else
60
+ Subscriptions.add self
61
+ end
62
+ end
63
+
64
+ def unsubscribe
65
+ Subscriptions.remove self
66
+ end
67
+
68
+ def public_channel?
69
+ !(private_channel? || presence_channel?)
70
+ end
71
+
72
+ def private_channel?
73
+ channel_prefix == 'private'
74
+ end
75
+
76
+ def presence_channel?
77
+ channel_prefix == 'presence'
78
+ end
79
+
80
+ def set_error(code, message)
81
+ @error = SubscriptionError.new(code, message)
82
+ end
83
+
84
+ def success?
85
+ @error.nil?
86
+ end
87
+
88
+ def channel
89
+ @data['channel']
90
+ end
91
+
92
+ def channel_prefix
93
+ channel[/^private-|presence-/].to_s[0...-1]
94
+ end
95
+
96
+ private
97
+
98
+ def invalid_channel?
99
+ !channel.is_a?(String) || channel.empty?
100
+ end
101
+
102
+ def invalid_signature?
103
+ return false if public_channel?
104
+
105
+ string_to_sign = [@connection.socket_id, channel].compact.join(':')
106
+ token(string_to_sign) != @data["auth"].split(':').last
107
+ end
108
+
109
+ def token(string_to_sign)
110
+ digest = OpenSSL::Digest::SHA256.new
111
+ OpenSSL::HMAC.hexdigest(digest, Config[:secret], string_to_sign)
112
+ end
113
+
114
+ def already_subscribed?
115
+ Subscriptions.include? self
116
+ end
117
+ end
118
+
119
+ class SubscriptionError < Struct.new(:code, :message)
120
+ end
121
+ end
@@ -0,0 +1,4 @@
1
+ module Garufa
2
+ VERSION = '0.0.1.alpha.0'
3
+ end
4
+
@@ -0,0 +1,23 @@
1
+ require 'garufa/connection'
2
+
3
+ module Garufa
4
+ WsServer = lambda do |env|
5
+ socket = Faye::WebSocket.new(env)
6
+ connection = Connection.new(socket, env.logger)
7
+
8
+ socket.on :open do |event|
9
+ connection.establish
10
+ end
11
+
12
+ socket.on :message do |event|
13
+ connection.handle_incomming_data(event.data)
14
+ end
15
+
16
+ socket.on :close do |event|
17
+ socket = nil
18
+ end
19
+
20
+ # Return async Rack response
21
+ socket.rack_response
22
+ end
23
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ $:.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
2
+
3
+ require "garufa"
4
+ require 'minitest/autorun'
data/test/message.rb ADDED
@@ -0,0 +1,85 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ module Garufa
4
+ describe Message do
5
+ describe '.connection_established' do
6
+ before do
7
+ @socket_id = '123123-123123'
8
+ @message = Message.connection_established @socket_id
9
+ end
10
+
11
+ it 'should response with data attribute as string' do
12
+ @message.data.class.must_equal String
13
+ end
14
+
15
+ it 'should response with expected event' do
16
+ @message.event.must_equal 'pusher:connection_established'
17
+ end
18
+
19
+ it 'should response with expected data' do
20
+ data = JSON.parse(@message.data)
21
+ data["socket_id"].must_equal @socket_id
22
+ data["activity_timeout"].must_equal 120
23
+ end
24
+ end
25
+
26
+ describe '.channel_event' do
27
+ before do
28
+ @channel = 'channel-123'
29
+ @event = 'my-event'
30
+ @data = { itemId: 1, value: 'Sample Item' }
31
+ @message = Message.channel_event @channel, @event, @data
32
+ end
33
+
34
+ it 'should response with expected event' do
35
+ @message.event.must_equal @event
36
+ end
37
+
38
+ it 'should response with expected data' do
39
+ @message.data.must_equal @data
40
+ end
41
+
42
+ it 'should response with expected channel' do
43
+ @message.channel.must_equal @channel
44
+ end
45
+ end
46
+
47
+ describe '.subscription_succeeded' do
48
+ before do
49
+ @channel = 'channel-123'
50
+ @message = Message.subscription_succeeded @channel
51
+ end
52
+
53
+ it 'should response with expected event' do
54
+ @message.event.must_equal 'pusher_internal:subscription_succeeded'
55
+ end
56
+
57
+ it 'should response with expected channel' do
58
+ @message.channel.must_equal @channel
59
+ end
60
+ end
61
+
62
+ describe '.pong' do
63
+ it 'should response with a pong event' do
64
+ Message.pong.event.must_equal 'pusher:pong'
65
+ end
66
+ end
67
+
68
+ describe '.error' do
69
+ before do
70
+ @code, @error_message = 4000, 'There was an error!'
71
+ @message = Message.error @code, @error_message
72
+ end
73
+
74
+ it 'should response with expected event' do
75
+ @message.event.must_equal 'pusher:error'
76
+ end
77
+
78
+ it 'should response with expected data' do
79
+ data = JSON.parse(@message.data)
80
+ data["code"].must_equal @code
81
+ data["message"].must_equal @error_message
82
+ end
83
+ end
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: garufa
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha.0
5
+ platform: ruby
6
+ authors:
7
+ - Juan Manuel Cuello
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: goliath
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faye-websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cuba
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: signature
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Garufa is a websocket server compatible with the Pusher service protocol.
70
+ email:
71
+ - juanmacuello@gmail.com
72
+ executables:
73
+ - garufa
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - lib/garufa/version.rb
81
+ - lib/garufa/ws_server.rb
82
+ - lib/garufa/message.rb
83
+ - lib/garufa/config.rb
84
+ - lib/garufa/cuba/authentication.rb
85
+ - lib/garufa/subscriptions.rb
86
+ - lib/garufa/garufa_app.rb
87
+ - lib/garufa/connection.rb
88
+ - lib/garufa/faye_websocket_patch.rb
89
+ - lib/garufa/api_server.rb
90
+ - lib/garufa.rb
91
+ - bin/garufa.pid
92
+ - bin/garufa
93
+ - bin/garufa.log_stdout.log
94
+ - bin/garufa.log
95
+ - garufa.gemspec
96
+ - test/helper.rb
97
+ - test/message.rb
98
+ homepage: http://github.com/Juanmcuello/garufa
99
+ licenses: []
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '>'
113
+ - !ruby/object:Gem::Version
114
+ version: 1.3.1
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.0.3
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Websocket server compatible with Pusher.
121
+ test_files: []
122
+ has_rdoc: