-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add native Rebex FTP upload handler and receiver #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| namespace SW.Bitween.NativeAdapters; | ||
|
|
||
| /// <summary> | ||
| /// Shared FTP protocol rules used by the native FTP adapters, so the handler and receiver | ||
| /// can never disagree on them. | ||
| /// </summary> | ||
| public static class FtpProtocol | ||
| { | ||
| /// <summary> | ||
| /// Password-authenticated protocols (ftp, sftp) require a password. sftpssh authenticates | ||
| /// with a private key, so its "password" is an optional key passphrase and is not enforced. | ||
| /// </summary> | ||
| public static void EnsurePasswordProvided(string protocol, string? password) | ||
| { | ||
| if (protocol.ToLower() is "ftp" or "sftp" && string.IsNullOrEmpty(password)) | ||
| throw new ArgumentException($"Password is required for the '{protocol}' protocol."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// sftpssh authenticates with a private key, so one must be provided. ftp/sftp authenticate | ||
| /// with a password instead, so no key is required for them. | ||
| /// </summary> | ||
| public static void EnsurePrivateKeyProvided(string protocol, string? privateKey) | ||
| { | ||
| if (protocol.ToLower() is "sftpssh" && string.IsNullOrWhiteSpace(privateKey)) | ||
| throw new ArgumentException($"A private key is required for the '{protocol}' protocol."); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,111 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Rebex.Net; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using SW.PrimitiveTypes; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace SW.Bitween.NativeAdapters.RebexFtpReceiver; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class NativeRebexFtpReceiver : INativeInfolinkReceiver | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly string? _licenseKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private RebexFtpReceiverInput _options = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private IFtp _ftpOrSftp = null!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public NativeRebexFtpReceiver(string? licenseKey = null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _licenseKey = licenseKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task Initialize() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Rebex.Licensing.Key = _licenseKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FtpProtocol.EnsurePasswordProvided(_options.Protocol, _options.Password); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FtpProtocol.EnsurePrivateKeyProvided(_options.Protocol, _options.PrivateKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (_options.Protocol.ToLower()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "sftpssh": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sftpssh = new Sftp(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var privateKey = new SshPrivateKey(keyBytes, _options.Password); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await sftpssh.LoginAsync(_options.Username, privateKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ftpOrSftp = sftpssh; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
hamzahalq marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "sftp": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sftp = new Sftp(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await sftp.ConnectAsync(_options.Host, _options.Port ?? 22); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ftpOrSftp = sftp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.LoginAsync(_options.Username, _options.Password); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "ftp": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ftp = new Rebex.Net.Ftp(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ftp.ConnectAsync(_options.Host, _options.Port ?? 21); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ftpOrSftp = ftp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.LoginAsync(_options.Username, _options.Password); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new ArgumentException($"Unknown protocol '{_options.Protocol}'"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!string.IsNullOrEmpty(_options.TargetPath)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.ChangeDirectoryAsync(_options.TargetPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task Finalize() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.DisconnectAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ftpOrSftp.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Connection leaked and
🔒 Proposed fix case "sftpssh":
var sftpssh = new Sftp();
await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22);
+ _ftpOrSftp = sftpssh;
var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey));
var privateKey = new SshPrivateKey(keyBytes, _options.Password);
await sftpssh.LoginAsync(_options.Username, privateKey);
-
- _ftpOrSftp = sftpssh;
break; public async Task Finalize()
{
+ if (_ftpOrSftp is null)
+ return;
await _ftpOrSftp.DisconnectAsync();
_ftpOrSftp.Dispose();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<IEnumerable<string>> ListFiles() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var files = await _ftpOrSftp.GetListAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return files | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Where(i => i.IsFile) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Take(_options.BatchSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Select(i => i.Name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ToList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<XchangeFile> GetFile(string fileId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await using var stream = new MemoryStream(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.GetFileAsync(fileId, stream); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var data = stream.ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _options.ResponseEncoding.ToLower() switch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "base64" => new XchangeFile(Convert.ToBase64String(data), fileId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "utf8" => new XchangeFile(Encoding.UTF8.GetString(data), fileId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ => throw new ArgumentException( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $"Unknown {nameof(RebexFtpReceiverInput.ResponseEncoding)} '{_options.ResponseEncoding}'") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task DeleteFile(string fileId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_options.CheckFileExistence && !await _ftpOrSftp.FileExistsAsync(fileId)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (string.IsNullOrWhiteSpace(_options.DeleteMovesFileTo)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.DeleteFileAsync(fileId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _ftpOrSftp.RenameAsync(fileId, _options.DeleteMovesFileTo + "/" + fileId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string Name => "NativeRebexFtpReceiver"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void InitializeStartupValues(IDictionary<string, string> settings) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _options = settings.ConvertTo<RebexFtpReceiverInput>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Type StartupValuesType => typeof(RebexFtpReceiverInput); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| using System.ComponentModel; | ||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace SW.Bitween.NativeAdapters.RebexFtpReceiver; | ||
|
|
||
| public class RebexFtpReceiverInput | ||
| { | ||
| [Required] | ||
| public string Host { get; set; } = string.Empty; | ||
|
|
||
| public int? Port { get; set; } | ||
|
|
||
| [Required] | ||
| public string Username { get; set; } = string.Empty; | ||
|
|
||
| [Secure] | ||
| public string? Password { get; set; } | ||
|
|
||
| public string? TargetPath { get; set; } | ||
|
|
||
| [DefaultValue(50)] | ||
| public int BatchSize { get; set; } = 50; | ||
|
|
||
| [DefaultValue("utf8")] | ||
| public string ResponseEncoding { get; set; } = "utf8"; | ||
|
|
||
| public string? DeleteMovesFileTo { get; set; } | ||
|
|
||
| [DefaultValue("sftp")] | ||
| public string Protocol { get; set; } = "sftp"; | ||
|
|
||
| [DefaultValue(true)] | ||
| public bool CheckFileExistence { get; set; } = true; | ||
|
|
||
| [Secure] | ||
| public string? PrivateKey { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||
| using System.Text; | ||||||||||||
| using Rebex.Net; | ||||||||||||
| using SW.PrimitiveTypes; | ||||||||||||
|
|
||||||||||||
| namespace SW.Bitween.NativeAdapters.RebexFtpUploadHandler; | ||||||||||||
|
|
||||||||||||
| public class NativeRebexFtpUploadHandler : INativeInfolinkHandler | ||||||||||||
| { | ||||||||||||
| private readonly string? _licenseKey; | ||||||||||||
| private RebexFtpUploadHandlerInput _options = new(); | ||||||||||||
|
|
||||||||||||
| public NativeRebexFtpUploadHandler(string? licenseKey = null) | ||||||||||||
| { | ||||||||||||
| _licenseKey = licenseKey; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| public async Task<XchangeFile> Handle(XchangeFile xchangeFile) | ||||||||||||
| { | ||||||||||||
| Rebex.Licensing.Key = _licenseKey; | ||||||||||||
| FtpProtocol.EnsurePasswordProvided(_options.Protocol, _options.Password); | ||||||||||||
| FtpProtocol.EnsurePrivateKeyProvided(_options.Protocol, _options.PrivateKey); | ||||||||||||
|
|
||||||||||||
| IFtp ftpOrSftp; | ||||||||||||
| switch (_options.Protocol.ToLower()) | ||||||||||||
| { | ||||||||||||
| case "sftpssh": | ||||||||||||
| var sftpssh = new Sftp(); | ||||||||||||
| await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22); | ||||||||||||
|
|
||||||||||||
| var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey)); | ||||||||||||
| var sshPrivateKey = new SshPrivateKey(keyBytes, _options.Password); | ||||||||||||
| await sftpssh.LoginAsync(_options.Username, sshPrivateKey); | ||||||||||||
|
|
||||||||||||
| ftpOrSftp = sftpssh; | ||||||||||||
| break; | ||||||||||||
|
hamzahalq marked this conversation as resolved.
|
||||||||||||
|
|
||||||||||||
| case "sftp": | ||||||||||||
| var sftp = new Sftp(); | ||||||||||||
| await sftp.ConnectAsync(_options.Host, _options.Port ?? 22); | ||||||||||||
| ftpOrSftp = sftp; | ||||||||||||
| await ftpOrSftp.LoginAsync(_options.Username, _options.Password); | ||||||||||||
| break; | ||||||||||||
|
|
||||||||||||
| case "ftp": | ||||||||||||
| var ftp = new Rebex.Net.Ftp(); | ||||||||||||
| await ftp.ConnectAsync(_options.Host, _options.Port ?? 21); | ||||||||||||
| ftpOrSftp = ftp; | ||||||||||||
| await ftpOrSftp.LoginAsync(_options.Username, _options.Password); | ||||||||||||
| break; | ||||||||||||
|
|
||||||||||||
| default: | ||||||||||||
| throw new ArgumentException($"Unknown protocol '{_options.Protocol}'"); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var bytes = _options.DataEncoding.ToLower() switch | ||||||||||||
| { | ||||||||||||
| "base64" => Convert.FromBase64String(xchangeFile.Data), | ||||||||||||
| "utf8" => Encoding.UTF8.GetBytes(xchangeFile.Data), | ||||||||||||
| _ => throw new ArgumentException( | ||||||||||||
| $"Unknown {nameof(RebexFtpUploadHandlerInput.DataEncoding)} '{_options.DataEncoding}'") | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| await using var stream = new MemoryStream(bytes); | ||||||||||||
|
|
||||||||||||
| var filename = xchangeFile.Filename; | ||||||||||||
| if (string.IsNullOrWhiteSpace(filename)) | ||||||||||||
| { | ||||||||||||
| var currentDate = DateTime.UtcNow; | ||||||||||||
| filename = | ||||||||||||
| $"{currentDate.Year:0000}{currentDate.Month:00}{currentDate.Day:00}{currentDate.Hour:00}{currentDate.Minute:00}{currentDate.Second:00}{currentDate.Millisecond:000}"; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if (!string.IsNullOrWhiteSpace(_options.FileNamePrefix)) | ||||||||||||
| filename = $"{_options.FileNamePrefix}_{filename}"; | ||||||||||||
|
|
||||||||||||
| await ftpOrSftp.PutFileAsync(stream, $"{_options.TargetPath}/{filename}"); | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Uploads land at the FTP root when
🐛 Proposed fix- await ftpOrSftp.PutFileAsync(stream, $"{_options.TargetPath}/{filename}");
+ var remotePath = string.IsNullOrWhiteSpace(_options.TargetPath)
+ ? filename
+ : $"{_options.TargetPath.TrimEnd('/')}/{filename}";
+ await ftpOrSftp.PutFileAsync(stream, remotePath);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| await ftpOrSftp.DisconnectAsync(); | ||||||||||||
| return new XchangeFile(string.Empty); | ||||||||||||
|
Comment on lines
+23
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Connection is never disposed, and leaks entirely on any failure between connect and upload.
🔒 Proposed fix (wrap in try/finally, assign before login)- IFtp ftpOrSftp;
- switch (_options.Protocol.ToLower())
- {
- case "sftpssh":
- var sftpssh = new Sftp();
- await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22);
-
- var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey));
- var sshPrivateKey = new SshPrivateKey(keyBytes, _options.Password);
- await sftpssh.LoginAsync(_options.Username, sshPrivateKey);
-
- ftpOrSftp = sftpssh;
- break;
- ...
- }
- ...
- await ftpOrSftp.PutFileAsync(stream, $"{_options.TargetPath}/{filename}");
- await ftpOrSftp.DisconnectAsync();
- return new XchangeFile(string.Empty);
+ IFtp? ftpOrSftp = null;
+ try
+ {
+ switch (_options.Protocol.ToLower())
+ {
+ case "sftpssh":
+ var sftpssh = new Sftp();
+ await sftpssh.ConnectAsync(_options.Host, _options.Port ?? 22);
+ ftpOrSftp = sftpssh;
+
+ var keyBytes = Encoding.UTF8.GetBytes(SshKeyNormalizer.Normalize(_options.PrivateKey));
+ var sshPrivateKey = new SshPrivateKey(keyBytes, _options.Password);
+ await sftpssh.LoginAsync(_options.Username, sshPrivateKey);
+ break;
+ // ... other cases, assign ftpOrSftp before login too
+ }
+ // ... build bytes/stream/filename
+ await ftpOrSftp.PutFileAsync(stream, remotePath);
+ return new XchangeFile(string.Empty);
+ }
+ finally
+ {
+ if (ftpOrSftp is not null)
+ {
+ await ftpOrSftp.DisconnectAsync();
+ ftpOrSftp.Dispose();
+ }
+ }🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| public string Name => "NativeRebexFtpUploadHandler"; | ||||||||||||
|
|
||||||||||||
| public void InitializeStartupValues(IDictionary<string, string> settings) | ||||||||||||
| { | ||||||||||||
| _options = settings.ConvertTo<RebexFtpUploadHandlerInput>(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| public Type StartupValuesType => typeof(RebexFtpUploadHandlerInput); | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using System.ComponentModel; | ||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace SW.Bitween.NativeAdapters.RebexFtpUploadHandler; | ||
|
|
||
| public class RebexFtpUploadHandlerInput | ||
| { | ||
| [Required] | ||
| public string Host { get; set; } = string.Empty; | ||
|
|
||
| public int? Port { get; set; } | ||
|
|
||
| [Required] | ||
| public string Username { get; set; } = string.Empty; | ||
|
|
||
| [Secure] | ||
| public string? Password { get; set; } | ||
|
|
||
| public string? TargetPath { get; set; } | ||
|
|
||
| public string? FileNamePrefix { get; set; } | ||
|
|
||
| [DefaultValue("utf8")] | ||
| public string DataEncoding { get; set; } = "utf8"; | ||
|
|
||
| [DefaultValue("sftp")] | ||
| public string Protocol { get; set; } = "sftp"; | ||
|
|
||
| [Secure] | ||
| public string? PrivateKey { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| using System.Text; | ||
| using System.Text.RegularExpressions; | ||
|
|
||
| namespace SW.Bitween.NativeAdapters; | ||
|
|
||
| /// <summary> | ||
| /// Normalizes an SSH private key coming from adapter settings into a shape SSH clients accept. | ||
| /// Shared by every native adapter that does key-based auth, so their handling can never diverge. | ||
| /// | ||
| /// Rules: | ||
| /// - Blank input -> empty string. | ||
| /// - Already multi-line -> returned untouched (never risk corrupting a well-formed key). | ||
| /// - Flattened PEM (single line, has BEGIN/END markers) -> rebuilt with the header/footer | ||
| /// preserved exactly and the base64 body re-wrapped at 64 chars. | ||
| /// - Anything else (e.g. a flattened non-PEM format) -> returned as-is rather than mangled. | ||
| /// </summary> | ||
| public static class SshKeyNormalizer | ||
| { | ||
| private static readonly Regex PemShape = | ||
| new(@"^(-----BEGIN [^-]+-----)(.*?)(-----END [^-]+-----)$", RegexOptions.Singleline | RegexOptions.Compiled); | ||
|
|
||
| public static string Normalize(string? rawKey) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(rawKey)) | ||
| return string.Empty; | ||
|
|
||
| var key = rawKey.Trim(); | ||
|
|
||
| // Well-formed multi-line key: trust it exactly as given. | ||
| if (key.Contains('\n')) | ||
| return key; | ||
|
|
||
| // Single flattened line: only reconstruct if it's a PEM key we recognize. | ||
| var match = PemShape.Match(key); | ||
| if (!match.Success) | ||
| return key; | ||
|
|
||
| var header = match.Groups[1].Value.Trim(); | ||
| var body = Regex.Replace(match.Groups[2].Value, @"\s+", string.Empty); | ||
| var footer = match.Groups[3].Value.Trim(); | ||
|
|
||
| var sb = new StringBuilder(); | ||
| sb.Append(header).Append('\n'); | ||
| for (var i = 0; i < body.Length; i += 64) | ||
| sb.Append(body.Substring(i, Math.Min(64, body.Length - i))).Append('\n'); | ||
| sb.Append(footer).Append('\n'); | ||
| return sb.ToString(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Add unit tests for
EnsurePasswordProvided.This guard is the sole validation gate shared by both Rebex adapters (ftp/sftp require password, sftpssh doesn't), but no test file covers it (only
SshKeyNormalizerTests.cswas added). A regression here silently breaks auth validation for both adapters.🤖 Prompt for AI Agents
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Duplicated connect/login switch across both adapters is a good candidate for consolidation here.
NativeRebexFtpReceiver.InitializeandNativeRebexFtpUploadHandler.Handleboth implement a nearly identicalsftpssh/sftp/ftpswitch (connect, key setup, login, unknown-protocol throw). This file already centralizes the shared password rule — extending it with a sharedConnect(protocol, host, port, username, password, privateKey) : Task<IFtp>factory would remove ~30 duplicated lines per adapter and prevent the two implementations from silently diverging (see the assignment-order and target-path issues flagged in the receiver/handler files, which only exist in one of the two copies).🤖 Prompt for AI Agents