smart_proxy_remote_execution_ssh 0.2.0 → 0.2.1

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
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