FRENDS

Extension HowTo - SCP Endpoint

Scenario: Get / put file from a server that only supports SCP

FRENDS Cobalt supports SFTP, which provides secure file transfers. We recommend using SFTP when possible, but if SCP is the only secure option for some reason, then this example may be of help. SCP has some limitations, for example it does not support file listings, appending, or checking if a file exists. For more information about SCP, see its Wikipedia page.

Configuration

The SCP endpoint utilizes the SFTP settings, since both SFTP and SCP are based on SSH connections. To use the SCP sample endpoint, you therefore need to configure the endpoint as you would an SFTP endpoint, i.e. using the Server and SFTP entries etc. See the main Cobalt documentation for more on the SFTP configuration.

NOTE: This SCP endpoint currently supports only getting one file at a time when used as the Source endpoint. This is due to Cobalt always transferring one file at a time, based on the directory listing: because SCP does not allow listing the directory contents, the ListFiles() method just returns the name of the file given as the FileName parameter. Therefore the FileName cannot contain any wildcard characters; the file name must be exactly the name of the file to get.

Therefore to configure the SCP endpoint, use the Server and SFTP settings, just make sure you use the custom endpoint instead of SFTP, i.e. the following settings:

Also note that because the SCP protocol is so limited, the SCP endpoint does not support most of the available settings in the Parameter section:

 For more details on creating new endpoints, see the Endpoint from scratch HowTo - Creating a new endpoint.

