Skip to content

ScpClient: Remote path transformation

Gert Driesen edited this page Sep 9, 2017 · 13 revisions

Problem statement

By default, SSH.NET applies a simple quoting mechanism for remote paths in ScpClient. More precisely, any remote path is enclosed in double quotes before it is passed to the scp command on the remote host.

This mechanism does not work on all types of SCP server.

At a high level, we can distinguish the following groups of SCP servers:

  • Shell-based servers
  • Non-shell based server

Shell-based SCP servers typically run on Unix or Linux hosts and require that certain metacharacters - that would for example otherwise result in history expansion - be quoted or escaped to preserve their literal value.

Non-shell based servers typically do not require escaping or quoting of characters, and - equally important - do not unescape or unquote the path that is passed to the scp command.

To perform SCP operations for a remote host, the path on which to perform action must be supplied to the scp command. This path should take into account the quoting requirements (or lack thereof) of the remote host.

The SCP protocol does not provide a mechanism to "encode" a path using the quoting mechanism of the remote host, nor does it allow you to discover the preferred quoting mechanism of the remote host or the list of characters that require encoding/quoting for the remote host.

A quoting mechanism that works fine - or is even required - for a given remote host, may produce errors or garbled file names when applied for another host.

Example:

To pass an exclamation mark as a literal value to an SCP server that is based on C shell, the exclamation mark must be escaped with a backslash ('!' -> '\!'). To pass that same character to a non-shell based server escaping MUST NOT be applied. If - for a non-shell based server - the exclamation mark were to be escaped with a backslash, then the literal value of this backslash would be passed to the IO layer and you would end up having '\!' in your file or directory path.

Solution

As of version 2016.1.0-beta3, the quoting mechanism that ScpClient applies to remote paths can be modified using a newly introduced RemotePathTransformation property:

using (var client = new ScpClient("HOST", 22, "USER", "PWD"))
{
    client.RemotePathTransformation = RemotePathTransformation.ShellQuote;
    client.Connect();
    ...
}

The specified transformation is applied to remote paths that are specified in any of the following methods of ScpClient:

  • void Upload(Stream source, string path)
  • void Download(string filename, Stream destination)
  • void Upload(FileInfo fileInfo, string path)
  • void Upload(DirectoryInfo directoryInfo, string path)

SSH.NET comes with the following path transformations that are exposed through the RemotePathTransformation class (in Renci.SshNet):

  • DoubleQuote

    Encloses a path in double quotes, and escapes any embedded double quote with a backslash.

    This is the default, as it works fine with most SCP servers.

  • ShellQuote

    Quotes a path in a way to be suitable to be used with a shell-based SCP server.

    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.

    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 ().

    All other character are enclosed in single-quotes, and grouped in a single pair of single quotes where contiguous.

    This is the preferred mechanism for shell-based SCP servers, which typically means all Unix-like systems.

  • None

    Performs no transformation to the path.

    This is the recommended transformation when the remote host does not require any quoting to preserve the literal value of metacharacters, or when remote paths are guaranteed not to contain such characters.

Example:

using Renci.SshNet;

public class Program
{
    static void Main()
    {
        using (var client = new ScpClient("HOST", 22, "USER", "PWD"))
        {
            client.RemotePathTransformation = RemotePathTransformation.ShellQuote;
            client.Connect();

            using (var ms = new MemoryStream())
            {
                client.Download("/home/sshnet/file 123", ms);
            }
        }
    }
}

Since we've configured ScpClient to use the ShellQuote transformation, the /home/sshnet/file 123 path will automatically be enclosed in single quotes.

The actual path that is passed to the scp command on the remote host is therefore '/home/sshnet/file 123'.

The table below shows the effect that the out-of-the-box transformations have on a variety of paths:

Path ShellQuote DoubleQuote None
/var/log/auth.log '/var/log/auth.log' "/var/log/auth.log" /var/log/auth.log
/var/mp3/Guns N' Roses /var/mp3/Guns\ N\'\ Roses "/var/mp3/Guns N' Roses" /var/mp3/Guns N' Roses
/var/garbage!/temp '/var/garbage'\!'/temp' "/var/garbage!/temp" /var/garbage!/temp
/var/would be 'kewl'!, not? '/var/would be '"'"'kewl'"'"\!', not?' "/var/would be 'kewl'!, not?'" /var/would be 'kewl'!, not?
!ignore! \!'ignore'\! "!ignore!" !ignore!
(zero-length path) '' "" (zero-length path)
Hello "World" 'Hello "World"' "Hello \"World\"" Hello "World"

Custom path transformation

While the path transformations that are provided out-of-the-box should cover most, if not all, SCP servers, SSH.NET allows for a custom path transformation to be plugged in to ScpClient.

This is useful when a given SCP server has unusual and/or propietary quoting requirements.

Such a custom transformation should implement IRemotePathTransformation (in Renci.SshNet).

Example:

using Renci.SshNet;

/// <summary>
/// Encloses a path in dollar ($) signs.
/// </summary>
public class RemotePathTrumpTransformation : IRemotePathTransformation
{
    /// <summary>
    /// Encloses the specified path in dollar ($) signs.
    /// </summary>
    /// <returns>
    /// The path enclosed in dollar ($) signs.
    /// </returns>
    public string Transform(string path)
    {
        return "$" + path + "$";
    }
}

A ScpClient instance can then be instructed to use the custom transformation using the RemotePathTransformation property:

using Renci.SshNet;

public class Program
{
    static void Main()
    {
        using (var client = new ScpClient("HOST", 22, "USER", "PWD"))
        {
            client.RemotePathTransformation = new RemotePathTrumpTransformation();
            client.Connect();
            ...
        }
    }
}

Metacharacters

The following characters have a special meaning in Bourne and/or C shell:

  • # (hash)
  • & (ampersand)
  • * (asterisk)
  • ? (question mark)
  • [ and ] (square brackets)
  • ( and ) (parentheses)
  • = (equals sign)
  • | (pipe)
  • ^ (circumflex)
  • ; (semicolon)
  • < (less than sign)
  • > (greater than sign)
  • ` (backtick)
  • $ (dollar sign)
  • " (double quote)
  • ' (single quote)
  • \ (backslash)
  • space
  • tab
  • newline
  • ! (exclamation mark)
  • { and } (curly brackets)
  • ~ (tilde)

The space (0x20) and horizontal tab (0x09) characters are generally considered as argument separator.