Просмотр исходного кода

Add a RemotePathTransformation property to ScpClient that can be used to control if and how a remote path should be transformed before passed on to the scp command.

This allows for a custom transformation (escaping/quoting) - that implements IRemotePathTransformation - to be plugged into ScpClient.

Out of the box, we only provide two implementations that are exposed through the RemotePathTransformation (enum-like) class: Quote and None.

Fixes issue #256.
Gert Driesen 8 лет назад
Родитель
Сommit
7dde512a9c

+ 16 - 1
src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj

@@ -308,6 +308,9 @@
     <Compile Include="..\Renci.SshNet\IForwardedPort.cs">
       <Link>IForwardedPort.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IRemotePathTransformation.cs">
+      <Link>IRemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\IServiceFactory.cs">
       <Link>IServiceFactory.cs</Link>
     </Compile>
@@ -578,6 +581,18 @@
     <Compile Include="..\Renci.SshNet\ProxyTypes.cs">
       <Link>ProxyTypes.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathEscapeTransformation.cs">
+      <Link>RemotePathEscapeTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathNoneTransformation.cs">
+      <Link>RemotePathNoneTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathQuoteTransformation.cs">
+      <Link>RemotePathQuoteTransformation.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\RemotePathTransformation.cs">
+      <Link>RemotePathTransformation.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\ScpClient.cs">
       <Link>ScpClient.cs</Link>
     </Compile>
@@ -956,7 +971,7 @@
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

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

@@ -116,5 +116,14 @@ namespace Renci.SshNet.Tests.Classes.Common
 
             Assert.AreEqual("'one\n\ntwo'", actual);
         }
+
+        public void SequenceOfSingleQuoteAndExclamationMark()
+        {
+            var value = "/var/would be 'kewl'!/not?";
+
+            var actual = value.ShellQuote();
+
+            Assert.AreEqual("'/var/would be '\"'\"'kewl'\"'\"\\!'/not?'", actual);
+        }
     }
 }

+ 19 - 0
src/Renci.SshNet/IRemotePathTransformation.cs

@@ -0,0 +1,19 @@
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Represents a transformation that can be applied to a remote path.
+    /// </summary>
+    public interface IRemotePathTransformation
+    {
+        /// <summary>
+        /// Transforms the specified remote path.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// The transformed path.
+        /// </returns>
+        string Transform(string path);
+    }
+
+
+}

+ 10 - 0
src/Renci.SshNet/IServiceFactory.cs

@@ -62,5 +62,15 @@ namespace Renci.SshNet
         ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSession, uint bufferSize);
 
         ISftpResponseFactory CreateSftpResponseFactory();
+
+        /// <summary>
+        /// Creates an <see cref="IRemotePathTransformation"/>  that quotes a path in a way to be suitable
+        /// to be used with a shell.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IRemotePathTransformation"/>  that quotes a path in a way to be suitable to be used
+        /// with a shell.
+        /// </returns>
+        IRemotePathTransformation CreateRemotePathQuoteTransformation();
     }
 }

+ 20 - 0
src/Renci.SshNet/RemotePathNoneTransformation.cs

@@ -0,0 +1,20 @@
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Performs no transformation.
+    /// </summary>
+    internal class RemotePathNoneTransformation : IRemotePathTransformation
+    {
+        /// <summary>
+        /// Returns the specified path without applying a transformation.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// The specified path as is.
+        /// </returns>
+        public string Transform(string path)
+        {
+            return path;
+        }
+    }
+}

+ 161 - 0
src/Renci.SshNet/RemotePathQuoteTransformation.cs

