using System; using System.IO; using System.Reflection; using System.Collections.Generic; using System.Threading; using Renci.SshNet; using System.Linq; using Renci.SshNet.Common; using System.Threading.Tasks; namespace bdf { public class sftp { public class ConnectionDetails { public Int32 maxAttempts = 5; public Int32 timeOutSecs = 30; public Int32 retrySecs = 5; public Int32 OperationTimeout = 30; public Int32 KeepAliveInterval = 15; public string ppkFile = ""; public string passPhrase = ""; public string username = ""; public string password = ""; public string host = ""; public Int32 port = 22; } public static Renci.SshNet.ConnectionInfo BuildConnectionInfo(ConnectionDetails cd) { var auth = new System.Collections.Generic.List(); PrivateKeyFile pkFile = null; if (cd.username != "" && cd.ppkFile != "") { if (cd.ppkFile.Contains("Embedded")) { Logger.Log(2, " Processing internal PPK"); // Get the assembly where the resource is stored Assembly asm = Assembly.GetExecutingAssembly(); Stream stream = Stream.Null; if (asm.GetManifestResourceNames().Contains(cd.ppkFile)) // Fully-qualified resource name (namespace + filename) stream = asm.GetManifestResourceStream(cd.ppkFile); else { Logger.Log(0, "EXCEPTION: PPK resource not found."); throw new Exception("Private key file not found. "); } //Console.WriteLine("b - {0} {1}", cd.ppkFile, stream == null); try { if (stream == null || !stream.CanRead) throw new Exception("Private key file not found/not readable. "); else stream.Position = 0; // reset stream position } catch (Exception e) { Logger.Log(3, "EXCEPTION PPK {0}:{1}", e.Message, e.InnerException.Message); } try { pkFile = string.IsNullOrEmpty(cd.passPhrase) ? new PrivateKeyFile(stream) : new PrivateKeyFile(stream, cd.passPhrase); } catch (Renci.SshNet.Common.SshException e) { throw new Exception("Problems with your internal private key and/or passphrase. " + e.Message); } stream.Close(); Logger.Log(5, " Internal PPK loaded"); } else { try { pkFile = string.IsNullOrEmpty(cd.passPhrase) ? new PrivateKeyFile(cd.ppkFile) : new PrivateKeyFile(cd.ppkFile, cd.passPhrase); } catch (Renci.SshNet.Common.SshException e) { throw new Exception("Problems with your private key and/or passphrase. " + e.Message); } } if (pkFile != null) { auth.Add(new Renci.SshNet.PrivateKeyAuthenticationMethod(cd.username, pkFile)); //preferred Logger.Log(1, " -added PPK auth method."); } } if (!string.IsNullOrEmpty(cd.password)) { var kb = new Renci.SshNet.KeyboardInteractiveAuthenticationMethod(cd.username); kb.AuthenticationPrompt += (s, e) => { foreach (var p in e.Prompts) p.Response = cd.password; }; auth.Add(kb); auth.Add(new Renci.SshNet.PasswordAuthenticationMethod(cd.username, cd.password)); Logger.Log(1, " -added user/pass auth method."); } return new Renci.SshNet.ConnectionInfo(cd.host, cd.port, cd.username, auth.ToArray()) { Timeout = System.TimeSpan.FromSeconds(30), RetryAttempts = 1 }; } public static Renci.SshNet.SftpClient TrySftpConnectWithHardTimeout( Renci.SshNet.ConnectionInfo connectionInfo, int maxAttempts, int timeoutSeconds) { for (int i = 0; i < maxAttempts; i++) { Renci.SshNet.SftpClient client = null; try { var task = System.Threading.Tasks.Task.Run(() => { client = new Renci.SshNet.SftpClient(connectionInfo); // These are still useful, but not sufficient alone client.ConnectionInfo.Timeout = System.TimeSpan.FromSeconds(30); client.KeepAliveInterval = System.TimeSpan.FromSeconds(15); client.Connect(); }); bool completed = task.Wait(System.TimeSpan.FromSeconds(timeoutSeconds)); if (!completed) { // HARD TIMEOUT try { client?.Dispose(); } catch { } //return null; } else { return client; } } catch { try { client?.Dispose(); } catch { } //return null; } System.Threading.Thread.Sleep(5000); } return null; } public static bool serverExists(string host) { try { // 1. DNS resolution System.Net.IPAddress[] addresses = System.Net.Dns.GetHostAddresses(host); if (addresses == null || addresses.Length == 0) return false; // 2. Ping first resolved address using (System.Net.NetworkInformation.Ping ping = new System.Net.NetworkInformation.Ping()) { Logger.Log(1, "Validating server: {0} at {1}", host, addresses[0]); System.Net.NetworkInformation.PingReply reply = ping.Send(addresses[0], 3000); // 3s timeout return reply != null && reply.Status == System.Net.NetworkInformation.IPStatus.Success; } } catch { return false; } } public static void Upload_Stream(MemoryStream ms, string filename, ConnectionDetails cd) { try { //using (var sftpClient = CreateClient(host, port, username, password)) var sftpInfo = BuildConnectionInfo(cd); Renci.SshNet.SftpClient sftpClient = null; try { sftpClient = TrySftpConnectWithHardTimeout(sftpInfo, cd.maxAttempts, cd.retrySecs); } catch (Renci.SshNet.Common.SshConnectionException e) { Logger.Log(1, " connection exception ({0}) : {1}", "", e.Message); } if (sftpClient == null) { Logger.Log(1, " null connection exception. Transfer failed."); return; } string cpath = EnsureSftpDirectoryExists(sftpClient, filename); // file upload try { ms.Position = 0; sftpClient.UploadFile( ms, filename, uploaded => { Logger.Log(2, $"Uploaded {Math.Round((double)uploaded / ms.Length * 100)}% of the file."); }); } catch (Renci.SshNet.Common.SshException e) { Logger.Log(0, " SSH upload Exception {0}:{1}", e.Message, e.InnerException.Message); } Logger.Log(0, " -- uploaded: {0} ({3}KB) to {1}:{2}", filename, cd.host, cd.port, (ms.Length / 1024)+1); //Console.WriteLine("starting dir pull {0}", cpath); var files = sftpClient.ListDirectory(cpath).Where(f => f.IsRegularFile && (f.Name.EndsWith(".pdf", System.StringComparison.OrdinalIgnoreCase) || //pdf f.Name.EndsWith(".zip", System.StringComparison.OrdinalIgnoreCase) || //pdf f.Name.EndsWith(".xlsx", System.StringComparison.OrdinalIgnoreCase))); foreach (var s in files) { Logger.Log(1, " => {0} {1}", s.FullName, s.Attributes); } sftpClient.Disconnect(); Logger.Log(" SSH Disconnected"); try { sftpClient?.Dispose(); } catch { } return; // success } catch (Exception e) { Logger.Log(0, "SFTP exception {0}:{1}", e.Message, e.InnerException.Message); } } public static string EnsureSftpDirectoryExists(SftpClient client, string path) { // The path needs to be absolute for reliable operation across different SFTP servers. // Ensure the path starts with a '/' if it's meant to be absolute from the root. if (!path.StartsWith(Path.DirectorySeparatorChar.ToString() )) { path = Path.DirectorySeparatorChar + path; } // Split the path into components, handling potential leading slash var parts = path.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); string currentPath = Path.DirectorySeparatorChar.ToString(); Logger.Log(3, "Checking path: {0}", path); foreach (var part in parts) { Logger.Log(4, " checking path part {0}", part); if (string.IsNullOrEmpty(part)) continue; if (parts[parts.Length-1].ToString() == part) break; // last part is the filename so skip pit. arrays are 0-based currentPath = currentPath == Path.DirectorySeparatorChar.ToString() ? Path.DirectorySeparatorChar.ToString() + part : currentPath + Path.DirectorySeparatorChar.ToString() + part; try { // Attempt to get attributes of the current path component var attrs = client.GetAttributes(currentPath); if (!attrs.IsDirectory) { // Handle the case where a file exists with the same name as a directory component //throw new Exception($"A file exists with the same name as the directory component: {currentPath}"); Logger.Log(4, " found existing filename {0}", part); break; } } catch (SftpPathNotFoundException) { // If the path is not found, create the directory client.CreateDirectory(currentPath); Logger.Log(0, "Creating SFTP directory: {0}", currentPath); System.Threading.Thread.Sleep(2000); } catch (Exception ex) { // Handle other potential exceptions (permissions, etc.) Logger.Log(0, "Error checking/creating directory {0}: {1}",currentPath,ex.Message); throw; // Re-throw to propagate the error } } return currentPath; } } }