diff --git a/plugins/communicators/winssh/communicator.rb b/plugins/communicators/winssh/communicator.rb index 208ec0f90..96477ec40 100644 --- a/plugins/communicators/winssh/communicator.rb +++ b/plugins/communicators/winssh/communicator.rb @@ -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 diff --git a/test/unit/plugins/communicators/winssh/communicator_test.rb b/test/unit/plugins/communicators/winssh/communicator_test.rb index 7c000f839..99e8a90dc 100644 --- a/test/unit/plugins/communicators/winssh/communicator_test.rb +++ b/test/unit/plugins/communicators/winssh/communicator_test.rb @@ -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