@@ -0,0 +1,161 @@
+using System;
+using System.Text;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Quotes a path in a way to be suitable to be used with a shell.
+    /// </summary>
+    internal class RemotePathQuoteTransformation : IRemotePathTransformation
+    {
+        /// <summary>
+        /// Quotes a path in a way to be suitable to be used with a shell.
+        /// </summary>
+        /// <param name="path">The path to transform.</param>
+        /// <returns>
+        /// A quoted path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// <para>
+        /// If <paramref name="path"/> contains a single-quote, that character is embedded
+        /// in quotation marks (eg. "'"). Sequences of single-quotes are grouped in a single
+        /// pair of quotation marks.
+        /// </para>
+        /// <para>
+        /// If a shell command 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>
+        /// <returns>
+        /// The transformed path.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
+        public string Transform(string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            // result is at least value and (likely) leading/trailing single-quotes
+            var sb = new StringBuilder(path.Length + 2);
+            var state = ShellQuoteState.Unquoted;
+
+            foreach (var c in path)
+            {
+                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();
+        }
+
+        private enum ShellQuoteState
+        {
+            Unquoted = 1,
+            SingleQuoted = 2,
+            Quoted = 3
+        }
+    }
+}

+ 83 - 0
src/Renci.SshNet/RemotePathTransformation.cs

@@ -0,0 +1,83 @@
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Allow access to built-in remote path transformations.
+    /// </summary>
+    public static class RemotePathTransformation
+    {
+        private static readonly IRemotePathTransformation QuoteTransformation = new RemotePathQuoteTransformation();
+        private static readonly IRemotePathTransformation NoneTransformation = new RemotePathNoneTransformation();
+
+        /// <summary>
+        /// Quotes a path in a way to be suitable to be used with a shell.
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// If the path contains a single-quote, that character is embedded in quotation marks.
+        /// Sequences of single-quotes are grouped in a single pair of quotation marks.
+        /// </para>
+        /// <para>
+        /// An exclamation mark (!) is escaped with a backslash, because the C shell would otherwise
+        /// interprete it as a meta-character for history substitution. It does this even if it's
+        /// enclosed in single-quotes or quotation marks, unless escaped with a backslash (\).
+        /// </para>
+        /// <para>
+        /// All other character are enclosed in single-quotes, and grouped in a single pair of
+        /// single quotes where contiguous.
+        /// </para>
+        /// </remarks>
+        /// <example>
+        /// <list type="table">
+        ///   <listheader>
+        ///     <term>Original</term>
+        ///     <term>Quoted</term>
+        ///   </listheader>
+        ///   <item>
+        ///     <term>/var/log/auth.log</term>
+        ///     <term>'/var/log/auth.log'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/mp3/Guns N' Roses</term>
+        ///     <term>'/var/mp3/Guns N'"'"' Roses'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>'/var/garbage\!/temp'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/garbage!/temp</term>
+        ///     <term>'/var/garbage'\!'/temp'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>/var/would be 'kewl'!/not?</term>
+        ///     <term>'/var/would be '"'"'kewl'"'"\!'/not?'</term>
+        ///   </item>
+        ///   <item>
+        ///     <term>!ignore!</term>
+        ///     <term>\!'ignore'\!</term>
+        ///   </item>
+        ///   <item>
+        ///     <term></term>
+        ///     <term>''</term>
+        ///   </item>
+        /// </list>
+        /// </example>
+        public static IRemotePathTransformation Quote
+        {
+            get { return QuoteTransformation; }
+        }
+
+        /// <summary>
+        /// Performs no transformation.
+        /// </summary>
+        /// <remarks>
+        /// This transformation should be used for servers that do not support escape sequences in paths
+        /// or paths enclosed in quotes, or would preserve the escape characters or quotes in the path that
+        /// is handed off to the IO layer. This is recommended for servers that are not shell-based.
+        /// </remarks>
+        public static IRemotePathTransformation None
+        {
+            get { return NoneTransformation; }
+        }
+    }
+}

+ 4 - 0
src/Renci.SshNet/Renci.SshNet.csproj

@@ -153,6 +153,7 @@
     <Compile Include="IClientAuthentication.cs" />
     <Compile Include="IConnectionInfo.cs" />
     <Compile Include="IForwardedPort.cs" />
+    <Compile Include="IRemotePathTransformation.cs" />
     <Compile Include="IServiceFactory.cs" />
     <Compile Include="IServiceFactory.NET.cs" />
     <Compile Include="ISession.cs" />
