smart_proxy_remote_execution_ssh 0.1.6 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f1fa864f93c540a4c0c0e1cb837c4ed96962bc5d
4
- data.tar.gz: 8e25bfe21c97d5b15039c6396eb18d86ee3d0eb8
2
+ SHA256:
3
+ metadata.gz: a04bba60b9857717faf87e96b17f1734feb66dab9ab009517843d843868791ec
4
+ data.tar.gz: 27a2bc2d81c77709a4a414f04c7ee1dbcbdbe532727c2083b2734384e4b80c3e
5
5
  SHA512:
6
- metadata.gz: f525bf0fcd9c1ebd907bd18c18cda1619641f3284b8e908273f950d9b8e5bd2ddddfcee1ab72c1a8f0aab12433670effea419bf7df5c5cb69a8761b135f4349c
7
- data.tar.gz: eee8395b05faed6a1b0889dc46d3c4a1a73807187701d8c505b4bc8650573d3507aeda0e9de92401cefaac2442a46c0d68dd53a88b9ed882b779a1b23d41f8ad
6
+ metadata.gz: 3b9bd1e86b27273b27de65083052611fc4de5eb012705f2f0b31a5e77843b647d81fd67c10bd28abcb22d928e1db65bb6c18131fb872cb4bdaac4fd319061dde
7
+ data.tar.gz: 20596fd3597915a897eac49051c45228c12fb91753164c23d64b8f7cccefbe1b6567f4e2b5620edebe7108bf7063b61ea037c55ecdc7d87c4177862ca56413fc
data/README.md CHANGED
@@ -51,7 +51,7 @@ The simplest thing one can do is just to trigger a command:
51
51
  ```
52
52
  curl http://my-proxy.example.com:9292/dynflow/tasks \
53
53
  -X POST -H 'Content-Type: application/json'\
54
- -d '{"action_name": "Proxy::RemoteExecution::Ssh::CommandAction",
54
+ -d '{"action_name": "ForemanRemoteExecutionCore::Actions::RunScript",
55
55
  "action_input": {"task_id" : "1234'$RANDOM'",
56
56
  "script": "/usr/bin/ls",
57
57
  "hostname": "localhost",
@@ -1,5 +1,6 @@
1
1
  require 'smart_proxy_remote_execution_ssh/version'
2
2
  require 'smart_proxy_dynflow'
3
+ require 'smart_proxy_remote_execution_ssh/webrick_ext'
3
4
  require 'smart_proxy_remote_execution_ssh/plugin'
4
5
 
5
6
  module Proxy::RemoteExecution
@@ -1,9 +1,42 @@
1
+ require 'net/ssh'
2
+ require 'base64'
3
+
1
4
  module Proxy::RemoteExecution
2
5
  module Ssh
6
+
3
7
  class Api < ::Sinatra::Base
8
+ include Sinatra::Authorization::Helpers
9
+
4
10
  get "/pubkey" do
5
11
  File.read(Ssh.public_key_file)
6
12
  end
13
+
14
+ post "/session" do
15
+ do_authorize_any
16
+ session = Cockpit::Session.new(env)
17
+ unless session.valid?
18
+ return [ 400, "Invalid request: /ssh/session requires connection upgrade to 'raw'" ]
19
+ end
20
+ session.hijack!
21
+ 101
22
+ end
23
+
24
+ delete '/known_hosts/:name' do |name|
25
+ do_authorize_any
26
+ keys = Net::SSH::KnownHosts.search_for(name)
27
+ return [204] if keys.empty?
28
+ ssh_keys = keys.map { |key| Base64.strict_encode64 key.to_blob }
29
+ Net::SSH::KnownHosts.hostfiles({}, :user)
30
+ .map { |file| File.expand_path file }
31
+ .select { |file| File.readable?(file) && File.writable?(file) }
32
+ .each do |host_file|
33
+ lines = File.foreach(host_file).reject do |line|
34
+ ssh_keys.any? { |key| line.end_with? "#{key}\n" }
35
+ end
36
+ File.open(host_file, 'w') { |f| f.write lines.join }
37
+ end
38
+ 204
39
+ end
7
40
  end
8
41
  end
9
42
  end
@@ -0,0 +1,269 @@
1
+ require 'net/ssh'
2
+ require 'forwardable'
3
+
4
+ module Proxy::RemoteExecution
5
+ module Cockpit
6
+ # A wrapper class around different kind of sockets to comply with Net::SSH event loop
7
+ class BufferedSocket
8
+ include Net::SSH::BufferedIo
9
+ extend Forwardable
10
+
11
+ # The list of methods taken from OpenSSL::SSL::SocketForwarder for the object to act like a socket
12
+ def_delegators(:@socket, :to_io, :addr, :peeraddr, :setsockopt,
13
+ :getsockopt, :fcntl, :close, :closed?, :do_not_reverse_lookup=)
14
+
15
+ def initialize(socket)
16
+ @socket = socket
17
+ initialize_buffered_io
18
+ end
19
+
20
+ def recv
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def send
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def self.applies_for?(socket)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def self.build(socket)
33
+ klass = [OpenSSLBufferedSocket, MiniSSLBufferedSocket, StandardBufferedSocket].find do |potential_class|
34
+ potential_class.applies_for?(socket)
35
+ end
36
+ raise "No suitable implementation of buffered socket available for #{socket.inspect}" unless klass
37
+ klass.new(socket)
38
+ end
39
+ end
40
+
41
+ class StandardBufferedSocket < BufferedSocket
42
+ def_delegators(:@socket, :send, :recv)
43
+
44
+ def self.applies_for?(socket)
45
+ socket.respond_to?(:send) && socket.respond_to?(:recv)
46
+ end
47
+ end
48
+
49
+ class OpenSSLBufferedSocket < BufferedSocket
50
+ def self.applies_for?(socket)
51
+ socket.is_a? ::OpenSSL::SSL::SSLSocket
52
+ end
53
+ def_delegators(:@socket, :read_nonblock, :write_nonblock, :close)
54
+
55
+ def recv(n)
56
+ res = ""
57
+ begin
58
+ # To drain a SSLSocket before we can go back to the event
59
+ # loop, we need to repeatedly call read_nonblock; a single
60
+ # call is not enough.
61
+ loop do
62
+ res += @socket.read_nonblock(n)
63
+ end
64
+ rescue IO::WaitReadable
65
+ # Sometimes there is no payload after reading everything
66
+ # from the underlying socket, but a empty string is treated
67
+ # as EOF by Net::SSH. So we block a bit until we have
68
+ # something to return.
69
+ if res == ""
70
+ IO.select([@socket.to_io])
71
+ retry
72
+ else
73
+ res
74
+ end
75
+ rescue IO::WaitWritable
76
+ # A renegotiation is happening, let it proceed.
77
+ IO.select(nil, [@socket.to_io])
78
+ retry
79
+ end
80
+ end
81
+
82
+ def send(mesg, flags)
83
+ @socket.write_nonblock(mesg)
84
+ rescue IO::WaitWritable
85
+ 0
86
+ rescue IO::WaitReadable
87
+ IO.select([@socket.to_io])
88
+ retry
89
+ end
90
+ end
91
+
92
+ class MiniSSLBufferedSocket < BufferedSocket
93
+ def self.applies_for?(socket)
94
+ socket.is_a? ::Puma::MiniSSL::Socket
95
+ end
96
+ def_delegators(:@socket, :read_nonblock, :write_nonblock, :close)
97
+
98
+ def recv(n)
99
+ @socket.read_nonblock(n)
100
+ end
101
+
102
+ def send(mesg, flags)
103
+ @socket.write_nonblock(mesg)
104
+ end
105
+
106
+ def closed?
107
+ @socket.to_io.closed?
108
+ end
109
+ end
110
+
111
+ class Session
112
+ include ::Proxy::Log
113
+
114
+ def initialize(env)
115
+ @env = env
116
+ end
117
+
118
+ def valid?
119
+ @env["HTTP_CONNECTION"] == "upgrade" && @env["HTTP_UPGRADE"].to_s.split(',').any? { |part| part.strip == "raw" }
120
+ end
121
+
122
+ def hijack!
123
+ @socket = nil
124
+ if @env['ext.hijack!']
125
+ @socket = @env['ext.hijack!'].call
126
+ elsif @env['rack.hijack?']
127
+ begin
128
+ @env['rack.hijack'].call
129
+ rescue NotImplementedError
130
+ end
131
+ @socket = @env['rack.hijack_io']
132
+ end
133
+ raise 'Internal error: request hijacking not available' unless @socket
134
+ ssh_on_socket
135
+ end
136
+
137
+ private
138
+
139
+ def ssh_on_socket
140
+ with_error_handling { start_ssh_loop }
141
+ end
142
+
143
+ def with_error_handling
144
+ yield
145
+ rescue Net::SSH::AuthenticationFailed => e
146
+ send_error(401, e.message)
147
+ rescue Errno::EHOSTUNREACH
148
+ send_error(400, "No route to #{host}")
149
+ rescue SystemCallError => e
150
+ send_error(400, e.message)
151
+ rescue SocketError => e
152
+ send_error(400, e.message)
153
+ rescue Exception => e
154
+ logger.error e.message
155
+ logger.debug e.backtrace.join("\n")
156
+ send_error(500, "Internal error") unless @started
157
+ ensure
158
+ unless buf_socket.closed?
159
+ buf_socket.wait_for_pending_sends
160
+ buf_socket.close
161
+ end
162
+ end
163
+
164
+ def start_ssh_loop
165
+ err_buf = ""
166
+
167
+ Net::SSH.start(host, ssh_user, ssh_options) do |ssh|
168
+ channel = ssh.open_channel do |ch|
169
+ ch.exec(command) do |ch, success|
170
+ raise "could not execute command" unless success
171
+
172
+ ssh.listen_to(buf_socket)
173
+
174
+ ch.on_process do
175
+ if buf_socket.available.positive?
176
+ ch.send_data(buf_socket.read_available)
177
+ end
178
+ if buf_socket.closed?
179
+ ch.close
180
+ end
181
+ end
182
+
183
+ ch.on_data do |ch2, data|
184
+ send_start
185
+ buf_socket.enqueue(data)
186
+ end
187
+
188
+ ch.on_request('exit-status') do |ch, data|
189
+ code = data.read_long
190
+ send_start if code.zero?
191
+ err_buf += "Process exited with code #{code}.\r\n"
192
+ ch.close
193
+ end
194
+
195
+ ch.on_request('exit-signal') do |ch, data|
196
+ err_buf += "Process was terminated with signal #{data.read_string}.\r\n"
197
+ ch.close
198
+ end
199
+
200
+ ch.on_extended_data do |ch2, type, data|
201
+ err_buf += data
202
+ end
203
+ end
204
+ end
205
+
206
+ channel.wait
207
+ send_error(400, err_buf) unless @started
208
+ end
209
+ end
210
+
211
+ def send_start
212
+ unless @started
213
+ @started = true
214
+ buf_socket.enqueue("Status: 101\r\n")
215
+ buf_socket.enqueue("Connection: upgrade\r\n")
216
+ buf_socket.enqueue("Upgrade: raw\r\n")
217
+ buf_socket.enqueue("\r\n")
218
+ end
219
+ end
220
+
221
+ def send_error(code, msg)
222
+ buf_socket.enqueue("Status: #{code}\r\n")
223
+ buf_socket.enqueue("Connection: close\r\n")
224
+ buf_socket.enqueue("\r\n")
225
+ buf_socket.enqueue(msg)
226
+ end
227
+
228
+ def params
229
+ @params ||= MultiJson.load(@env["rack.input"].read)
230
+ end
231
+
232
+ def key_file
233
+ @key_file ||= Proxy::RemoteExecution::Ssh.private_key_file
234
+ end
235
+
236
+ def buf_socket
237
+ @buffered_socket ||= BufferedSocket.build(@socket)
238
+ end
239
+
240
+ def command
241
+ params["command"]
242
+ end
243
+
244
+ def ssh_user
245
+ params["ssh_user"]
246
+ end
247
+
248
+ def host
249
+ params["hostname"]
250
+ end
251
+
252
+ def ssh_options
253
+ auth_methods = %w[publickey]
254
+ auth_methods.unshift('password') if params["ssh_password"]
255
+
256
+ ret = {}
257
+ ret[:port] = params["ssh_port"] if params["ssh_port"]
258
+ ret[:keys] = [key_file] if key_file
259
+ ret[:password] = params["ssh_password"] if params["ssh_password"]
260
+ ret[:passphrase] = params["ssh_key_passphrase"] if params["ssh_key_passphrase"]
261
+ ret[:keys_only] = true
262
+ ret[:auth_methods] = auth_methods
263
+ ret[:verify_host_key] = true
264
+ ret[:number_of_password_prompts] = 1
265
+ ret
266
+ end
267
+ end
268
+ end
269
+ end
@@ -15,6 +15,7 @@ module Proxy::RemoteExecution::Ssh
15
15
  after_activation do
16
16
  require 'smart_proxy_dynflow'
17
17
  require 'smart_proxy_remote_execution_ssh/version'
18
+ require 'smart_proxy_remote_execution_ssh/cockpit'
18
19
  require 'smart_proxy_remote_execution_ssh/api'
19
20
 
20
21
  begin
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.1.6'
4
+ VERSION = '0.3.2'
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,17 @@
1
+ module SmartProxyRemoteExecutionSsh
2
+ module WEBrickExt
3
+ # An extension to ::WEBrick::HTTPRequest to expost the socket object for highjacking for cockpit
4
+ module HTTPRequestExt
5
+ def meta_vars
6
+ super.merge('ext.hijack!' => -> {
7
+ # This stops Webrick from sending its own reply.
8
+ @request_line = nil;
9
+ # This stops Webrick from trying to read the next request on the socket.
10
+ @keep_alive = false;
11
+ return @socket;
12
+ })
13
+ end
14
+ end
15
+ end
16
+ ::WEBrick::HTTPRequest.send(:prepend, WEBrickExt::HTTPRequestExt)
17
+ end
@@ -10,3 +10,8 @@
10
10
  # for new data leave empty to use the runner's default
11
11
  # (1 second for regular, 60 seconds with async_ssh enabled)
12
12
  # :runner_refresh_interval:
13
+
14
+ # Defines the verbosity of logging coming from Net::SSH
15
+ # one of :debug, :info, :warn, :error, :fatal
16
+ # must be lower than general log level
17
+ # :ssh_log_level: fatal
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_remote_execution_ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-06 00:00:00.000000000 Z
11
+ date: 2021-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -98,32 +98,45 @@ dependencies:
98
98
  name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - '='
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.32.1
103
+ version: 0.82.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - '='
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.32.1
110
+ version: 0.82.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: smart_proxy_dynflow
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 0.1.0
117
+ version: '0.1'
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 0.1.0
125
- description: |2
126
- Ssh remote execution provider for Foreman Smart-Proxy
124
+ version: '0.1'
125
+ - !ruby/object:Gem::Dependency
126
+ name: net-ssh
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: " Ssh remote execution provider for Foreman Smart-Proxy\n"
127
140
  email:
128
141
  - inecas@redhat.com
129
142
  executables: []
@@ -137,15 +150,17 @@ files:
137
150
  - bundler.plugins.d/remote_execution_ssh.rb
138
151
  - lib/smart_proxy_remote_execution_ssh.rb
139
152
  - lib/smart_proxy_remote_execution_ssh/api.rb
153
+ - lib/smart_proxy_remote_execution_ssh/cockpit.rb
140
154
  - lib/smart_proxy_remote_execution_ssh/http_config.ru
141
155
  - lib/smart_proxy_remote_execution_ssh/plugin.rb
142
156
  - lib/smart_proxy_remote_execution_ssh/version.rb
157
+ - lib/smart_proxy_remote_execution_ssh/webrick_ext.rb
143
158
  - settings.d/remote_execution_ssh.yml.example
144
159
  homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
145
160
  licenses:
146
- - GPLv3
161
+ - GPL-3.0
147
162
  metadata: {}
148
- post_install_message:
163
+ post_install_message:
149
164
  rdoc_options: []
150
165
  require_paths:
151
166
  - lib
@@ -160,9 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
175
  - !ruby/object:Gem::Version
161
176
  version: '0'
162
177
  requirements: []
163
- rubyforge_project:
164
- rubygems_version: 2.6.12
165
- signing_key:
178
+ rubygems_version: 3.1.2
179
+ signing_key:
166
180
  specification_version: 4
167
181
  summary: Ssh remote execution provider for Foreman Smart-Proxy
168
182
  test_files: []