335 lines
13 KiB
C#
335 lines
13 KiB
C#
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<Renci.SshNet.AuthenticationMethod>();
|
|
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;
|
|
}
|
|
|
|
}
|
|
} |