@@ -162,6 +163,9 @@
     <Compile Include="Messages\Transport\KeyExchangeEcdhInitMessage.cs" />
     <Compile Include="Messages\Transport\KeyExchangeEcdhReplyMessage.cs" />
     <Compile Include="Netconf\INetConfSession.cs" />
+    <Compile Include="RemotePathNoneTransformation.cs" />
+    <Compile Include="RemotePathQuoteTransformation.cs" />
+    <Compile Include="RemotePathTransformation.cs" />
     <Compile Include="Security\Cryptography\HMACMD5.cs" />
     <Compile Include="Security\Cryptography\HMACSHA1.cs" />
     <Compile Include="Security\Cryptography\HMACSHA256.cs" />

+ 4 - 4
src/Renci.SshNet/ScpClient.NET.cs

@@ -36,7 +36,7 @@ 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.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -74,7 +74,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // start recursive upload
-                if (!channel.SendExecRequest(string.Format("scp -rt {0}", path.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -rt {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -109,7 +109,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -pf {0}", filename.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -pf {0}", _remotePathTransformation.Transform(filename))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -143,7 +143,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 // Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -prf {0}", directoryName.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -prf {0}", _remotePathTransformation.Transform(directoryName))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }

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

@@ -33,6 +33,8 @@ namespace Renci.SshNet
         private static readonly byte[] SuccessConfirmationCode = {0};
         private static readonly byte[] ErrorConfirmationCode = { 1 };
 
+        private IRemotePathTransformation _remotePathTransformation;
+
         /// <summary>
         /// Gets or sets the operation timeout.
         /// </summary>
@@ -50,6 +52,28 @@ namespace Renci.SshNet
         /// </value>
         public uint BufferSize { get; set; }
 
+        /// <summary>
+        /// Gets or sets the transformation to apply to remote paths.
+        /// </summary>
+        /// <value>
+        /// The transformation to apply to remote paths. The default is <see cref="SshNet.RemotePathTransformation.Quote"/>.
+        /// </value>
+        /// <exception cref="RemotePathTransformation"><paramref name="value"/> is <c>null</c>.</exception>
+        /// <remarks>
+        /// This transformation is applied to the remote file or directory path that is passed to the
+        /// <c>scp</c> command.
+        /// </remarks>
+        public IRemotePathTransformation RemotePathTransformation
+        {
+            get { return _remotePathTransformation; }
+            set
+            {
+                if (value == null)
+                    throw new ArgumentNullException("value");
+                _remotePathTransformation = value;
+            }
+        }
+
         /// <summary>
         /// Occurs when downloading file.
         /// </summary>
@@ -162,6 +186,7 @@ namespace Renci.SshNet
         {
             OperationTimeout = SshNet.Session.InfiniteTimeSpan;
             BufferSize = 1024 * 16;
+            _remotePathTransformation = SshNet.RemotePathTransformation.Quote;
         }
 
         #endregion
@@ -183,7 +208,7 @@ namespace Renci.SshNet
 
                 // pass the full path to ensure the server does not create the directory part
                 // as a file in case the directory does not exist
-                if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }
@@ -220,7 +245,7 @@ namespace Renci.SshNet
                 channel.Open();
 
                 //  Send channel command request
-                if (!channel.SendExecRequest(string.Format("scp -f {0}", filename.ShellQuote())))
+                if (!channel.SendExecRequest(string.Format("scp -f {0}", _remotePathTransformation.Transform(filename))))
                 {
                     throw SecureExecutionRequestRejectedException();
                 }

+ 13 - 0
src/Renci.SshNet/ServiceFactory.cs

@@ -136,5 +136,18 @@ namespace Renci.SshNet
         {
             return new SftpResponseFactory();
         }
+
+        /// <summary>
+        /// Creates an <see cref="IRemotePathTransformation"/>  that quotes a path in a way to be suitable
+        /// to be used with a shell.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IRemotePathTransformation"/>  that quotes a path in a way to be suitable to be used
+        /// with a shell.
+        /// </returns>
+        public IRemotePathTransformation CreateRemotePathQuoteTransformation()
+        {
+            return new RemotePathQuoteTransformation();
+        }
     }
 }