ksconnect 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +70 -0
- data/lib/ksconnect.rb +40 -0
- data/lib/ksconnect/api.rb +25 -0
- data/lib/ksconnect/api/plugin.rb +102 -0
- data/lib/ksconnect/api/plugin/config.rb +13 -0
- data/lib/ksconnect/api/plugin/data.rb +87 -0
- data/lib/ksconnect/api/plugin/domain.rb +41 -0
- data/lib/ksconnect/helpers.rb +63 -0
- data/lib/logs.rb +25 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e7d84e395ec798ed06bbd384d903b8d792e40be4
|
4
|
+
data.tar.gz: 33129b381efe034aabbd70828f4327c7d81cd4ee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 035bfbbf2af3736d47754bbc370cb7ab2dbac83110c435b39909cdb81b37022b9d824b61f6623d3c028e5a94eccb9188381353e2af4c09222bf6d4d06c72f989
|
7
|
+
data.tar.gz: 669b9a916103bb0e6a2ee8e8756a8cb860c8acedfa733b3cf15a459592a8ab86a70ae7433ee2323e0e1b9f9fa044bcca0b474199fd82d014f7e2df77e05d55b8
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Kloudsec
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# KSConnect
|
2
|
+
|
3
|
+
KSConnect provides a Ruby connection interface for Kloudsec plugins by exposing a simple to use
|
4
|
+
API for managing and synchronizing plugin data.
|
5
|
+
|
6
|
+
# Usage
|
7
|
+
|
8
|
+
Initialize the api:
|
9
|
+
|
10
|
+
`api = KSConnect.new(:ssl).api`
|
11
|
+
|
12
|
+
Use it:
|
13
|
+
|
14
|
+
`api.domains['domain-1.com'].data['some_flag'] = true`
|
15
|
+
|
16
|
+
## Options
|
17
|
+
|
18
|
+
Initialize with additional (untested) helpers:
|
19
|
+
|
20
|
+
```
|
21
|
+
api = KSConnect.new(:ssl, use_helpers: true).api
|
22
|
+
api.ip_address_for('example.com') => # 127.0.0.1
|
23
|
+
```
|
24
|
+
|
25
|
+
## Getting the full domain list
|
26
|
+
|
27
|
+
All domain objects:
|
28
|
+
|
29
|
+
`api.all_domains # => { 'domain-1.com' => <Domain>, ... }`
|
30
|
+
|
31
|
+
Domain names only:
|
32
|
+
|
33
|
+
`api.all_domains.keys # => ['domain-1.com',..]`
|
34
|
+
|
35
|
+
## Getting data
|
36
|
+
|
37
|
+
`api.domains['domain-1.com'].data['key'] # => 'value'`
|
38
|
+
|
39
|
+
`api.domains['domain-1.com'].data.getall # => { 'key' => 'value', ... }`
|
40
|
+
|
41
|
+
## Setting data
|
42
|
+
|
43
|
+
`api.domains['domain-1.com'].data['key'] = new_value`
|
44
|
+
|
45
|
+
`api.domains['domain-1.com'].data.setall = { 'key': new_value, ... }`
|
46
|
+
|
47
|
+
## Event callbacks
|
48
|
+
|
49
|
+
Available callbacks for `<plugin>:push`:
|
50
|
+
|
51
|
+
- When plugin is enabled: `on_initialize`
|
52
|
+
- When plugin's IP is updated: `on_update`
|
53
|
+
- When plugin is removed: `on_teardown`
|
54
|
+
- Any other incoming message: `on_push`
|
55
|
+
|
56
|
+
```
|
57
|
+
api = KSConnect.new(:ssl).api
|
58
|
+
api.configure do |config|
|
59
|
+
config.on_initialize = lambda { |msg| do_some_initialization(msg) }
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
NOTE: there is no need to re-message proxy / core plugins to re-read configuration. This is done automatically.
|
64
|
+
|
65
|
+
## Channels
|
66
|
+
|
67
|
+
Safely acquire a redis channel in a separate thread by doing:
|
68
|
+
|
69
|
+
`new_channel = KSConnect.channel('channel_name') { |msg| puts msg }`
|
70
|
+
|
data/lib/ksconnect.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'logs'
|
2
|
+
|
3
|
+
class KSConnect
|
4
|
+
include Logs
|
5
|
+
attr_reader :api
|
6
|
+
attr_reader :plugin
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
plugins = args
|
10
|
+
|
11
|
+
additional_options = args.last.is_a?(Hash) ? args.last : nil
|
12
|
+
if additional_options
|
13
|
+
plugins.pop
|
14
|
+
else
|
15
|
+
additional_options = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
@api = KSConnect::API.new(enabled_plugins: plugins, use_helpers: additional_options[:use_helpers])
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.channel(name)
|
22
|
+
Thread.start do
|
23
|
+
begin
|
24
|
+
Redis.new.subscribe(name) do |on|
|
25
|
+
logger.info "Subscribing to redis channel: #{name}"
|
26
|
+
on.message do |channel, message|
|
27
|
+
yield message
|
28
|
+
end
|
29
|
+
$stdout.flush
|
30
|
+
end
|
31
|
+
rescue Exception => error
|
32
|
+
logger.error "#{error} on redis channel #{name}, restarting in 0.5s"
|
33
|
+
logger.error error.backtrace
|
34
|
+
$stdout.flush
|
35
|
+
sleep 0.5
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class KSConnect
|
2
|
+
class API
|
3
|
+
attr_reader :plugins
|
4
|
+
attr_reader :plugin # the current / default plugin
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
enabled_plugins = *opts[:enabled_plugins] || []
|
8
|
+
plugins = enabled_plugins.unshift(:core).uniq # always load core first
|
9
|
+
@plugins = plugins.reduce({}) { |hash, plugin_name| hash.tap { |h| h[plugin_name] = KSConnect::API::Plugin.new(plugin_name.to_s) } }
|
10
|
+
@plugin = @plugins[*opts[:enabled_plugins].first || :core]
|
11
|
+
|
12
|
+
if opts[:use_helpers]
|
13
|
+
extend KSConnect::Helpers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def domains
|
18
|
+
@plugin.domains
|
19
|
+
end
|
20
|
+
|
21
|
+
def all_domains
|
22
|
+
@plugins[:core].domains
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class KSConnect
|
5
|
+
class API
|
6
|
+
class Plugin
|
7
|
+
attr_accessor :domains
|
8
|
+
attr_reader :name
|
9
|
+
attr_writer :config
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@domains = {}
|
14
|
+
|
15
|
+
load_domains
|
16
|
+
subscribe_to_events
|
17
|
+
end
|
18
|
+
|
19
|
+
def config
|
20
|
+
@config ||= Config.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield(config)
|
25
|
+
end
|
26
|
+
|
27
|
+
# This method loads the domain list from Redis, adding or removing domains as appropriate.
|
28
|
+
# Note that it does not update the ip address of existing domains.
|
29
|
+
def load_domains
|
30
|
+
# load domain list
|
31
|
+
domain_to_ip = redis.hgetall(domains_key)
|
32
|
+
|
33
|
+
# add new domains
|
34
|
+
new_domains = domain_to_ip.keys - @domains.values.map(&:name)
|
35
|
+
new_domains.each { |domain_name| @domains[domain_name] = Domain.new(domain_name, domain_to_ip[domain_name], @name) }
|
36
|
+
|
37
|
+
# remove old domains
|
38
|
+
@domains.select! { |k, _| domain_to_ip.keys.include?(k) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def subscribe_to_events
|
42
|
+
KSConnect.channel("#{name}:push") do |message|
|
43
|
+
begin
|
44
|
+
msg = JSON.parse(message)
|
45
|
+
rescue Exception => e
|
46
|
+
puts "Error parsing message as JSON: #{msg}"
|
47
|
+
next
|
48
|
+
end
|
49
|
+
|
50
|
+
if %w(initialize update teardown).include? msg['request_type']
|
51
|
+
perform_request(msg)
|
52
|
+
else
|
53
|
+
config.on_push.call(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def perform_request(request)
|
59
|
+
request.stringify_keys!
|
60
|
+
|
61
|
+
domain_name = request['domain_name']
|
62
|
+
puts "Invalid push request with no domain: #{request}" and return unless domain_name
|
63
|
+
|
64
|
+
request_type = request['request_type']
|
65
|
+
case request_type
|
66
|
+
when 'initialize'
|
67
|
+
@domains[domain_name] = Domain.new(domain_name, get_ip_for(domain_name), @name)
|
68
|
+
config.on_initialize.call(request)
|
69
|
+
when 'update'
|
70
|
+
@domains[domain_name].ip_address = get_ip_for(domain_name)
|
71
|
+
config.on_update.call(request)
|
72
|
+
when 'teardown'
|
73
|
+
@domains.delete(domain_name)
|
74
|
+
config.on_teardown.call(request)
|
75
|
+
else
|
76
|
+
raise "Invalid request type"
|
77
|
+
end
|
78
|
+
|
79
|
+
redis.publish("core:push", { domain_name: domain_name, plugin_name: @name, request_type: request_type }.to_json) unless plugin_is_core?
|
80
|
+
end
|
81
|
+
|
82
|
+
def domains_key
|
83
|
+
@domain_list_uuid ||= redis.hget("#{@name}:data", "domain_names")
|
84
|
+
"kloudsec_data:#{@domain_list_uuid}"
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def plugin_is_core?
|
90
|
+
@name == 'core'
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_ip_for(domain_name)
|
94
|
+
redis.hget(domains_key, domain_name)
|
95
|
+
end
|
96
|
+
|
97
|
+
def redis
|
98
|
+
Redis.current
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class KSConnect
|
2
|
+
class API
|
3
|
+
class Plugin
|
4
|
+
class Config
|
5
|
+
attr_accessor :on_initialize, :on_update, :on_teardown, :on_push
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@on_initialize = @on_update = @on_teardown = @on_push = lambda { |msg| "No callback set for msg: #{msg}" }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'active_support/hash_with_indifferent_access'
|
3
|
+
|
4
|
+
class KSConnect
|
5
|
+
class API
|
6
|
+
class Plugin
|
7
|
+
class Data
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
def initialize(plugin_name, domain_name, type = :data, use_cache = true)
|
11
|
+
@plugin_name = plugin_name
|
12
|
+
@domain_name = domain_name
|
13
|
+
@type = type
|
14
|
+
@use_cache = use_cache
|
15
|
+
|
16
|
+
@data = ActiveSupport::HashWithIndifferentAccess.new if @use_cache
|
17
|
+
@data_uuid = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(field, value)
|
21
|
+
@data[field] = value if @use_cache
|
22
|
+
redis.hset(key, field, value)
|
23
|
+
redis.publish("core:push", { plugin_name: @plugin_name, domain_name: @domain_name, request_type: 'update' })
|
24
|
+
end
|
25
|
+
|
26
|
+
def setall(hash)
|
27
|
+
@data = @data.merge(hash) if @use_cache
|
28
|
+
redis.mapped_hmset(key, hash)
|
29
|
+
redis.publish("core:push", { plugin_name: @plugin_name, domain_name: @domain_name, request_type: 'update' })
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](field)
|
33
|
+
if @use_cache
|
34
|
+
@data ||= redis.hgetall(key)
|
35
|
+
@data[field]
|
36
|
+
else
|
37
|
+
redis.hget(key, field)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def getall
|
42
|
+
if @use_cache
|
43
|
+
@data ||= redis.hgetall(key)
|
44
|
+
else
|
45
|
+
redis.hgetall(key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def reload
|
50
|
+
@data = redis.hgetall(key) if @use_cache
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(field)
|
54
|
+
@data.delete(field) if @use_cache
|
55
|
+
redis.hdel(key, field)
|
56
|
+
end
|
57
|
+
|
58
|
+
def key
|
59
|
+
set_data_uuid unless @data_uuid
|
60
|
+
"kloudsec_data:#{@data_uuid}"
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def set_data_uuid
|
66
|
+
begin
|
67
|
+
tries ||= 3
|
68
|
+
id = redis.hget("#{@plugin_name}:#{@type}", @domain_name)
|
69
|
+
if id
|
70
|
+
@data_uuid = id
|
71
|
+
else
|
72
|
+
@data_uuid = SecureRandom.uuid
|
73
|
+
raise "Race on setting data key failed." unless redis.hsetnx("#{@plugin_name}:#{@type}", @domain_name, @data_uuid)
|
74
|
+
end
|
75
|
+
rescue Exception => e
|
76
|
+
puts e.message
|
77
|
+
retry unless (tries -= 1).zero?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def redis
|
82
|
+
Redis.current
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class KSConnect
|
4
|
+
class API
|
5
|
+
class Plugin
|
6
|
+
class Domain
|
7
|
+
attr_accessor :ip_address
|
8
|
+
attr_reader :data
|
9
|
+
attr_reader :private_data
|
10
|
+
attr_reader :name
|
11
|
+
attr_reader :plugin_name
|
12
|
+
|
13
|
+
def initialize(name, ip_address, plugin_name)
|
14
|
+
@name = name
|
15
|
+
@ip_address = ip_address
|
16
|
+
@plugin_name = plugin_name
|
17
|
+
@data = Data.new(plugin_name, name, :data)
|
18
|
+
@private_data = Data.new(plugin_name, name, :private_data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def notify(data)
|
22
|
+
redis.lpush("kloudsec_notifications", data.merge({ domain_name: @name, plugin_name: @plugin_name }))
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_issue(issue_type, data)
|
26
|
+
redis.lpush("kloudsec_issues", data.merge({ domain_name: @name, plugin_name: @plugin_name, issue_type: issue_type, request_type: 'add' }))
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear_issue(issue_type)
|
30
|
+
redis.lpush("kloudsec_issues", { domain_name: @name, plugin_name: @plugin_name, issue_type: issue_type, request_type: 'remove' })
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def redis
|
36
|
+
Redis.current
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class KSConnect
|
2
|
+
module Helpers
|
3
|
+
def ip_address_for(domain)
|
4
|
+
if all_domains[domain].present?
|
5
|
+
all_domains[domain].ip_address
|
6
|
+
else
|
7
|
+
"kloudsec.com" # go to something safe if ip is invalid
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def https_enabled?(domain)
|
12
|
+
k, c = ssl_key_and_cert_for(domain)
|
13
|
+
k && c
|
14
|
+
end
|
15
|
+
|
16
|
+
def https_redirect_enabled?(domain)
|
17
|
+
https_enabled?(domain) && plugins[:ssl].domains[domain].data['redirect'] == "true"
|
18
|
+
end
|
19
|
+
|
20
|
+
def https_rewriting_enabled?(domain)
|
21
|
+
https_enabled?(domain) && plugins[:ssl].domains[domain].data['rewriteHTTPS'] == "true"
|
22
|
+
end
|
23
|
+
|
24
|
+
def waf_enabled?(domain)
|
25
|
+
plugins[:web_shield].domains.include?(domain)
|
26
|
+
end
|
27
|
+
|
28
|
+
def waf_learning?(domain)
|
29
|
+
waf_enabled?(domain) && plugins[:web_shield].domains[domain].data['learning']
|
30
|
+
end
|
31
|
+
|
32
|
+
def pagespeed_enabled?(domain)
|
33
|
+
plugins[:mod_cache].domains.include?(domain)
|
34
|
+
end
|
35
|
+
|
36
|
+
def pending_autossl?(domain)
|
37
|
+
p, k = autossl_verification_path_and_key_for(domain)
|
38
|
+
plugins[:autossl].domains.include?(domain) && p && k
|
39
|
+
end
|
40
|
+
|
41
|
+
def autossl_verification_path_and_key_for(domain)
|
42
|
+
d = plugins[:autossl].domains[domain].private_data
|
43
|
+
if d
|
44
|
+
return d['verify_endpoint'], d['verify_content']
|
45
|
+
else
|
46
|
+
return nil, nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssl_key_and_cert_for(domain)
|
51
|
+
ssl = plugins[:ssl].domains[domain].private_data
|
52
|
+
autossl = plugins[:autossl].domains[domain].private_data
|
53
|
+
|
54
|
+
if ssl && ssl['key'] && ssl['cert']
|
55
|
+
return ssl['key'], ssl['cert']
|
56
|
+
elsif autossl && autossl['key'] && autossl['cert']
|
57
|
+
return autossl['key'], autossl['cert']
|
58
|
+
else
|
59
|
+
return nil, nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/logs.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
module Logs
|
5
|
+
def self.included base
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def logger
|
12
|
+
self.class.logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def logger
|
18
|
+
return @_logger if @_logger
|
19
|
+
|
20
|
+
@_logger = Logger.new(STDOUT)
|
21
|
+
@_logger.level = "Logger::#{ENV['LOG_LEVEL'] || INFO}".constantize
|
22
|
+
@_logger
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ksconnect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Poon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-28 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |-
|
14
|
+
KSConnect provides a connection interface for Kloudsec plugins by exposing a simple to use
|
15
|
+
API for managing and synchronizing plugin data.
|
16
|
+
email: ivan@kloudsec.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENSE
|
22
|
+
- README.md
|
23
|
+
- lib/ksconnect.rb
|
24
|
+
- lib/ksconnect/api.rb
|
25
|
+
- lib/ksconnect/api/plugin.rb
|
26
|
+
- lib/ksconnect/api/plugin/config.rb
|
27
|
+
- lib/ksconnect/api/plugin/data.rb
|
28
|
+
- lib/ksconnect/api/plugin/domain.rb
|
29
|
+
- lib/ksconnect/helpers.rb
|
30
|
+
- lib/logs.rb
|
31
|
+
homepage: https://kloudsec.com
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.4.8
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Connection API for Kloudsec
|
55
|
+
test_files: []
|
56
|
+
has_rdoc:
|