bcome 1.1.4 → 1.3.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 +4 -4
- data/lib/objects/bcome/version.rb +1 -1
- data/lib/objects/command/local.rb +1 -1
- data/lib/objects/driver/ec2.rb +7 -0
- data/lib/objects/encryptor.rb +99 -0
- data/lib/objects/exception/invalid_metadata_encryption_key.rb +7 -0
- data/lib/objects/modules/workspace_menu.rb +6 -0
- data/lib/objects/node/attributes.rb +1 -1
- data/lib/objects/node/base.rb +9 -1
- data/lib/objects/node/factory.rb +24 -2
- data/lib/objects/node/meta/base.rb +3 -1
- data/lib/objects/node/meta_data_loader.rb +24 -2
- data/lib/objects/node/server/base.rb +15 -0
- data/lib/objects/ssh/driver.rb +21 -0
- data/lib/objects/ssh/tunnel/local_port_forward.rb +20 -0
- data/patches/string-encrypt.rb +40 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5175174eb843f0f210c02ca12ded004f75d77368
|
4
|
+
data.tar.gz: ffd09a8ba7edc0df044a17b55db02d3176e0feee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 855151285002aa2270d025f97ff85c8beead1f3b7aabcad797bcd8eb03885c38f296a2ad1f1e01c2b1be6631a3d9ba724f57fae6046e6132f8ec8e95ae3e9ee9
|
7
|
+
data.tar.gz: 09500dc2f2ac59b5a1c70252c07ca0bbc665f32eac78a52fc1ded53d3e9d79ba09d0367ac56921eab5d56d9ddd300665924640733d3e0d436150badbec7f55db
|
data/lib/objects/driver/ec2.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module Bcome::Driver
|
2
2
|
class Ec2 < Bcome::Driver::Base
|
3
|
+
|
4
|
+
PATH_TO_FOG_CREDENTIALS = "#{ENV['HOME']}/.fog".freeze
|
5
|
+
|
3
6
|
def initialize(*params)
|
4
7
|
super
|
5
8
|
raise Bcome::Exception::Ec2DriverMissingProvisioningRegion, params.inspect unless provisioning_region
|
@@ -22,6 +25,10 @@ module Bcome::Driver
|
|
22
25
|
fog_client.servers.all({})
|
23
26
|
end
|
24
27
|
|
28
|
+
def raw_fog_credentials
|
29
|
+
@raw_fog_credentials ||= YAML.load_file(PATH_TO_FOG_CREDENTIALS)[credentials_key]
|
30
|
+
end
|
31
|
+
|
25
32
|
def credentials_key
|
26
33
|
@params[:credentials_key]
|
27
34
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Bcome
|
2
|
+
class Encryptor
|
3
|
+
|
4
|
+
UNENC_SIGNIFIER = "".freeze
|
5
|
+
ENC_SIGNIFIER = "enc".freeze
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_reader :key
|
10
|
+
|
11
|
+
def pack
|
12
|
+
# Bcome currently works with a single encryption key - the same one - for all files
|
13
|
+
# When we attempt an encrypt we'll check first to see if any encrypted files already exists, and
|
14
|
+
# we'll try our key on it. If the fails to unpack the file, we abort the encryption attempt.
|
15
|
+
prompt_for_key
|
16
|
+
if has_files_to_encrypt?
|
17
|
+
verify_presented_key if has_encrypted_files?
|
18
|
+
toggle_packed_files(all_unencrypted_filenames, :encrypt)
|
19
|
+
else
|
20
|
+
puts "\nNo unencrypted files to encrypt.\n".warning
|
21
|
+
end
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
def prompt_for_key
|
26
|
+
message = "Please enter an encryption key (and if your data is already encrypted, you must provide the same key): ".informational
|
27
|
+
@key = ::Readline.readline("\n#{message}", true).squeeze('').to_s
|
28
|
+
puts "\n"
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_encrypted_files?
|
32
|
+
all_encrypted_filenames.any?
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_files_to_encrypt?
|
36
|
+
all_unencrypted_filenames.any?
|
37
|
+
end
|
38
|
+
|
39
|
+
def verify_presented_key
|
40
|
+
# We attempt a decrypt of any encrypted file in order to verify that a newly presented key
|
41
|
+
# matches the key used to previously encrypt. Bcome operates on a one-key-per-implementation basis.
|
42
|
+
test_file = all_encrypted_filenames.first
|
43
|
+
file_contents = File.read(test_file)
|
44
|
+
file_contents.decrypt(@key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def unpack
|
48
|
+
prompt_for_key
|
49
|
+
toggle_packed_files(all_encrypted_filenames,:decrypt)
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
def toggle_packed_files(filenames, packer_method)
|
54
|
+
raise "Missing encryption key. Please set an encryption key" unless @key
|
55
|
+
filenames.each do |filename|
|
56
|
+
# Get raw
|
57
|
+
raw_contents = File.read(filename)
|
58
|
+
|
59
|
+
if packer_method == :decrypt
|
60
|
+
filename =~ /#{path_to_metadata}\/(.+)\.enc/
|
61
|
+
opposing_filename = $1
|
62
|
+
action = "Unpacking"
|
63
|
+
else
|
64
|
+
filename =~ /#{path_to_metadata}\/(.*)/
|
65
|
+
opposing_filename = "#{$1}.enc"
|
66
|
+
action = "Packing"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Write encrypted/decryption action
|
70
|
+
enc_decrypt_result = raw_contents.send(packer_method, @key)
|
71
|
+
puts "#{action}\s".informational + filename + "\sto\s".informational + "#{path_to_metadata}/" + opposing_filename
|
72
|
+
write_file(opposing_filename, enc_decrypt_result)
|
73
|
+
end
|
74
|
+
puts "\ndone".informational
|
75
|
+
end
|
76
|
+
|
77
|
+
def path_to_metadata
|
78
|
+
"bcome/metadata"
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_file(filename, contents)
|
82
|
+
filepath = "#{path_to_metadata}/#{filename}"
|
83
|
+
File.open("#{filepath}", 'w') { |f| f.write(contents) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def all_unencrypted_filenames
|
87
|
+
Dir["#{metadata_path}/*"].reject {|f| f =~ /\.enc/}
|
88
|
+
end
|
89
|
+
|
90
|
+
def all_encrypted_filenames
|
91
|
+
Dir["#{metadata_path}/*.enc"]
|
92
|
+
end
|
93
|
+
|
94
|
+
def metadata_path
|
95
|
+
"bcome/metadata"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -113,6 +113,12 @@ module Bcome::WorkspaceMenu
|
|
113
113
|
meta: {
|
114
114
|
description: 'Print out all metadata related to this node'
|
115
115
|
},
|
116
|
+
pack_metadata: {
|
117
|
+
description: 'Encrypt your metadata files',
|
118
|
+
},
|
119
|
+
unpack_metadata: {
|
120
|
+
description: 'Decrypt and expose your encrypted metadata files',
|
121
|
+
},
|
116
122
|
registry: {
|
117
123
|
description: 'List all user defined commands present in your registry, and available to this namespace',
|
118
124
|
console_only: false
|
@@ -41,7 +41,7 @@ module Bcome::Node::Attributes
|
|
41
41
|
def recurse_hash_data_for_instance_var(instance_var_name, parent_key)
|
42
42
|
instance_data = instance_variable_defined?(instance_var_name) ? instance_variable_get(instance_var_name) : {}
|
43
43
|
instance_data ||= {}
|
44
|
-
instance_data = parent.send(parent_key).
|
44
|
+
instance_data = parent.send(parent_key).deep_merge(instance_data) if has_parent?
|
45
45
|
instance_data
|
46
46
|
end
|
47
47
|
end
|
data/lib/objects/node/base.rb
CHANGED
@@ -50,7 +50,7 @@ module Bcome::Node
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def enabled_menu_items
|
53
|
-
[:ls, :lsa, :workon, :enable, :disable, :enable!, :disable!, :run, :tree, :ping, :put, :rsync, :cd, :meta, :registry, :interactive, :execute_script]
|
53
|
+
[:ls, :lsa, :workon, :enable, :disable, :enable!, :disable!, :run, :tree, :ping, :put, :rsync, :cd, :meta, :pack_metadata, :unpack_metadata, :registry, :interactive, :execute_script]
|
54
54
|
end
|
55
55
|
|
56
56
|
def has_proxy?
|
@@ -91,6 +91,14 @@ module Bcome::Node
|
|
91
91
|
end
|
92
92
|
results
|
93
93
|
end
|
94
|
+
|
95
|
+
def pack_metadata
|
96
|
+
::Bcome::Encryptor.instance.pack
|
97
|
+
end
|
98
|
+
|
99
|
+
def unpack_metadata
|
100
|
+
::Bcome::Encryptor.instance.unpack
|
101
|
+
end
|
94
102
|
|
95
103
|
def validate_attributes
|
96
104
|
validate_identifier
|
data/lib/objects/node/factory.rb
CHANGED
@@ -6,6 +6,8 @@ module Bcome::Node
|
|
6
6
|
|
7
7
|
CONFIG_PATH = 'bcome'.freeze
|
8
8
|
DEFAULT_CONFIG_NAME = 'networks.yml'.freeze
|
9
|
+
SERVER_OVERRIDE_CONFIG_NAME = 'machines-data.yml'.freeze
|
10
|
+
|
9
11
|
INVENTORY_KEY = 'inventory'.freeze
|
10
12
|
COLLECTION_KEY = 'collection'.freeze
|
11
13
|
SUBSELECT_KEY = 'inventory-subselect'.freeze
|
@@ -24,6 +26,10 @@ module Bcome::Node
|
|
24
26
|
"#{CONFIG_PATH}/#{config_file_name}"
|
25
27
|
end
|
26
28
|
|
29
|
+
def machines_data_path
|
30
|
+
"#{CONFIG_PATH}/#{SERVER_OVERRIDE_CONFIG_NAME}"
|
31
|
+
end
|
32
|
+
|
27
33
|
def config_file_name
|
28
34
|
@config_file_name ||= ENV['CONF'] ? ENV['CONF'] : DEFAULT_CONFIG_NAME
|
29
35
|
end
|
@@ -46,7 +52,7 @@ module Bcome::Node
|
|
46
52
|
raise Bcome::Exception::InvalidNetworkConfig, 'missing config type' unless config[:type]
|
47
53
|
|
48
54
|
klass = klass_for_view_type[config[:type]]
|
49
|
-
|
55
|
+
|
50
56
|
raise Bcome::Exception::InvalidNetworkConfig, "invalid config type #{config[:type]}" unless klass
|
51
57
|
|
52
58
|
node = klass.new(views: config, parent: parent)
|
@@ -84,6 +90,14 @@ module Bcome::Node
|
|
84
90
|
@estate_config ||= reformat_config(load_estate_config)
|
85
91
|
end
|
86
92
|
|
93
|
+
def machines_data
|
94
|
+
@machines_data ||= load_machines_data
|
95
|
+
end
|
96
|
+
|
97
|
+
def machines_data_for_namespace(namespace)
|
98
|
+
machines_data[namespace] ? machines_data[namespace] : {}
|
99
|
+
end
|
100
|
+
|
87
101
|
def rewrite_estate_config(data)
|
88
102
|
File.open(config_path, 'w') do |file|
|
89
103
|
file.write data.to_yaml
|
@@ -94,12 +108,20 @@ module Bcome::Node
|
|
94
108
|
config = YAML.load_file(config_path).deep_symbolize_keys
|
95
109
|
return config
|
96
110
|
rescue ArgumentError, Psych::SyntaxError => e
|
97
|
-
raise Bcome::Exception::InvalidNetworkConfig, 'Invalid yaml in config' + e.message
|
111
|
+
raise Bcome::Exception::InvalidNetworkConfig, 'Invalid yaml in network config' + e.message
|
98
112
|
rescue Errno::ENOENT
|
99
113
|
raise Bcome::Exception::DeprecationWarning if is_running_deprecated_configs?
|
100
114
|
raise Bcome::Exception::MissingNetworkConfig, config_path
|
101
115
|
end
|
102
116
|
|
117
|
+
def load_machines_data
|
118
|
+
return {} unless File.exist?(machines_data_path)
|
119
|
+
config = YAML.load_file(machines_data_path).deep_symbolize_keys
|
120
|
+
return config
|
121
|
+
rescue ArgumentError, Psych::SyntaxError => e
|
122
|
+
raise Bcome::Exception::InvalidNetworkConfig, 'Invalid yaml in machines data config' + e.message
|
123
|
+
end
|
124
|
+
|
103
125
|
def is_running_deprecated_configs?
|
104
126
|
File.exist?("bcome/config/platform.yml")
|
105
127
|
end
|
@@ -25,9 +25,11 @@ module Bcome::Node::Meta
|
|
25
25
|
@data
|
26
26
|
end
|
27
27
|
|
28
|
-
def fetch(key)
|
28
|
+
def fetch(key, default = nil)
|
29
29
|
if @data.key?(key)
|
30
30
|
@data[key]
|
31
|
+
elsif default
|
32
|
+
return default
|
31
33
|
else
|
32
34
|
raise Bcome::Exception::CantFindKeyInMetadata, key unless @data.key?(key)
|
33
35
|
end
|
@@ -8,6 +8,10 @@ module Bcome::Node
|
|
8
8
|
@all_metadata_filenames = Dir["#{META_DATA_FILE_PATH_PREFIX}/*"]
|
9
9
|
end
|
10
10
|
|
11
|
+
def decryption_key
|
12
|
+
@decryption_key
|
13
|
+
end
|
14
|
+
|
11
15
|
def data
|
12
16
|
@data ||= do_load
|
13
17
|
end
|
@@ -16,11 +20,29 @@ module Bcome::Node
|
|
16
20
|
data[namespace.to_sym] ? data[namespace.to_sym] : {}
|
17
21
|
end
|
18
22
|
|
23
|
+
def prompt_for_decryption_key
|
24
|
+
message = "Please enter your metadata encryption key: ".informational
|
25
|
+
@decryption_key = ::Readline.readline("\n#{message}", true).squeeze('').to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_file_data_for(filepath)
|
29
|
+
if filepath =~ /.enc/ # encrypted file contents
|
30
|
+
prompt_for_decryption_key unless decryption_key
|
31
|
+
encrypted_contents = File.read(filepath)
|
32
|
+
decrypted_contents = encrypted_contents.decrypt(decryption_key)
|
33
|
+
return YAML.load(decrypted_contents)
|
34
|
+
else # unencrypted
|
35
|
+
return YAML.load_file(filepath)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
19
39
|
def do_load
|
20
40
|
all_meta_data = {}
|
21
|
-
@all_metadata_filenames.each do |
|
41
|
+
@all_metadata_filenames.each do |filepath|
|
42
|
+
next if filepath =~ /-unenc/ # we only read from the encrypted, packed files.
|
43
|
+
|
22
44
|
begin
|
23
|
-
filedata =
|
45
|
+
filedata = load_file_data_for(filepath)
|
24
46
|
all_meta_data.deep_merge!(filedata)
|
25
47
|
rescue Psych::SyntaxError => e
|
26
48
|
raise Bcome::Exception::InvalidMetaDataConfig, "Error: #{e.message}"
|
@@ -10,6 +10,17 @@ module Bcome::Node::Server
|
|
10
10
|
@bootstrap = false
|
11
11
|
end
|
12
12
|
|
13
|
+
# override a server namespace's parameters. This enables features such as specific SSH parameters for a specific server, e.g. my use case was a
|
14
|
+
# single debian box within an ubuntu network, where I needed to access the machine bootstrapping mode with the 'admin' rather 'ubuntu' username.
|
15
|
+
def set_view_attributes
|
16
|
+
super
|
17
|
+
overridden_attributes = ::Bcome::Node::Factory.instance.machines_data_for_namespace(namespace.to_sym)
|
18
|
+
overridden_attributes.each do |override_key, override_value|
|
19
|
+
instance_variable_name = "@#{override_key}"
|
20
|
+
instance_variable_set(instance_variable_name, override_value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
13
24
|
def bootstrap?
|
14
25
|
@bootstrap ? true : false
|
15
26
|
end
|
@@ -98,6 +109,10 @@ module Bcome::Node::Server
|
|
98
109
|
base_items
|
99
110
|
end
|
100
111
|
|
112
|
+
def local_port_forward(start_port, end_port)
|
113
|
+
ssh_driver.local_port_forward(start_port, end_port)
|
114
|
+
end
|
115
|
+
|
101
116
|
def open_ssh_connection
|
102
117
|
ssh_driver.ssh_connection
|
103
118
|
end
|
data/lib/objects/ssh/driver.rb
CHANGED
@@ -79,6 +79,27 @@ module Bcome::Ssh
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
+
def local_port_forward(start_port, end_port)
|
83
|
+
if has_proxy?
|
84
|
+
|
85
|
+
if bootstrap?
|
86
|
+
tunnel_command = "ssh -N -L #{start_port}:#{@context_node.internal_ip_address}:#{end_port} -i #{@bootstrap_settings.ssh_key_path} #{bastion_host_user}@#{@proxy_data.host}"
|
87
|
+
else
|
88
|
+
tunnel_command = "ssh -N -L #{start_port}:#{@context_node.internal_ip_address}:#{end_port} #{bastion_host_user}@#{@proxy_data.host}"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
if bootstrap?
|
92
|
+
tunnel_command = "ssh -i #{@bootstrap_settings.ssh_key_path} -N -L #{start_port}:#{@context_node.public_ip_address}:#{end_port}"
|
93
|
+
else
|
94
|
+
tunnel_command = "ssh -N -L #{start_port}:#{@context_node.public_ip_address}:#{end_port}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
tunnel = ::Bcome::Ssh::Tunnel::LocalPortForward.new(tunnel_command)
|
99
|
+
tunnel.open!
|
100
|
+
return tunnel
|
101
|
+
end
|
102
|
+
|
82
103
|
def bootstrap_ssh_command
|
83
104
|
if has_proxy?
|
84
105
|
"ssh -i #{@bootstrap_settings.ssh_key_path} -t #{bastion_host_user}@#{@proxy_data.host} ssh -t #{user}@#{@context_node.internal_ip_address}"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bcome::Ssh::Tunnel
|
2
|
+
class LocalPortForward
|
3
|
+
|
4
|
+
def initialize(tunnel_command)
|
5
|
+
@tunnel_command = tunnel_command
|
6
|
+
@process_pid = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def open!
|
10
|
+
puts "Opening tunnel: #{@tunnel_command}".informational
|
11
|
+
@process_pid = spawn(@tunnel_command)
|
12
|
+
end
|
13
|
+
|
14
|
+
def close!
|
15
|
+
puts "Closing tunnel with process pid ##{@process_pid}: #{@tunnel_command}".informational
|
16
|
+
::Process.kill("HUP", @process_pid)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
# Adapted from https://stackoverflow.com/questions/39033577/opensslcipherciphererror-wrong-final-block-length
|
4
|
+
|
5
|
+
class String
|
6
|
+
|
7
|
+
ALGORITHM = 'AES-256-ECB'
|
8
|
+
|
9
|
+
def encrypt(key)
|
10
|
+
begin
|
11
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
12
|
+
cipher.encrypt()
|
13
|
+
cipher.key = key.as_256_bit_key
|
14
|
+
crypt = cipher.update(self) + cipher.final()
|
15
|
+
crypt_string = (Base64.encode64(crypt))
|
16
|
+
return crypt_string
|
17
|
+
rescue Exception => e
|
18
|
+
puts "Failed to encrypt: #{e.message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def decrypt(key)
|
23
|
+
begin
|
24
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
25
|
+
cipher.decrypt()
|
26
|
+
cipher.key = key.as_256_bit_key
|
27
|
+
tempkey = Base64.decode64(self)
|
28
|
+
crypt = cipher.update(tempkey)
|
29
|
+
crypt << cipher.final()
|
30
|
+
return crypt
|
31
|
+
rescue Exception => e
|
32
|
+
raise ::Bcome::Exception::InvalidMetaDataEncryptionKey.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def as_256_bit_key
|
37
|
+
::Digest::SHA256.digest self
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bcome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Roderick (Webzakimbo)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -163,6 +163,7 @@ files:
|
|
163
163
|
- lib/objects/driver/bucket.rb
|
164
164
|
- lib/objects/driver/ec2.rb
|
165
165
|
- lib/objects/driver/static.rb
|
166
|
+
- lib/objects/encryptor.rb
|
166
167
|
- lib/objects/exception/argument_error_invoking_method_from_command_line.rb
|
167
168
|
- lib/objects/exception/base.rb
|
168
169
|
- lib/objects/exception/can_only_subselect_on_inventory.rb
|
@@ -186,6 +187,7 @@ files:
|
|
186
187
|
- lib/objects/exception/invalid_machines_cache_config.rb
|
187
188
|
- lib/objects/exception/invalid_matcher_query.rb
|
188
189
|
- lib/objects/exception/invalid_meta_data_config.rb
|
190
|
+
- lib/objects/exception/invalid_metadata_encryption_key.rb
|
189
191
|
- lib/objects/exception/invalid_network_config.rb
|
190
192
|
- lib/objects/exception/invalid_network_driver_type.rb
|
191
193
|
- lib/objects/exception/invalid_proxy_config.rb
|
@@ -264,9 +266,11 @@ files:
|
|
264
266
|
- lib/objects/ssh/driver.rb
|
265
267
|
- lib/objects/ssh/proxy_data.rb
|
266
268
|
- lib/objects/ssh/script_exec.rb
|
269
|
+
- lib/objects/ssh/tunnel/local_port_forward.rb
|
267
270
|
- lib/objects/system/local.rb
|
268
271
|
- lib/objects/workspace.rb
|
269
272
|
- patches/irb.rb
|
273
|
+
- patches/string-encrypt.rb
|
270
274
|
- patches/string.rb
|
271
275
|
- patches/string_stylesheet.rb
|
272
276
|
homepage: https://github.com/webzakimbo/bcome-kontrol
|