smart_proxy_remote_execution_ssh 0.2.0 → 0.4.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: f90b757baa9c530c89d5c6c960ebf69601b173886c2f74560513592ab78697e4
4
+ data.tar.gz: edf9bc88acd2f859f86dd15ddee26deb948f6b963ddcd6aee2c6d839e12198ed
5
5
  SHA512:
6
- metadata.gz: 4e0b5ddece76bc8bbcf24300d3e80dfe75244f6e11333dc247c63a2f6a2216ca9915e7765f24aedcd781eb14e3dadd43142f64c02b44ab63a1ee4ec5c37fbd96
7
- data.tar.gz: ef98b2d1813f30d0ce5f35c79497e40ffabc73534f770fd2b82f10bbd03d996833a5f999fdd937177b3c639daa0bacd3908d2610c9a3c63ea565369bf9cec0c5
6
+ metadata.gz: 56cb17a0d7ac5eb456533e72e83aaffb07892a725609685f84c95571afa4fb0868dbd4017c4cf5ceadafa35aaf01beca27bdadb424b5bf0c0a5280c704f21e2c
7
+ data.tar.gz: 04c1feec777172574e45369f48219a6d2ea80e7e704a695198ef56bb11b5d4c178c68650414a190f4e57751521a065ad5c0c649ad8842f99be03259f79f3c9f3
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,6 +1,7 @@
1
- require 'smart_proxy_remote_execution_ssh/version'
2
1
  require 'smart_proxy_dynflow'
2
+ require 'smart_proxy_remote_execution_ssh/version'
3
3
  require 'smart_proxy_remote_execution_ssh/plugin'
4
+ require 'smart_proxy_remote_execution_ssh/webrick_ext'
4
5
 
5
6
  module Proxy::RemoteExecution
6
7
  module Ssh
@@ -18,6 +19,8 @@ module Proxy::RemoteExecution
18
19
  unless File.exist?(public_key_file)
19
20
  raise "Ssh public key file #{public_key_file} doesn't exist"
20
21
  end
22
+
23
+ validate_ssh_log_level!
21
24
  end
22
25
 
23
26
  def private_key_file
@@ -27,6 +30,27 @@ module Proxy::RemoteExecution
27
30
  def public_key_file
28
31
  File.expand_path("#{private_key_file}.pub")
29
32
  end
33
+
34
+ def validate_ssh_log_level!
35
+ wanted_level = Plugin.settings.ssh_log_level.to_s
36
+ levels = Plugin::SSH_LOG_LEVELS
37
+ unless levels.include? wanted_level
38
+ raise "Wrong value '#{Plugin.settings.ssh_log_level}' for ssh_log_level, must be one of #{levels.join(', ')}"
39
+ end
40
+
41
+ current = ::Proxy::SETTINGS.log_level.to_s.downcase
42
+
43
+ # regular log levels correspond to upcased ssh logger levels
44
+ ssh, regular = [wanted_level, current].map do |wanted|
45
+ levels.each_with_index.find { |value, _index| value == wanted }.last
46
+ end
47
+
48
+ if ssh < regular
49
+ raise 'ssh_log_level cannot be more verbose than regular log level'
50
+ end
51
+
52
+ Plugin.settings.ssh_log_level = Plugin.settings.ssh_log_level.to_sym
53
+ end
30
54
  end
31
55
  end
32
56
  end
