smart_proxy_remote_execution_ssh 0.2.0 → 0.2.1
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 +4 -4
- data/lib/smart_proxy_remote_execution_ssh/api.rb +13 -0
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +252 -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 +11 -0
- data/lib/smart_proxy_remote_execution_ssh.rb +1 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81a9638ca473014a1f61251fa81823fb907cf725ae0b56a5921c0ad96c6b39be
|
4
|
+
data.tar.gz: 850685e0501af25d38a42061e1422db61b28fe99d2f56aff6edcdfd5501eb69c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce27e71082ff1eed48c4b32bedaf6ea6f8a2998bae010e154938b24de3704ff8297e1000ca3b75a33fc76c46593f555c10236b7a9cb38389322d65d05466cd08
|
7
|
+
data.tar.gz: c733a1a0a5865180b3e62a9b10d2bff8e38dca375fed0acc3a0cb80202784d0dec6b185919d00e8e096d55b8c9ef4c6353d376cc06fae99c307094123dbd6255
|
@@ -1,9 +1,22 @@
|
|
1
1
|
module Proxy::RemoteExecution
|
2
2
|
module Ssh
|
3
|
+
|
3
4
|
class Api < ::Sinatra::Base
|
5
|
+
include Sinatra::Authorization::Helpers
|
6
|
+
|
4
7
|
get "/pubkey" do
|
5
8
|
File.read(Ssh.public_key_file)
|
6
9
|
end
|
10
|
+
|
11
|
+
post "/session" do
|
12
|
+
do_authorize_any
|
13
|
+
session = Cockpit::Session.new(env)
|
14
|
+
unless session.valid?
|
15
|
+
return [ 400, "Invalid request: /ssh/session requires connection upgrade to 'raw'" ]
|
16
|
+
end
|
17
|
+
session.hijack!
|
18
|
+
101
|
19
|
+
end
|
7
20
|
end
|
8
21
|
end
|
9
22
|
end
|
@@ -0,0 +1,252 @@
|
|
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, :getsockopt, :fcntl, :close, :closed?, :do_not_reverse_lookup=)
|
13
|
+
|
14
|
+
def initialize(socket)
|
15
|
+
@socket = socket
|
16
|
+
initialize_buffered_io
|
17
|
+
end
|
18
|
+
|
19
|
+
def recv
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def send
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.applies_for?(socket)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build(socket)
|
32
|
+
klass = [OpenSSLBufferedSocket, StandardBufferedSocket].find do |potential_class|
|
33
|
+
potential_class.applies_for?(socket)
|
34
|
+
end
|
35
|
+
raise "No suitable implementation of buffered socket available for #{socket.inspect}" unless klass
|
36
|
+
klass.new(socket)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class StandardBufferedSocket < BufferedSocket
|
41
|
+
def_delegators(:@socket, :send, :recv)
|
42
|
+
|
43
|
+
def self.applies_for?(socket)
|
44
|
+
socket.respond_to?(:send) && socket.respond_to?(:recv)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class OpenSSLBufferedSocket < BufferedSocket
|
49
|
+
def self.applies_for?(socket)
|
50
|
+
socket.is_a? ::OpenSSL::SSL::SSLSocket
|
51
|
+
end
|
52
|
+
def_delegators(:@socket, :read_nonblock, :write_nonblock, :close)
|
53
|
+
|
54
|
+
def recv(n)
|
55
|
+
res = ""
|
56
|
+
begin
|
57
|
+
# To drain a SSLSocket before we can go back to the event
|
58
|
+
# loop, we need to repeatedly call read_nonblock; a single
|
59
|
+
# call is not enough.
|
60
|
+
while true
|
61
|
+
res += @socket.read_nonblock(n)
|
62
|
+
end
|
63
|
+
rescue IO::WaitReadable
|
64
|
+
# Sometimes there is no payload after reading everything
|
65
|
+
# from the underlying socket, but a empty string is treated
|
66
|
+
# as EOF by Net::SSH. So we block a bit until we have
|
67
|
+
# something to return.
|
68
|
+
if res == ""
|
69
|
+
IO.select([@socket.to_io])
|
70
|
+
retry
|
71
|
+
else
|
72
|
+
res
|
73
|
+
end
|
74
|
+
rescue IO::WaitWritable
|
75
|
+
# A renegotiation is happening, let it proceed.
|
76
|
+
IO.select(nil, [@socket.to_io])
|
77
|
+
retry
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def send(mesg, flags)
|
82
|
+
begin
|
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
|
+
end
|
92
|
+
|
93
|
+
class Session
|
94
|
+
include ::Proxy::Log
|
95
|
+
|
96
|
+
def initialize(env)
|
97
|
+
@env = env
|
98
|
+
end
|
99
|
+
|
100
|
+
def valid?
|
101
|
+
@env["HTTP_CONNECTION"] == "upgrade" && @env["HTTP_UPGRADE"].to_s.split(',').any? { |part| part.strip == "raw" }
|
102
|
+
end
|
103
|
+
|
104
|
+
def hijack!
|
105
|
+
@socket = nil
|
106
|
+
if @env['WEBRICK_SOCKET']
|
107
|
+
@socket = @env['WEBRICK_SOCKET']
|
108
|
+
elsif @env['rack.hijack?']
|
109
|
+
begin
|
110
|
+
@env['rack.hijack'].call
|
111
|
+
rescue NotImplementedError
|
112
|
+
end
|
113
|
+
@socket = @env['rack.hijack_io']
|
114
|
+
end
|
115
|
+
raise 'Internal error: request hijacking not available' unless @socket
|
116
|
+
ssh_on_socket
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def ssh_on_socket
|
122
|
+
with_error_handling { start_ssh_loop }
|
123
|
+
end
|
124
|
+
|
125
|
+
def with_error_handling
|
126
|
+
yield
|
127
|
+
rescue Net::SSH::AuthenticationFailed => e
|
128
|
+
send_error(401, e.message)
|
129
|
+
rescue Errno::EHOSTUNREACH
|
130
|
+
send_error(400, "No route to #{host}")
|
131
|
+
rescue SystemCallError => e
|
132
|
+
send_error(400, e.message)
|
133
|
+
rescue SocketError => e
|
134
|
+
send_error(400, e.message)
|
135
|
+
rescue Exception => e
|
136
|
+
logger.error e.message
|
137
|
+
logger.debug e.backtrace.join("\n")
|
138
|
+
send_error(500, "Internal error") unless @started
|
139
|
+
ensure
|
140
|
+
unless buf_socket.closed?
|
141
|
+
buf_socket.wait_for_pending_sends
|
142
|
+
buf_socket.close
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def start_ssh_loop
|
147
|
+
err_buf = ""
|
148
|
+
|
149
|
+
Net::SSH.start(host, ssh_user, ssh_options) do |ssh|
|
150
|
+
channel = ssh.open_channel do |ch|
|
151
|
+
ch.exec(command) do |ch, success|
|
152
|
+
raise "could not execute command" unless success
|
153
|
+
|
154
|
+
ssh.listen_to(buf_socket)
|
155
|
+
|
156
|
+
ch.on_process do
|
157
|
+
if buf_socket.available > 0
|
158
|
+
ch.send_data(buf_socket.read_available)
|
159
|
+
end
|
160
|
+
if buf_socket.closed?
|
161
|
+
ch.close
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
ch.on_data do |ch2, data|
|
166
|
+
send_start
|
167
|
+
buf_socket.enqueue(data)
|
168
|
+
end
|
169
|
+
|
170
|
+
ch.on_request('exit-status') do |ch, data|
|
171
|
+
code = data.read_long
|
172
|
+
send_start.call if code.zero?
|
173
|
+
err_buf += "Process exited with code #{code}.\r\n"
|
174
|
+
ch.close
|
175
|
+
end
|
176
|
+
|
177
|
+
ch.on_request('exit-signal') do |ch, data|
|
178
|
+
err_buf += "Process was terminated with signal #{data.read_string}.\r\n"
|
179
|
+
ch.close
|
180
|
+
end
|
181
|
+
|
182
|
+
ch.on_extended_data do |ch2, type, data|
|
183
|
+
err_buf += data
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
channel.wait
|
189
|
+
send_error(400, err_buf) unless @started
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def send_start
|
194
|
+
unless @started
|
195
|
+
@started = true
|
196
|
+
buf_socket.enqueue("Status: 101\r\n")
|
197
|
+
buf_socket.enqueue("Connection: upgrade\r\n")
|
198
|
+
buf_socket.enqueue("Upgrade: raw\r\n")
|
199
|
+
buf_socket.enqueue("\r\n")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def send_error(code, msg)
|
204
|
+
buf_socket.enqueue("Status: #{code}\r\n")
|
205
|
+
buf_socket.enqueue("Connection: close\r\n")
|
206
|
+
buf_socket.enqueue("\r\n")
|
207
|
+
buf_socket.enqueue(msg)
|
208
|
+
end
|
209
|
+
|
210
|
+
def params
|
211
|
+
@params ||= MultiJson.load(@env["rack.input"].read)
|
212
|
+
end
|
213
|
+
|
214
|
+
def key_file
|
215
|
+
@key_file ||= Proxy::RemoteExecution::Ssh.private_key_file
|
216
|
+
end
|
217
|
+
|
218
|
+
def buf_socket
|
219
|
+
@buffered_socket ||= BufferedSocket.build(@socket)
|
220
|
+
end
|
221
|
+
|
222
|
+
def command
|
223
|
+
params["command"]
|
224
|
+
end
|
225
|
+
|
226
|
+
def ssh_user
|
227
|
+
params["ssh_user"]
|
228
|
+
end
|
229
|
+
|
230
|
+
def host
|
231
|
+
params["hostname"]
|
232
|
+
end
|
233
|
+
|
234
|
+
def ssh_options
|
235
|
+
auth_methods = %w(publickey)
|
236
|
+
auth_methods.unshift('password') if params["ssh_password"]
|
237
|
+
|
238
|
+
ret = { }
|
239
|
+
ret[:port] = params["ssh_port"] if params["ssh_port"]
|
240
|
+
ret[:keys] = [ key_file ] if key_file
|
241
|
+
ret[:password] = params["ssh_password"] if params["ssh_password"]
|
242
|
+
ret[:passphrase] = params[:ssh_key_passphrase] if params[:ssh_key_passphrase]
|
243
|
+
ret[:keys_only] = true
|
244
|
+
ret[:auth_methods] = auth_methods
|
245
|
+
ret[:verify_host_key] = true
|
246
|
+
ret[:number_of_password_prompts] = 1
|
247
|
+
ret
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,11 @@
|
|
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('WEBRICK_SOCKET' => @socket)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
::WEBrick::HTTPRequest.send(:prepend, WEBrickExt::HTTPRequestExt)
|
11
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_remote_execution_ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Nečas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -128,6 +128,20 @@ dependencies:
|
|
128
128
|
- - "<"
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: 0.3.0
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: net-ssh
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :runtime
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
131
145
|
description: " Ssh remote execution provider for Foreman Smart-Proxy\n"
|
132
146
|
email:
|
133
147
|
- inecas@redhat.com
|
@@ -142,9 +156,11 @@ files:
|
|
142
156
|
- bundler.plugins.d/remote_execution_ssh.rb
|
143
157
|
- lib/smart_proxy_remote_execution_ssh.rb
|
144
158
|
- lib/smart_proxy_remote_execution_ssh/api.rb
|
159
|
+
- lib/smart_proxy_remote_execution_ssh/cockpit.rb
|
145
160
|
- lib/smart_proxy_remote_execution_ssh/http_config.ru
|
146
161
|
- lib/smart_proxy_remote_execution_ssh/plugin.rb
|
147
162
|
- lib/smart_proxy_remote_execution_ssh/version.rb
|
163
|
+
- lib/smart_proxy_remote_execution_ssh/webrick_ext.rb
|
148
164
|
- settings.d/remote_execution_ssh.yml.example
|
149
165
|
homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
|
150
166
|
licenses:
|
@@ -166,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
182
|
version: '0'
|
167
183
|
requirements: []
|
168
184
|
rubyforge_project:
|
169
|
-
rubygems_version: 2.7.
|
185
|
+
rubygems_version: 2.7.6
|
170
186
|
signing_key:
|
171
187
|
specification_version: 4
|
172
188
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|