remotus 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.
@@ -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