Quellcode durchsuchen

ScpClient: Implement proper quoting of paths.
Fixes issue #256.

Gert Driesen vor 8 Jahren
Ursprung
Commit
6184a0e826

+ 22 - 1
src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -279,6 +279,27 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExceptionEventArgsTest.cs">
       <Link>Classes\Common\ExceptionEventArgsTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Concat.cs">
+      <Link>Classes\Common\ExtensionsTest_Concat.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs">
+      <Link>Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Reverse.cs">
+      <Link>Classes\Common\ExtensionsTest_Reverse.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_ShellQuote.cs">
+      <Link>Classes\Common\ExtensionsTest_ShellQuote.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Take_Count.cs">
+      <Link>Classes\Common\ExtensionsTest_Take_Count.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs">
+      <Link>Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\Common\ExtensionsTest_TrimLeadingZeros.cs">
+      <Link>Classes\Common\ExtensionsTest_TrimLeadingZeros.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\Common\HostKeyEventArgsTest.cs">
       <Link>Classes\Common\HostKeyEventArgsTest.cs</Link>
     </Compile>
@@ -1548,7 +1569,7 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 120 - 0
src/Renci.SshNet.Tests/Classes/Common/ExtensionsTest_ShellQuote.cs

@@ -0,0 +1,120 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+
+namespace Renci.SshNet.Tests.Classes.Common
+{
+    [TestClass]
+    public class ExtensionsTest_ShellQuote
+    {
+        [TestMethod]
+        public void Null()
+        {
+            const string value = null;
+
+            try
+            {
+                value.ShellQuote();
+                Assert.Fail();
+            }
+            catch (ArgumentNullException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual("value", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Empty()
+        {
+            var value = string.Empty;
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("''", actual);
+        }
+
+        [TestMethod]
+        public void RegularCharacters()
+        {
+            var value = "onetwo";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'onetwo'", actual);
+        }
+
+        /// <summary>
+        /// Tests all special character listed <a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">here</a>
+        /// except for newline and single-quote, which are tested separately.
+        /// </summary>
+        [TestMethod]
+        public void SpecialCharacters()
+        {
+            var value = "|&;<>()$`\\\" \t\n*?[#~=%";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'|&;<>()$`\\\" \t\n*?[#~=%'", actual);
+        }
+
+        [TestMethod]
+        public void SingleExclamationPoint()
+        {
+            var value = "!one!two!";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("\\!'one'\\!'two'\\!", actual);
+        }
+
+        [TestMethod]
+        public void SequenceOfExclamationPoints()
+        {
+            var value = "one!!!two";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'one'\\!\\!\\!'two'", actual);
+        }
+
+        [TestMethod]
+        public void SingleQuotes()
+        {
+            var value = "'a'b'c'd'";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("\"'\"'a'\"'\"'b'\"'\"'c'\"'\"'d'\"'\"", actual);
+        }
+
+        [TestMethod]
+        public void SequenceOfSingleQuotes()
+        {
+            var value = "one''two";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'one'\"''\"'two'", actual);
+        }
+
+        [TestMethod]
+        public void LineFeeds()
+        {
+            var value = "one\ntwo\nthree\nfour";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'one\ntwo\nthree\nfour'", actual);
+        }
+
+        [TestMethod]
+        public void SequenceOfLineFeeds()
+        {
+            var value = "one\n\ntwo";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'one\n\ntwo'", actual);
+        }
+    }
+}

+ 1 - 0
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -147,6 +147,7 @@
     <Compile Include="Classes\Common\ExtensionsTest_Concat.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_IsEqualTo_ByteArray.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Reverse.cs" />
+    <Compile Include="Classes\Common\ExtensionsTest_ShellQuote.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Take_Count.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_Take_OffsetAndCount.cs" />
     <Compile Include="Classes\Common\ExtensionsTest_TrimLeadingZeros.cs" />

+ 148 - 2
src/Renci.SshNet/Common/Extensions.cs

@@ -16,12 +16,158 @@ namespace Renci.SshNet
     /// </summary>
     internal static partial class Extensions
     {
+        private enum ShellQuoteState
+        {
+            Unquoted = 1,
+            SingleQuoted = 2,
+            Quoted = 3
+        }
+
+        /// <summary>
+        /// Quotes a <see cref="string"/> in a way to be suitable to be used with a shell.
+        /// </summary>
+        /// <param name="value">The <see cref="string"/> to quote.</param>
+        /// <returns>
+        /// A quoted <see cref="string"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="value"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// <para>
+        /// If <paramref name="value"/> contains a single-quote, that character is embedded
+        /// in quotation marks (eg. "'"). Sequences of single-quotes are grouped in a one
+        /// pair of quotation marks.
+        /// </para>
+        /// <para>
+        /// If the <see cref="string"/> contains an exclamation mark (!), the C-Shell interprets
+        /// it as a meta-character for history substitution. This even works inside single-quotes
+        /// or quotation marks, unless escaped with a backslash (\).
+        /// </para>
+        /// <para>
+        /// References:
+        /// <list type="bullet">
+        ///   <item>
+        ///     <description><a href="http://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html">Shell Command Language</a></description>
+        ///   </item>
+        ///   <item>
+        ///     <description><a href="https://earthsci.stanford.edu/computing/unix/shell/specialchars.php">Unix C-Shell special characters and their uses</a></description>
+        ///   </item>
+        ///   <item>
+        ///     <description><a href="https://docstore.mik.ua/orelly/unix3/upt/ch27_13.htm">Differences Between Bourne and C Shell Quoting</a></description>
+        ///   </item>
+        /// </list>
+        /// </para>
+        /// </remarks>
+        public static string ShellQuote(this string value)
+        {
+            if (value == null)
+            {
+                throw new ArgumentNullException("value");
+            }
+
+            // result is at least value and leading/trailing single-quote
+            var sb = new StringBuilder(value.Length + 2);
+            var state = ShellQuoteState.Unquoted;
+
+            foreach (var c in value)
+            {
+                switch (c)
+                {
+                    case '\'':
+                        // embed a single-quote in quotes
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                // Start quoted string
+                                sb.Append('"');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Continue quoted string
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Close single quoted string
+                                sb.Append('\'');
+                                // Start quoted string
+                                sb.Append('"');
+                                break;
+                        }
+                        state = ShellQuoteState.Quoted;
+                        break;
+                    case '!':
+                        // In C-Shell, an exclamatation point can only be protected from shell interpretation
+                        // when escaped by a backslash
+                        // Source:
+                        // https://earthsci.stanford.edu/computing/unix/shell/specialchars.php
+
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                sb.Append('\\');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Close quoted string
+                                sb.Append('"');
+                                sb.Append('\\');
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Close single quoted string
+                                sb.Append('\'');
+                                sb.Append('\\');
+                                break;
+                        }
+                        state = ShellQuoteState.Unquoted;
+                        break;
+                    default:
+                        switch (state)
+                        {
+                            case ShellQuoteState.Unquoted:
+                                // Start single-quoted string
+                                sb.Append('\'');
+                                break;
+                            case ShellQuoteState.Quoted:
+                                // Close quoted string
+                                sb.Append('"');
+                                // Start single quoted string
+                                sb.Append('\'');
+                                break;
+                            case ShellQuoteState.SingleQuoted:
+                                // Continue single quoted string
+                                break;
+                        }
+                        state = ShellQuoteState.SingleQuoted;
+                        break;
+                }
+
+                sb.Append(c);
+            }
+
+            switch (state)
+            {
+                case ShellQuoteState.Unquoted:
+                    break;
+                case ShellQuoteState.Quoted:
+                    // Close quoted string
+                    sb.Append('"');
+                    break;
+                case ShellQuoteState.SingleQuoted:
+                    /* Close single quoted string */
+                    sb.Append('\'');
+                    break;
+            }
+
+            if (sb.Length == 0)
+            {
+                sb.Append("''");
+            }
+
+            return sb.ToString();
+        }
+
         /// <summary>
-        /// Determines whether [is null or white space] [the specified value].
+        /// Determines whether the specified value is null or white space.
         /// </summary>
         /// <param name="value">The value.</param>
         /// <returns>
-        ///   <c>true</c> if [is null or white space] [the specified value]; otherwise, <c>false</c>.
+        /// <c>true</c> if <paramref name="value"/> is null or white space; otherwise, <c>false</c>.
         /// </returns>
         public static bool IsNullOrWhiteSpace(this string value)
         {

+ 24 - 14
src/Renci.SshNet/ScpClient.NET.cs

@@ -34,12 +34,12 @@ namespace Renci.SshNet
                 channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
                 channel.Open();
 
-                if (!channel.SendExecRequest(string.Format("scp -t \"{0}\"", path)))
+                if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
                     throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
 
                 CheckReturnCode(input);
 
-                InternalUpload(channel, input, fileInfo, fileInfo.Name);
+                InternalUpload(channel, input, fileInfo);
             }
         }
 