The code

    public class ScpEndPoint : EndPointBase
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(ScpEndPoint));
        protected Scp ScpClient;
        protected TransferEndPointConfig EndPointConfig { get; set; }

        /// <inheritdoc />
        public ScpEndPoint(TransferEndPointConfig endPointConfig)
            : base(endPointConfig)
        {
            EndPointConfig = endPointConfig;

            ScpClient = new Scp();
        }

        /// <inheritdoc />
        public override void Dispose()
        {
            Dispose(true);
        }

        /// <inheritdoc />
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (ScpClient != null)
                {
                    ScpClient.Dispose();
                    ScpClient = null;
                }
            }
        }

        /// <summary>
        /// Delete operation is not supported by SCP, throws NotSupportedException
        /// </summary>
        /// <param name="remoteFile"></param>
        public override void Delete(string remoteFile)
        {
            throw new NotSupportedException("Delete action is not supported by SCP, this means that SourceOperation 'Delete' is not supported");
        }

        /// <summary>
        /// Rename operation is not supported by SCP, throws NotSupportedException
        /// </summary>
        /// <param name="remoteFile"></param>
        /// <param name="toFile"></param>
        /// <returns></returns>
        public override string Rename(string remoteFile, string toFile)
        {
            throw new NotSupportedException("Rename action is not supported by SCP, this means that Parameters RenaeSourceFileBeforeTransfer and RenameDestinationFileDuringTransfer are not supported.");
        }

        /// <summary>
        /// Move operation is not supported by SCP, throws NotSupportedException
        /// </summary>
        /// <param name="remoteFile"></param>
        /// <param name="toPath"></param>
        /// <returns></returns>
        public override string Move(string remoteFile, string toPath)
        {
            throw new NotSupportedException("Move action is not supported by SCP, this means other SourceOperations than Nothing are not supported.");
        }

        /// Opens the connection to the remote server. Uses the SftpParameters from the 
        /// endpoint configuration as authentication options. 
        public override void Open()
        {
            if (Log.IsDebugEnabled)
            {
                // enable verbose logging
                ScpClient.LogWriter = new Log4NetRebexLogWriter(Log, Rebex.LogLevel.Verbose);
            }

            Connect(EndPointConfig.ServerAddress, EndPointConfig.ServerPort);

            var scpParameters = EndPointConfig.SftpParameters;
            if (!String.IsNullOrEmpty(scpParameters.ServerFingerPrint))
            {
                string fp = ScpClient.Fingerprint;
                string verify = scpParameters.ServerFingerPrint;
                if (!verify.Equals(fp))
                {
                    ScpClient.Disconnect();
                    throw new Exception(String.Format("Can't trust SCP server. The server fingerprint does not match. Expected fingerprint: '{0}', but was: '{1}'",
                                                        verify, fp));
                }
            }

            var loginType = (SftpLoginType)Enum.Parse(typeof(SftpLoginType), scpParameters.LoginType);
            if (loginType == SftpLoginType.UsernamePassword)
            {
                ScpClient.Login(EndPointConfig.ServerUsername, EndPointConfig.ServerPassword);
            }
            else if (loginType == SftpLoginType.UsernamePasswordPrivatekey)
            {
                ScpClient.Login(EndPointConfig.ServerUsername, EndPointConfig.ServerPassword, scpParameters.GetPrivateKey());
            }
            else if (loginType == SftpLoginType.UsernamePrivateKey)
            {
                ScpClient.Login(EndPointConfig.ServerUsername, scpParameters.GetPrivateKey());
            }
            else
            {
                ScpClient.Disconnect();
                throw new Exception("Unknown SFTP login type.");
            }


        }

        private static readonly Mutex ConnectMutex = new Mutex();

        //Timeout for acquiring the Connect mutex
        private const int MutexTimeout = 300000;

        /// <inheritdoc />
        protected void Connect(string host, int port)
        {
            if (ConnectMutex.WaitOne(MutexTimeout, false))
            {
                try
                {
                    ScpClient.Connect(host, port);
                }
                finally
                {
                    ConnectMutex.ReleaseMutex();
                }
            }
            else
            {
                throw new TimeoutException("Timeout while waiting for connection mutex.");
            }
        }

        /// <inheritdoc />
        public override void Close()
        {
            ScpClient.Disconnect();
        }

        /// <summary>
        /// Helper to execute SCP actions with a reconnect and retry, if the actions fails again,
        /// the exception from the retry is thrown out.
        /// </summary>
        /// <param name="action"></param>
        private void ExecuteActionWithOneRetry(Action<Scp> action)
        {
            try
            {
                action.Invoke(ScpClient);
            }
            catch (InvalidOperationException ioe)
            {
                ReconnectAndRetry(action, ioe);
            }
            catch (ScpException e)
            {
                ReconnectAndRetry(action, e);
            }
        }

        /// <summary>
        /// Reconnects to the server and then retries the action once, any exception that is thrown by the action is allowed to be thrown up
        /// </summary>
        /// <param name="action"></param>
        /// <param name="ex"></param>
        private void ReconnectAndRetry(Action<Scp> action, Exception ex)
        {
            Log.InfoFormat("Error when trying to execute action '{0}'. Error: '{1}'. Retrying once.", action.Method, ex.Message);

            // Rebex does not support checking the connection state for SCP so
            // we reopen the connection just in case
            Reopen();
            action.Invoke(ScpClient);
        }

        /// <summary>
        /// Closes and reopens the connection.
        /// </summary>
        private void Reopen()
        {
            Close();
            Open();
        }

        /// <inheritdoc />
        public override void Get(string remoteFile, string localFilePath)
        {
            ExecuteActionWithOneRetry(c => c.GetFile(remoteFile, localFilePath));
        }

        /// <inheritdoc />
        public override void Put(string sourceFile, string remoteFile)
        {
            ExecuteActionWithOneRetry(c => c.PutFile(sourceFile, remoteFile));
        }

        /// <summary>
        /// Append operation is not supported by SCP, throws NotSupportedException
        /// </summary>
        /// <param name="sourceFile"></param>
        /// <param name="remoteFile"></param>
        public override void Append(string sourceFile, string remoteFile)
        {
            throw new NotSupportedException("Append action is not supported by SCP, only supported value for DestinationFileExistAction is Overwrite.");
        }

        /// <summary>
        /// Listing files is not supported by SCP, instead is assumes the file always exists.
        /// </summary>
        /// <returns>List which contains one element, which is the source file name.</returns>
        public override IList<FileItem> ListFiles()
        {
            //SCP does not support listing files or checking if a file exists,
            //therefore we assume that a single file that is exactly the filemask exists
            return new List<FileItem>()
                       {
                           new ScpFileItem(FileName)
                       };
        }

        /// <summary>
        /// Checking for file existance is not directly supported by SCP, we would have to try
        /// to copy the file to check for its existence. Since it would be ugly, and SCP always
        /// overwrites an existing destination file, we can always assume that the file does not
        /// exist.
        /// </summary>
        /// <param name="remoteFilePath"></param>
        /// <returns>false</returns>
        public override bool FileExists(string remoteFilePath)
        {
            //Checking for file existence is not supported by SCP and the existing files are always
            //overwritten so we return false.

            return false;
        }
    }

    /// <summary>
    /// ScpFileItem, derived from FileItem and only has the Name property set as SCP does not
    /// provide any other information about files.
    /// </summary>
    public class ScpFileItem : FileItem
    {
        public ScpFileItem(string fileName)
        {
            Name = fileName;
            //Other properties are unattainable with the SCP protocol            
        }
    }