smart_proxy_remote_execution_ssh 0.1.6 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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: []