@@ -64,7 +64,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // start recursive upload
-                channel.SendExecRequest(string.Format("scp -rt \"{0}\"", path));
+                channel.SendExecRequest(string.Format("scp -rt {0}", path.ShellQuote()));
                 CheckReturnCode(input);
 
                 // set last write and last access time on specified remote path
@@ -101,9 +101,10 @@ namespace Renci.SshNet
                 channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
                 channel.Open();
 
-                //  Send channel command request
-                channel.SendExecRequest(string.Format("scp -pf \"{0}\"", filename));
-                SendConfirmation(channel); //  Send reply
+                // Send channel command request
+                channel.SendExecRequest(string.Format("scp -pf {0}", filename.ShellQuote()));
+                // Send reply
+                SendConfirmation(channel);
 
                 InternalDownload(channel, input, fileInfo);
             }
@@ -129,20 +130,29 @@ namespace Renci.SshNet
                 channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
                 channel.Open();
 
-                //  Send channel command request
-                channel.SendExecRequest(string.Format("scp -prf \"{0}\"", directoryName));
-                SendConfirmation(channel); //  Send reply
+                // Send channel command request
+                channel.SendExecRequest(string.Format("scp -prf {0}", directoryName.ShellQuote()));
+                // Send reply
+                SendConfirmation(channel);
 
                 InternalDownload(channel, input, directoryInfo);
             }
         }
 
