Perform scp commands with powershell.exe

This commit includes a monkey patch for Net::SCP#start_command so that
PowerShell commands are escaped correctly.
This commit is contained in:
Jeff Bonhag 2020-02-13 16:38:55 -05:00 committed by Chris Roberts
parent 85f0fce57a
commit b6e8262bf2
2 changed files with 84 additions and 5 deletions

View File

@ -207,7 +207,7 @@ SCRIPT
@logger.debug("Uploading file #{path} to remote #{dest}")
upload_file = File.open(path, "rb")
begin
scp.upload!(upload_file, dest)
scp.upload!(upload_file, dest, shell: "powershell.exe")
ensure
upload_file.close
end
@ -234,3 +234,53 @@ SCRIPT
end
end
end
# This monkey patches Net::SCP#start_command so that we don't apply special
# shell escaping rules to the remote path when using powershell.exe as a shell.
# PowerShell also needs single quotes around the remote path if the path has
# spaces in it.
#
# Here is an example of a properly formatted scp command for PowerShell:
#
# scp.exe -t 'c:/destination path'
#
module Net
class SCP
def start_command(mode, local, remote, options={}, &callback)
session.open_channel do |channel|
if options[:shell].to_s == "powershell.exe"
powershell_escaped = remote.gsub(/'/, "''")
command = "#{options[:shell]} -c #{scp_command(mode, options)} '#{powershell_escaped}'"
elsif options[:shell]
escaped_file = shellescape(remote).gsub(/'/) { |m| "'\\''" }
command = "#{options[:shell]} -c #{scp_command(mode, options)} #{escaped_file}"
else
command = "#{scp_command(mode, options)} #{shellescape(remote)}"
end
channel.exec(command) do |ch, success|
if success
channel[:local ] = local
channel[:remote ] = remote
channel[:options ] = options.dup
channel[:callback] = callback
channel[:buffer ] = Net::SSH::Buffer.new
channel[:state ] = "#{mode}_start"
channel[:stack ] = []
channel[:error_string] = ''
channel.on_close { |ch| send("#{channel[:state]}_state", channel); raise Net::SCP::Error, "SCP did not finish successfully (#{channel[:exit]}): #{channel[:error_string]}" if channel[:exit] != 0 }
channel.on_data { |ch, data| channel[:buffer].append(data) }
channel.on_extended_data { |ch, type, data| debug { data.chomp } }
channel.on_request("exit-status") { |ch, data| channel[:exit] = data.read_long }
channel.on_process { send("#{channel[:state]}_state", channel) }
else
channel.close
raise Net::SCP::Error, "could not exec scp on the remote host"
end
end
end
end
end
end

View File

@ -274,7 +274,7 @@ describe VagrantPlugins::CommunicatorWinSSH::Communicator do
it "uploads a directory if local path is a directory" do
Dir.mktmpdir('vagrant-test') do |dir|
FileUtils.touch(File.join(dir, "test-file"))
expect(scp).to receive(:upload!).with(an_instance_of(File), /test-file/)
expect(scp).to receive(:upload!).with(an_instance_of(File), /test-file/, {shell: "powershell.exe"})
communicator.upload(dir, 'C:\destination')
end
end
@ -282,7 +282,7 @@ describe VagrantPlugins::CommunicatorWinSSH::Communicator do
it "uploads a file if local path is a file" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file')
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file', {shell: "powershell.exe"})
expect(Vagrant::Util::Platform).to receive(:unix_windows_path).with('C:\destination\file').
and_call_original
communicator.upload(file.path, 'C:\destination\file')
@ -294,7 +294,7 @@ describe VagrantPlugins::CommunicatorWinSSH::Communicator do
it "raises custom error on permission errors" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file').
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file', {shell: "powershell.exe"}).
and_raise("Permission denied")
expect{ communicator.upload(file.path, 'C:\destination\file') }.to(
raise_error(Vagrant::Errors::SCPPermissionDenied)
@ -307,7 +307,7 @@ describe VagrantPlugins::CommunicatorWinSSH::Communicator do
it "does not raise custom error on non-permission errors" do
file = Tempfile.new('vagrant-test')
begin
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file').
expect(scp).to receive(:upload!).with(instance_of(File), 'C:/destination/file', {shell: "powershell.exe"}).
and_raise("Some other error")
expect{ communicator.upload(file.path, 'C:\destination\file') }.to raise_error(RuntimeError)
ensure
@ -585,3 +585,32 @@ describe VagrantPlugins::CommunicatorWinSSH::Communicator do
end
end
end
# Tests for Net::SCP#start_command patch
describe Net::SCP do
include_context "unit"
let(:session) do
double("session",
logger: nil)
end
let(:scp){ @scp ||= described_class.new(session) }
let(:channel) { double("channel") }
before do
allow(session).to receive(:open_channel).and_yield(channel)
end
describe "#start_command" do
context "with shell set to powershell.exe" do
let(:options) { {:shell => "powershell.exe" } }
it "escapes single quotes in the destination" do
expect(channel).to receive(:exec).with(/C:\/vagrant''s scripts/)
scp.start_command("", anything, "C:/vagrant's scripts", options)
end
end
end
end