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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8dc1047c21ec17cf7485df70ef6bf56fa5eff1d05b1e1ba5cc38f334a4c6ed1
4
- data.tar.gz: b262ea28dc1bd77278a83134afbe334d6c2a413977eacf3eb6fd6e877123164b
3
+ metadata.gz: 81a9638ca473014a1f61251fa81823fb907cf725ae0b56a5921c0ad96c6b39be
4
+ data.tar.gz: 850685e0501af25d38a42061e1422db61b28fe99d2f56aff6edcdfd5501eb69c
5
5
  SHA512:
6
- metadata.gz: 4e0b5ddece76bc8bbcf24300d3e80dfe75244f6e11333dc247c63a2f6a2216ca9915e7765f24aedcd781eb14e3dadd43142f64c02b44ab63a1ee4ec5c37fbd96
7
- data.tar.gz: ef98b2d1813f30d0ce5f35c79497e40ffabc73534f770fd2b82f10bbd03d996833a5f999fdd937177b3c639daa0bacd3908d2610c9a3c63ea565369bf9cec0c5
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
@@ -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.2.0'
4
+ VERSION = '0.2.1'
5
5
  end
6
6
  end
7
7
  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
@@ -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
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.0
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: 2018-04-06 00:00:00.000000000 Z
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.3
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