-        private void InternalUpload(IChannelSession channel, Stream input, FileInfo fileInfo, string filename)
+        /// <summary>
+        /// Uploads the file in the active directory context, and set
+        /// </summary>
+        /// <param name="channel">The channel to perform the upload in.</param>
+        /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
+        /// <param name="fileInfo">The file to upload.</param>
+        private void InternalUpload(IChannelSession channel, Stream input, FileInfo fileInfo)
         {
-            InternalSetTimestamp(channel, input, fileInfo.LastWriteTimeUtc, fileInfo.LastAccessTimeUtc);
             using (var source = fileInfo.OpenRead())
             {
-                InternalUpload(channel, input, source, filename);
+                // set the last write and last access time for the next file uploaded
+                InternalSetTimestamp(channel, input, fileInfo.LastWriteTimeUtc, fileInfo.LastAccessTimeUtc);
+                // upload the actual file
+                InternalUpload(channel, input, source, fileInfo.Name);
             }
         }
 
@@ -152,7 +162,7 @@ namespace Renci.SshNet
             var files = directoryInfo.GetFiles();
             foreach (var file in files)
             {
-                InternalUpload(channel, input, file, file.Name);
+                InternalUpload(channel, input, file);
             }
 
             //  Upload directories
@@ -213,7 +223,7 @@ namespace Renci.SshNet
                     }
                     else
                     {
-                        //  Dont create directory for first level
+                        //  Don't create directory for first level
                         newDirectoryInfo = fileSystemInfo as DirectoryInfo;
                     }
 

+ 3 - 2
src/Renci.SshNet/ScpClient.cs

@@ -220,7 +220,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 //  Send channel command request
-                channel.SendExecRequest(string.Format("scp -f \"{0}\"", filename));
+                channel.SendExecRequest(string.Format("scp -f {0}", filename.ShellQuote()));
                 SendConfirmation(channel); //  Send reply
 
                 var message = ReadString(input);
@@ -256,7 +256,8 @@ namespace Renci.SshNet
         {
             var length = source.Length;
 
-            SendData(channel, string.Format("C0644 {0} {1}\n", length, Path.GetFileName(filename)));
+            // specify permissions, length and name of file being upload
+            SendData(channel, string.Format("C0644 {0} {1}\n", length, filename));
             CheckReturnCode(input);
 
             var buffer = new byte[BufferSize];