/// <summary> /// Parses the recieved hash value into the FtpHash object /// </summary> private Match ParseHashValue(FtpReply reply, FtpHash hash) { Match m; // Current draft says the server should return this: // SHA-256 0-49 169cd22282da7f147cb491e559e9dd filename.ext if (!(m = Regex.Match(reply.Message, @"(?<algorithm>.+)\s" + @"(?<bytestart>\d+)-(?<byteend>\d+)\s" + @"(?<hash>.+)\s" + @"(?<filename>.+)")).Success) { // Current version of FileZilla returns this: // SHA-1 21c2ca15cf570582949eb59fb78038b9c27ffcaf m = Regex.Match(reply.Message, @"(?<algorithm>.+)\s(?<hash>.+)\s"); } if (m != null && m.Success) { hash.Algorithm = FtpHashAlgorithms.FromString(m.Groups["algorithm"].Value); hash.Value = m.Groups["hash"].Value; } else { LogStatus(FtpTraceLevel.Warn, "Failed to parse hash from: " + reply.Message); } return(m); }
/// <summary> /// Tests if the specified directory exists on the server asynchronously. This /// method works by trying to change the working directory to /// the path specified. If it succeeds, the directory is changed /// back to the old working directory and true is returned. False /// is returned otherwise and since the CWD failed it is assumed /// the working directory is still the same. /// </summary> /// <param name='path'>The full or relative path of the directory to check for</param> /// <param name="token">The token that can be used to cancel the entire process</param> /// <returns>True if the directory exists. False otherwise.</returns> public async Task <bool> DirectoryExistsAsync(string path, CancellationToken token = default(CancellationToken)) { string pwd; // don't verify args as blank/null path is OK //if (path.IsBlank()) // throw new ArgumentException("Required parameter is null or blank.", "path"); LogFunc(nameof(DirectoryExistsAsync), new object[] { path }); // quickly check if root path, then it always exists! var ftppath = path.GetFtpPath(); if (ftppath == "." || ftppath == "./" || ftppath == "/") { return(true); } // check if a folder exists by changing the working dir to it pwd = await GetWorkingDirectoryAsync(token); if ((await ExecuteAsync("CWD " + ftppath, token)).Success) { FtpReply reply = await ExecuteAsync("CWD " + pwd.GetFtpPath(), token); if (!reply.Success) { throw new FtpException("DirectoryExists(): Failed to restore the working directory."); } return(true); } return(false); }
/// <summary> /// Performs a login on the server. This method is overridable so /// that the login procedure can be changed to support, for example, /// a FTP proxy. /// </summary> /// <exception cref="FtpAuthenticationException">On authentication failures</exception> /// <remarks> /// To handle authentication failures without retries, catch FtpAuthenticationException. /// </remarks> protected virtual async Task AuthenticateAsync(string userName, string password, string account, CancellationToken token) { // send the USER command along with the FTP username FtpReply reply = await ExecuteAsync("USER " + userName, token); // check the reply to the USER command if (!reply.Success) { throw new FtpAuthenticationException(reply); } // if it was accepted else if (reply.Type == FtpResponseType.PositiveIntermediate) { // send the PASS command along with the FTP password reply = await ExecuteAsync("PASS " + password, token); // fix for #620: some servers send multiple responses that must be read and decoded, // otherwise the connection is aborted and remade and it goes into an infinite loop var staleData = await ReadStaleDataAsync(false, true, true, token); if (staleData != null) { var staleReply = new FtpReply(); if (DecodeStringToReply(staleData, ref staleReply) && !staleReply.Success) { throw new FtpAuthenticationException(staleReply); } } // check the first reply to the PASS command if (!reply.Success) { throw new FtpAuthenticationException(reply); } // only possible 3** here is `332 Need account for login` if (reply.Type == FtpResponseType.PositiveIntermediate) { reply = await ExecuteAsync("ACCT " + account, token); if (!reply.Success) { throw new FtpAuthenticationException(reply); } else { m_IsAuthenticated = true; } } else if (reply.Type == FtpResponseType.PositiveCompletion) { m_IsAuthenticated = true; } } }
/// <summary> /// Checks if a file exists on the server asynchronously. /// </summary> /// <param name="path">The full or relative path to the file</param> /// <param name="token">The token that can be used to cancel the entire process</param> /// <returns>True if the file exists, false otherwise</returns> public async Task <bool> FileExistsAsync(string path, CancellationToken token = default(CancellationToken)) { // verify args if (path.IsBlank()) { throw new ArgumentException("Required parameter is null or blank.", "path"); } path = path.GetFtpPath(); LogFunc(nameof(FileExistsAsync), new object[] { path }); // calc the absolute filepath path = await GetAbsolutePathAsync(path, token); // since FTP does not include a specific command to check if a file exists // here we check if file exists by attempting to get its filesize (SIZE) if (HasFeature(FtpCapability.SIZE)) { // Fix #328: get filesize in ASCII or Binary mode as required by server FtpSizeReply sizeReply = new FtpSizeReply(); await GetFileSizeInternalAsync(path, -1, token, sizeReply); // handle known errors to the SIZE command var sizeKnownError = CheckFileExistsBySize(sizeReply); if (sizeKnownError.HasValue) { return(sizeKnownError.Value); } } // check if file exists by attempting to get its date modified (MDTM) if (HasFeature(FtpCapability.MDTM)) { FtpReply reply = await ExecuteAsync("MDTM " + path, token); var ch = reply.Code[0]; if (ch == '2') { return(true); } if (ch == '5' && reply.Message.IsKnownError(FtpServerStrings.fileNotFound)) { return(false); } } // check if file exists by getting a name listing (NLST) string[] fileList = await GetNameListingAsync(path.GetFtpDirectoryName(), token); return(FileListings.FileExistsInNameListing(fileList, path)); }
/// <summary> /// Closes the connection and reads the server's reply /// </summary> public new FtpReply Close() { base.Close(); try { if (ControlConnection != null) { return(ControlConnection.CloseDataStream(this)); } } finally { m_commandStatus = new FtpReply(); m_control = null; } return(new FtpReply()); }
/// <summary> /// Called during Connect(). Typically extended by FTP proxies. /// </summary> protected virtual void Handshake() { FtpReply reply; if (!(reply = GetReply()).Success) { if (reply.Code == null) { throw new IOException("The connection was terminated before a greeting could be read."); } else { throw new FtpCommandException(reply); } } HandshakeReply = reply; }
/// <summary> /// Called during <see cref="ConnectAsync()"/>. Typically extended by FTP proxies. /// </summary> protected virtual async Task HandshakeAsync(CancellationToken token = default(CancellationToken)) { FtpReply reply; if (!(reply = await GetReplyAsync(token)).Success) { if (reply.Code == null) { throw new IOException("The connection was terminated before a greeting could be read."); } else { throw new FtpCommandException(reply); } } HandshakeReply = reply; }
/// <summary> /// Disconnects a data stream /// </summary> /// <param name="stream">The data stream to close</param> internal FtpReply CloseDataStream(FtpDataStream stream) { FtpTrace.WriteFunc("CloseDataStream"); FtpReply reply = new FtpReply(); if (stream == null) { throw new ArgumentException("The data stream parameter was null"); } #if !CORE14 lock (m_lock) { #endif try { if (IsConnected) { // if the command that required the data connection was // not successful then there will be no reply from // the server, however if the command was successful // the server will send a reply when the data connection // is closed. if (stream.CommandStatus.Type == FtpResponseType.PositivePreliminary) { if (!(reply = GetReply()).Success) { throw new FtpCommandException(reply); } } } } finally { // if this is a clone of the original control // connection we should Dispose() if (IsClone) { Disconnect(); Dispose(); } } #if !CORE14 } #endif return(reply); }
private string ParseWorkingDirectory(FtpReply reply) { Match m; if ((m = Regex.Match(reply.Message, "\"(?<pwd>.*)\"")).Success) { return(m.Groups["pwd"].Value.GetFtpPath()); } // check for MODCOMP ftp path mentioned in forums: https://netftp.codeplex.com/discussions/444461 if ((m = Regex.Match(reply.Message, "PWD = (?<pwd>.*)")).Success) { return(m.Groups["pwd"].Value.GetFtpPath()); } LogStatus(FtpTraceLevel.Warn, "Failed to parse working directory from: " + reply.Message); return("/"); }
/// <summary> /// Tests if the specified directory exists on the server. This /// method works by trying to change the working directory to /// the path specified. If it succeeds, the directory is changed /// back to the old working directory and true is returned. False /// is returned otherwise and since the CWD failed it is assumed /// the working directory is still the same. /// </summary> /// <param name="path">The path of the directory</param> /// <returns>True if it exists, false otherwise.</returns> /// <example><code source="..\Examples\DirectoryExists.cs" lang="cs" /></example> public bool DirectoryExists(string path) { string pwd; // dont verify args as blank/null path is OK //if (path.IsBlank()) // throw new ArgumentException("Required parameter is null or blank.", "path"); this.LogFunc("DirectoryExists", new object[] { path }); // quickly check if root path, then it always exists! string ftppath = path.GetFtpPath(); if (ftppath == "." || ftppath == "./" || ftppath == "/") { return(true); } // check if a folder exists by changing the working dir to it #if !CORE14 lock (m_lock) { #endif pwd = GetWorkingDirectory(); if (Execute("CWD " + ftppath).Success) { FtpReply reply = Execute("CWD " + pwd.GetFtpPath()); if (!reply.Success) { throw new FtpException("DirectoryExists(): Failed to restore the working directory."); } return(true); } #if !CORE14 } #endif return(false); }
/// <summary> /// Performs a login on the server. This method is overridable so /// that the login procedure can be changed to support, for example, /// a FTP proxy. /// </summary> /// <exception cref="FtpAuthenticationException">On authentication failures</exception> /// <remarks> /// To handle authentication failures without retries, catch FtpAuthenticationException. /// </remarks> protected virtual void Authenticate(string userName, string password) { // send the USER command along with the FTP username FtpReply reply = Execute("USER " + userName); // check the reply to the USER command if (!reply.Success) { throw new FtpAuthenticationException(reply); } // if it was accepted else if (reply.Type == FtpResponseType.PositiveIntermediate) { // send the PASS command along with the FTP password reply = Execute("PASS " + password); // fix for #620: some servers send multiple responses that must be read and decoded, // otherwise the connection is aborted and remade and it goes into an infinite loop var staleData = ReadStaleData(false, true, true); if (staleData != null) { var staleReply = new FtpReply(); if (DecodeStringToReply(staleData, ref staleReply) && !staleReply.Success) { throw new FtpAuthenticationException(staleReply); } } // check the first reply to the PASS command if (!reply.Success) { throw new FtpAuthenticationException(reply); } } }
/// <summary> /// Download a file from the server and write the data into the given stream asynchronously. /// Reads data in chunks. Retries if server disconnects midway. /// </summary> private async Task <bool> DownloadFileInternalAsync(string remotePath, Stream outStream, long restartPosition, IProgress <FtpProgress> progress, CancellationToken token = default(CancellationToken)) { Stream downStream = null; try { // get file size if downloading in binary mode (in ASCII mode we read until EOF) long fileLen = 0; if (DownloadDataType == FtpDataType.Binary && progress != null) { fileLen = await GetFileSizeAsync(remotePath, token); } // open the file for reading downStream = await OpenReadAsync(remotePath, DownloadDataType, restartPosition, fileLen > 0, token); // if the server has not provided a length for this file // we read until EOF instead of reading a specific number of bytes var readToEnd = fileLen <= 0; const int rateControlResolution = 100; var rateLimitBytes = DownloadRateLimit != 0 ? (long)DownloadRateLimit * 1024 : 0; var chunkSize = TransferChunkSize; if (m_transferChunkSize == null && rateLimitBytes > 0) { // reduce chunk size to optimize rate control const int chunkSizeMin = 64; while (chunkSize > chunkSizeMin) { var chunkLenInMs = 1000L * chunkSize / rateLimitBytes; if (chunkLenInMs <= rateControlResolution) { break; } chunkSize = Math.Max(chunkSize >> 1, chunkSizeMin); } } // loop till entire file downloaded var buffer = new byte[chunkSize]; var offset = restartPosition; var transferStarted = DateTime.Now; var sw = new Stopwatch(); while (offset < fileLen || readToEnd) { try { // read a chunk of bytes from the FTP stream var readBytes = 1; long limitCheckBytes = 0; long bytesProcessed = 0; sw.Start(); while ((readBytes = await downStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) { // write chunk to output stream await outStream.WriteAsync(buffer, 0, readBytes, token); offset += readBytes; bytesProcessed += readBytes; limitCheckBytes += readBytes; // send progress reports if (progress != null) { ReportProgress(progress, fileLen, offset, bytesProcessed, DateTime.Now - transferStarted); } // honor the rate limit var swTime = sw.ElapsedMilliseconds; if (rateLimitBytes > 0) { var timeShouldTake = limitCheckBytes * 1000 / rateLimitBytes; if (timeShouldTake > swTime) { await Task.Delay((int)(timeShouldTake - swTime), token); token.ThrowIfCancellationRequested(); } else if (swTime > timeShouldTake + rateControlResolution) { limitCheckBytes = 0; sw.Restart(); } } } // if we reach here means EOF encountered // stop if we are in "read until EOF" mode if (readToEnd || offset == fileLen) { break; } // zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]"); } catch (IOException ex) { // resume if server disconnected midway, or throw if there is an exception doing that as well var resumeResult = await ResumeDownloadAsync(remotePath, downStream, offset, ex); if (resumeResult.Item1) { downStream = resumeResult.Item2; } else { sw.Stop(); throw; } } } sw.Stop(); // disconnect FTP stream before exiting await outStream.FlushAsync(token); downStream.Dispose(); // FIX : if this is not added, there appears to be "stale data" on the socket // listen for a success/failure reply if (!m_threadSafeDataChannels) { FtpReply status = await GetReplyAsync(token); } return(true); } catch (Exception ex1) { // close stream before throwing error try { downStream.Dispose(); } catch (Exception) { } if (ex1 is OperationCanceledException) { LogStatus(FtpTraceLevel.Info, "Upload cancellation requested"); throw; } // absorb "file does not exist" exceptions and simply return false if (ex1.Message.Contains("No such file") || ex1.Message.Contains("not exist") || ex1.Message.Contains("missing file") || ex1.Message.Contains("unknown file")) { LogStatus(FtpTraceLevel.Error, "File does not exist: " + ex1); return(false); } // catch errors during upload throw new FtpException("Error while downloading the file from the server. See InnerException for more info.", ex1); } }
/// <summary> /// Download a file from the server and write the data into the given stream. /// Reads data in chunks. Retries if server disconnects midway. /// </summary> private bool DownloadFileInternal(string remotePath, Stream outStream, long restartPosition, IProgress <FtpProgress> progress) { Stream downStream = null; try { // get file size if downloading in binary mode (in ASCII mode we read until EOF) long fileLen = 0; if (DownloadDataType == FtpDataType.Binary && progress != null) { fileLen = GetFileSize(remotePath); } // open the file for reading downStream = OpenRead(remotePath, DownloadDataType, restartPosition, fileLen > 0); // if the server has not provided a length for this file // we read until EOF instead of reading a specific number of bytes bool readToEnd = (fileLen <= 0); // loop till entire file downloaded byte[] buffer = new byte[TransferChunkSize]; long offset = restartPosition; DateTime transferStarted = DateTime.Now; Stopwatch sw = new Stopwatch(); long rateLimitBytes = DownloadRateLimit != 0 ? DownloadRateLimit * 1024 : 0; while (offset < fileLen || readToEnd) { try { // read a chunk of bytes from the FTP stream int readBytes = 1; double limitCheckBytes = 0; long bytesProcessed = 0; sw.Start(); while ((readBytes = downStream.Read(buffer, 0, buffer.Length)) > 0) { // write chunk to output stream outStream.Write(buffer, 0, readBytes); offset += readBytes; bytesProcessed += readBytes; limitCheckBytes += readBytes; // send progress reports if (progress != null) { ReportProgress(progress, fileLen, offset, bytesProcessed, DateTime.Now - transferStarted); } // honor the rate limit int swTime = (int)sw.ElapsedMilliseconds; if (rateLimitBytes > 0 && swTime >= 1000) { double timeShouldTake = limitCheckBytes / rateLimitBytes * 1000; if (timeShouldTake > swTime) { #if CORE14 Task.Delay((int)(timeShouldTake - swTime)).Wait(); #else Thread.Sleep((int)(timeShouldTake - swTime)); #endif } limitCheckBytes = 0; sw.Restart(); } } // if we reach here means EOF encountered // stop if we are in "read until EOF" mode if (readToEnd || offset == fileLen) { break; } // zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]"); } catch (IOException ex) { // resume if server disconnected midway, or throw if there is an exception doing that as well if (!ResumeDownload(remotePath, ref downStream, offset, ex)) { sw.Stop(); throw; } } } sw.Stop(); // disconnect FTP stream before exiting outStream.Flush(); downStream.Dispose(); // FIX : if this is not added, there appears to be "stale data" on the socket // listen for a success/failure reply if (!m_threadSafeDataChannels) { FtpReply status = GetReply(); } return(true); } catch (Exception ex1) { // close stream before throwing error try { downStream.Dispose(); } catch (Exception) { } // absorb "file does not exist" exceptions and simply return false if (ex1.Message.Contains("No such file") || ex1.Message.Contains("not exist") || ex1.Message.Contains("missing file") || ex1.Message.Contains("unknown file")) { this.LogStatus(FtpTraceLevel.Error, "File does not exist: " + ex1); return(false); } // catch errors during upload throw new FtpException("Error while downloading the file from the server. See InnerException for more info.", ex1); } }
/// <summary> /// Checks if a file exists on the server asynchronously. /// </summary> /// <param name="path">The full or relative path to the file</param> /// <param name="token">The token that can be used to cancel the entire process</param> /// <returns>True if the file exists, false otherwise</returns> public async Task <bool> FileExistsAsync(string path, CancellationToken token = default(CancellationToken)) { // verify args if (path.IsBlank()) { throw new ArgumentException("Required parameter is null or blank.", "path"); } path = path.GetFtpPath(); LogFunc(nameof(FileExistsAsync), new object[] { path }); // A check for path.StartsWith("/") tells us, even if it is z/OS, we can use the normal unix logic // Do not need GetAbsolutePath(path) if z/OS if (ServerType != FtpServer.IBMzOSFTP || path.StartsWith("/")) { // calc the absolute filepath path = await GetAbsolutePathAsync(path, token); } // since FTP does not include a specific command to check if a file exists // here we check if file exists by attempting to get its filesize (SIZE) // If z/OS: Do not do SIZE, unless we have a leading slash if (HasFeature(FtpCapability.SIZE) && (ServerType != FtpServer.IBMzOSFTP || path.StartsWith("/"))) { // Fix #328: get filesize in ASCII or Binary mode as required by server FtpSizeReply sizeReply = new FtpSizeReply(); await GetFileSizeInternalAsync(path, -1, token, sizeReply); // handle known errors to the SIZE command var sizeKnownError = CheckFileExistsBySize(sizeReply); if (sizeKnownError.HasValue) { return(sizeKnownError.Value); } } // check if file exists by attempting to get its date modified (MDTM) // If z/OS: Do not do MDTM, unless we have a leading slash if (HasFeature(FtpCapability.MDTM) && (ServerType != FtpServer.IBMzOSFTP || path.StartsWith("/"))) { FtpReply reply = await ExecuteAsync("MDTM " + path, token); var ch = reply.Code[0]; if (ch == '2') { return(true); } if (ch == '5' && reply.Message.IsKnownError(FtpServerStrings.fileNotFound)) { return(false); } } // If z/OS: different handling, unless we have a leading slash if (ServerType == FtpServer.IBMzOSFTP && !path.StartsWith("/")) { var fileList = await GetNameListingAsync(path, token); return(fileList.Count() > 0); } else // check if file exists by getting a name listing (NLST) { var fileList = await GetNameListingAsync(path.GetFtpDirectoryName(), token); return(FileListings.FileExistsInNameListing(fileList, path)); } }
/// <summary> /// Initializes a new instance of a FtpAuthenticationException /// </summary> /// <param name="reply">The FtpReply to build the exception from</param> public FtpAuthenticationException(FtpReply reply) : base(reply) { }
/// <summary> /// Populates the capabilities flags based on capabilities /// supported by this server. This method is overridable /// so that new features can be supported /// </summary> /// <param name="reply">The reply object from the FEAT command. The InfoMessages property will /// contain a list of the features the server supported delimited by a new line '\n' character.</param> protected virtual void GetFeatures(FtpReply reply) { FtpServerSpecificHandler.GetFeatures(this, m_capabilities, ref m_hashAlgorithms, reply.InfoMessages.Split('\n')); }
/// <summary> /// Initalizes a new instance of a FtpResponseException /// </summary> /// <param name="reply">The FtpReply to build the exception from</param> public FtpCommandException(FtpReply reply) : this(reply.Code, reply.ErrorMessage) { }
/// <summary> /// Populates the capabilities flags based on capabilities /// supported by this server. This method is overridable /// so that new features can be supported /// </summary> /// <param name="reply">The reply object from the FEAT command. The InfoMessages property will /// contain a list of the features the server supported delimited by a new line '\n' character.</param> protected virtual void GetFeatures(FtpReply reply) { GetFeatures(reply.InfoMessages.Split('\n')); }