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 +5 -5
- data/README.md +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +1 -0
- data/lib/smart_proxy_remote_execution_ssh/api.rb +33 -0
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +269 -0
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +1 -0
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh/webrick_ext.rb +17 -0
- data/settings.d/remote_execution_ssh.yml.example +5 -0
- metadata +38 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a04bba60b9857717faf87e96b17f1734feb66dab9ab009517843d843868791ec
|
4
|
+
data.tar.gz: 27a2bc2d81c77709a4a414f04c7ee1dbcbdbe532727c2083b2734384e4b80c3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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": "
|
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,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
|
@@ -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.
|
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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.
|
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.
|
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
|
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
|
125
|
-
|
126
|
-
|
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
|
-
-
|
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
|
-
|
164
|
-
|
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: []
|