@@ -0,0 +1,21 @@
1
+ require 'smart_proxy_dynflow/action/shareable'
2
+ require 'smart_proxy_dynflow/action/runner'
3
+
4
+ module Proxy::RemoteExecution::Ssh
5
+ module Actions
6
+ class RunScript < Proxy::Dynflow::Action::Runner
7
+ def initiate_runner
8
+ additional_options = {
9
+ :step_id => run_step_id,
10
+ :uuid => execution_plan_id,
11
+ }
12
+ Proxy::RemoteExecution::Ssh::Plugin.runner_class.build(input.merge(additional_options),
13
+ suspended_action: suspended_action)
14
+ end
15
+
16
+ def runner_dispatcher
17
+ Dispatcher.instance
18
+ end
19
+ end
20
+ end
21
+ end
@@ -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,110 @@
1
+ #!/bin/sh
2
+ #
3
+ # Control script for the remote execution jobs.
4
+ #
5
+ # The initial script calls `$CONTROL_SCRIPT init-script-finish` once the original script exits.
6
+ # In automatic mode, the exit code is sent back to the proxy on `init-script-finish`.
7
+ #
8
+ # What the script provides is also a manual mode, where the author of the rex script can take
9
+ # full control of the job lifecycle. This allows keeping the marked as running even when
10
+ # the initial script finishes.
11
+ #
12
+ # The manual mode is turned on by calling `$CONTROL_SCRIPT manual-control`. After calling this,
13
+ # one can call `echo message | $CONTROL_SCRIPT update` to send output to the remote execution jobs
14
+ # and `$CONTROL_SCRIPT finish 0` once finished (with 0 as exit code) to send output to the remote execution jobs
15
+ # and `$CONTROL_SCRIPT finish 0` once finished (with 0 as exit code)
16
+ BASE_DIR="$(dirname "$(readlink -f "$0")")"
17
+
18
+ if ! command -v curl >/dev/null; then
19
+ echo 'curl is required' >&2
20
+ exit 1
21
+ fi
22
+
23
+ # send the callback data to proxy
24
+ update() {
25
+ "$BASE_DIR/retrieve.sh" push_update
26
+ }
27
+
28
+ # wait for named pipe $1 to retrieve data. If $2 is provided, it serves as timeout
29
+ # in seconds on how long to wait when reading.
30
+ wait_for_pipe() {
31
+ pipe_path=$1
32
+ if [ -n "$2" ]; then
33
+ timeout="-t $2"
34
+ fi
35
+ if read $timeout <>"$pipe_path"; then
36
+ rm "$pipe_path"
37
+ return 0
38
+ else
39
+ return 1
40
+ fi
41
+ }
42
+
43
+ # function run in background, when receiving update data via STDIN.
44
+ periodic_update() {
45
+ interval=1
46
+ # reading some data from periodic_update_control signals we're done
47
+ while ! wait_for_pipe "$BASE_DIR/periodic_update_control" "$interval"; do
48
+ update
49
+ done
50
+ # one more update before we finish
51
+ update
52
+ # signal the main process that we are finished
53
+ echo > "$BASE_DIR/periodic_update_finished"
54
+ }
55
+
56
+ # signal the periodic_update process that the main process is finishing
57
+ periodic_update_finish() {
58
+ if [ -e "$BASE_DIR/periodic_update_control" ]; then
59
+ echo > "$BASE_DIR/periodic_update_control"
60
+ fi
61
+ }
62
+
63
+ ACTION=${1:-finish}
64
+
65
+ case "$ACTION" in
66
+ init-script-finish)
67
+ if ! [ -e "$BASE_DIR/manual_mode" ]; then
68
+ # make the exit code of initialization script the exit code of the whole job
69
+ cp init_exit_code exit_code
70
+ update
71
+ fi
72
+ ;;
73
+ finish)
74
+ # take exit code passed via the command line, with fallback
75
+ # to the exit code of the initialization script
76
+ exit_code=${2:-$(cat "$BASE_DIR/init_exit_code")}
77
+ echo $exit_code > "$BASE_DIR/exit_code"
78
+ update
79
+ if [ -e "$BASE_DIR/manual_mode" ]; then
80
+ rm "$BASE_DIR/manual_mode"
81
+ fi
82
+ ;;
83
+ update)
84
+ # read data from input when redirected though a pipe
85
+ if ! [ -t 0 ]; then
86
+ # couple of named pipes to coordinate the main process with the periodic_update
87
+ mkfifo "$BASE_DIR/periodic_update_control"
88
+ mkfifo "$BASE_DIR/periodic_update_finished"
89
+ trap "periodic_update_finish" EXIT
90
+ # run periodic update as separate process to keep sending updates in output to server
91
+ periodic_update &
92
+ # redirect the input into output
93
+ tee -a "$BASE_DIR/output"
94
+ periodic_update_finish
95
+ # ensure the periodic update finished before we return
96
+ wait_for_pipe "$BASE_DIR/periodic_update_finished"
97
+ else
98
+ update
99
+ fi
100
+ ;;
101
+ # mark the script to be in manual mode: this means the script author needs to use `update` and `finish`
102
+ # commands to send output to the remote execution job or mark it as finished.
103
+ manual-mode)
104
+ touch "$BASE_DIR/manual_mode"
105
+ ;;
106
+ *)
107
+ echo "Unknown action $ACTION"
108
+ exit 1
109
+ ;;
110
+ esac
@@ -0,0 +1,151 @@
1
+ #!/bin/sh
2
+
3
+ if ! pgrep --help 2>/dev/null >/dev/null; then
4
+ echo DONE 1
5
+ echo "pgrep is required" >&2
6
+ exit 1
7
+ fi
8
+
9
+ BASE_DIR="$(dirname "$(readlink -f "$0")")"
10
+
11
+ # load the data required for generating the callback
12
+ . "$BASE_DIR/env.sh"
13
+ URL_PREFIX="$CALLBACK_HOST/dynflow/tasks/$TASK_ID"
14
+ AUTH="$TASK_ID:$OTP"
15
+ CURL="curl --silent --show-error --fail --max-time 10"
16
+
17
+ MY_LOCK_FILE="$BASE_DIR/retrieve_lock.$$"
18
+ MY_PID=$$
19
+ echo $MY_PID >"$MY_LOCK_FILE"
20
+ LOCK_FILE="$BASE_DIR/retrieve_lock"
21
+ TMP_OUTPUT_FILE="$BASE_DIR/tmp_output"
22
+
23
+ RUN_TIMEOUT=30 # for how long can the script hold the lock
24
+ WAIT_TIMEOUT=60 # for how long the script is trying to acquire the lock
25
+ START_TIME=$(date +%s)
26
+
27
+ fail() {
28
+ echo RUNNING
29
+ echo "$1"
30
+ exit 1
31
+ }
32
+
33
+ acquire_lock() {
34
+ # try to acquire lock by creating the file (ln should be atomic an fail in case
35
+ # another process succeeded first). We also check the content of the lock file,
36
+ # in case our process won when competing over the lock while invalidating
37
+ # the lock on timeout.
38
+ ln "$MY_LOCK_FILE" "$LOCK_FILE" 2>/dev/null || [ "$(head -n1 "$LOCK_FILE")" = "$MY_PID" ]
39
+ return $?
40
+ }
41
+
42
+ # acquiring the lock before proceeding, to ensure only one instance of the script is running
43
+ while ! acquire_lock; do
44
+ # we failed to create retrieve_lock - assuming there is already another retrieve script running
45
+ current_pid=$(head -n1 "$LOCK_FILE")
46
+ if [ -z "$current_pid" ]; then
47
+ continue
48
+ fi
49
+ # check whether the lock is not too old (compared to $RUN_TIMEOUT) and try to kill
50
+ # if it is, so that we don't have a stalled processes here
51
+ lock_lines_count=$(wc -l < "$LOCK_FILE")
52
+ current_lock_time=$(stat --format "%Y" "$LOCK_FILE")
53
+ current_time=$(date +%s)
54
+
55
+ if [ "$(( current_time - START_TIME ))" -gt "$WAIT_TIMEOUT" ]; then
56
+ # We were waiting for the lock for too long - just give up
57
+ fail "Wait time exceeded $WAIT_TIMEOUT"
58
+ elif [ "$(( current_time - current_lock_time ))" -gt "$RUN_TIMEOUT" ]; then
59
+ # The previous lock it hold for too long - re-acquiring procedure
60
+ if [ "$lock_lines_count" -gt 1 ]; then
61
+ # there were multiple processes waiting for lock without resolution
62
+ # longer than the $RUN_TIMEOUT - we reset the lock file and let processes
63
+ # to compete
64
+ echo "RETRY" > "$LOCK_FILE"
65
+ fi
66
+ if [ "$current_pid" != "RETRY" ]; then
67
+ # try to kill the currently stalled process
68
+ kill -9 "$current_pid" 2>/dev/null
69
+ fi
70
+ # try to add our process as one candidate
71
+ echo $MY_PID >> "$LOCK_FILE"
72
+ if [ "$( head -n2 "$LOCK_FILE" | tail -n1 )" = "$MY_PID" ]; then
73
+ # our process won the competition for the new lock: it is the first pid
74
+ # after the original one in the lock file - take ownership of the lock
75
+ # next iteration only this process will get through
76
+ echo $MY_PID >"$LOCK_FILE"
77
+ fi
78
+ else
79
+ # still waiting for the original owner to finish
80
+ sleep 1
81
+ fi
82
+ done
83
+
84
+ release_lock() {
85
+ rm "$MY_LOCK_FILE"
86
+ rm "$LOCK_FILE"
87
+ }
88
+ # ensure the release the lock at exit
89
+ trap "release_lock" EXIT
90
+
91
+ # make sure we clear previous tmp output file
92
+ if [ -e "$TMP_OUTPUT_FILE" ]; then
93
+ rm "$TMP_OUTPUT_FILE"
94
+ fi
95
+
96
+ pid=$(cat "$BASE_DIR/pid")
97
+ [ -f "$BASE_DIR/position" ] || echo 1 > "$BASE_DIR/position"
98
+ position=$(cat "$BASE_DIR/position")
99
+
100
+ prepare_output() {
101
+ if [ -e "$BASE_DIR/manual_mode" ] || ([ -n "$pid" ] && pgrep -P "$pid" >/dev/null 2>&1); then
102
+ echo RUNNING
103
+ else
104
+ echo "DONE $(cat "$BASE_DIR/exit_code" 2>/dev/null)"
105
+ fi
106
+ [ -f "$BASE_DIR/output" ] || exit 0
107
+ tail --bytes "+${position}" "$BASE_DIR/output" > "$TMP_OUTPUT_FILE"
108
+ cat "$TMP_OUTPUT_FILE"
109
+ }
110
+
111
+ # prepare the callback payload
112
+ payload() {
113
+ if [ -n "$1" ]; then
114
+ exit_code="$1"
115
+ else
116
+ exit_code=null
117
+ fi
118
+
119
+ if [ -e "$BASE_DIR/manual_mode" ]; then
120
+ manual_mode=true
121
+ output=$(prepare_output | base64 -w0)
122
+ else
123
+ manual_mode=false
124
+ fi
125
+
126
+ echo "{ \"exit_code\": $exit_code,"\
127
+ " \"step_id\": \"$STEP_ID\","\
128
+ " \"manual_mode\": $manual_mode,"\
129
+ " \"output\": \"$output\" }"
130
+ }
131
+
132
+ if [ "$1" = "push_update" ]; then
133
+ if [ -e "$BASE_DIR/exit_code" ]; then
134
+ exit_code="$(cat "$BASE_DIR/exit_code")"
135
+ action="done"
136
+ else
137
+ exit_code=""
138
+ action="update"
139
+ fi
140
+ $CURL -X POST -d "$(payload $exit_code)" -u "$AUTH" "$URL_PREFIX"/$action 2>>"$BASE_DIR/curl_stderr"
141
+ success=$?
142
+ else
143
+ prepare_output
144
+ success=$?
145
+ fi
146
+
147
+ if [ "$success" = 0 ] && [ -e "$TMP_OUTPUT_FILE" ]; then
148
+ # in case the retrieval was successful, move the position of the cursor to be read next time
149
+ bytes=$(wc --bytes < "$TMP_OUTPUT_FILE")
150
+ expr "${position}" + "${bytes}" > "$BASE_DIR/position"
151
+ fi
@@ -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