garufa 0.0.1.alpha.0

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