remotus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "remotus/auth/credential"
4
+ require "remotus/auth/store"
5
+ require "remotus/auth/hash_store"
6
+
7
+ module Remotus
8
+ # Module containing remote authentication classes and modules
9
+ module Auth
10
+ #
11
+ # Gets authentication credentials for the given connection and options
12
+ #
13
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection remote connection
14
+ # @param [Hash] options options hash
15
+ # options may be used by different credential stores.
16
+ #
17
+ # @return [Remotus::Auth::Credential] found credential
18
+ #
19
+ def self.credential(connection, **options)
20
+ return cache[connection.host] if cache.key?(connection.host)
21
+
22
+ stores.each do |store|
23
+ host_cred = store.credential(connection, **options)
24
+ if host_cred
25
+ cache[connection.host] = host_cred
26
+ return host_cred
27
+ end
28
+ end
29
+ raise Remotus::MissingCredential, "Could not find credential for #{connection.host} in any credential store (#{stores.join(", ")})."
30
+ end
31
+
32
+ #
33
+ # Gets the credential cache
34
+ #
35
+ # @return [Hash{String => Remotus::Auth::Credential}] credential cache with hostname keys
36
+ #
37
+ def self.cache
38
+ @cache ||= {}
39
+ end
40
+
41
+ #
42
+ # Clears all entries in the credential cache
43
+ #
44
+ def self.clear_cache
45
+ @cache = {}
46
+ end
47
+
48
+ #
49
+ # Gets the list of associated credential stores
50
+ #
51
+ # @return [Array<Remotus::Auth::Store>] credential stores
52
+ #
53
+ def self.stores
54
+ @stores ||= []
55
+ end
56
+
57
+ #
58
+ # Sets the list of associated credential stores
59
+ #
60
+ # @param [Array<Remotus::Auth::Store>] stores credential stores
61
+ #
62
+ def self.stores=(stores)
63
+ @stores = stores
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Remotus
6
+ module Auth
7
+ # Authentication credential
8
+ class Credential
9
+ # @return [String] gets or sets user
10
+ attr_accessor :user
11
+
12
+ # @return [String] gets or sets private key path
13
+ attr_accessor :private_key
14
+
15
+ #
16
+ # Generates a new credential from a hash
17
+ #
18
+ # @param [Hash] hash hash with :user, :password, :private_key, and :private_key_data keys
19
+ # @option hash [String] :user user name
20
+ # @option hash [String] :password user password
21
+ # @option hash [String] :private_key private key path
22
+ # @option hash [String] :private_key_data private key data as a string
23
+ #
24
+ # @return [Remotus::Auth::Credential] newly initialized credential
25
+ #
26
+ def self.from_hash(hash)
27
+ Credential.new(
28
+ hash[:user],
29
+ hash[:password],
30
+ private_key: hash[:private_key],
31
+ private_key_data: hash[:private_key_data]
32
+ )
33
+ end
34
+
35
+ #
36
+ # Creates a new instance of a Remotus::Auth::Credential
37
+ #
38
+ # @param [String] user user name
39
+ # @param [String] password user password
40
+ # @param [Hash] options options hash
41
+ # @option options [String] :private_key private key path
42
+ # @option options [String] :private_key_data private key data as a string
43
+ #
44
+ def initialize(user, password = nil, **options)
45
+ @user = user
46
+ @crypt_info = { password: {}, private_key_data: {} }
47
+ @private_key = options[:private_key]
48
+ self.password = password
49
+ self.private_key_data = options[:private_key_data]
50
+ end
51
+
52
+ #
53
+ # Retrieved decrypted password
54
+ #
55
+ # @return [String, nil] decrypted password or nil if unset
56
+ #
57
+ def password
58
+ return unless @password
59
+
60
+ decrypt(@password, :password)
61
+ end
62
+
63
+ #
64
+ # Sets password
65
+ #
66
+ # @param [String] password new password
67
+ #
68
+ def password=(password)
69
+ @password = password ? encrypt(password.to_s, :password) : nil
70
+ end
71
+
72
+ #
73
+ # Retrieves decrypted private key data
74
+ #
75
+ # @return [String, nil] decrypted private key data or nil if unset
76
+ #
77
+ def private_key_data
78
+ return unless @private_key_data
79
+
80
+ decrypt(@private_key_data, :private_key_data)
81
+ end
82
+
83
+ #
84
+ # Sets private key data
85
+ #
86
+ # @param [String] private_key_data private key data
87
+ #
88
+ def private_key_data=(private_key_data)
89
+ @private_key_data = private_key_data ? encrypt(private_key_data.to_s, :private_key_data) : nil
90
+ end
91
+
92
+ #
93
+ # Converts credential to a string
94
+ #
95
+ # @return [String] Credential represented as a string
96
+ #
97
+ def to_s
98
+ "user: #{@user}"
99
+ end
100
+
101
+ #
102
+ # Inspects credential
103
+ #
104
+ # @return [String] Credential represented as an inspection string
105
+ #
106
+ def inspect
107
+ "#{self.class.name}: (#{self})"
108
+ end
109
+
110
+ private
111
+
112
+ #
113
+ # Encrypts string data
114
+ #
115
+ # @param [String] data data to encrypt
116
+ # @param [Symbol] crypt_key key in @crypt_info to store the key and iv for decryption
117
+ #
118
+ # @return [Object] encrypted data
119
+ #
120
+ def encrypt(data, crypt_key)
121
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
122
+ cipher.encrypt
123
+ @crypt_info[crypt_key][:key] = cipher.random_key
124
+ @crypt_info[crypt_key][:iv] = cipher.random_iv
125
+ cipher.update(data) + cipher.final
126
+ end
127
+
128
+ #
129
+ # Decrypts data to a string
130
+ #
131
+ # @param [Object] data encrypted data
132
+ # @param [Symbol] crypt_key key in @crypt_info containing the key and iv for decryption
133
+ #
134
+ # @return [String] decrypted string
135
+ #
136
+ def decrypt(data, crypt_key)
137
+ decipher = OpenSSL::Cipher.new("aes-256-cbc")
138
+ decipher.decrypt
139
+ decipher.key = @crypt_info[crypt_key][:key]
140
+ decipher.iv = @crypt_info[crypt_key][:iv]
141
+ decipher.update(data) + decipher.final
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remotus
4
+ module Auth
5
+ # Hash-based authentication store that requires credentials to be added manually
6
+ class HashStore < Store
7
+ #
8
+ # Creates the HashStore
9
+ #
10
+ def initialize
11
+ super
12
+ @store = {}
13
+ end
14
+
15
+ #
16
+ # Retrieves a credential from the hash store
17
+ #
18
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection <description>
19
+ # @param [Hash] _options unused options hash
20
+ #
21
+ # @return [Remotus::Auth::Credential, nil] found credential or nil
22
+ #
23
+ def credential(connection, **_options)
24
+ @store[connection.host.downcase]
25
+ end
26
+
27
+ #
28
+ # Adds a credential to the store for a given connection
29
+ #
30
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
31
+ # @param [Remotus::Auth::Credential] credential new credential
32
+ #
33
+ def add(connection, credential)
34
+ @store[connection.host.downcase] = credential
35
+ end
36
+
37
+ #
38
+ # Removes a credential from the store for a given connection
39
+ #
40
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
41
+ #
42
+ def remove(connection)
43
+ @store.delete(connection.host.downcase)
44
+ end
45
+
46
+ #
47
+ # String representation of the hash store
48
+ #
49
+ # @return [String] string representation of the hash store
50
+ #
51
+ def to_s
52
+ "HashStore"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remotus
4
+ module Auth
5
+ # Authentication store base class
6
+ class Store
7
+ #
8
+ # Base method fo retrieving a credential from the hash store.
9
+ # This must be overridden in derived classes or it will raise an exception.
10
+ #
11
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] _connection unused associated connection
12
+ # @param [Hash] _options unused options hash
13
+ #
14
+ def credential(_connection, **_options)
15
+ raise Remotus::MissingOverride, "credential method not implemented in credential store #{self.class}"
16
+ end
17
+
18
+ #
19
+ # Gets the user for a given connection and options
20
+ #
21
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
22
+ # @param [Hash] options options hash
23
+ #
24
+ # @return [String] user
25
+ #
26
+ def user(connection, **options)
27
+ credential(connection, **options)&.user
28
+ end
29
+
30
+ #
31
+ # Gets the password for a given connection and options
32
+ #
33
+ # @param [Remotus::SshConnection, Remotus::WinrmConnection, #host] connection associated connection
34
+ # @param [Hash] options options hash
35
+ #
36
+ # @return [String] password
37
+ #
38
+ def password(connection, **options)
39
+ credential(connection, **options)&.password
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "remotus"
4
+ require "remotus/auth"
5
+ require "remotus/ssh_connection"
6
+ require "remotus/winrm_connection"
7
+ require "connection_pool"
8
+
9
+ module Remotus
10
+ # Class representing a remote connection pool (SSH or WinRM) for a single host
11
+ class HostPool
12
+ # @return [Time] when the host pool will expire
13
+ attr_reader :expiration_time
14
+
15
+ # @return [Integer] size of the host connection pool
16
+ attr_reader :size
17
+
18
+ # @return [Integer] Number of seconds to wait for a connection from the pool
19
+ attr_reader :timeout
20
+
21
+ # @return [Symbol] host pool protocol (:ssh or :winrm)
22
+ attr_reader :proto
23
+
24
+ # @return [String] host pool remote host
25
+ attr_reader :host
26
+
27
+ # Number of seconds to wait for a connection from the pool by default
28
+ DEFAULT_EXPIRATION_SECONDS = 600
29
+
30
+ # Default size of the connection pool
31
+ DEFAULT_POOL_SIZE = 2
32
+
33
+ #
34
+ # Creates a host pool for a specific host
35
+ #
36
+ # @param [String] host hostname
37
+ # @param [Integer] size number of connections in the pool (optional)
38
+ # @param [Integer] timeout amount of time to wait for a connection from the pool (optional)
39
+ # @param [Integer] port port to use for the connection
40
+ # @param [Symbol] proto protocol to use for the connection (:winrm, :ssh), must be specified if port is specified
41
+ #
42
+ def initialize(host, size: DEFAULT_POOL_SIZE, timeout: DEFAULT_EXPIRATION_SECONDS, port: nil, proto: nil)
43
+ Remotus.logger.debug { "Creating host pool for #{host}" }
44
+
45
+ @host = host
46
+ @proto = proto || Remotus.host_type(host)
47
+
48
+ raise Remotus::HostTypeDeterminationError, "Could not determine whether to use SSH or WinRM for #{host}" unless @proto
49
+
50
+ connection_class = Object.const_get("Remotus::#{@proto.capitalize}Connection")
51
+ port ||= connection_class::REMOTE_PORT
52
+
53
+ @pool = ConnectionPool.new(size: size, timeout: timeout) { connection_class.new(host, port) }
54
+ @size = size.to_i
55
+ @timeout = timeout.to_i
56
+ @expiration_time = Time.now + timeout
57
+ end
58
+
59
+ #
60
+ # Whether the pool is currently expired
61
+ #
62
+ # @return [Boolean] whether pool is expired
63
+ #
64
+ def expired?
65
+ Time.now > @expiration_time
66
+ end
67
+
68
+ #
69
+ # Force immediate expiration of the pool
70
+ #
71
+ def expire
72
+ Remotus.logger.debug { "Expiring #{@proto} host pool #{object_id} (#{@host})" }
73
+ @expiration_time = Time.now
74
+ end
75
+
76
+ #
77
+ # Provides an SSH or WinRM connection to a given block of code
78
+ #
79
+ # @example Run a command over SSH or WinRM using a pooled connection
80
+ # pool.with { |c| c.run("ls") }
81
+ #
82
+ # @param [Hash] options options hash
83
+ # @option options [Integer] :timeout amount of time to wait for a connection if none is available
84
+ #
85
+ # @return [Object] return value of the provided block
86
+ #
87
+ def with(**options, &block)
88
+ # Update expiration time since the pool has been used
89
+ @expiration_time = Time.now + (@timeout + options[:timeout].to_i)
90
+ Remotus.logger.debug { "Updating #{@proto} host pool #{object_id} (#{@host}) expiration time to #{@expiration_time}" }
91
+
92
+ # Run the provided block against the connection
93
+ Remotus.logger.debug { "Running block in #{@proto} host pool #{object_id} (#{@host})" }
94
+ @pool.with(options, &block)
95
+ end
96
+
97
+ #
98
+ # Gets remote host connection port
99
+ # @see Remotus::SshConnection#port
100
+ # @see Remotus::WinrmConnection#port
101
+ #
102
+ def port
103
+ Remotus.logger.debug { "Getting port from #{@proto} host pool #{object_id} (#{@host})" }
104
+ with(&:port)
105
+ end
106
+
107
+ #
108
+ # Checks if connection port is open on the remote host
109
+ # @see Remotus::SshConnection#port_open?
110
+ # @see Remotus::WinrmConnection#port_open?
111
+ #
112
+ def port_open?
113
+ Remotus.logger.debug { "Checking if port is open from #{@proto} host pool #{object_id} (#{@host})" }
114
+ with(&:port_open?)
115
+ end
116
+
117
+ #
118
+ # Runs command on the remote host
119
+ # @see Remotus::SshConnection#run
120
+ # @see Remotus::WinrmConnection#run
121
+ #
122
+ def run(command, *args, **options)
123
+ with { |c| c.run(command, *args, **options) }
124
+ end
125
+
126
+ #
127
+ # Runs script on the remote host
128
+ # @see Remotus::SshConnection#run_script
129
+ # @see Remotus::WinrmConnection#run_script
130
+ #
131
+ def run_script(local_path, remote_path, *args, **options)
132
+ with { |c| c.run_script(local_path, remote_path, *args, **options) }
133
+ end
134
+
135
+ #
136
+ # Uploads file to the remote host
137
+ # @see Remotus::SshConnection#upload
138
+ # @see Remotus::WinrmConnection#upload
139
+ #
140
+ def upload(local_path, remote_path, **options)
141
+ with { |c| c.upload(local_path, remote_path, **options) }
142
+ end
143
+
144
+ #
145
+ # Downloads file from the remote host
146
+ # @see Remotus::SshConnection#download
147
+ # @see Remotus::WinrmConnection#download
148
+ #
149
+ def download(remote_path, local_path = nil, **options)
150
+ with { |c| c.download(remote_path, local_path, **options) }
151
+ end
152
+
153
+ #
154
+ # Checks if file exists on the remote host
155
+ # @see Remotus::SshConnection#file_exist?
156
+ # @see Remotus::WinrmConnection#file_exist?
157
+ #
158
+ def file_exist?(remote_path, **options)
159
+ with { |c| c.file_exist?(remote_path, **options) }
160
+ end
161
+
162
+ #
163
+ # Gets the current host credential (if any)
164
+ # @see Remotus::Auth#credential
165
+ #
166
+ def credential(**options)
167
+ with { |c| Remotus::Auth.credential(c, **options) }
168
+ end
169
+
170
+ #
171
+ # Sets the current host credential
172
+ #
173
+ # @param [Remotus::Auth::Credential, Hash] credential new credential
174
+ #
175
+ def credential=(credential)
176
+ # If the credential is a hash, transform it prior to setting it
177
+ credential = Remotus::Auth::Credential.from_hash(credential) unless credential.is_a?(Remotus::Auth::Credential)
178
+ Remotus::Auth.cache[host] = credential
179
+ end
180
+ end
181
+ end