├── FxSshSftpServer ├── Pages │ ├── Index.razor │ ├── Error.cshtml.cs │ ├── General.razor │ ├── General.razor.cs │ ├── NavMenu.razor.css │ ├── NavMenu.razor │ ├── _Host.cshtml │ ├── Error.cshtml │ ├── Users.razor │ └── Users.razor.cs ├── wwwroot │ ├── favicon.ico │ ├── css │ │ ├── open-iconic │ │ │ ├── font │ │ │ │ └── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ └── open-iconic.woff │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── FONT-LICENSE │ │ └── site.css │ └── js │ │ └── site.js ├── Components │ ├── ToolTip.razor │ ├── TwoColumnRow.razor │ ├── ToolTip.razor.cs │ ├── TwoColumnRow.razor.cs │ ├── ToolTip.Razor.css │ └── FileDownload.cs ├── appsettings.json ├── appsettings.Development.json ├── App.razor ├── Shared │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── _Imports.razor ├── Properties │ └── launchSettings.json ├── FxSshSftpServer.csproj └── Program.cs ├── FxSsh ├── Algorithms │ ├── CipherModeEx.cs │ ├── CompressionAlgorithm.cs │ ├── NoCompression.cs │ ├── HmacInfo.cs │ ├── KexAlgorithmContract.cs │ ├── KexAlgorithm.cs │ ├── CompressionAlgorithmContract.cs │ ├── DiffieHellmanGroupSha1.cs │ ├── DiffieHellmanGroupSha256.cs │ ├── HmacAlgorithm.cs │ ├── CipherInfo.cs │ ├── PublicKeyAlgorithmContract.cs │ ├── EncryptionAlgorithm.cs │ ├── PublicKeyAlgorithm.cs │ ├── CtrModeCryptoTransform.cs │ ├── DssKey.cs │ ├── RsaKey.cs │ └── DiffieHellman.cs ├── Messages │ ├── ConnectionServiceMessage.cs │ ├── UserauthServiceMessage.cs │ ├── Connection │ │ ├── SessionOpenConfirmationMessage.cs │ │ ├── ShellRequestMessage.cs │ │ ├── CommandRequestMessage.cs │ │ ├── ExitStatusMessage.cs │ │ ├── SessionOpenMessage.cs │ │ ├── EnvMessage.cs │ │ ├── ShouldIgnoreMessage.cs │ │ ├── ChannelFailureMessage.cs │ │ ├── ChannelSuccessMessage.cs │ │ ├── ChannelEofMessage.cs │ │ ├── ChannelCloseMessage.cs │ │ ├── SubsystemRequestMessage.cs │ │ ├── ChannelDataMessage.cs │ │ ├── PTYRequestMessage.cs │ │ ├── ChannelWindowAdjustMessage.cs │ │ ├── ChannelOpenConfirmationMessage.cs │ │ ├── ChannelOpenFailureMessage.cs │ │ ├── ChannelOpenMessage.cs │ │ ├── ForwardedTcpIpMessage.cs │ │ ├── ChannelRequestMessage.cs │ │ └── DirectTcpIpMessage.cs │ ├── Userauth │ │ ├── SuccessMessage.cs │ │ ├── FailureMessage.cs │ │ ├── PasswordRequestMessage.cs │ │ ├── PublicKeyOkMessage.cs │ │ ├── RequestMessage.cs │ │ └── PublicKeyRequestMessage.cs │ ├── NewKeysMessage.cs │ ├── KeyExchangeDhInitMessage.cs │ ├── ServiceRequestMessage.cs │ ├── MessageAttribute.cs │ ├── UnimplementedMessage.cs │ ├── ServiceAcceptMessage.cs │ ├── KeyExchangeDhReplyMessage.cs │ ├── DisconnectMessage.cs │ ├── Message.cs │ └── KeyExchangeInitMessage.cs ├── ChannelOpenFailureReason.cs ├── SshServerSettings.cs ├── Services │ ├── PasswordUserauthArgs.cs │ ├── SessionChannel.cs │ ├── SshService.cs │ ├── PKUserauthArgs.cs │ ├── EnvironmentArgs.cs │ ├── CommandRequestedArgs.cs │ ├── TcpRequestArgs.cs │ ├── SessionRequestedArgs.cs │ ├── KeyExchangeArgs.cs │ ├── PtyArgs.cs │ ├── UserauthArgs.cs │ ├── UserauthService.cs │ └── Channel.cs ├── StartingInfo.cs ├── SshConnectionException.cs ├── DisconnectReason.cs ├── IPA-DN-Fork-SSHServer.csproj ├── FxSsh.csproj ├── KeyUtils.cs ├── DynamicInvoker.cs ├── KeepAliveTimer.cs ├── SshServer.cs └── SshDataWorker.cs ├── TestClient ├── packages.config ├── TestClient.csproj └── Program.cs ├── SshServer.Interfaces ├── SshServer.Interfaces.csproj ├── IFileSystemFactory.cs ├── ISettingsRepository.cs ├── User.cs ├── ServerSettings.cs └── IFileSystem.cs ├── SshServer.Settings.Sql ├── Startup.cs └── SshServer.Settings.Sql.csproj ├── SshServer.Filesystem.LocalDisk ├── Startup.cs ├── SshServer.Filesystem.LocalDisk.csproj └── LocalFileSystemFactory.cs ├── SshServer.Settings.LiteDb ├── Startup.cs ├── SshServer.Settings.LiteDb.csproj └── SettingsRepository.cs ├── ServerConsoleApp ├── ServerConsoleApp.csproj └── Program.cs ├── SshServerModule ├── SshServerModule.csproj ├── CommandService.cs ├── Settings │ └── UserLogics.cs ├── Services │ ├── TcpForwardService.cs │ ├── Permissions.cs │ └── SftpFileAttributes.cs └── Constants.cs ├── LICENSE.md ├── .vscode ├── launch.json └── tasks.json ├── .gitattributes ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── README.md └── FreeSftpSharp.sln /FxSshSftpServer/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Hello, world!

4 | 5 | Welcome to your new app. 6 | 7 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelliljedahl/freesftpsharp/HEAD/FxSshSftpServer/wwwroot/favicon.ico -------------------------------------------------------------------------------- /FxSshSftpServer/Components/ToolTip.razor: -------------------------------------------------------------------------------- 1 | 2 |
3 | @Text 4 | @ChildContent 5 |
6 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/CipherModeEx.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Algorithms 2 | { 3 | public enum CipherModeEx 4 | { 5 | CBC, 6 | CTR, 7 | } 8 | } -------------------------------------------------------------------------------- /FxSsh/Messages/ConnectionServiceMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | public abstract class ConnectionServiceMessage : Message 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /FxSsh/Messages/UserauthServiceMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | public abstract class UserauthServiceMessage : Message 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /TestClient/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelliljedahl/freesftpsharp/HEAD/FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelliljedahl/freesftpsharp/HEAD/FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelliljedahl/freesftpsharp/HEAD/FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaelliljedahl/freesftpsharp/HEAD/FxSshSftpServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/SessionOpenConfirmationMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages.Connection 2 | { 3 | public class SessionOpenConfirmationMessage : ChannelOpenConfirmationMessage 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ShellRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages.Connection 4 | { 5 | public class ShellRequestMessage : ChannelRequestMessage 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /FxSshSftpServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /FxSshSftpServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SshServer.Interfaces/SshServer.Interfaces.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /SshServer.Interfaces/IFileSystemFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SshServer.Interfaces; 9 | public interface IFileSystemFactory 10 | { 11 | public IFileSystem GetFileSystem(string username); 12 | } -------------------------------------------------------------------------------- /SshServer.Settings.Sql/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SshServer.Settings.Sql 4 | { 5 | public static class Startup 6 | { 7 | public static IServiceCollection AddSshServerSettingsSql(this IServiceCollection services) 8 | { 9 | 10 | return services; 11 | } 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/CompressionAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Algorithms 4 | { 5 | [ContractClass(typeof(CompressionAlgorithmContract))] 6 | public abstract class CompressionAlgorithm 7 | { 8 | public abstract byte[] Compress(byte[] input); 9 | 10 | public abstract byte[] Decompress(byte[] input); 11 | } 12 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/NoCompression.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Algorithms 2 | { 3 | public class NoCompression : CompressionAlgorithm 4 | { 5 | public override byte[] Compress(byte[] input) 6 | { 7 | return input; 8 | } 9 | 10 | public override byte[] Decompress(byte[] input) 11 | { 12 | return input; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Components/TwoColumnRow.razor: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | @HeaderText 5 |
6 |
7 | @ChildContent 8 |
9 |
10 | @if (TooltipText != null) 11 | { 12 | ? 13 | } 14 |
15 |
16 | -------------------------------------------------------------------------------- /FxSsh/ChannelOpenFailureReason.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace FxSsh 4 | { 5 | public enum ChannelOpenFailureReason 6 | { 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | None = 0, // Not used by protocol 9 | 10 | AdministrativelyProhibited = 1, 11 | ConnectFailed = 2, 12 | UnknownChannelType = 3, 13 | ResourceShortage = 4, 14 | } 15 | } -------------------------------------------------------------------------------- /TestClient/TestClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FxSshSftpServer/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /SshServer.Settings.Sql/SshServer.Settings.Sql.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /FxSsh/SshServerSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FxSsh 4 | { 5 | public class SshServerSettings 6 | { 7 | public const int DefaultPort = 22; 8 | 9 | public IPAddress LocalAddress { get; set; } = IPAddress.IPv6Any; 10 | public int Port { get; set; } = DefaultPort; 11 | 12 | public string ServerBanner { get; set; } = "FxSSHSFTP"; 13 | 14 | public int IdleTimeout { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/CommandRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Connection 5 | { 6 | public class CommandRequestMessage : ChannelRequestMessage 7 | { 8 | public string Command { get; private set; } 9 | 10 | protected override void OnLoad(SshDataWorker reader) 11 | { 12 | base.OnLoad(reader); 13 | 14 | Command = reader.ReadString(Encoding.ASCII); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/SuccessMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FxSsh.Messages.Userauth 4 | { 5 | [Message("SSH_MSG_USERAUTH_SUCCESS", MessageNumber)] 6 | public class SuccessMessage : UserauthServiceMessage 7 | { 8 | private const byte MessageNumber = 52; 9 | 10 | public override byte MessageType { get { return MessageNumber; } } 11 | 12 | protected override void OnGetPacket(SshDataWorker writer) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SshServer.Filesystem.LocalDisk/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using SshServer.Interfaces; 3 | 4 | namespace SshServer.Filesystem.LocalDisk 5 | { 6 | public static class Startup 7 | { 8 | public static IServiceCollection AddLocalFileSystemHosting(this IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | return services; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FxSshSftpServer/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using FxSshSftpServer 10 | @using FxSshSftpServer.Shared 11 | @using Radzen 12 | @using Radzen.Blazor 13 | -------------------------------------------------------------------------------- /SshServer.Interfaces/ISettingsRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SshServer.Interfaces; 4 | 5 | public interface ISettingsRepository 6 | { 7 | void Dispose(); 8 | ServerSettings ServerSettings { get; } 9 | bool UpdateServerSettings(ServerSettings updatedSettings); 10 | User GetUser(string username); 11 | bool RemoveUser(string username); 12 | List GetAllUsers(); 13 | bool AddUser(User newUser); 14 | bool UpdateUser(User updatedUser); 15 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ExitStatusMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | public class ExitStatusMessage : ChannelRequestMessage 5 | { 6 | public uint ExitStatus { get; set; } 7 | 8 | protected override void OnGetPacket(SshDataWorker writer) 9 | { 10 | RequestType = "exit-status"; 11 | WantReply = false; 12 | 13 | base.OnGetPacket(writer); 14 | 15 | writer.Write(ExitStatus); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/SessionOpenMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Connection 5 | { 6 | public class SessionOpenMessage : ChannelOpenMessage 7 | { 8 | protected override void OnLoad(SshDataWorker reader) 9 | { 10 | base.OnLoad(reader); 11 | 12 | if (ChannelType != "session") 13 | throw new ArgumentException(string.Format("Channel type {0} is not valid.", ChannelType)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SshServer.Settings.LiteDb/Startup.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SshServer.Interfaces; 4 | 5 | namespace SshServer.Settings.LiteDb 6 | { 7 | public static class Startup 8 | { 9 | public static IServiceCollection AddSshServerSettingsLiteDb(this IServiceCollection services) 10 | { 11 | 12 | services.AddSingleton(); 13 | return services; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FxSsh/Messages/NewKeysMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | [Message("SSH_MSG_NEWKEYS", MessageNumber)] 4 | public class NewKeysMessage : Message 5 | { 6 | private const byte MessageNumber = 21; 7 | 8 | public override byte MessageType { get { return MessageNumber; } } 9 | 10 | protected override void OnLoad(SshDataWorker reader) 11 | { 12 | } 13 | 14 | protected override void OnGetPacket(SshDataWorker writer) 15 | { 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /FxSsh/Messages/KeyExchangeDhInitMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | [Message("SSH_MSG_KEXDH_INIT", MessageNumber)] 4 | public class KeyExchangeDhInitMessage : Message 5 | { 6 | private const byte MessageNumber = 30; 7 | 8 | public byte[] E { get; private set; } 9 | 10 | public override byte MessageType { get { return MessageNumber; } } 11 | 12 | protected override void OnLoad(SshDataWorker reader) 13 | { 14 | E = reader.ReadMpint(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /FxSsh/Services/PasswordUserauthArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class PasswordUserAuthArgs : UserAuthArgs 6 | { 7 | public PasswordUserAuthArgs(Session session, string username, string password) : base(session, username, password) 8 | { 9 | Contract.Requires(username != null); 10 | Contract.Requires(password != null); 11 | 12 | this.Username = username; 13 | this.Password = password; 14 | } 15 | 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/EnvMessage.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Messages.Connection; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages 5 | { 6 | public class EnvMessage : ChannelRequestMessage 7 | { 8 | public string Name { get; private set; } 9 | public string Value { get; private set; } 10 | 11 | protected override void OnLoad(SshDataWorker reader) 12 | { 13 | base.OnLoad(reader); 14 | 15 | Name = reader.ReadString(Encoding.ASCII); 16 | Value = reader.ReadString(Encoding.ASCII); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ShouldIgnoreMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_IGNORE", MessageNumber)] 5 | public class ShouldIgnoreMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 2; 8 | 9 | public override byte MessageType { get { return MessageNumber; } } 10 | 11 | protected override void OnLoad(SshDataWorker reader) 12 | { 13 | } 14 | 15 | protected override void OnGetPacket(SshDataWorker writer) 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SshServer.Filesystem.LocalDisk/SshServer.Filesystem.LocalDisk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /FxSsh/Messages/ServiceRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages 4 | { 5 | [Message("SSH_MSG_SERVICE_REQUEST", MessageNumber)] 6 | public class ServiceRequestMessage : Message 7 | { 8 | private const byte MessageNumber = 5; 9 | 10 | public string ServiceName { get; private set; } 11 | 12 | public override byte MessageType { get { return MessageNumber; } } 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | ServiceName = reader.ReadString(Encoding.ASCII); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelFailureMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_FAILURE", MessageNumber)] 5 | public class ChannelFailureMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 100; 8 | 9 | public uint RecipientChannel { get; set; } 10 | 11 | public override byte MessageType { get { return MessageNumber; } } 12 | 13 | protected override void OnGetPacket(SshDataWorker writer) 14 | { 15 | writer.Write(RecipientChannel); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelSuccessMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_SUCCESS", MessageNumber)] 5 | public class ChannelSuccessMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 99; 8 | 9 | public uint RecipientChannel { get; set; } 10 | 11 | public override byte MessageType { get { return MessageNumber; } } 12 | 13 | protected override void OnGetPacket(SshDataWorker writer) 14 | { 15 | writer.Write(RecipientChannel); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FxSsh/StartingInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FxSsh 4 | { 5 | public class StartingInfo 6 | { 7 | public const int DefaultPort = 22; 8 | 9 | public StartingInfo() 10 | : this(IPAddress.IPv6Any, DefaultPort) 11 | { 12 | } 13 | 14 | public StartingInfo(IPAddress localAddress, int port) 15 | { 16 | LocalAddress = localAddress; 17 | Port = port; 18 | } 19 | 20 | public IPAddress LocalAddress { get; private set; } 21 | public int Port { get; private set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /FxSsh/Messages/MessageAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace FxSsh.Messages 5 | { 6 | [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] 7 | public sealed class MessageAttribute : Attribute 8 | { 9 | public MessageAttribute(string name, byte number) 10 | { 11 | Contract.Requires(name != null); 12 | 13 | Name = name; 14 | Number = number; 15 | } 16 | 17 | public string Name { get; private set; } 18 | public byte Number { get; private set; } 19 | } 20 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/HmacInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Security.Cryptography; 4 | 5 | namespace FxSsh.Algorithms 6 | { 7 | public class HmacInfo 8 | { 9 | public HmacInfo(KeyedHashAlgorithm algorithm, int keySize) 10 | { 11 | Contract.Requires(algorithm != null); 12 | 13 | KeySize = keySize; 14 | Hmac = key => new HmacAlgorithm(algorithm, keySize, key); 15 | } 16 | 17 | public int KeySize { get; private set; } 18 | 19 | public Func Hmac { get; private set; } 20 | } 21 | } -------------------------------------------------------------------------------- /FxSsh/Services/SessionChannel.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Net.Sockets; 4 | using System.Threading.Tasks; 5 | using FxSsh.Messages.Connection; 6 | 7 | namespace FxSsh.Services 8 | { 9 | public class SessionChannel : Channel 10 | { 11 | public SessionChannel(ConnectionService connectionService, 12 | uint clientChannelId, uint clientInitialWindowSize, uint clientMaxPacketSize, 13 | uint serverChannelId) 14 | : base(connectionService, clientChannelId, clientInitialWindowSize, clientMaxPacketSize, serverChannelId) 15 | { 16 | 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SshServer.Interfaces/User.cs: -------------------------------------------------------------------------------- 1 | namespace SshServer.Interfaces; 2 | 3 | 4 | 5 | /// 6 | /// Either provide Password or Key for login 7 | /// 8 | /// 9 | public class User 10 | { 11 | public int Id { get; set; } 12 | public string Username { get; set; } 13 | public string UserRootDirectory { get; set; } 14 | public string HashedPassword { get; set; } 15 | public string RsaPublicKey { get; set; } 16 | public bool OnlyWhitelistedIps { get; set; } 17 | public List WhitelistedIps { get; set; } 18 | public DateTime LastSuccessfulLogin { get; set; } 19 | 20 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/KexAlgorithmContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | [ContractClassFor(typeof(KexAlgorithm))] 7 | internal abstract class KexAlgorithmContract : KexAlgorithm 8 | { 9 | public override byte[] CreateKeyExchange() 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | public override byte[] DecryptKeyExchange(byte[] exchangeData) 15 | { 16 | Contract.Requires(exchangeData != null); 17 | 18 | throw new NotImplementedException(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /FxSsh/Services/SshService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Net; 3 | 4 | namespace FxSsh.Services 5 | { 6 | public abstract class SshService 7 | { 8 | protected internal readonly Session _session; 9 | 10 | public EndPoint LocalEndpoint => _session.LocalEndpoint; 11 | public EndPoint RemoteEndpoint => _session.RemoteEndpoint; 12 | 13 | public SshService(Session session) 14 | { 15 | Contract.Requires(session != null); 16 | 17 | _session = session; 18 | } 19 | 20 | internal protected abstract void CloseService(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SshServer.Settings.LiteDb/SshServer.Settings.LiteDb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /FxSsh/Messages/UnimplementedMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | [Message("SSH_MSG_UNIMPLEMENTED", MessageNumber)] 4 | public class UnimplementedMessage : Message 5 | { 6 | private const byte MessageNumber = 3; 7 | 8 | public uint SequenceNumber { get; set; } 9 | 10 | public byte UnimplementedMessageType { get; set; } 11 | 12 | public override byte MessageType { get { return MessageNumber; } } 13 | 14 | protected override void OnGetPacket(SshDataWorker writer) 15 | { 16 | writer.Write(MessageNumber); 17 | writer.Write(SequenceNumber); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/FailureMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Userauth 5 | { 6 | [Message("SSH_MSG_USERAUTH_FAILURE", MessageNumber)] 7 | public class FailureMessage : UserauthServiceMessage 8 | { 9 | private const byte MessageNumber = 51; 10 | 11 | public override byte MessageType { get { return MessageNumber; } } 12 | 13 | protected override void OnGetPacket(SshDataWorker writer) 14 | { 15 | writer.Write("publickey,password", Encoding.ASCII); // only accept public key and password 16 | writer.Write(false); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/KexAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Security.Cryptography; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | [ContractClass(typeof(KexAlgorithmContract))] 7 | public abstract class KexAlgorithm 8 | { 9 | protected HashAlgorithm _hashAlgorithm; 10 | 11 | public abstract byte[] CreateKeyExchange(); 12 | 13 | public abstract byte[] DecryptKeyExchange(byte[] exchangeData); 14 | 15 | public byte[] ComputeHash(byte[] input) 16 | { 17 | Contract.Requires(input != null); 18 | 19 | return _hashAlgorithm.ComputeHash(input); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/CompressionAlgorithmContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | [ContractClassFor(typeof(CompressionAlgorithm))] 7 | internal abstract class CompressionAlgorithmContract : CompressionAlgorithm 8 | { 9 | public override byte[] Compress(byte[] input) 10 | { 11 | Contract.Requires(input != null); 12 | 13 | throw new NotImplementedException(); 14 | } 15 | 16 | public override byte[] Decompress(byte[] input) 17 | { 18 | Contract.Requires(input != null); 19 | 20 | throw new NotImplementedException(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /FxSsh/Messages/ServiceAcceptMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages 4 | { 5 | [Message("SSH_MSG_SERVICE_ACCEPT", MessageNumber)] 6 | public class ServiceAcceptMessage : Message 7 | { 8 | private const byte MessageNumber = 6; 9 | 10 | public ServiceAcceptMessage(string name) 11 | { 12 | ServiceName = name; 13 | } 14 | 15 | public string ServiceName { get; private set; } 16 | 17 | public override byte MessageType { get { return MessageNumber; } } 18 | 19 | protected override void OnGetPacket(SshDataWorker writer) 20 | { 21 | writer.Write(ServiceName, Encoding.ASCII); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/PasswordRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace FxSsh.Messages.Userauth 6 | { 7 | public class PasswordRequestMessage : RequestMessage 8 | { 9 | public string Password { get; private set; } 10 | 11 | protected override void OnLoad(SshDataWorker reader) 12 | { 13 | base.OnLoad(reader); 14 | 15 | if (MethodName != "password") 16 | throw new ArgumentException(string.Format("Method name {0} is not valid.", MethodName)); 17 | 18 | var isFalse = reader.ReadBoolean(); 19 | Password = reader.ReadString(Encoding.ASCII); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FxSsh/Messages/KeyExchangeDhReplyMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FxSsh.Messages 2 | { 3 | [Message("SSH_MSG_KEXDH_REPLY", MessageNumber)] 4 | public class KeyExchangeDhReplyMessage : Message 5 | { 6 | private const byte MessageNumber = 31; 7 | 8 | public byte[] HostKey { get; set; } 9 | public byte[] F { get; set; } 10 | public byte[] Signature { get; set; } 11 | 12 | public override byte MessageType { get { return MessageNumber; } } 13 | 14 | protected override void OnGetPacket(SshDataWorker writer) 15 | { 16 | writer.WriteBinary(HostKey); 17 | writer.WriteMpint(F); 18 | writer.WriteBinary(Signature); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /FxSsh/SshConnectionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FxSsh 4 | { 5 | public class SshConnectionException : Exception 6 | { 7 | public SshConnectionException() 8 | { 9 | } 10 | 11 | public SshConnectionException(string message, DisconnectReason disconnectReason = DisconnectReason.None) 12 | : base(message) 13 | { 14 | DisconnectReason = disconnectReason; 15 | } 16 | 17 | public DisconnectReason DisconnectReason { get; private set; } 18 | 19 | public override string ToString() 20 | { 21 | return string.Format("SSH connection disconnected bacause {0}", DisconnectReason.ToString()); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelEofMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_EOF", MessageNumber)] 5 | public class ChannelEofMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 96; 8 | 9 | public uint RecipientChannel { get; set; } 10 | 11 | public override byte MessageType { get { return MessageNumber; } } 12 | 13 | protected override void OnLoad(SshDataWorker reader) 14 | { 15 | RecipientChannel = reader.ReadUInt32(); 16 | } 17 | 18 | protected override void OnGetPacket(SshDataWorker writer) 19 | { 20 | writer.Write(RecipientChannel); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/PublicKeyOkMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Userauth 5 | { 6 | [Message("SSH_MSG_USERAUTH_PK_OK", MessageNumber)] 7 | public class PublicKeyOkMessage : UserauthServiceMessage 8 | { 9 | private const byte MessageNumber = 60; 10 | 11 | public string KeyAlgorithmName { get; set; } 12 | public byte[] PublicKey { get; set; } 13 | 14 | public override byte MessageType { get { return MessageNumber; } } 15 | 16 | protected override void OnGetPacket(SshDataWorker writer) 17 | { 18 | writer.Write(KeyAlgorithmName, Encoding.ASCII); 19 | writer.WriteBinary(PublicKey); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelCloseMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_CLOSE", MessageNumber)] 5 | public class ChannelCloseMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 97; 8 | 9 | public uint RecipientChannel { get; set; } 10 | 11 | public override byte MessageType { get { return MessageNumber; } } 12 | 13 | protected override void OnLoad(SshDataWorker reader) 14 | { 15 | RecipientChannel = reader.ReadUInt32(); 16 | } 17 | 18 | protected override void OnGetPacket(SshDataWorker writer) 19 | { 20 | writer.Write(RecipientChannel); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /FxSsh/Services/PKUserauthArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class PKUserAuthArgs : UserAuthArgs 6 | { 7 | public PKUserAuthArgs(Session session, string username, string keyAlgorithm, string fingerprint, byte[] key) : base(session, username, keyAlgorithm, fingerprint, key) 8 | { 9 | Contract.Requires(username != null); 10 | Contract.Requires(keyAlgorithm != null); 11 | Contract.Requires(fingerprint != null); 12 | Contract.Requires(key != null); 13 | 14 | KeyAlgorithm = keyAlgorithm; 15 | Fingerprint = fingerprint; 16 | Key = key; 17 | Username = username; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Components/ToolTip.razor.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Logging; 6 | using Radzen; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace FxSshSftpServer.Components 14 | { 15 | // https://chrissainty.com/building-a-simple-tooltip-component-for-blazor-in-under-10-lines-of-code/ 16 | public partial class ToolTip : ComponentBase 17 | { 18 | 19 | [Parameter] public RenderFragment ChildContent { get; set; } 20 | [Parameter] public string Text { get; set; } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/SubsystemRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Messages.Connection; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Connection 5 | { 6 | public class SubsystemRequestMessage : ChannelRequestMessage 7 | { 8 | public string SubsystemName { get; private set; } 9 | 10 | protected override void OnLoad(SshDataWorker reader) 11 | { 12 | /* 13 | byte SSH_MSG_CHANNEL_REQUEST 14 | uint32 recipient channel 15 | string "subsystem" 16 | boolean want reply 17 | string subsystem name 18 | */ 19 | base.OnLoad(reader); 20 | 21 | SubsystemName = reader.ReadString(Encoding.ASCII); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FxSsh/DisconnectReason.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace FxSsh 4 | { 5 | public enum DisconnectReason 6 | { 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | None = 0, // Not used by protocol 9 | HostNotAllowedToConnect = 1, 10 | ProtocolError = 2, 11 | KeyExchangeFailed = 3, 12 | Reserved = 4, 13 | MacError = 5, 14 | CompressionError = 6, 15 | ServiceNotAvailable = 7, 16 | ProtocolVersionNotSupported = 8, 17 | HostKeyNotVerifiable = 9, 18 | ConnectionLost = 10, 19 | ByApplication = 11, 20 | TooManyConnections = 12, 21 | AuthCancelledByUser = 13, 22 | NoMoreAuthMethodsAvailable = 14, 23 | IllegalUserName = 15 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FxSshSftpServer/Components/TwoColumnRow.razor.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Logging; 6 | using Radzen; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace FxSshSftpServer.Components 14 | { 15 | 16 | public partial class TwoColumnRow : ComponentBase 17 | { 18 | 19 | 20 | 21 | [Parameter] 22 | public RenderFragment ChildContent { get; set; } 23 | 24 | [Parameter] 25 | public string TooltipText { get; set; } 26 | 27 | [Parameter] 28 | public string HeaderText { get; set; } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /FxSsh/IPA-DN-Fork-SSHServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | IPA.DN.Fork.SSHServer 6 | 7.2 7 | 8 | 9 | 10 | 7.2 11 | 12 | 13 | 14 | false 15 | 7.2 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /FxSshSftpServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:8252", 7 | "sslPort": 44311 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "FxSshSftpServer": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SshServer.Interfaces/ServerSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SshServer.Interfaces 6 | { 7 | public class ServerSettings 8 | { 9 | public int Id{ get; set; } 10 | public string ServerRsaKey { get; set; } 11 | public string ServerBanner { get; set; } 12 | 13 | public int ListenToPort { get; set; } 14 | public string ServerRootDirectory { get; set; } 15 | public List BindToAddress { get; set; } 16 | public List BlacklistedIps { get; set; } 17 | public int MaxLoginAttemptsBeforeBan { get; set; } 18 | 19 | public int IdleTimeout { get; set; } 20 | public bool EnableCommand { get; set; } 21 | public bool EnableDirectTcpIp { get; set; } 22 | 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FxSshSftpServer/Components/ToolTip.Razor.css: -------------------------------------------------------------------------------- 1 | .tooltip-wrapper { 2 | position: relative; 3 | display: inline-block; 4 | border-bottom: 1px dotted black; 5 | cursor: help; 6 | } 7 | 8 | span { 9 | visibility: hidden; 10 | position: absolute; 11 | width: 120px; 12 | bottom: 100%; 13 | left: 50%; 14 | margin-left: -60px; 15 | background-color: #363636; 16 | color: #fff; 17 | text-align: center; 18 | padding: 5px 0; 19 | border-radius: 6px; 20 | z-index: 1; 21 | } 22 | 23 | span::after { 24 | content: ""; 25 | position: absolute; 26 | top: 100%; 27 | left: 50%; 28 | margin-left: -5px; 29 | border-width: 5px; 30 | border-style: solid; 31 | border-color: #555 transparent transparent transparent; 32 | } 33 | 34 | .tooltip-wrapper:hover span { 35 | visibility: visible; 36 | } -------------------------------------------------------------------------------- /FxSshSftpServer/FxSshSftpServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelDataMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_DATA", MessageNumber)] 5 | public class ChannelDataMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 94; 8 | 9 | public uint RecipientChannel { get; set; } 10 | public byte[] Data { get; set; } 11 | 12 | public override byte MessageType { get { return MessageNumber; } } 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | RecipientChannel = reader.ReadUInt32(); 17 | Data = reader.ReadBinary(); 18 | } 19 | 20 | protected override void OnGetPacket(SshDataWorker writer) 21 | { 22 | writer.Write(RecipientChannel); 23 | writer.WriteBinary(Data); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/RequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Userauth 5 | { 6 | [Message("SSH_MSG_USERAUTH_REQUEST", MessageNumber)] 7 | public class RequestMessage : UserauthServiceMessage 8 | { 9 | protected const byte MessageNumber = 50; 10 | 11 | public string Username { get; protected set; } 12 | public string ServiceName { get; protected set; } 13 | public string MethodName { get; protected set; } 14 | 15 | public override byte MessageType { get { return MessageNumber; } } 16 | 17 | protected override void OnLoad(SshDataWorker reader) 18 | { 19 | Username = reader.ReadString(Encoding.UTF8); 20 | ServiceName = reader.ReadString(Encoding.ASCII); 21 | MethodName = reader.ReadString(Encoding.ASCII); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SshServer.Filesystem.LocalDisk/LocalFileSystemFactory.cs: -------------------------------------------------------------------------------- 1 | using SshServer.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SshServer.Filesystem.LocalDisk 9 | { 10 | public class LocalFileSystemFactory : IFileSystemFactory 11 | { 12 | private ISettingsRepository _settingsRepository; 13 | 14 | public LocalFileSystemFactory(ISettingsRepository settingsRepository) 15 | { 16 | _settingsRepository = settingsRepository; 17 | 18 | } 19 | public IFileSystem GetFileSystem(string username) 20 | { 21 | var user = _settingsRepository.GetUser(username); 22 | 23 | LocalFileSystem fs = new LocalFileSystem(user.UserRootDirectory); 24 | return fs; 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/PTYRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages.Connection 4 | { 5 | public class PtyRequestMessage : ChannelRequestMessage 6 | { 7 | public string Terminal = ""; 8 | public uint widthChars = 0; 9 | public uint heightRows = 0; 10 | public uint widthPx = 0; 11 | public uint heightPx = 0; 12 | public string modes = ""; 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | base.OnLoad(reader); 17 | 18 | Terminal = reader.ReadString(Encoding.ASCII); 19 | widthChars = reader.ReadUInt32(); 20 | heightRows = reader.ReadUInt32(); 21 | widthPx = reader.ReadUInt32(); 22 | heightPx = reader.ReadUInt32(); 23 | modes = reader.ReadString(Encoding.ASCII); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ServerConsoleApp/ServerConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/DiffieHellmanGroupSha1.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Security.Cryptography; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class DiffieHellmanGroupSha1 : KexAlgorithm 7 | { 8 | private readonly DiffieHellman _exchangeAlgorithm; 9 | 10 | public DiffieHellmanGroupSha1(DiffieHellman algorithm) 11 | { 12 | Contract.Requires(algorithm != null); 13 | 14 | _exchangeAlgorithm = algorithm; 15 | _hashAlgorithm = new SHA1CryptoServiceProvider(); 16 | } 17 | 18 | public override byte[] CreateKeyExchange() 19 | { 20 | return _exchangeAlgorithm.CreateKeyExchange(); 21 | } 22 | 23 | public override byte[] DecryptKeyExchange(byte[] exchangeData) 24 | { 25 | return _exchangeAlgorithm.DecryptKeyExchange(exchangeData); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelWindowAdjustMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_WINDOW_ADJUST", MessageNumber)] 5 | public class ChannelWindowAdjustMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 93; 8 | 9 | public uint RecipientChannel { get; set; } 10 | public uint BytesToAdd { get; set; } 11 | 12 | public override byte MessageType { get { return MessageNumber; } } 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | RecipientChannel = reader.ReadUInt32(); 17 | BytesToAdd = reader.ReadUInt32(); 18 | } 19 | 20 | protected override void OnGetPacket(SshDataWorker writer) 21 | { 22 | writer.Write(RecipientChannel); 23 | writer.Write(BytesToAdd); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/DiffieHellmanGroupSha256.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Security.Cryptography; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class DiffieHellmanGroupSha256 : KexAlgorithm 7 | { 8 | private readonly DiffieHellman _exchangeAlgorithm; 9 | 10 | public DiffieHellmanGroupSha256(DiffieHellman algorithm) 11 | { 12 | Contract.Requires(algorithm != null); 13 | 14 | _exchangeAlgorithm = algorithm; 15 | _hashAlgorithm = new SHA256CryptoServiceProvider(); 16 | } 17 | 18 | public override byte[] CreateKeyExchange() 19 | { 20 | return _exchangeAlgorithm.CreateKeyExchange(); 21 | } 22 | 23 | public override byte[] DecryptKeyExchange(byte[] exchangeData) 24 | { 25 | return _exchangeAlgorithm.DecryptKeyExchange(exchangeData); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /SshServerModule/SshServerModule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FxSsh/Services/EnvironmentArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class EnvironmentArgs 6 | { 7 | public EnvironmentArgs(SessionChannel channel, string name, string value, UserAuthArgs userauthArgs) 8 | { 9 | Contract.Requires(channel != null); 10 | Contract.Requires(name != null); 11 | Contract.Requires(value != null); 12 | Contract.Requires(userauthArgs != null); 13 | 14 | Channel = channel; 15 | Name = name; 16 | Value = value; 17 | AttachedUserAuthArgs = userauthArgs; 18 | } 19 | 20 | public SessionChannel Channel { get; private set; } 21 | public string Name { get; private set; } 22 | public string Value { get; private set; } 23 | public UserAuthArgs AttachedUserAuthArgs { get; private set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelOpenConfirmationMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace FxSsh.Messages.Connection 3 | { 4 | [Message("SSH_MSG_CHANNEL_OPEN_CONFIRMATION", MessageNumber)] 5 | public class ChannelOpenConfirmationMessage : ConnectionServiceMessage 6 | { 7 | private const byte MessageNumber = 91; 8 | 9 | public uint RecipientChannel { get; set; } 10 | public uint SenderChannel { get; set; } 11 | public uint InitialWindowSize { get; set; } 12 | public uint MaximumPacketSize { get; set; } 13 | 14 | public override byte MessageType { get { return MessageNumber; } } 15 | 16 | protected override void OnGetPacket(SshDataWorker writer) 17 | { 18 | writer.Write(RecipientChannel); 19 | writer.Write(SenderChannel); 20 | writer.Write(InitialWindowSize); 21 | writer.Write(MaximumPacketSize); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FxSsh/Services/CommandRequestedArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class CommandRequestedArgs 6 | { 7 | public CommandRequestedArgs(SessionChannel channel, string type, string command, UserAuthArgs userauthArgs) 8 | { 9 | Contract.Requires(channel != null); 10 | Contract.Requires(command != null); 11 | Contract.Requires(userauthArgs != null); 12 | 13 | Channel = channel; 14 | CommandText = command; 15 | AttachedUserAuthArgs = userauthArgs; 16 | SubSystemName = type; 17 | } 18 | 19 | public SessionChannel Channel { get; private set; } 20 | public string CommandText { get; private set; } 21 | public string SubSystemName { get; private set; } 22 | public UserAuthArgs AttachedUserAuthArgs { get; private set; } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/HmacAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Security.Cryptography; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class HmacAlgorithm 7 | { 8 | private readonly KeyedHashAlgorithm _algorithm; 9 | 10 | public HmacAlgorithm(KeyedHashAlgorithm algorithm, int keySize, byte[] key) 11 | { 12 | Contract.Requires(algorithm != null); 13 | Contract.Requires(key != null); 14 | Contract.Requires(keySize == key.Length << 3); 15 | 16 | _algorithm = algorithm; 17 | algorithm.Key = key; 18 | } 19 | 20 | public int DigestLength 21 | { 22 | get { return _algorithm.HashSize >> 3; } 23 | } 24 | 25 | public byte[] ComputeHash(byte[] input) 26 | { 27 | Contract.Requires(input != null); 28 | 29 | return _algorithm.ComputeHash(input); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelOpenFailureMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages.Connection 4 | { 5 | [Message("SSH_MSG_CHANNEL_OPEN_FAILURE", MessageNumber)] 6 | public class ChannelOpenFailureMessage : ConnectionServiceMessage 7 | { 8 | private const byte MessageNumber = 92; 9 | 10 | public uint RecipientChannel { get; set; } 11 | public ChannelOpenFailureReason ReasonCode { get; set; } 12 | public string Description { get; set; } 13 | public string Language { get; set; } 14 | 15 | public override byte MessageType { get { return MessageNumber; } } 16 | 17 | protected override void OnGetPacket(SshDataWorker writer) 18 | { 19 | writer.Write(RecipientChannel); 20 | writer.Write((uint)ReasonCode); 21 | writer.Write(Description, Encoding.ASCII); 22 | writer.Write(Language ?? "en", Encoding.ASCII); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelOpenMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages.Connection 5 | { 6 | [Message("SSH_MSG_CHANNEL_OPEN", MessageNumber)] 7 | public class ChannelOpenMessage : ConnectionServiceMessage 8 | { 9 | private const byte MessageNumber = 90; 10 | 11 | public string ChannelType { get; private set; } 12 | public uint SenderChannel { get; private set; } 13 | public uint InitialWindowSize { get; private set; } 14 | public uint MaximumPacketSize { get; private set; } 15 | 16 | public override byte MessageType { get { return MessageNumber; } } 17 | 18 | protected override void OnLoad(SshDataWorker reader) 19 | { 20 | ChannelType = reader.ReadString(Encoding.ASCII); 21 | SenderChannel = reader.ReadUInt32(); 22 | InitialWindowSize = reader.ReadUInt32(); 23 | MaximumPacketSize = reader.ReadUInt32(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace FxSshSftpServer.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | [IgnoreAntiforgeryToken] 14 | public class ErrorModel : PageModel 15 | { 16 | public string RequestId { get; set; } 17 | 18 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 19 | 20 | private readonly ILogger _logger; 21 | 22 | public ErrorModel(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | public void OnGet() 28 | { 29 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FxSsh/FxSsh.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | true 6 | Aimeast, Lirunki 7 | 8 8 | https://github.com/Lirunki/FxSsh/blob/master/LICENSE.md 9 | https://github.com/Lirunki/FxSsh 10 | ssh,ssh-reinforcement,ssh-server,csharp,dotnet-core 11 | FxSsh is a lightweight SSH server side application 12 | This version is a mod on FxSsh that allows password authentication and is prepared for the sftp protocol. 13 | All credit for this work goes to Aimeast. 14 | FxSsh.PwAuth 15 | Aimeast, Lirunki 16 | 1.0.2 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ForwardedTcpIpMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text; 4 | 5 | namespace FxSsh.Messages.Connection 6 | { 7 | public class ForwardedTcpIpMessage : ChannelOpenMessage 8 | { 9 | public string Address { get; private set; } 10 | public uint Port { get; private set; } 11 | public string OriginatorIPAddress { get; private set; } 12 | public uint OriginatorPort { get; private set; } 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | base.OnLoad(reader); 17 | 18 | if (ChannelType != "forwarded-tcpip") 19 | throw new ArgumentException(string.Format("Channel type {0} is not valid.", ChannelType)); 20 | 21 | Address = reader.ReadString(Encoding.ASCII); 22 | Port = reader.ReadUInt32(); 23 | OriginatorIPAddress = reader.ReadString(Encoding.ASCII); 24 | OriginatorPort = reader.ReadUInt32(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/CipherInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | 6 | namespace FxSsh.Algorithms 7 | { 8 | public class CipherInfo 9 | { 10 | public CipherInfo(SymmetricAlgorithm algorithm, int keySize, CipherModeEx mode) 11 | { 12 | Contract.Requires(algorithm != null); 13 | Contract.Requires(algorithm.LegalKeySizes.Any(x => 14 | x.MinSize <= keySize && keySize <= x.MaxSize && keySize % x.SkipSize == 0)); 15 | 16 | algorithm.KeySize = keySize; 17 | KeySize = algorithm.KeySize; 18 | BlockSize = algorithm.BlockSize; 19 | Cipher = (key, vi, isEncryption) => new EncryptionAlgorithm(algorithm, keySize, mode, key, vi, isEncryption); 20 | } 21 | 22 | public int KeySize { get; private set; } 23 | 24 | public int BlockSize { get; private set; } 25 | 26 | public Func Cipher { get; private set; } 27 | } 28 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Components/FileDownload.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.JSInterop; 7 | using Radzen; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Diagnostics; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace FxSshSftpServer.Components 15 | { 16 | 17 | public static class FileDownload 18 | { 19 | 20 | public static async Task SaveFile(this IJSRuntime JSRuntime, byte[] file, string fileName) 21 | { 22 | string contentType = "application/octet-stream"; 23 | 24 | // Check if the IJSRuntime is the WebAssembly implementation of the JSRuntime 25 | 26 | // Fall back to the slow method if not in WebAssembly 27 | await JSRuntime.InvokeVoidAsync("BlazorDownloadFile", fileName, contentType, file); 28 | 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/ChannelRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FxSsh.Messages.Connection 4 | { 5 | [Message("SSH_MSG_CHANNEL_REQUEST", MessageNumber)] 6 | public class ChannelRequestMessage : ConnectionServiceMessage 7 | { 8 | private const byte MessageNumber = 98; 9 | 10 | public uint RecipientChannel { get; set; } 11 | public string RequestType { get; set; } 12 | public bool WantReply { get; set; } 13 | 14 | public override byte MessageType { get { return MessageNumber; } } 15 | 16 | protected override void OnLoad(SshDataWorker reader) 17 | { 18 | RecipientChannel = reader.ReadUInt32(); 19 | RequestType = reader.ReadString(Encoding.ASCII); 20 | WantReply = reader.ReadBoolean(); 21 | } 22 | 23 | protected override void OnGetPacket(SshDataWorker writer) 24 | { 25 | writer.Write(RecipientChannel); 26 | writer.Write(RequestType, Encoding.ASCII); 27 | writer.Write(WantReply); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/General.razor: -------------------------------------------------------------------------------- 1 | @using Radzen.Blazor 2 | @using FxSshSftpServer.Components 3 | 4 | @page "/general" 5 | 6 | 7 |

Server settings

8 | 9 | @if (ServerSettings != null) 10 | { 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | OnSave()) Text="Save" Style="margin-bottom: 20px; width: 150px" /> 25 | 26 | @if (Savesuccess) 27 | { 28 | Settings were saved! 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /FxSsh/Services/TcpRequestArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class TcpRequestArgs 6 | { 7 | public TcpRequestArgs(SessionChannel channel, string host, int port, string originatorIP, int originatorPort, UserAuthArgs userauthArgs) 8 | { 9 | Contract.Requires(channel != null); 10 | Contract.Requires(host != null); 11 | Contract.Requires(originatorIP != null); 12 | 13 | Channel = channel; 14 | Host = host; 15 | Port = port; 16 | OriginatorIP = originatorIP; 17 | OriginatorPort = originatorPort; 18 | AttachedUserAuthArgs = userauthArgs; 19 | } 20 | 21 | public SessionChannel Channel { get; private set; } 22 | public string Host { get; private set; } 23 | public int Port { get; private set; } 24 | public string OriginatorIP { get; private set; } 25 | public int OriginatorPort { get; private set; } 26 | public UserAuthArgs AttachedUserAuthArgs { get; private set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aimeast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SshServer.Interfaces/IFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SshServer.Interfaces; 9 | 10 | public interface IFileSystem 11 | { 12 | bool RemoveDirectory(string pathToRemove); 13 | bool MakeDirectory(string newPath); 14 | bool DirectoryExists(string path); 15 | Stream OpenStreamRead(string path); 16 | Stream OpenStreamWrite(string path); 17 | IEnumerable GetFiles(string path); 18 | IEnumerable GetDirectories(string path); 19 | bool RemoveFile(string path); 20 | ulong GetSize(string path); 21 | DateTime GetDirectoryLastModified(string path); 22 | DateTime GetDirectoryLastAccessed(string path); 23 | DateTime GetFileLastModified(string path); 24 | DateTime GetFileLastAccessed(string path); 25 | bool MoveFileOrDirectory(string oldpath, string newpath); 26 | } 27 | 28 | public record struct Resource(string Name, string FullName, ResourceType Type, ulong Length); 29 | 30 | public enum ResourceType 31 | { 32 | File = 1, 33 | Folder = 2 34 | } 35 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /FxSsh/Services/SessionRequestedArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class SessionRequestedArgs 6 | { 7 | public SessionRequestedArgs(SessionChannel channel, string command, UserAuthArgs userauthArgs) 8 | : this(channel, null, command, userauthArgs) 9 | { 10 | } 11 | 12 | public SessionRequestedArgs(SessionChannel channel, string subsystem, string command, UserAuthArgs userauthArgs) 13 | { 14 | Contract.Requires(channel != null); 15 | Contract.Requires(command != null); 16 | Contract.Requires(userauthArgs != null); 17 | 18 | Channel = channel; 19 | CommandText = command; 20 | AttachedUserAuthArgs = userauthArgs; 21 | SubSystemName = subsystem; 22 | } 23 | public System.Net.EndPoint remoteEndpoint { get; private set; } 24 | public SessionChannel Channel { get; private set; } 25 | public string CommandText { get; private set; } 26 | public string SubSystemName { get; private set; } 27 | public UserAuthArgs AttachedUserAuthArgs { get; private set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FxSsh/Services/KeyExchangeArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Text; 5 | 6 | namespace FxSsh.Services 7 | { 8 | public class KeyExchangeArgs 9 | { 10 | public KeyExchangeArgs(Session s) 11 | { 12 | this.Session = s; 13 | } 14 | 15 | public Session Session { get; private set; } 16 | 17 | public byte[] Cookie { get; private set; } 18 | 19 | public string[] KeyExchangeAlgorithms { get; set; } 20 | 21 | public string[] ServerHostKeyAlgorithms { get; set; } 22 | 23 | public string[] EncryptionAlgorithmsClientToServer { get; set; } 24 | 25 | public string[] EncryptionAlgorithmsServerToClient { get; set; } 26 | 27 | public string[] MacAlgorithmsClientToServer { get; set; } 28 | 29 | public string[] MacAlgorithmsServerToClient { get; set; } 30 | 31 | public string[] CompressionAlgorithmsClientToServer { get; set; } 32 | 33 | public string[] CompressionAlgorithmsServerToClient { get; set; } 34 | 35 | public string[] LanguagesClientToServer { get; set; } 36 | 37 | public string[] LanguagesServerToClient { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/FxSshSftpServer/bin/Debug/net7.0/FxSshSftpServer.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/FxSshSftpServer", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /FxSsh/Messages/Userauth/PublicKeyRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace FxSsh.Messages.Userauth 6 | { 7 | public class PublicKeyRequestMessage : RequestMessage 8 | { 9 | public bool HasSignature { get; private set; } 10 | public string KeyAlgorithmName { get; private set; } 11 | public byte[] PublicKey { get; private set; } 12 | public byte[] Signature { get; private set; } 13 | 14 | public byte[] PayloadWithoutSignature { get; private set; } 15 | 16 | protected override void OnLoad(SshDataWorker reader) 17 | { 18 | base.OnLoad(reader); 19 | 20 | if (MethodName != "publickey") 21 | throw new ArgumentException(string.Format("Method name {0} is not valid.", MethodName)); 22 | 23 | HasSignature = reader.ReadBoolean(); 24 | KeyAlgorithmName = reader.ReadString(Encoding.ASCII); 25 | PublicKey = reader.ReadBinary(); 26 | 27 | if (HasSignature) 28 | { 29 | Signature = reader.ReadBinary(); 30 | PayloadWithoutSignature = RawBytes.Take(RawBytes.Length - Signature.Length - 5).ToArray(); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/General.razor.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using Microsoft.Extensions.Logging; 6 | using SshServer.Interfaces; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace FxSshSftpServer.Pages 14 | { 15 | 16 | public partial class General : ComponentBase 17 | { 18 | [Inject] 19 | public ISettingsRepository settingsRepository { get; set; } 20 | public ServerSettings ServerSettings { get; private set; } 21 | public bool Savesuccess { get; private set; } 22 | 23 | protected override void OnAfterRender(bool firstRender) 24 | { 25 | if (firstRender) 26 | { 27 | ServerSettings = settingsRepository.ServerSettings; 28 | _ = InvokeAsync(StateHasChanged); 29 | } 30 | } 31 | 32 | 33 | private void OnSave() 34 | { 35 | Savesuccess = settingsRepository.UpdateServerSettings(ServerSettings); 36 | 37 | 38 | 39 | } 40 | 41 | 42 | 43 | } 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /FxSsh/Services/PtyArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | public class PtyArgs 6 | { 7 | public PtyArgs(SessionChannel channel, string terminal, uint heightPx, uint heightRows, uint widthPx, uint widthChars, string modes, UserAuthArgs userauthArgs) 8 | { 9 | Contract.Requires(channel != null); 10 | Contract.Requires(terminal != null); 11 | Contract.Requires(modes != null); 12 | Contract.Requires(userauthArgs != null); 13 | 14 | Channel = channel; 15 | Terminal = terminal; 16 | HeightPx = heightPx; 17 | HeightRows = heightRows; 18 | WidthPx = widthPx; 19 | WidthChars = widthChars; 20 | Modes = modes; 21 | 22 | AttachedUserAuthArgs = userauthArgs; 23 | } 24 | 25 | public SessionChannel Channel { get; private set; } 26 | public string Terminal { get; private set; } 27 | public uint HeightPx { get; private set; } 28 | public uint HeightRows { get; private set; } 29 | public uint WidthPx { get; private set; } 30 | public uint WidthChars { get; private set; } 31 | public string Modes { get; private set; } 32 | public UserAuthArgs AttachedUserAuthArgs { get; private set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 27 |
28 | 29 | @code { 30 | private bool collapseNavMenu = true; 31 | 32 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 33 | 34 | private void ToggleNavMenu() 35 | { 36 | collapseNavMenu = !collapseNavMenu; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/FxSshSftpServer/FxSshSftpServer.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/FxSshSftpServer/FxSshSftpServer.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/FxSshSftpServer/FxSshSftpServer.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /ServerConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using Serilog.Events; 6 | using SshServer.Filesystem.LocalDisk; 7 | using SshServer.Interfaces; 8 | using System.Threading.Tasks; 9 | 10 | Log.Logger = new LoggerConfiguration() 11 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 12 | .MinimumLevel.Override("System", LogEventLevel.Warning) 13 | .Enrich.FromLogContext() 14 | .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) 15 | //.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() 16 | //.WithDefaultDestructurers() 17 | //.WithDestructurers(new[] { new DbUpdateExceptionDestructurer() })) 18 | .CreateLogger(); 19 | 20 | 21 | await new HostBuilder() 22 | .ConfigureServices((hostContext, services) => 23 | { 24 | services.AddLogging(); 25 | Log.Information("Starting host service"); 26 | services.AddLogging(configure => 27 | { 28 | configure.AddSerilog(Log.Logger); 29 | }); 30 | services.AddSingleton(); 31 | services.AddSingleton(); 32 | services.AddHostedService(); 33 | 34 | 35 | }) 36 | .RunConsoleAsync(); 37 | -------------------------------------------------------------------------------- /FxSsh/Messages/Connection/DirectTcpIpMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text; 4 | 5 | namespace FxSsh.Messages.Connection 6 | { 7 | public class DirectTcpIpMessage : ChannelOpenMessage 8 | { 9 | public string Host { get; private set; } 10 | public uint Port { get; private set; } 11 | public string OriginatorIPAddress { get; private set; } 12 | public uint OriginatorPort { get; private set; } 13 | 14 | protected override void OnLoad(SshDataWorker reader) 15 | { 16 | base.OnLoad(reader); 17 | 18 | if (ChannelType != "direct-tcpip") 19 | throw new ArgumentException(string.Format("Channel type {0} is not valid.", ChannelType)); 20 | 21 | Host = reader.ReadString(Encoding.ASCII); 22 | Port = reader.ReadUInt32(); 23 | OriginatorIPAddress = reader.ReadString(Encoding.ASCII); 24 | OriginatorPort = reader.ReadUInt32(); 25 | /* 26 | byte SSH_MSG_CHANNEL_OPEN 27 | string "direct-tcpip" 28 | uint32 sender channel 29 | uint32 initial window size 30 | uint32 maximum packet size 31 | string host to connect 32 | uint32 port to connect 33 | string originator IP address 34 | uint32 originator port 35 | */ 36 | 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace FxSshSftpServer.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | FxSshSftpServer 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | An error has occurred. This application may no longer respond until reloaded. 26 | 27 | 28 | An unhandled exception has occurred. See browser dev tools for details. 29 | 30 | Reload 31 | 🗙 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /FxSshSftpServer/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /TestClient/Program.cs: -------------------------------------------------------------------------------- 1 | using Renci.SshNet; 2 | using System; 3 | using System.Net; 4 | 5 | namespace TestClient 6 | { 7 | internal class Program 8 | { 9 | private static void Main(string[] args) 10 | { 11 | var testUsername = "test_person"; 12 | var testPassword = "1234"; 13 | 14 | 15 | var connInfo = new ConnectionInfo(IPAddress.Loopback.ToString(), 22, testUsername, 16 | new AuthenticationMethod[] 17 | { 18 | // Password auth 19 | new PasswordAuthenticationMethod(testUsername, testPassword) 20 | } 21 | ); 22 | 23 | using var sftpclient = new SftpClient(connInfo); 24 | sftpclient.Connect(); 25 | sftpclient.ListDirectory("/"); 26 | sftpclient.Disconnect(); 27 | 28 | //using (var sshClient = new SshClient(connInfo)) 29 | //{ 30 | // sshClient.Connect(); 31 | 32 | 33 | // var commands = new[] { "info", "hello", "rm -rf /" }; 34 | // foreach (var cmd in commands) 35 | // { 36 | // using (var sshCmd = sshClient.CreateCommand(cmd)) 37 | // { 38 | // sshCmd.Execute(); 39 | // var result = sshCmd.Result; 40 | // Console.Write($"exec: {cmd}, result: {result}"); 41 | // } 42 | // } 43 | //} 44 | Console.ReadLine(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /FxSsh/KeyUtils.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Algorithms; 2 | using System; 3 | using System.Diagnostics.Contracts; 4 | using System.Security.Cryptography; 5 | 6 | namespace FxSsh 7 | { 8 | public static class KeyUtils 9 | { 10 | public static string GetFingerprint(string sshkey) 11 | { 12 | Contract.Requires(sshkey != null); 13 | 14 | using (var md5 = new MD5CryptoServiceProvider()) 15 | { 16 | var bytes = Convert.FromBase64String(sshkey); 17 | bytes = md5.ComputeHash(bytes); 18 | return BitConverter.ToString(bytes).Replace('-', ':'); 19 | } 20 | } 21 | 22 | private static PublicKeyAlgorithm GetKeyAlgorithm(string type) 23 | { 24 | Contract.Requires(type != null); 25 | 26 | switch (type) 27 | { 28 | case "ssh-rsa": 29 | return new RsaKey(); 30 | 31 | case "ssh-dss": 32 | return new DssKey(); 33 | 34 | default: 35 | throw new ArgumentOutOfRangeException("type"); 36 | } 37 | } 38 | 39 | public static string GeneratePrivateKey(string type) 40 | { 41 | Contract.Requires(type != null); 42 | 43 | var alg = GetKeyAlgorithm(type); 44 | var bytes = alg.ExportKey(); 45 | return Convert.ToBase64String(bytes); 46 | } 47 | 48 | public static string[] SupportedAlgorithms 49 | { 50 | get { return new string[] { "ssh-rsa", "ssh-dss" }; } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /FxSsh/Messages/DisconnectMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages 5 | { 6 | [Message("SSH_MSG_DISCONNECT", MessageNumber)] 7 | public class DisconnectMessage : Message 8 | { 9 | private const byte MessageNumber = 1; 10 | 11 | public DisconnectMessage() 12 | { 13 | } 14 | 15 | public DisconnectMessage(DisconnectReason reasonCode, string description = "", string language = "en") 16 | { 17 | Contract.Requires(description != null); 18 | Contract.Requires(language != null); 19 | 20 | ReasonCode = reasonCode; 21 | Description = description; 22 | Language = language; 23 | } 24 | 25 | public DisconnectReason ReasonCode { get; private set; } 26 | public string Description { get; private set; } 27 | public string Language { get; private set; } 28 | 29 | public override byte MessageType { get { return MessageNumber; } } 30 | 31 | protected override void OnLoad(SshDataWorker reader) 32 | { 33 | ReasonCode = (DisconnectReason)reader.ReadUInt32(); 34 | Description = reader.ReadString(Encoding.UTF8); 35 | if (reader.DataAvailable >= 4) 36 | Language = reader.ReadString(Encoding.UTF8); 37 | } 38 | 39 | protected override void OnGetPacket(SshDataWorker writer) 40 | { 41 | writer.Write((uint)ReasonCode); 42 | writer.Write(Description, Encoding.UTF8); 43 | writer.Write(Language ?? "en", Encoding.UTF8); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FxSshSftpServer.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /FxSsh/DynamicInvoker.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Messages; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace FxSsh 8 | { 9 | public static class DynamicInvoker 10 | { 11 | static Dictionary> _cache = 12 | new Dictionary>(); 13 | 14 | public static void InvokeHandleMessage(this IDynamicInvoker instance, Message message) 15 | { 16 | var instype = instance.GetType(); 17 | var msgtype = message.GetType(); 18 | 19 | var key = instype.Name + '!' + msgtype.Name; 20 | var action = _cache.ContainsKey(key) ? _cache[key] : null; 21 | if (action == null) 22 | { 23 | var method = instance.GetType() 24 | .GetMethod("HandleMessage", 25 | BindingFlags.NonPublic | BindingFlags.Instance, 26 | null, 27 | new[] { message.GetType() }, 28 | null); 29 | var insparam = Expression.Parameter(typeof(IDynamicInvoker)); 30 | var msgparam = Expression.Parameter(typeof(Message)); 31 | var call = Expression.Call( 32 | Expression.Convert(insparam, instype), 33 | method, 34 | Expression.Convert(msgparam, msgtype)); 35 | action = Expression.Lambda>(call, insparam, msgparam).Compile(); 36 | _cache[key] = action; 37 | } 38 | action(instance, message); 39 | } 40 | } 41 | 42 | public interface IDynamicInvoker { } 43 | } 44 | -------------------------------------------------------------------------------- /FxSsh/Messages/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace FxSsh.Messages 5 | { 6 | public abstract class Message 7 | { 8 | public abstract byte MessageType { get; } 9 | 10 | protected byte[] RawBytes { get; set; } 11 | 12 | public void Load(byte[] bytes) 13 | { 14 | Contract.Requires(bytes != null); 15 | 16 | RawBytes = bytes; 17 | using (var worker = new SshDataWorker(bytes)) 18 | { 19 | var number = worker.ReadByte(); 20 | if (number != MessageType) 21 | throw new ArgumentException(string.Format("Message type {0} is not valid.", number)); 22 | 23 | OnLoad(worker); 24 | } 25 | } 26 | 27 | public byte[] GetPacket() 28 | { 29 | using (var worker = new SshDataWorker()) 30 | { 31 | worker.Write(MessageType); 32 | 33 | OnGetPacket(worker); 34 | 35 | return worker.ToByteArray(); 36 | } 37 | } 38 | 39 | public static T LoadFrom(Message message) where T : Message, new() 40 | { 41 | Contract.Requires(message != null); 42 | 43 | var msg = new T(); 44 | msg.Load(message.RawBytes); 45 | return msg; 46 | } 47 | 48 | protected virtual void OnLoad(SshDataWorker reader) 49 | { 50 | Contract.Requires(reader != null); 51 | 52 | throw new NotSupportedException(); 53 | } 54 | 55 | protected virtual void OnGetPacket(SshDataWorker writer) 56 | { 57 | Contract.Requires(writer != null); 58 | 59 | throw new NotSupportedException(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /FxSsh/Services/UserauthArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | 3 | namespace FxSsh.Services 4 | { 5 | 6 | public abstract class UserAuthArgs 7 | { 8 | public UserAuthArgs(Session session, string username, string keyAlgorithm, string fingerprint, byte[] key) : this(AuthType.PublicKey) 9 | { 10 | Contract.Requires(username != null); 11 | Contract.Requires(keyAlgorithm != null); 12 | Contract.Requires(fingerprint != null); 13 | Contract.Requires(key != null); 14 | 15 | 16 | Username = username; 17 | KeyAlgorithm = keyAlgorithm; 18 | Fingerprint = fingerprint; 19 | Key = key; 20 | Session = session; 21 | } 22 | 23 | public UserAuthArgs(Session session, string username, string password) : this(AuthType.Password) 24 | { 25 | Contract.Requires(username != null); 26 | Contract.Requires(password != null); 27 | 28 | Username = username; 29 | Password = password; 30 | Session = session; 31 | } 32 | 33 | protected UserAuthArgs(AuthType authType) 34 | { 35 | AuthenticationType = authType; 36 | } 37 | 38 | public enum AuthType 39 | { 40 | PublicKey, 41 | Password 42 | } 43 | 44 | // Info 45 | public AuthType AuthenticationType { get; set; } 46 | 47 | public Session Session { get; set; } 48 | 49 | public string Username { get; set; } 50 | 51 | // Public Key Auth 52 | public string KeyAlgorithm { get; set; } 53 | public string Fingerprint { get; set; } 54 | public byte[] Key { get; set; } 55 | public bool Result { get; set; } 56 | 57 | 58 | // Password Auth 59 | public string Password { get; set; } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/PublicKeyAlgorithmContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | [ContractClassFor(typeof(PublicKeyAlgorithm))] 7 | abstract class PublicKeyAlgorithmContract : PublicKeyAlgorithm 8 | { 9 | public PublicKeyAlgorithmContract() 10 | : base(null) 11 | { 12 | } 13 | 14 | public override string Name 15 | { 16 | get { throw new NotImplementedException(); } 17 | } 18 | 19 | public override void ImportKey(byte[] bytes) 20 | { 21 | Contract.Requires(bytes != null); 22 | 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public override void LoadKeyAndCertificatesData(byte[] data) 27 | { 28 | Contract.Requires(data != null); 29 | 30 | throw new NotImplementedException(); 31 | } 32 | 33 | public override byte[] CreateKeyAndCertificatesData() 34 | { 35 | throw new NotImplementedException(); 36 | } 37 | 38 | public override bool VerifyData(byte[] data, byte[] signature) 39 | { 40 | Contract.Requires(data != null); 41 | Contract.Requires(signature != null); 42 | 43 | throw new NotImplementedException(); 44 | } 45 | 46 | public override bool VerifyHash(byte[] hash, byte[] signature) 47 | { 48 | Contract.Requires(hash != null); 49 | Contract.Requires(signature != null); 50 | 51 | throw new NotImplementedException(); 52 | } 53 | 54 | public override byte[] SignData(byte[] data) 55 | { 56 | Contract.Requires(data != null); 57 | 58 | throw new NotImplementedException(); 59 | } 60 | 61 | public override byte[] SignHash(byte[] hash) 62 | { 63 | Contract.Requires(hash != null); 64 | 65 | throw new NotImplementedException(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /FxSsh/KeepAliveTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Timers; 5 | 6 | namespace FxSsh 7 | { 8 | public class KeepAliveTimer : IDisposable 9 | { 10 | private readonly Timer _timer; 11 | private DateTime _startTime; 12 | private TimeSpan? _runTime; 13 | 14 | 15 | public Action Action { get; set; } 16 | public bool Running { get; private set; } 17 | 18 | public KeepAliveTimer(double intervalmilliseconds, Action action) 19 | { 20 | 21 | Action = action; 22 | _timer = new Timer(intervalmilliseconds ); 23 | _timer.Elapsed += TimerExpired; 24 | _timer.Start(); 25 | } 26 | 27 | public void Dispose() 28 | { 29 | _timer.Stop(); 30 | _timer.Dispose(); 31 | } 32 | 33 | private void TimerExpired(object sender, EventArgs e) 34 | { 35 | lock (_timer) 36 | { 37 | Running = false; 38 | _timer.Stop(); 39 | _runTime = DateTime.UtcNow.Subtract(_startTime); 40 | Action(); 41 | _startTime = DateTime.UtcNow; 42 | _timer.Start(); 43 | Running = true; 44 | } 45 | } 46 | 47 | public void Nudge() 48 | { 49 | lock (_timer) 50 | { 51 | if (!Running) 52 | { 53 | _startTime = DateTime.UtcNow; 54 | _runTime = null; 55 | _timer.Start(); 56 | Running = true; 57 | } 58 | else 59 | { 60 | //Reset the timer 61 | _timer.Stop(); 62 | _timer.Start(); 63 | } 64 | } 65 | } 66 | 67 | public TimeSpan GetTimeSpan() 68 | { 69 | return _runTime ?? DateTime.UtcNow.Subtract(_startTime); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/EncryptionAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics.Contracts; 3 | using System.Security.Cryptography; 4 | 5 | namespace FxSsh.Algorithms 6 | { 7 | public class EncryptionAlgorithm 8 | { 9 | private readonly SymmetricAlgorithm _algorithm; 10 | private readonly CipherModeEx _mode; 11 | private readonly ICryptoTransform _transform; 12 | 13 | public EncryptionAlgorithm(SymmetricAlgorithm algorithm, int keySize, CipherModeEx mode, byte[] key, byte[] iv, bool isEncryption) 14 | { 15 | Contract.Requires(algorithm != null); 16 | Contract.Requires(key != null); 17 | Contract.Requires(iv != null); 18 | Contract.Requires(keySize == key.Length << 3); 19 | 20 | algorithm.KeySize = keySize; 21 | algorithm.Key = key; 22 | algorithm.IV = iv; 23 | algorithm.Padding = PaddingMode.None; 24 | 25 | _algorithm = algorithm; 26 | _mode = mode; 27 | 28 | _transform = CreateTransform(isEncryption); 29 | } 30 | 31 | public int BlockBytesSize 32 | { 33 | get { return _algorithm.BlockSize >> 3; } 34 | } 35 | 36 | public byte[] Transform(byte[] input) 37 | { 38 | var output = new byte[input.Length]; 39 | _transform.TransformBlock(input, 0, input.Length, output, 0); 40 | return output; 41 | } 42 | 43 | private ICryptoTransform CreateTransform(bool isEncryption) 44 | { 45 | switch (_mode) 46 | { 47 | case CipherModeEx.CBC: 48 | _algorithm.Mode = CipherMode.CBC; 49 | return isEncryption 50 | ? _algorithm.CreateEncryptor() 51 | : _algorithm.CreateDecryptor(); 52 | 53 | case CipherModeEx.CTR: 54 | return new CtrModeCryptoTransform(_algorithm); 55 | 56 | default: 57 | throw new InvalidEnumArgumentException(string.Format("Invalid mode: {0}", _mode)); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /FxSshSftpServer/Program.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Serilog; 7 | using Serilog.Events; 8 | using SshServer.Filesystem.LocalDisk; 9 | using SshServer.Settings.LiteDb; 10 | using System; 11 | 12 | var builder = WebApplication.CreateBuilder(args); 13 | Log.Logger = new LoggerConfiguration() 14 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 15 | .MinimumLevel.Override("System", LogEventLevel.Warning) 16 | .Enrich.FromLogContext() 17 | .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) 18 | //.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() 19 | //.WithDefaultDestructurers() 20 | //.WithDestructurers(new[] { new DbUpdateExceptionDestructurer() })) 21 | .CreateLogger(); 22 | builder.Services.AddRazorPages(); 23 | builder.Services.AddServerSideBlazor(); 24 | builder.Services.AddHostedService(); 25 | 26 | builder.Services.AddSshServerSettingsLiteDb(); 27 | builder.Services.AddLocalFileSystemHosting(); 28 | 29 | builder.Logging.AddSerilog(Log.Logger); 30 | 31 | Log.Information("Starting host service"); 32 | var app = builder.Build(); 33 | 34 | 35 | if (app.Environment.IsDevelopment()) 36 | { 37 | app.UseDeveloperExceptionPage(); 38 | } 39 | else 40 | { 41 | app.UseExceptionHandler("/Home/Error"); 42 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 43 | app.UseHsts(); 44 | } 45 | 46 | app.UseHttpsRedirection(); 47 | app.UseStaticFiles(); 48 | app.UseRouting(); 49 | 50 | app.UseEndpoints(endpoints => 51 | { 52 | endpoints.MapBlazorHub(); 53 | endpoints.MapFallbackToPage("/_Host"); 54 | }); 55 | 56 | 57 | try 58 | { 59 | Log.Information("Starting service"); 60 | app.Run(); 61 | return 0; 62 | } 63 | catch (Exception ex) 64 | { 65 | Log.Fatal(ex, "Service terminated unexpectedly"); 66 | return 1; 67 | } 68 | finally 69 | { 70 | Log.CloseAndFlush(); 71 | } 72 | 73 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // https://www.meziantou.net/generating-and-downloading-a-file-in-a-blazor-webassembly-application.htm 2 | 3 | function BlazorDownloadFile(filename, contentType, content) { 4 | // Blazor marshall byte[] to a base64 string, so we first need to convert the string (content) to a Uint8Array to create the File 5 | const data = base64DecToArr(content); 6 | 7 | // Create the URL 8 | const file = new File([data], filename, { type: contentType }); 9 | const exportUrl = URL.createObjectURL(file); 10 | 11 | // Create the element and click on it 12 | const a = document.createElement("a"); 13 | document.body.appendChild(a); 14 | a.href = exportUrl; 15 | a.download = filename; 16 | a.target = "_self"; 17 | a.click(); 18 | 19 | // We don't need to keep the url, let's release the memory 20 | // On Safari it seems you need to comment this line... (please let me know if you know why) 21 | URL.revokeObjectURL(exportUrl); 22 | } 23 | 24 | // Convert a base64 string to a Uint8Array. This is needed to create a blob object from the base64 string. 25 | // The code comes from: https://developer.mozilla.org/fr/docs/Web/API/WindowBase64/D%C3%A9coder_encoder_en_base64 26 | function b64ToUint6(nChr) { 27 | return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; 28 | } 29 | 30 | function base64DecToArr(sBase64, nBlocksSize) { 31 | var 32 | sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), 33 | nInLen = sB64Enc.length, 34 | nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, 35 | taBytes = new Uint8Array(nOutLen); 36 | 37 | for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { 38 | nMod4 = nInIdx & 3; 39 | nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; 40 | if (nMod4 === 3 || nInLen - nInIdx === 1) { 41 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { 42 | taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; 43 | } 44 | nUint24 = 0; 45 | } 46 | } 47 | return taBytes; 48 | } -------------------------------------------------------------------------------- /SshServerModule/CommandService.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using System.Diagnostics; 4 | //using System.Linq; 5 | //using System.Text; 6 | //using System.Threading.Tasks; 7 | 8 | //namespace FxSsh.SshServerModule 9 | //{ 10 | // public class CommandService 11 | // { 12 | // private Process _process = null; 13 | // private ProcessStartInfo _startInfo = null; 14 | 15 | // public CommandService(string command, string args) 16 | // { 17 | // _startInfo = new ProcessStartInfo(command, args) 18 | // { 19 | // CreateNoWindow = true, 20 | // RedirectStandardError = true, 21 | // RedirectStandardInput = true, 22 | // RedirectStandardOutput = true, 23 | // UseShellExecute = false, 24 | // }; 25 | // } 26 | 27 | // public event EventHandler DataReceived; 28 | // public event EventHandler EofReceived; 29 | // public event EventHandler CloseReceived; 30 | 31 | // public void Start() 32 | // { 33 | // _process = Process.Start(_startInfo); 34 | // Task.Run(() => MessageLoop()); 35 | // } 36 | 37 | // public void OnData(byte[] data) 38 | // { 39 | // _process.StandardInput.BaseStream.Write(data, 0, data.Length); 40 | // _process.StandardInput.BaseStream.Flush(); 41 | // } 42 | 43 | // public void OnClose() 44 | // { 45 | // _process.StandardInput.BaseStream.Close(); 46 | // } 47 | 48 | // private void MessageLoop() 49 | // { 50 | // var bytes = new byte[1024 * 64]; 51 | // while (true) 52 | // { 53 | // var len = _process.StandardOutput.BaseStream.Read(bytes, 0, bytes.Length); 54 | // if (len <= 0) 55 | // break; 56 | 57 | // var data = bytes.Length != len 58 | // ? bytes.Take(len).ToArray() 59 | // : bytes; 60 | // DataReceived?.Invoke(this, data); 61 | // } 62 | // EofReceived?.Invoke(this, EventArgs.Empty); 63 | // CloseReceived?.Invoke(this, (uint)_process.ExitCode); 64 | // } 65 | // } 66 | //} 67 | -------------------------------------------------------------------------------- /SshServerModule/Settings/UserLogics.cs: -------------------------------------------------------------------------------- 1 | using SshServer.Interfaces; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Security.Cryptography; 7 | 8 | namespace FxSsh.SshServerModule 9 | { 10 | 11 | 12 | internal static class UserLogics 13 | { 14 | 15 | internal static bool VerifyUserIpWhitelisted(User user, EndPoint remoteEndpoint) 16 | { 17 | if (!user.OnlyWhitelistedIps || user.WhitelistedIps == null || user.WhitelistedIps.Count == 0) // No check needed 18 | return true; 19 | 20 | var endpoint = remoteEndpoint as IPEndPoint; 21 | 22 | // https://github.com/lduchosal/ipnetwork 23 | var ipaddress = IPAddress.Parse(endpoint.Address.ToString()); 24 | 25 | foreach(var whitelisted in user.WhitelistedIps) 26 | { 27 | IPNetwork ipnetwork = IPNetwork.Parse(whitelisted); // whitelisted must contain CIDR e.g. /16 28 | var success = ipnetwork.Contains(ipaddress); 29 | if (success) 30 | return true; 31 | } 32 | 33 | return false; 34 | } 35 | 36 | internal static bool VerifyUserKey(User user, byte[] key, string fingerprint, string keyAlgorithm) 37 | { 38 | var savedkey = Convert.FromBase64String(user.RsaPublicKey); 39 | var keyAlg = new Algorithms.RsaKey(null); 40 | keyAlg.ImportKey(savedkey); 41 | var fingprint2 = keyAlg.GetFingerprint(); 42 | 43 | return fingerprint == fingprint2; 44 | 45 | } 46 | 47 | internal static byte[] ConvertFingerprintToByteArray(string fingerprint) 48 | { 49 | return fingerprint.Split(':').Select(s => Convert.ToByte(s, 16)).ToArray(); 50 | } 51 | 52 | internal static bool VerifyUserPassword(User user, string password) 53 | { 54 | var sha256 = new SHA256CryptoServiceProvider(); 55 | var pwhashed = sha256.ComputeHash(System.Text.Encoding.ASCII.GetBytes(password)); 56 | var base64encoded = Convert.ToBase64String(pwhashed); 57 | 58 | //var testpw = "A6xnQhbz4Vx2HuGl4lXwZ5U2I8iziLRFnhP5eNfIRvQ="; // "1234" 59 | 60 | if (base64encoded == user.HashedPassword) 61 | return true; 62 | else 63 | return false; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/PublicKeyAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace FxSsh.Algorithms 7 | { 8 | [ContractClass(typeof(PublicKeyAlgorithmContract))] 9 | public abstract class PublicKeyAlgorithm 10 | { 11 | public PublicKeyAlgorithm(string key) 12 | { 13 | if (!string.IsNullOrEmpty(key)) 14 | { 15 | var bytes = Convert.FromBase64String(key); 16 | ImportKey(bytes); 17 | } 18 | } 19 | 20 | public abstract string Name { get; } 21 | 22 | public string GetFingerprint() 23 | { 24 | using (var md5 = MD5.Create()) 25 | { 26 | var bytes = md5.ComputeHash(CreateKeyAndCertificatesData()); 27 | return BitConverter.ToString(bytes).Replace('-', ':'); 28 | } 29 | } 30 | 31 | public byte[] GetSignature(byte[] signatureData) 32 | { 33 | Contract.Requires(signatureData != null); 34 | 35 | using (var worker = new SshDataWorker(signatureData)) 36 | { 37 | if (worker.ReadString(Encoding.ASCII) != this.Name) 38 | throw new CryptographicException("Signature was not created with this algorithm."); 39 | 40 | var signature = worker.ReadBinary(); 41 | return signature; 42 | } 43 | } 44 | 45 | public byte[] CreateSignatureData(byte[] data) 46 | { 47 | Contract.Requires(data != null); 48 | 49 | using (var worker = new SshDataWorker()) 50 | { 51 | var signature = SignData(data); 52 | 53 | worker.Write(this.Name, Encoding.ASCII); 54 | worker.WriteBinary(signature); 55 | 56 | return worker.ToByteArray(); 57 | } 58 | } 59 | 60 | public abstract void ImportKey(byte[] bytes); 61 | public abstract byte[] ExportKey(); 62 | 63 | public abstract void LoadKeyAndCertificatesData(byte[] data); 64 | 65 | public abstract byte[] CreateKeyAndCertificatesData(); 66 | 67 | public abstract bool VerifyData(byte[] data, byte[] signature); 68 | 69 | public abstract bool VerifyHash(byte[] hash, byte[] signature); 70 | 71 | public abstract byte[] SignData(byte[] data); 72 | 73 | public abstract byte[] SignHash(byte[] hash); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/CtrModeCryptoTransform.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Security.Cryptography; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class CtrModeCryptoTransform : ICryptoTransform 7 | { 8 | private readonly SymmetricAlgorithm _algorithm; 9 | private readonly ICryptoTransform _transform; 10 | private readonly byte[] _iv; 11 | private readonly byte[] _block; 12 | 13 | public CtrModeCryptoTransform(SymmetricAlgorithm algorithm) 14 | { 15 | Contract.Requires(algorithm != null); 16 | 17 | algorithm.Mode = CipherMode.ECB; 18 | algorithm.Padding = PaddingMode.None; 19 | 20 | _algorithm = algorithm; 21 | _transform = algorithm.CreateEncryptor(); 22 | _iv = algorithm.IV; 23 | _block = new byte[algorithm.BlockSize >> 3]; 24 | } 25 | 26 | public bool CanReuseTransform 27 | { 28 | get { return true; } 29 | } 30 | 31 | public bool CanTransformMultipleBlocks 32 | { 33 | get { return true; } 34 | } 35 | 36 | public int InputBlockSize 37 | { 38 | get { return _algorithm.BlockSize; } 39 | } 40 | 41 | public int OutputBlockSize 42 | { 43 | get { return _algorithm.BlockSize; } 44 | } 45 | 46 | public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) 47 | { 48 | var written = 0; 49 | var bytesPerBlock = InputBlockSize >> 3; 50 | 51 | for (var i = 0; i < inputCount; i += bytesPerBlock) 52 | { 53 | written += _transform.TransformBlock(_iv, 0, bytesPerBlock, _block, 0); 54 | 55 | for (var j = 0; j < bytesPerBlock; j++) 56 | outputBuffer[outputOffset + i + j] = (byte)(_block[j] ^ inputBuffer[inputOffset + i + j]); 57 | 58 | var k = _iv.Length; 59 | while (--k >= 0 && ++_iv[k] == 0) 60 | { 61 | } 62 | } 63 | 64 | return written; 65 | } 66 | 67 | public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) 68 | { 69 | var output = new byte[inputCount]; 70 | TransformBlock(inputBuffer, inputOffset, inputCount, output, 0); 71 | return output; 72 | } 73 | 74 | public void Dispose() 75 | { 76 | _transform.Dispose(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /FxSsh/Algorithms/DssKey.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class DssKey : PublicKeyAlgorithm 7 | { 8 | private readonly DSACryptoServiceProvider _algorithm = new DSACryptoServiceProvider(); 9 | 10 | public DssKey(string key = null) 11 | : base(key) 12 | { 13 | } 14 | 15 | public override string Name 16 | { 17 | get { return "ssh-dss"; } 18 | } 19 | 20 | public override void ImportKey(byte[] bytes) 21 | { 22 | _algorithm.ImportCspBlob(bytes); 23 | } 24 | 25 | public override byte[] ExportKey() 26 | { 27 | return _algorithm.ExportCspBlob(true); 28 | } 29 | 30 | public override void LoadKeyAndCertificatesData(byte[] data) 31 | { 32 | using (var worker = new SshDataWorker(data)) 33 | { 34 | if (worker.ReadString(Encoding.ASCII) != this.Name) 35 | throw new CryptographicException("Key and certificates were not created with this algorithm."); 36 | 37 | var args = new DSAParameters(); 38 | args.P = worker.ReadMpint(); 39 | args.Q = worker.ReadMpint(); 40 | args.G = worker.ReadMpint(); 41 | args.Y = worker.ReadMpint(); 42 | 43 | _algorithm.ImportParameters(args); 44 | } 45 | } 46 | 47 | public override byte[] CreateKeyAndCertificatesData() 48 | { 49 | using (var worker = new SshDataWorker()) 50 | { 51 | var args = _algorithm.ExportParameters(false); 52 | 53 | worker.Write(this.Name, Encoding.ASCII); 54 | worker.WriteMpint(args.P); 55 | worker.WriteMpint(args.Q); 56 | worker.WriteMpint(args.G); 57 | worker.WriteMpint(args.Y); 58 | 59 | return worker.ToByteArray(); 60 | } 61 | } 62 | 63 | public override bool VerifyData(byte[] data, byte[] signature) 64 | { 65 | return _algorithm.VerifyData(data, signature); 66 | } 67 | 68 | public override bool VerifyHash(byte[] hash, byte[] signature) 69 | { 70 | return _algorithm.VerifyHash(hash, "SHA1", signature); 71 | } 72 | 73 | public override byte[] SignData(byte[] data) 74 | { 75 | return _algorithm.SignData(data); 76 | } 77 | 78 | public override byte[] SignHash(byte[] hash) 79 | { 80 | return _algorithm.SignHash(hash, "SHA1"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/RsaKey.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace FxSsh.Algorithms 5 | { 6 | public class RsaKey : PublicKeyAlgorithm 7 | { 8 | private readonly RSACryptoServiceProvider _algorithm = new RSACryptoServiceProvider(); 9 | 10 | public RsaKey(string key = null) 11 | : base(key) 12 | { 13 | } 14 | 15 | public override string Name 16 | { 17 | get { return "ssh-rsa"; } 18 | } 19 | 20 | public override void ImportKey(byte[] bytes) 21 | { 22 | _algorithm.ImportCspBlob(bytes); 23 | } 24 | 25 | public override byte[] ExportKey() 26 | { 27 | return _algorithm.ExportCspBlob(true); 28 | } 29 | 30 | public override void LoadKeyAndCertificatesData(byte[] data) 31 | { 32 | using (var worker = new SshDataWorker(data)) 33 | { 34 | if (worker.ReadString(Encoding.ASCII) != this.Name) 35 | throw new CryptographicException("Key and certificates were not created with this algorithm."); 36 | 37 | var args = new RSAParameters(); 38 | args.Exponent = worker.ReadMpint(); 39 | args.Modulus = worker.ReadMpint(); 40 | 41 | _algorithm.ImportParameters(args); 42 | } 43 | } 44 | 45 | public override byte[] CreateKeyAndCertificatesData() 46 | { 47 | using (var worker = new SshDataWorker()) 48 | { 49 | var args = _algorithm.ExportParameters(false); 50 | 51 | worker.Write(this.Name, Encoding.ASCII); 52 | worker.WriteMpint(args.Exponent); 53 | worker.WriteMpint(args.Modulus); 54 | 55 | return worker.ToByteArray(); 56 | } 57 | } 58 | 59 | public override bool VerifyData(byte[] data, byte[] signature) 60 | { 61 | return _algorithm.VerifyData(data, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); 62 | } 63 | 64 | public override bool VerifyHash(byte[] hash, byte[] signature) 65 | { 66 | return _algorithm.VerifyHash(hash, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); 67 | } 68 | 69 | public override byte[] SignData(byte[] data) 70 | { 71 | return _algorithm.SignData(data, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); 72 | } 73 | 74 | public override byte[] SignHash(byte[] hash) 75 | { 76 | return _algorithm.SignHash(hash, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '31 4 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'csharp', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /SshServerModule/Services/TcpForwardService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Sockets; 5 | using System.Threading.Tasks; 6 | 7 | namespace FxSsh.SshServerModule.Services 8 | { 9 | 10 | 11 | public class TcpForwardService 12 | { 13 | private Socket _socket; 14 | private string _host; 15 | private int _port; 16 | private bool _connected; 17 | private List _blocked; 18 | 19 | public TcpForwardService(string host, int port, string originatorIP, int originatorPort) 20 | { 21 | _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); 22 | _host = host; 23 | _port = port; 24 | _connected = false; 25 | _blocked = new List(); 26 | } 27 | 28 | public event EventHandler DataReceived; 29 | public event EventHandler CloseReceived; 30 | 31 | public void Start() 32 | { 33 | Task.Run(() => 34 | { 35 | try 36 | { 37 | MessageLoop(); 38 | } 39 | catch 40 | { 41 | OnClose(); 42 | } 43 | }); 44 | } 45 | 46 | public void OnData(byte[] data) 47 | { 48 | try 49 | { 50 | if (_connected) 51 | { 52 | if (_blocked.Count > 0) 53 | { 54 | _socket.Send(_blocked.ToArray()); 55 | _blocked.Clear(); 56 | } 57 | _socket.Send(data); 58 | } 59 | else 60 | { 61 | _blocked.AddRange(data); 62 | } 63 | } 64 | catch 65 | { 66 | OnClose(); 67 | } 68 | } 69 | 70 | public void OnClose() 71 | { 72 | try 73 | { 74 | _socket.Shutdown(SocketShutdown.Send); 75 | } 76 | catch { } 77 | } 78 | 79 | private void MessageLoop() 80 | { 81 | _socket.Connect(_host, _port); 82 | _connected = true; 83 | OnData(new byte[0]); 84 | var bytes = new byte[1024 * 64]; 85 | while (true) 86 | { 87 | var len = _socket.Receive(bytes); 88 | if (len <= 0) 89 | break; 90 | 91 | var data = bytes.Length != len 92 | ? bytes.Take(len).ToArray() 93 | : bytes; 94 | DataReceived?.Invoke(this, data); 95 | } 96 | CloseReceived?.Invoke(this, EventArgs.Empty); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /FxSsh/Algorithms/DiffieHellman.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Numerics; 6 | using System.Security.Cryptography; 7 | 8 | namespace FxSsh.Algorithms 9 | { 10 | public class DiffieHellman : AsymmetricAlgorithm 11 | { 12 | // http://tools.ietf.org/html/rfc2412 13 | private const string Okley1024 = "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"; 14 | 15 | // http://tools.ietf.org/html/rfc3526 16 | private const string Okley2048 = "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"; 17 | 18 | private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); 19 | 20 | private BigInteger _p; 21 | private BigInteger _g; 22 | private BigInteger _x; 23 | 24 | public DiffieHellman(int bitlen) 25 | { 26 | Contract.Requires(bitlen == 1024 || bitlen == 2048); 27 | 28 | if (bitlen == 1024) 29 | { 30 | _p = BigInteger.Parse(Okley1024, NumberStyles.HexNumber); 31 | _g = new BigInteger(2); 32 | } 33 | else if (bitlen == 2048) 34 | { 35 | _p = BigInteger.Parse(Okley2048, NumberStyles.HexNumber); 36 | _g = new BigInteger(2); 37 | } 38 | else 39 | throw new ArgumentException("bitlen", "bitlen must equal 1024 or 2048"); 40 | 41 | var bytes = new byte[80]; // 80 * 8 = 640 bits 42 | _rng.GetBytes(bytes); 43 | _x = BigInteger.Abs(new BigInteger(bytes)); 44 | } 45 | 46 | public byte[] CreateKeyExchange() 47 | { 48 | var y = BigInteger.ModPow(_g, _x, _p); 49 | var bytes = BigintToBytes(y); 50 | return bytes; 51 | } 52 | 53 | public byte[] DecryptKeyExchange(byte[] keyEx) 54 | { 55 | Contract.Requires(keyEx != null); 56 | 57 | var pvr = BytesToBigint(keyEx); 58 | var z = BigInteger.ModPow(pvr, _x, _p); 59 | var bytes = BigintToBytes(z); 60 | return bytes; 61 | } 62 | 63 | private BigInteger BytesToBigint(byte[] bytes) 64 | { 65 | return new BigInteger(bytes.Reverse().Concat(new byte[] { 0 }).ToArray()); 66 | } 67 | 68 | private byte[] BigintToBytes(BigInteger bigint) 69 | { 70 | var bytes = bigint.ToByteArray(); 71 | if (bytes.Length > 1 && bytes[bytes.Length - 1] == 0) 72 | { 73 | return bytes.Reverse().Skip(1).ToArray(); 74 | } 75 | return bytes.Reverse().ToArray(); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | 49 | # Visual C++ cache files 50 | ipch/ 51 | *.aps 52 | *.ncb 53 | *.opensdf 54 | *.sdf 55 | *.cachefile 56 | 57 | # Visual Studio profiler 58 | *.psess 59 | *.vsp 60 | *.vspx 61 | 62 | # Guidance Automation Toolkit 63 | *.gpState 64 | 65 | # ReSharper is a .NET coding add-in 66 | _ReSharper*/ 67 | *.[Rr]e[Ss]harper 68 | 69 | # TeamCity is a build add-in 70 | _TeamCity* 71 | 72 | # DotCover is a Code Coverage Tool 73 | *.dotCover 74 | 75 | # NCrunch 76 | *.ncrunch* 77 | .*crunch*.local.xml 78 | 79 | # Installshield output folder 80 | [Ee]xpress/ 81 | 82 | # DocProject is a documentation generator add-in 83 | DocProject/buildhelp/ 84 | DocProject/Help/*.HxT 85 | DocProject/Help/*.HxC 86 | DocProject/Help/*.hhc 87 | DocProject/Help/*.hhk 88 | DocProject/Help/*.hhp 89 | DocProject/Help/Html2 90 | DocProject/Help/html 91 | 92 | # Click-Once directory 93 | publish/ 94 | 95 | # Publish Web Output 96 | *.Publish.xml 97 | 98 | # NuGet Packages Directory 99 | packages/ 100 | 101 | # Windows Azure Build Output 102 | csx 103 | *.build.csdef 104 | 105 | # Windows Store app package directory 106 | AppPackages/ 107 | 108 | # Others 109 | sql/ 110 | *.Cache 111 | ClientBin/ 112 | [Ss]tyle[Cc]op.* 113 | ~$* 114 | *~ 115 | *.dbmdl 116 | *.[Pp]ublish.xml 117 | *.pfx 118 | *.publishsettings 119 | 120 | # RIA/Silverlight projects 121 | Generated_Code/ 122 | 123 | # Backup & report files from converting an old project file to a newer 124 | # Visual Studio version. Backup files are not needed, because we have git ;-) 125 | _UpgradeReport_Files/ 126 | Backup*/ 127 | UpgradeLog*.XML 128 | UpgradeLog*.htm 129 | 130 | # SQL Server files 131 | App_Data/*.mdf 132 | App_Data/*.ldf 133 | 134 | 135 | #LightSwitch generated files 136 | GeneratedArtifacts/ 137 | _Pvt_Extensions/ 138 | ModelManifest.xml 139 | 140 | # ========================= 141 | # Windows detritus 142 | # ========================= 143 | 144 | # Windows image file caches 145 | Thumbs.db 146 | ehthumbs.db 147 | 148 | # Folder config file 149 | Desktop.ini 150 | 151 | # Recycle Bin used on file shares 152 | $RECYCLE.BIN/ 153 | 154 | # Mac desktop service store files 155 | .DS_Store 156 | /.vs/FxSsh/DesignTimeBuild/.dtbcache.v2 157 | /.vs/FxSsh/config/applicationhost.config 158 | *.txt 159 | *.sqlite 160 | /.vs/freesftpsharp/config/applicationhost.config 161 | *.db 162 | /.vs/freesftpsharp/DesignTimeBuild 163 | /.vs/freesftpsharp/FileContentIndex 164 | /.vs/freesftpsharp/v16/TestStore/0 165 | /.vs/freesftpsharp/v17 166 | /.vs/VSWorkspaceState.json 167 | /.vs/ProjectEvaluation/freesftpsharp.metadata.v7.bin 168 | /.vs/ProjectEvaluation/freesftpsharp.projects.v7.bin 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # SftpServer (SSHServer) 3 | 4 | Sftp Server implementation based on SSHServer library based heavily on [FxSsh](https://github.com/Aimeast/FxSsh). This fork (based on https://github.com/0xFireball/SSHServer) implements the sftp subsystem and a Graphical user interface for administration of users and server settings. 5 | 6 | ## Current status 7 | All basic Sftp command are now implemented except handling symbolic links. However some implementations such as returning correct file attributes are not finalized. Server was tested with FileZilla client only. There are probably some bugs left to fix. 8 | You are welcome to create PR's 9 | 10 | ## FxSsh 11 | 12 | FxSsh is a lightweight [SSH](http://en.wikipedia.org/wiki/Secure_Shell) server side application as SSH reinforcement of [GitCandy](https://github.com/Aimeast/GitCandy). 13 | 14 | ### Features in this repo that was not in the original FxSSH 15 | 16 | - Password Authentication is implemented and enabled by default 17 | - Public Key authentication relying on key fingerprint 18 | - Server key is autogenerated at first startup time 19 | - Logging based on Serilog. 20 | - SFTP subsystem added. 21 | - Running commands remotely is disabled by default (code is commented out but will later be a setting per user) 22 | - Server is started using IHostedService interface (useful for standalone service or hosted inside Asp.Net Core) 23 | - Server settings are saved in a LiteDb-database file. 24 | 25 | 26 | ### Features planned for the future 27 | - whitelist IP/ip-range (CIDR) per account. (Implemented in server but not in the admin UI) 28 | - Ban IP, auto-ban on multiple failed attempts to login 29 | - Option to use cloud Blob storage as sftp root. Filesystem will be an interface in the SFTP engine, then both local file system or blob storage can be used. Controlled using Dependency Injection 30 | - Different ways to store settings and users (using Dependency Injection). E.g. Azure SQL, SQLite, JSON, LiteDb 31 | - SCP subsystem 32 | 33 | ## Sample 34 | 35 | ### Server (Powered by this library) 36 | 37 | ```csharp 38 | using FxSsh; 39 | using FxSsh.Services; 40 | using System; 41 | using System.Text; 42 | using System.Threading.Tasks; 43 | 44 | namespace SshServerLoader 45 | { 46 | class Program 47 | { 48 | static async Task Main(string[] args) 49 | { 50 | await new HostBuilder() 51 | .ConfigureServices((hostContext, services) => 52 | { 53 | services.AddLogging(); 54 | Log.Information("Starting host service"); 55 | services.AddHostedService(); 56 | }) 57 | .RunConsoleAsync(); 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ### Client (powered by SSH.NET client) 64 | 65 | ```csharp 66 | using Renci.SshNet; 67 | using System; 68 | using System.Net; 69 | 70 | namespace TestClient 71 | { 72 | internal class Program 73 | { 74 | private static void Main(string[] args) 75 | { 76 | var testUsername = "test_person"; 77 | var testPassword = "1234"; 78 | 79 | 80 | var connInfo = new ConnectionInfo(IPAddress.Loopback.ToString(), 22, testUsername, 81 | new AuthenticationMethod[] 82 | { 83 | // Password auth 84 | new PasswordAuthenticationMethod(testUsername, testPassword) 85 | } 86 | ); 87 | 88 | using var sftpclient = new SftpClient(connInfo); 89 | sftpclient.Connect(); 90 | sftpclient.ListDirectory("/"); 91 | sftpclient.Disconnect(); 92 | 93 | Console.ReadLine(); 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ### License 100 | 101 | The MIT license 102 | -------------------------------------------------------------------------------- /FxSsh/Messages/KeyExchangeInitMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace FxSsh.Messages 5 | { 6 | [Message("SSH_MSG_KEXINIT", MessageNumber)] 7 | public class KeyExchangeInitMessage : Message 8 | { 9 | private const byte MessageNumber = 20; 10 | 11 | private static readonly RandomNumberGenerator _rng = new RNGCryptoServiceProvider(); 12 | 13 | public KeyExchangeInitMessage() 14 | { 15 | Cookie = new byte[16]; 16 | _rng.GetBytes(Cookie); 17 | } 18 | 19 | public byte[] Cookie { get; private set; } 20 | 21 | public string[] KeyExchangeAlgorithms { get; set; } 22 | 23 | public string[] ServerHostKeyAlgorithms { get; set; } 24 | 25 | public string[] EncryptionAlgorithmsClientToServer { get; set; } 26 | 27 | public string[] EncryptionAlgorithmsServerToClient { get; set; } 28 | 29 | public string[] MacAlgorithmsClientToServer { get; set; } 30 | 31 | public string[] MacAlgorithmsServerToClient { get; set; } 32 | 33 | public string[] CompressionAlgorithmsClientToServer { get; set; } 34 | 35 | public string[] CompressionAlgorithmsServerToClient { get; set; } 36 | 37 | public string[] LanguagesClientToServer { get; set; } 38 | 39 | public string[] LanguagesServerToClient { get; set; } 40 | 41 | public bool FirstKexPacketFollows { get; set; } 42 | 43 | public uint Reserved { get; set; } 44 | 45 | public override byte MessageType { get { return MessageNumber; } } 46 | 47 | protected override void OnLoad(SshDataWorker reader) 48 | { 49 | Cookie = reader.ReadBinary(16); 50 | KeyExchangeAlgorithms = reader.ReadString(Encoding.ASCII).Split(','); 51 | ServerHostKeyAlgorithms = reader.ReadString(Encoding.ASCII).Split(','); 52 | EncryptionAlgorithmsClientToServer = reader.ReadString(Encoding.ASCII).Split(','); 53 | EncryptionAlgorithmsServerToClient = reader.ReadString(Encoding.ASCII).Split(','); 54 | MacAlgorithmsClientToServer = reader.ReadString(Encoding.ASCII).Split(','); 55 | MacAlgorithmsServerToClient = reader.ReadString(Encoding.ASCII).Split(','); 56 | CompressionAlgorithmsClientToServer = reader.ReadString(Encoding.ASCII).Split(','); 57 | CompressionAlgorithmsServerToClient = reader.ReadString(Encoding.ASCII).Split(','); 58 | LanguagesClientToServer = reader.ReadString(Encoding.ASCII).Split(','); 59 | LanguagesServerToClient = reader.ReadString(Encoding.ASCII).Split(','); 60 | FirstKexPacketFollows = reader.ReadBoolean(); 61 | Reserved = reader.ReadUInt32(); 62 | } 63 | 64 | protected override void OnGetPacket(SshDataWorker writer) 65 | { 66 | writer.Write(Cookie); 67 | writer.Write(string.Join(",", KeyExchangeAlgorithms), Encoding.ASCII); 68 | writer.Write(string.Join(",", ServerHostKeyAlgorithms), Encoding.ASCII); 69 | writer.Write(string.Join(",", EncryptionAlgorithmsClientToServer), Encoding.ASCII); 70 | writer.Write(string.Join(",", EncryptionAlgorithmsServerToClient), Encoding.ASCII); 71 | writer.Write(string.Join(",", MacAlgorithmsClientToServer), Encoding.ASCII); 72 | writer.Write(string.Join(",", MacAlgorithmsServerToClient), Encoding.ASCII); 73 | writer.Write(string.Join(",", CompressionAlgorithmsClientToServer), Encoding.ASCII); 74 | writer.Write(string.Join(",", CompressionAlgorithmsServerToClient), Encoding.ASCII); 75 | writer.Write(string.Join(",", LanguagesClientToServer), Encoding.ASCII); 76 | writer.Write(string.Join(",", LanguagesServerToClient), Encoding.ASCII); 77 | writer.Write(FirstKexPacketFollows); 78 | writer.Write(Reserved); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /FxSshSftpServer/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/Users.razor: -------------------------------------------------------------------------------- 1 | @using Radzen.Blazor 2 | @using FxSshSftpServer.Components 3 | @using FxSsh.SshServerModule 4 | @using SshServer.Interfaces 5 | 6 | @page "/users" 7 | 8 | @inject IJSRuntime JSRuntime 9 | 10 |

Users

11 |
12 |
13 |
14 | OnUserSelected()) 16 | FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" FilterOperator="StringFilterOperator.StartsWith"> 17 | 20 | 21 |
22 |
23 | @if (SelectedUser == null) 24 | { 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | CreateUser()) Text="Create user"> 33 | 34 | } 35 | else 36 | { 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | @if (usekeyfile) 51 | { 52 | 53 | Keyfile type is .pem 54 | @if (!string.IsNullOrWhiteSpace(SelectedUser.RsaPublicKey)) 55 | { 56 | Key has already been created 57 | CreateKeyForUser()) Text="Generate a new client key"> 58 | } 59 | else 60 | { 61 | CreateKeyForUser()) Text="Generate client key"> 62 | } 63 | 64 | 65 | } 66 | else 67 | { 68 | 69 | @if (!string.IsNullOrWhiteSpace(SelectedUser.HashedPassword)) 70 | { 71 | Password has been set. You can change it by entering a new one 72 | } 73 | 74 | 75 | } 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | SaveUser()) Text="Save user"> 84 | 85 | 86 | @if (!SelectedUserNotSaved) 87 | { 88 | 89 | RemoveUser()) Text="Delete user"> 90 | 91 | } 92 | 93 | @if (!string.IsNullOrWhiteSpace(InfoText)) 94 | { 95 | @InfoText 96 | } 97 | 98 | } 99 |
100 |
101 |
102 | 103 | -------------------------------------------------------------------------------- /FxSshSftpServer/Pages/Users.razor.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.SshServerModule; 2 | using FxSshSftpServer.Components; 3 | using Microsoft.AspNetCore.Components; 4 | using SshServer.Interfaces; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Security.Cryptography; 8 | using System.Threading.Tasks; 9 | 10 | namespace FxSshSftpServer.Pages 11 | { 12 | 13 | public partial class Users : ComponentBase 14 | { 15 | [Inject] 16 | public ISettingsRepository settingsRepository { get; set; } 17 | public List AllUsers { get; set; } 18 | 19 | public User SelectedUser { get; set; } 20 | 21 | public string NewUsername { get; set; } 22 | 23 | public bool SelectedUserNotSaved { get; set; } 24 | public string newpassword { get; set; } 25 | 26 | public string InfoText { get; set; } 27 | public bool usekeyfile { get; set; } 28 | 29 | protected override void OnAfterRender(bool firstRender) 30 | { 31 | if (firstRender) 32 | { 33 | AllUsers = settingsRepository.GetAllUsers(); 34 | _ = InvokeAsync(StateHasChanged); 35 | } 36 | } 37 | 38 | void OnUserSelected() 39 | { 40 | SelectedUserNotSaved = false; 41 | 42 | if (!string.IsNullOrWhiteSpace(SelectedUser.RsaPublicKey)) 43 | usekeyfile = true; 44 | else 45 | usekeyfile = false; 46 | 47 | InfoText = ""; 48 | } 49 | 50 | private void LoadUser(string username) 51 | { 52 | SelectedUser = settingsRepository.GetUser(username); 53 | SelectedUserNotSaved = false; 54 | } 55 | private void RemoveUser() 56 | { 57 | var removesuccess = settingsRepository.RemoveUser(SelectedUser.Username); 58 | 59 | if (removesuccess) 60 | { 61 | InfoText = $"User {SelectedUser.Username} was deleted"; 62 | SelectedUser = null; 63 | AllUsers = settingsRepository.GetAllUsers(); 64 | 65 | } 66 | else 67 | InfoText = $"Error deleting user {SelectedUser.Username}"; 68 | 69 | _ = InvokeAsync(StateHasChanged); 70 | } 71 | 72 | private async Task CreateKeyForUser() 73 | { 74 | var csp = new System.Security.Cryptography.RSACryptoServiceProvider(1024); 75 | 76 | var rsakeyparam = csp.ExportParameters(true); 77 | 78 | var privkey = RSAConverter.ExportPrivateKey(csp); 79 | //var publicKey = RSAConverter.ExportPublicKey(csp); 80 | 81 | var publickeybytes = csp.ExportCspBlob(false); 82 | 83 | var publicKey = Convert.ToBase64String(publickeybytes); 84 | 85 | SelectedUser.RsaPublicKey = publicKey; 86 | 87 | var rsakeydata = System.Text.Encoding.UTF8.GetBytes(privkey); 88 | 89 | await JSRuntime.SaveFile(rsakeydata, $"{SelectedUser.Username}.pem"); 90 | SaveUser(); 91 | } 92 | 93 | private void CreateUser() 94 | { 95 | SelectedUser = new User(); 96 | SelectedUser.Username = NewUsername; 97 | AllUsers.Add(SelectedUser); 98 | SelectedUserNotSaved = true; 99 | usekeyfile = false; 100 | // HostedServer.settingsrepo.AddUser(new User() { Username = NewUsername }); 101 | } 102 | 103 | void OnChangeKeyfileOrPassword(bool? value) 104 | { 105 | _ = InvokeAsync(StateHasChanged); 106 | } 107 | private void SaveUser() 108 | { 109 | if (!string.IsNullOrWhiteSpace(newpassword)) 110 | { 111 | var sha256 = new SHA256CryptoServiceProvider(); 112 | var pwhashed = sha256.ComputeHash(System.Text.Encoding.ASCII.GetBytes(newpassword)); 113 | var base64encoded = Convert.ToBase64String(pwhashed); 114 | SelectedUser.HashedPassword = base64encoded; 115 | } 116 | 117 | if (SelectedUserNotSaved) 118 | { 119 | settingsRepository.AddUser(SelectedUser); 120 | InfoText = $"User {SelectedUser.Username} was created"; 121 | } 122 | else 123 | { 124 | settingsRepository.UpdateUser(SelectedUser); 125 | InfoText = $"Settings for user {SelectedUser.Username} were updated"; 126 | } 127 | } 128 | } 129 | 130 | } 131 | 132 | -------------------------------------------------------------------------------- /SshServerModule/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace FxSsh.SshServerModule 7 | { 8 | // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-connect-11#page-10 9 | // https://assets.ctfassets.net/0lvk5dbamxpi/6jBxT5LDgMqutNK4mPTGKd/4fa27cb4a130bca3b48a10c9045b0497/draft-ietf-secsh-filexfer-02 10 | 11 | public enum RequestPacketType 12 | { 13 | SSH_FXP_INIT = 1, 14 | SSH_FXP_VERSION = 2, 15 | SSH_FXP_OPEN = 3, 16 | SSH_FXP_CLOSE = 4, 17 | SSH_FXP_READ = 5, 18 | SSH_FXP_WRITE = 6, 19 | SSH_FXP_LSTAT = 7, 20 | SSH_FXP_FSTAT = 8, 21 | SSH_FXP_SETSTAT = 9, 22 | SSH_FXP_FSETSTAT = 10, 23 | SSH_FXP_OPENDIR = 11, 24 | SSH_FXP_READDIR = 12, 25 | SSH_FXP_REMOVE = 13, 26 | SSH_FXP_MKDIR = 14, 27 | SSH_FXP_RMDIR = 15, 28 | SSH_FXP_REALPATH = 16, 29 | SSH_FXP_STAT = 17, 30 | SSH_FXP_RENAME = 18, 31 | SSH_FXP_READLINK = 19, 32 | SSH_FXP_SYMLINK = 20, 33 | SSH_FXP_UNKNOWN = 100, 34 | SSH_FXP_STATUS = 101, 35 | SSH_FXP_HANDLE = 102, 36 | SSH_FXP_DATA = 103, 37 | SSH_FXP_NAME = 104, 38 | SSH_FXP_ATTRS = 105, 39 | SSH_FXP_EXTENDED = 200, 40 | SSH_FXP_EXTENDED_REPLY = 201 41 | } 42 | 43 | //public enum SftpEvents 44 | //{ 45 | // REALPATH, STAT, LSTAT, FSTAT, 46 | // OPENDIR, CLOSE, REMOVE, READDIR, 47 | // OPEN, READ, WRITE, RENAME, 48 | // MKDIR, RMDIR 49 | //} 50 | 51 | public enum SftpStatusType 52 | { 53 | SSH_FX_OK = 0, 54 | SSH_FX_EOF = 1, 55 | SSH_FX_NO_SUCH_FILE = 2, 56 | SSH_FX_PERMISSION_DENIED = 3, 57 | SSH_FX_FAILURE = 4, 58 | SSH_FX_BAD_MESSAGE = 5, 59 | SSH_FX_NO_CONNECTION = 6, 60 | SSH_FX_CONNECTION_LOST = 7, 61 | SSH_FX_OP_UNSUPPORTED = 8, // version 3 - last one 62 | SSH_FX_INVALID_HANDLE = 9, 63 | SSH_FX_NO_SUCH_PATH = 10, 64 | SSH_FX_FILE_ALREADY_EXISTS = 11, 65 | SSH_FX_WRITE_PROTECT = 12, 66 | SSH_FX_NO_MEDIA = 13, 67 | SSH_FX_NO_SPACE_ON_FILESYSTEM = 14, 68 | SSH_FX_QUOTA_EXCEEDED = 15, 69 | SSH_FX_UNKNOWN_PRINCIPAL = 16, 70 | SSH_FX_LOCK_CONFLICT = 17, 71 | SSH_FX_DIR_NOT_EMPTY = 18, 72 | SSH_FX_NOT_A_DIRECTORY = 19, 73 | SSH_FX_INVALID_FILENAME = 20, 74 | SSH_FX_LINK_LOOP = 21, 75 | SSH_FX_CANNOT_DELETE = 22, 76 | SSH_FX_INVALID_PARAMETER = 23, 77 | SSH_FX_FILE_IS_A_DIRECTORY = 24, 78 | SSH_FX_BYTE_RANGE_LOCK_CONFLICT = 25, 79 | SSH_FX_BYTE_RANGE_LOCK_REFUSED = 26, 80 | SSH_FX_DELETE_PENDING = 27, 81 | SSH_FX_FILE_CORRUPT = 28, 82 | SSH_FX_OWNER_INVALID = 29, 83 | SSH_FX_GROUP_INVALID = 30, 84 | SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31 85 | } 86 | 87 | public enum FileServerProtocol 88 | { 89 | Sftp = 1, 90 | Scp = 2, 91 | Shell = 3, 92 | Tunneling = 4, 93 | } 94 | 95 | public enum FileServerAction 96 | { 97 | OpenDirectory = 1, 98 | CreateDirectory = 2, 99 | DeleteDirectory = 3, 100 | OpenFile = 4, 101 | DeleteFile = 5, 102 | GetItemInfo = 6, 103 | SetItemInfo = 7, 104 | } 105 | 106 | [Flags] 107 | public enum FileSystemOperation : long 108 | { 109 | /// 110 | /// Open file for reading. Get flags for file / directory. 111 | /// 112 | Read = 1, 113 | /// 114 | /// Open file for writing. Set flags for file / directory. 115 | /// 116 | Write = 2, 117 | /// Create file or directory. 118 | Create = 4, 119 | /// Delete file or directory. 120 | Delete = 8, 121 | /// List content of directory. 122 | List = 16, // 0x0000000000000010 123 | /// All operations. 124 | All = 2047, // 0x00000000000007FF 125 | } 126 | 127 | [Flags] 128 | public enum FileOrDirAttributeType : byte 129 | { 130 | SSH_FILEXFER_TYPE_REGULAR = 1, 131 | SSH_FILEXFER_TYPE_DIRECTORY = 2, 132 | SSH_FILEXFER_TYPE_SYMLINK = 3, 133 | SSH_FILEXFER_TYPE_SPECIAL = 4, 134 | SSH_FILEXFER_TYPE_UNKNOWN = 5, 135 | SSH_FILEXFER_TYPE_SOCKET = 6, 136 | SSH_FILEXFER_TYPE_CHAR_DEVICE = 7, 137 | SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8, 138 | SSH_FILEXFER_TYPE_FIFO = 9 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /FxSsh/Services/UserauthService.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Messages; 2 | using FxSsh.Messages.Userauth; 3 | using System; 4 | using System.Diagnostics.Contracts; 5 | using System.Reflection; 6 | 7 | namespace FxSsh.Services 8 | { 9 | public class UserAuthService : SshService, IDynamicInvoker 10 | { 11 | public UserAuthService(Session session) 12 | : base(session) 13 | { 14 | } 15 | 16 | public event EventHandler UserAuth; 17 | 18 | public event EventHandler Succeed; 19 | 20 | 21 | protected internal override void CloseService() 22 | { 23 | } 24 | 25 | internal void HandleMessageCore(UserauthServiceMessage message) 26 | { 27 | Contract.Requires(message != null); 28 | 29 | typeof(UserAuthService) 30 | .GetMethod("HandleMessage", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { message.GetType() }, null) 31 | .Invoke(this, new[] { message }); 32 | } 33 | 34 | private void HandleMessage(RequestMessage message) 35 | { 36 | switch (message.MethodName) 37 | { 38 | case "publickey": 39 | var keyMsg = Message.LoadFrom(message); 40 | HandleMessage(keyMsg); 41 | break; 42 | case "password": 43 | var pswdMsg = Message.LoadFrom(message); 44 | HandleMessage(pswdMsg); 45 | break; 46 | case "hostbased": 47 | case "none": 48 | default: 49 | _session.SendMessage(new FailureMessage()); 50 | break; 51 | } 52 | } 53 | 54 | 55 | private void HandleMessage(PasswordRequestMessage message) 56 | { 57 | var verifed = false; 58 | 59 | var args = new PasswordUserAuthArgs(_session, message.Username, message.Password); 60 | if (UserAuth != null) 61 | { 62 | UserAuth(this, args); 63 | verifed = args.Result; 64 | } 65 | 66 | if (verifed) 67 | { 68 | _session.RegisterService(message.ServiceName, args); 69 | 70 | Succeed?.Invoke(this, message.ServiceName); 71 | 72 | _session.SendMessage(new SuccessMessage()); 73 | return; 74 | } 75 | else 76 | { 77 | _session.SendMessage(new FailureMessage()); 78 | // throw new SshConnectionException("Authentication fail.", DisconnectReason.NoMoreAuthMethodsAvailable); 79 | } 80 | } 81 | 82 | private void HandleMessage(PublicKeyRequestMessage message) 83 | { 84 | if (Session._publicKeyAlgorithms.ContainsKey(message.KeyAlgorithmName)) 85 | { 86 | var verifed = false; 87 | 88 | var keyAlg = Session._publicKeyAlgorithms[message.KeyAlgorithmName](null); 89 | keyAlg.LoadKeyAndCertificatesData(message.PublicKey); 90 | 91 | var args = new PKUserAuthArgs(base._session, message.Username, message.KeyAlgorithmName, keyAlg.GetFingerprint(), message.PublicKey); 92 | UserAuth?.Invoke(this, args); 93 | verifed = args.Result; 94 | 95 | if (!verifed) 96 | { 97 | _session.SendMessage(new FailureMessage()); 98 | return; 99 | } 100 | 101 | if (!message.HasSignature) 102 | { 103 | _session.SendMessage(new PublicKeyOkMessage { KeyAlgorithmName = message.KeyAlgorithmName, PublicKey = message.PublicKey }); 104 | return; 105 | } 106 | 107 | var sig = keyAlg.GetSignature(message.Signature); 108 | 109 | using (var worker = new SshDataWorker()) 110 | { 111 | worker.WriteBinary(_session.SessionId); 112 | worker.Write(message.PayloadWithoutSignature); 113 | 114 | verifed = keyAlg.VerifyData(worker.ToByteArray(), sig); 115 | } 116 | 117 | if (verifed && UserAuth != null) 118 | { 119 | UserAuth(this, args); 120 | verifed = args.Result; 121 | } 122 | 123 | if (verifed) 124 | { 125 | _session.RegisterService(message.ServiceName, args); 126 | Succeed?.Invoke(this, message.ServiceName); 127 | _session.SendMessage(new SuccessMessage()); 128 | return; 129 | } 130 | else 131 | { 132 | _session.SendMessage(new FailureMessage()); 133 | //throw new SshConnectionException("Authentication fail.", DisconnectReason.NoMoreAuthMethodsAvailable); 134 | } 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /FreeSftpSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34728.123 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxSsh", "FxSsh\FxSsh.csproj", "{E6C351AE-02D6-4145-931D-FCBCA37986C4}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SshServerModule", "SshServerModule\SshServerModule.csproj", "{B04031B6-8B5D-4774-8839-466933EBEDBE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestClient", "TestClient\TestClient.csproj", "{1EC2331C-7AFA-450F-BC7D-5DD07BFC566A}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FxSshSftpServer", "FxSshSftpServer\FxSshSftpServer.csproj", "{ACF087F2-C2BF-4F55-BBB4-A7E69FE8539C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerConsoleApp", "ServerConsoleApp\ServerConsoleApp.csproj", "{881A4234-A466-4E05-8D2F-00D45563C9C1}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SshServer.Interfaces", "SshServer.Interfaces\SshServer.Interfaces.csproj", "{2B4BF3CA-6AB1-4A42-B115-68D760068137}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SshServer.Filesystem.LocalDisk", "SshServer.Filesystem.LocalDisk\SshServer.Filesystem.LocalDisk.csproj", "{BA86E8ED-32FE-460B-A497-660711E93FBD}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SshServer.Settings.Sql", "SshServer.Settings.Sql\SshServer.Settings.Sql.csproj", "{3D5D9B6B-174C-478E-8140-228EFCD28370}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SshServer.Settings.LiteDb", "SshServer.Settings.LiteDb\SshServer.Settings.LiteDb.csproj", "{7B1C1AC4-D9AD-45E4-B017-0879FA71363E}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {E6C351AE-02D6-4145-931D-FCBCA37986C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E6C351AE-02D6-4145-931D-FCBCA37986C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E6C351AE-02D6-4145-931D-FCBCA37986C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E6C351AE-02D6-4145-931D-FCBCA37986C4}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {B04031B6-8B5D-4774-8839-466933EBEDBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {B04031B6-8B5D-4774-8839-466933EBEDBE}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {B04031B6-8B5D-4774-8839-466933EBEDBE}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {B04031B6-8B5D-4774-8839-466933EBEDBE}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {1EC2331C-7AFA-450F-BC7D-5DD07BFC566A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {1EC2331C-7AFA-450F-BC7D-5DD07BFC566A}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {1EC2331C-7AFA-450F-BC7D-5DD07BFC566A}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {1EC2331C-7AFA-450F-BC7D-5DD07BFC566A}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {ACF087F2-C2BF-4F55-BBB4-A7E69FE8539C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {ACF087F2-C2BF-4F55-BBB4-A7E69FE8539C}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {ACF087F2-C2BF-4F55-BBB4-A7E69FE8539C}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {ACF087F2-C2BF-4F55-BBB4-A7E69FE8539C}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {881A4234-A466-4E05-8D2F-00D45563C9C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {881A4234-A466-4E05-8D2F-00D45563C9C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {881A4234-A466-4E05-8D2F-00D45563C9C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {881A4234-A466-4E05-8D2F-00D45563C9C1}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {2B4BF3CA-6AB1-4A42-B115-68D760068137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {2B4BF3CA-6AB1-4A42-B115-68D760068137}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {2B4BF3CA-6AB1-4A42-B115-68D760068137}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {2B4BF3CA-6AB1-4A42-B115-68D760068137}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {BA86E8ED-32FE-460B-A497-660711E93FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {BA86E8ED-32FE-460B-A497-660711E93FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {BA86E8ED-32FE-460B-A497-660711E93FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {BA86E8ED-32FE-460B-A497-660711E93FBD}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {3D5D9B6B-174C-478E-8140-228EFCD28370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {3D5D9B6B-174C-478E-8140-228EFCD28370}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {3D5D9B6B-174C-478E-8140-228EFCD28370}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {3D5D9B6B-174C-478E-8140-228EFCD28370}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {7B1C1AC4-D9AD-45E4-B017-0879FA71363E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {7B1C1AC4-D9AD-45E4-B017-0879FA71363E}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {7B1C1AC4-D9AD-45E4-B017-0879FA71363E}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {7B1C1AC4-D9AD-45E4-B017-0879FA71363E}.Release|Any CPU.Build.0 = Release|Any CPU 66 | EndGlobalSection 67 | GlobalSection(SolutionProperties) = preSolution 68 | HideSolutionNode = FALSE 69 | EndGlobalSection 70 | GlobalSection(ExtensibilityGlobals) = postSolution 71 | SolutionGuid = {BC80F45C-E623-4F1E-AE8E-CF6B9C296BC6} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /SshServerModule/Services/Permissions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace SshServerModule.Services; 11 | 12 | internal class Permissions 13 | { 14 | #pragma warning disable IDE1006 // Naming Styles 15 | #pragma warning disable SA1310 // Field names should not contain underscore 16 | internal const uint S_IFMT = 0xF000; // bitmask for the file type bitfields 17 | internal const uint S_IFSOCK = 0xC000; // socket 18 | internal const uint S_IFLNK = 0xA000; // symbolic link 19 | internal const uint S_IFREG = 0x8000; // regular file 20 | internal const uint S_IFBLK = 0x6000; // block device 21 | internal const uint S_IFDIR = 0x4000; // directory 22 | internal const uint S_IFCHR = 0x2000; // character device 23 | internal const uint S_IFIFO = 0x1000; // FIFO 24 | internal const uint S_ISUID = 0x0800; // set UID bit 25 | internal const uint S_ISGID = 0x0400; // set-group-ID bit (see below) 26 | internal const uint S_ISVTX = 0x0200; // sticky bit (see below) 27 | internal const uint S_IRUSR = 0x0100; // owner has read permission 28 | internal const uint S_IWUSR = 0x0080; // owner has write permission 29 | internal const uint S_IXUSR = 0x0040; // owner has execute permission 30 | internal const uint S_IRGRP = 0x0020; // group has read permission 31 | internal const uint S_IWGRP = 0x0010; // group has write permission 32 | internal const uint S_IXGRP = 0x0008; // group has execute permission 33 | internal const uint S_IROTH = 0x0004; // others have read permission 34 | internal const uint S_IWOTH = 0x0002; // others have write permission 35 | internal const uint S_IXOTH = 0x0001; // others have execute permission 36 | #pragma warning restore SA1310 // Field names should not contain underscore 37 | #pragma warning restore IDE1006 // Naming Styles 38 | 39 | public Permissions(short mode, bool isDirectory) 40 | { 41 | 42 | if (mode is < 0 or > 999) 43 | { 44 | throw new ArgumentOutOfRangeException(nameof(mode)); 45 | } 46 | 47 | var modeBytes = mode.ToString(CultureInfo.InvariantCulture).PadLeft(3, '0').ToCharArray(); 48 | 49 | var permission = ((modeBytes[0] & 0x0F) * 8 * 8) + ((modeBytes[1] & 0x0F) * 8) + (modeBytes[2] & 0x0F); 50 | 51 | OwnerCanRead = (permission & S_IRUSR) == S_IRUSR; 52 | OwnerCanWrite = (permission & S_IWUSR) == S_IWUSR; 53 | OwnerCanExecute = (permission & S_IXUSR) == S_IXUSR; 54 | 55 | GroupCanRead = (permission & S_IRGRP) == S_IRGRP; 56 | GroupCanWrite = (permission & S_IWGRP) == S_IWGRP; 57 | GroupCanExecute = (permission & S_IXGRP) == S_IXGRP; 58 | 59 | OthersCanRead = (permission & S_IROTH) == S_IROTH; 60 | OthersCanWrite = (permission & S_IWOTH) == S_IWOTH; 61 | OthersCanExecute = (permission & S_IXOTH) == S_IXOTH; 62 | 63 | 64 | if (isDirectory) 65 | { 66 | IsRegularFile = false; 67 | IsDirectory = true; 68 | } 69 | else 70 | { 71 | IsRegularFile = true; 72 | IsDirectory = false; 73 | } 74 | } 75 | 76 | 77 | internal uint PermissionsAsUint 78 | { 79 | get 80 | { 81 | uint permission = 0; 82 | 83 | if (IsRegularFile) 84 | { 85 | permission |= S_IFREG; 86 | } 87 | if (IsDirectory) 88 | { 89 | permission |= S_IFDIR; 90 | } 91 | if (OwnerCanRead) 92 | { 93 | permission |= S_IRUSR; 94 | } 95 | if (OwnerCanWrite) 96 | { 97 | permission |= S_IWUSR; 98 | } 99 | if (OwnerCanExecute) 100 | { 101 | permission |= S_IXUSR; 102 | } 103 | if (GroupCanRead) 104 | { 105 | permission |= S_IRGRP; 106 | } 107 | if (GroupCanWrite) 108 | { 109 | permission |= S_IWGRP; 110 | } 111 | if (GroupCanExecute) 112 | { 113 | permission |= S_IXGRP; 114 | } 115 | if (OthersCanRead) 116 | { 117 | permission |= S_IROTH; 118 | } 119 | if (OthersCanWrite) 120 | { 121 | permission |= S_IWOTH; 122 | } 123 | if (OthersCanExecute) 124 | { 125 | permission |= S_IXOTH; 126 | } 127 | return permission; 128 | } 129 | } 130 | 131 | 132 | public bool OwnerCanRead { get; set; } 133 | public bool OwnerCanWrite { get; set; } 134 | public bool OwnerCanExecute { get; set; } 135 | public bool GroupCanRead { get; set; } 136 | public bool GroupCanWrite { get; set; } 137 | public bool GroupCanExecute { get; set; } 138 | public bool OthersCanRead { get; set; } 139 | public bool OthersCanWrite { get; set; } 140 | public bool OthersCanExecute { get; set; } 141 | public bool IsRegularFile { get; set; } 142 | public bool IsDirectory { get; set; } 143 | } 144 | -------------------------------------------------------------------------------- /FxSsh/SshServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Threading.Tasks; 7 | 8 | namespace FxSsh 9 | { 10 | public class SshServer : IDisposable 11 | { 12 | private readonly object _lock = new object(); 13 | private readonly List sessions = new List(); 14 | private readonly Dictionary hostKeys = new Dictionary(); 15 | private bool _isDisposed; 16 | private bool _started; 17 | private TcpListener _listener = null; 18 | 19 | public SshServer() 20 | : this(new SshServerSettings()) 21 | { } 22 | 23 | public SshServer(SshServerSettings settings) 24 | { 25 | Contract.Requires(settings != null); 26 | 27 | ServerSettings = settings; 28 | } 29 | 30 | public SshServerSettings ServerSettings { get; private set; } 31 | 32 | public event EventHandler ConnectionAccepted; 33 | 34 | public event EventHandler ExceptionRasied; 35 | 36 | public void Start() 37 | { 38 | lock (_lock) 39 | { 40 | CheckDisposed(); 41 | if (_started) 42 | throw new InvalidOperationException("The server is already started."); 43 | 44 | _listener = ServerSettings.LocalAddress == IPAddress.IPv6Any 45 | ? TcpListener.Create(ServerSettings.Port) // dual stack 46 | : new TcpListener(ServerSettings.LocalAddress, ServerSettings.Port); 47 | _listener.ExclusiveAddressUse = false; 48 | _listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); 49 | _listener.Start(); 50 | BeginAcceptSocket(); 51 | 52 | _started = true; 53 | } 54 | } 55 | 56 | public void Stop() 57 | { 58 | lock (_lock) 59 | { 60 | CheckDisposed(); 61 | if (!_started) 62 | throw new InvalidOperationException("The server is not started."); 63 | 64 | _listener.Stop(); 65 | 66 | _isDisposed = true; 67 | _started = false; 68 | 69 | try 70 | { 71 | foreach (var session in sessions) 72 | { 73 | try 74 | { 75 | session.Disconnect(); 76 | } 77 | catch 78 | { 79 | } 80 | } 81 | } 82 | catch 83 | { 84 | } 85 | } 86 | } 87 | 88 | public void AddHostKey(string type, string key) 89 | { 90 | Contract.Requires(type != null); 91 | Contract.Requires(key != null); 92 | 93 | if (!hostKeys.ContainsKey(type)) 94 | hostKeys.Add(type, key); 95 | } 96 | 97 | private void BeginAcceptSocket() 98 | { 99 | try 100 | { 101 | _listener.BeginAcceptSocket(AcceptSocket, null); 102 | } 103 | catch (ObjectDisposedException) 104 | { 105 | return; 106 | } 107 | catch 108 | { 109 | if (_started) 110 | BeginAcceptSocket(); 111 | } 112 | } 113 | 114 | private void AcceptSocket(IAsyncResult ar) 115 | { 116 | try 117 | { 118 | var socket = _listener.EndAcceptSocket(ar); 119 | 120 | 121 | Task.Run(() => 122 | { 123 | var session = new Session(socket, hostKeys, ServerSettings.ServerBanner, ServerSettings.IdleTimeout); 124 | session.Disconnected += (ss, ee) => { lock (_lock) 125 | 126 | sessions.Remove(session); 127 | }; 128 | lock (_lock) 129 | sessions.Add(session); 130 | try 131 | { 132 | ConnectionAccepted?.Invoke(this, session); 133 | session.EstablishConnection(); 134 | } 135 | catch (SshConnectionException ex) 136 | { 137 | session.Disconnect(ex.DisconnectReason, ex.Message); 138 | ExceptionRasied?.Invoke(this, ex); 139 | } 140 | catch (Exception ex) 141 | { 142 | session.Disconnect(); 143 | ExceptionRasied?.Invoke(this, ex); 144 | } 145 | }); 146 | } 147 | catch 148 | { 149 | } 150 | finally 151 | { 152 | BeginAcceptSocket(); 153 | } 154 | } 155 | 156 | private void CheckDisposed() 157 | { 158 | if (_isDisposed) 159 | throw new ObjectDisposedException(GetType().FullName); 160 | } 161 | 162 | #region IDisposable 163 | 164 | public void Dispose() 165 | { 166 | lock (_lock) 167 | { 168 | if (_isDisposed) 169 | return; 170 | Stop(); 171 | } 172 | } 173 | 174 | #endregion IDisposable 175 | } 176 | } -------------------------------------------------------------------------------- /FxSsh/SshDataWorker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace FxSsh 7 | { 8 | public class SshDataWorker : IDisposable 9 | { 10 | private readonly MemoryStream _ms; 11 | 12 | public SshDataWorker() 13 | { 14 | _ms = new MemoryStream(512); 15 | } 16 | 17 | public SshDataWorker(byte[] buffer) 18 | { 19 | Contract.Requires(buffer != null); 20 | 21 | _ms = new MemoryStream(buffer); 22 | } 23 | 24 | public long DataAvailable 25 | { 26 | get 27 | { 28 | return _ms.Length - _ms.Position; 29 | } 30 | } 31 | 32 | public void Write(bool value) 33 | { 34 | _ms.WriteByte(value ? (byte)1 : (byte)0); 35 | } 36 | 37 | public void Write(byte value) 38 | { 39 | _ms.WriteByte(value); 40 | } 41 | 42 | public void Write(uint value) 43 | { 44 | var bytes = new[] { (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)(value & 0xFF) }; 45 | _ms.Write(bytes, 0, 4); 46 | } 47 | 48 | public void Write(ulong value) 49 | { 50 | var bytes = new[] { 51 | (byte)(value >> 56), (byte)(value >> 48), (byte)(value >> 40), (byte)(value >> 32), 52 | (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)(value & 0xFF) 53 | }; 54 | _ms.Write(bytes, 0, 8); 55 | } 56 | 57 | public void Write(string str, Encoding encoding) 58 | { 59 | Contract.Requires(str != null); 60 | Contract.Requires(encoding != null); 61 | 62 | var bytes = encoding.GetBytes(str); 63 | WriteBinary(bytes); 64 | } 65 | 66 | public void WriteMpint(byte[] data) 67 | { 68 | Contract.Requires(data != null); 69 | 70 | if (data.Length == 1 && data[0] == 0) 71 | { 72 | Write(new byte[4]); 73 | } 74 | else 75 | { 76 | var length = (uint)data.Length; 77 | var high = ((data[0] & 0x80) != 0); 78 | if (high) 79 | { 80 | Write(length + 1); 81 | Write((byte)0); 82 | Write(data); 83 | } 84 | else 85 | { 86 | Write(length); 87 | Write(data); 88 | } 89 | } 90 | } 91 | 92 | public void Write(byte[] data) 93 | { 94 | Contract.Requires(data != null); 95 | 96 | _ms.Write(data, 0, data.Length); 97 | } 98 | 99 | public void WriteBinary(byte[] buffer) 100 | { 101 | Contract.Requires(buffer != null); 102 | 103 | Write((uint)buffer.Length); 104 | _ms.Write(buffer, 0, buffer.Length); 105 | } 106 | 107 | public void WriteBinary(byte[] buffer, int offset, int count) 108 | { 109 | Contract.Requires(buffer != null); 110 | 111 | Write((uint)count); 112 | _ms.Write(buffer, offset, count); 113 | } 114 | 115 | public bool ReadBoolean() 116 | { 117 | var num = _ms.ReadByte(); 118 | 119 | if (num == -1) 120 | throw new EndOfStreamException(); 121 | return num != 0; 122 | } 123 | 124 | public byte ReadByte() 125 | { 126 | var data = ReadBinary(1); 127 | return data[0]; 128 | } 129 | 130 | public uint ReadUInt32() 131 | { 132 | var data = ReadBinary(4); 133 | return (uint)(data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]); 134 | } 135 | 136 | public ulong ReadUInt64() 137 | { 138 | var data = ReadBinary(8); 139 | return ((ulong)data[0] << 56 | (ulong)data[1] << 48 | (ulong)data[2] << 40 | (ulong)data[3] << 32 | 140 | (ulong)data[4] << 24 | (ulong)data[5] << 16 | (ulong)data[6] << 8 | data[7]); 141 | } 142 | 143 | public string ReadString(Encoding encoding, int length) 144 | { 145 | Contract.Requires(encoding != null); 146 | 147 | var bytes = ReadBinary(length); 148 | return encoding.GetString(bytes); 149 | } 150 | 151 | public string ReadString(Encoding encoding) 152 | { 153 | Contract.Requires(encoding != null); 154 | 155 | var bytes = ReadBinary(); 156 | return encoding.GetString(bytes); 157 | } 158 | 159 | public byte[] ReadMpint() 160 | { 161 | var data = ReadBinary(); 162 | 163 | if (data.Length == 0) 164 | return new byte[1]; 165 | 166 | if (data[0] == 0) 167 | { 168 | var output = new byte[data.Length - 1]; 169 | Array.Copy(data, 1, output, 0, output.Length); 170 | return output; 171 | } 172 | 173 | return data; 174 | } 175 | 176 | public byte[] ReadBinary(int length) 177 | { 178 | var data = new byte[length]; 179 | var bytesRead = _ms.Read(data, 0, length); 180 | 181 | if (bytesRead < length) 182 | return data; 183 | //throw new ArgumentOutOfRangeException("length"); 184 | 185 | return data; 186 | } 187 | 188 | public byte[] ReadBinary() 189 | { 190 | var length = ReadUInt32(); 191 | 192 | return ReadBinary((int)length); 193 | } 194 | 195 | public byte[] ToByteArray() 196 | { 197 | return _ms.ToArray(); 198 | } 199 | 200 | public void Dispose() 201 | { 202 | _ms.Dispose(); 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /SshServer.Settings.LiteDb/SettingsRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using LiteDB; 6 | using SshServer.Interfaces; 7 | 8 | 9 | namespace FxSsh.SshServerModule 10 | { 11 | public class ServerSettingsWithObjectId : ServerSettings 12 | { 13 | public new ObjectId Id { get; set; } 14 | public ServerSettings MapToServerSettings() 15 | { 16 | var user = (ServerSettings)this; 17 | user.Id = this.Id.GetHashCode(); 18 | return user; 19 | } 20 | } 21 | 22 | public class UserWithObjectId : User 23 | { 24 | public new ObjectId Id { get; set; } 25 | 26 | public User MapToUser() 27 | { 28 | var user = (User)this; 29 | user.Id = this.Id.GetHashCode(); 30 | return user; 31 | } 32 | 33 | public UserWithObjectId(User user) 34 | { 35 | this.Username = user.Username; 36 | this.HashedPassword = user.HashedPassword; 37 | this.LastSuccessfulLogin = user.LastSuccessfulLogin; 38 | this.OnlyWhitelistedIps = user.OnlyWhitelistedIps; 39 | this.RsaPublicKey = user.RsaPublicKey; 40 | this.UserRootDirectory = user.UserRootDirectory; 41 | this.WhitelistedIps = user.WhitelistedIps; 42 | this.Id = new ObjectId(); 43 | } 44 | 45 | public UserWithObjectId() 46 | { 47 | } 48 | } 49 | 50 | 51 | public class SettingsRepository : IDisposable, ISettingsRepository 52 | { 53 | public void Dispose() 54 | { 55 | if (db != null) 56 | db.Dispose(); 57 | } 58 | 59 | private LiteDatabase db; 60 | private ILiteCollection serversettingsCollection; 61 | 62 | ServerSettingsWithObjectId _serverSettings { get; set; } 63 | 64 | ServerSettings ISettingsRepository.ServerSettings => _serverSettings.MapToServerSettings(); 65 | 66 | public bool UpdateServerSettings(ServerSettings UpdatedSettings) 67 | { 68 | var serversettings = db.GetCollection("settings"); 69 | 70 | _serverSettings = serversettings.FindOne(s => s.ServerRsaKey != null); 71 | 72 | _serverSettings.BindToAddress = UpdatedSettings.BindToAddress; 73 | _serverSettings.BlacklistedIps = UpdatedSettings.BlacklistedIps; 74 | _serverSettings.EnableCommand = UpdatedSettings.EnableCommand; 75 | _serverSettings.EnableDirectTcpIp = UpdatedSettings.EnableDirectTcpIp; 76 | _serverSettings.IdleTimeout = UpdatedSettings.IdleTimeout; 77 | _serverSettings.ListenToPort = UpdatedSettings.ListenToPort; 78 | _serverSettings.MaxLoginAttemptsBeforeBan = UpdatedSettings.MaxLoginAttemptsBeforeBan; 79 | _serverSettings.ServerRootDirectory = UpdatedSettings.ServerRootDirectory; 80 | _serverSettings.ServerRsaKey = UpdatedSettings.ServerRsaKey; 81 | 82 | return serversettings.Update(_serverSettings); 83 | } 84 | 85 | public User GetUser(string Username) 86 | { 87 | // Get a collection (or create, if doesn't exist) 88 | var usercol = db.GetCollection("users"); 89 | 90 | return usercol.FindOne(x => x.Username == Username); 91 | 92 | } 93 | public bool RemoveUser(string Username) 94 | { 95 | // Get a collection (or create, if doesn't exist) 96 | var usercol = db.GetCollection("users"); 97 | 98 | var usertoremove = usercol.FindOne(x => x.Username == Username); 99 | 100 | return usercol.Delete(usertoremove.Id); 101 | 102 | } 103 | 104 | 105 | public List GetAllUsers() 106 | { 107 | // Get a collection (or create, if doesn't exist) 108 | var usercol = db.GetCollection("users"); 109 | 110 | return usercol.FindAll().ToList().Select(u => u.MapToUser()).ToList(); 111 | 112 | } 113 | 114 | public bool AddUser(User NewUser) 115 | { 116 | // Get a collection (or create, if doesn't exist) 117 | var usercol = db.GetCollection("users"); 118 | 119 | usercol.Insert(new UserWithObjectId(NewUser)); 120 | 121 | return true; 122 | 123 | } 124 | 125 | 126 | public bool UpdateUser(User UpdatedUser) 127 | { 128 | // Get a collection (or create, if doesn't exist) 129 | var usercol = db.GetCollection("users"); 130 | 131 | var user = usercol.FindOne(x => x.Username == UpdatedUser.Username); 132 | 133 | 134 | user.HashedPassword = UpdatedUser.HashedPassword; 135 | user.LastSuccessfulLogin = UpdatedUser.LastSuccessfulLogin; 136 | user.OnlyWhitelistedIps = UpdatedUser.OnlyWhitelistedIps; 137 | user.RsaPublicKey = UpdatedUser.RsaPublicKey; 138 | user.UserRootDirectory = UpdatedUser.UserRootDirectory; 139 | user.WhitelistedIps = UpdatedUser.WhitelistedIps; 140 | return usercol.Update(user); 141 | 142 | } 143 | 144 | public SettingsRepository() 145 | { 146 | db = new LiteDatabase("config.db"); 147 | 148 | serversettingsCollection = db.GetCollection("settings"); 149 | 150 | // Get a collection (or create, if doesn't exist) 151 | var usercol = db.GetCollection("users"); 152 | 153 | // Index users using username property 154 | usercol.EnsureIndex(x => x.Username, true); 155 | 156 | _serverSettings = serversettingsCollection.FindOne(s=>s.ServerRsaKey != null); 157 | 158 | 159 | if (_serverSettings == null) 160 | { 161 | // Insert new settings document (Id will be auto-incremented) 162 | _serverSettings = new ServerSettingsWithObjectId(); 163 | 164 | var csp = new System.Security.Cryptography.RSACryptoServiceProvider(4096); 165 | 166 | var rsakeydata = csp.ExportCspBlob(true); 167 | _serverSettings.ServerRsaKey = Convert.ToBase64String( rsakeydata ); 168 | _serverSettings.ListenToPort = 22; // default port 169 | 170 | serversettingsCollection.Insert(_serverSettings); 171 | 172 | } 173 | 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /FxSsh/Services/Channel.cs: -------------------------------------------------------------------------------- 1 | using FxSsh.Messages.Connection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.Contracts; 5 | using System.Linq; 6 | using System.Threading; 7 | 8 | namespace FxSsh.Services 9 | { 10 | public abstract class Channel 11 | { 12 | protected ConnectionService _connectionService; 13 | protected EventWaitHandle _sendingWindowWaitHandle = new ManualResetEvent(false); 14 | 15 | public Channel(ConnectionService connectionService, 16 | uint clientChannelId, uint clientInitialWindowSize, uint clientMaxPacketSize, 17 | uint serverChannelId) 18 | { 19 | Contract.Requires(connectionService != null); 20 | 21 | _connectionService = connectionService; 22 | 23 | ClientChannelId = clientChannelId; 24 | ClientInitialWindowSize = clientInitialWindowSize; 25 | ClientWindowSize = clientInitialWindowSize; 26 | ClientMaxPacketSize = clientMaxPacketSize; 27 | 28 | ServerChannelId = serverChannelId; 29 | ServerInitialWindowSize = Session.InitialLocalWindowSize; 30 | ServerWindowSize = Session.InitialLocalWindowSize; 31 | ServerMaxPacketSize = Session.LocalChannelDataPacketSize; 32 | } 33 | 34 | public uint ClientChannelId { get; private set; } 35 | public uint ClientInitialWindowSize { get; private set; } 36 | public uint ClientWindowSize { get; protected set; } 37 | public uint ClientMaxPacketSize { get; private set; } 38 | 39 | public uint ServerChannelId { get; private set; } 40 | public uint ServerInitialWindowSize { get; private set; } 41 | public uint ServerWindowSize { get; protected set; } 42 | public uint ServerMaxPacketSize { get; private set; } 43 | 44 | public bool ClientClosed { get; private set; } 45 | public bool ClientMarkedEof { get; private set; } 46 | public bool ServerClosed { get; private set; } 47 | public bool ServerMarkedEof { get; private set; } 48 | 49 | public event EventHandler DataReceived; 50 | public event EventHandler EofReceived; 51 | public event EventHandler CloseReceived; 52 | 53 | public void SendData(byte[] data) 54 | { 55 | Contract.Requires(data != null); 56 | this.SendData(data, 0, data.Length); 57 | } 58 | 59 | public void SendData(ICollection data) 60 | { 61 | Contract.Requires(data != null); 62 | this.SendData(data.ToArray(), 0, data.Count); 63 | } 64 | 65 | public void SendData(byte[] data, int offset, int count) 66 | { 67 | Contract.Requires(data != null); 68 | 69 | if (data.Length == 0) 70 | { 71 | return; 72 | } 73 | 74 | var msg = new ChannelDataMessage(); 75 | msg.RecipientChannel = ClientChannelId; 76 | 77 | var total = (uint)count; 78 | byte[] buf = null; 79 | do 80 | { 81 | var packetSize = Math.Min(Math.Min(ClientWindowSize, ClientMaxPacketSize), total); 82 | if (packetSize == 0) 83 | { 84 | _sendingWindowWaitHandle.WaitOne(); 85 | continue; 86 | } 87 | 88 | if (buf == null || packetSize != buf.Length) 89 | buf = new byte[packetSize]; 90 | 91 | 92 | Array.Copy(data, offset, buf, 0, packetSize); 93 | 94 | msg.Data = buf; 95 | _connectionService._session.SendMessage(msg); 96 | 97 | ClientWindowSize -= packetSize; 98 | total -= packetSize; 99 | offset += (int)packetSize; 100 | } while (total > 0); 101 | } 102 | 103 | public void SendEof() 104 | { 105 | if (ServerMarkedEof) 106 | return; 107 | 108 | ServerMarkedEof = true; 109 | var msg = new ChannelEofMessage { RecipientChannel = ClientChannelId }; 110 | _connectionService._session.SendMessage(msg); 111 | } 112 | 113 | public void SendClose(uint? exitCode = null) 114 | { 115 | if (ServerClosed) 116 | return; 117 | 118 | ServerClosed = true; 119 | if (exitCode.HasValue) 120 | _connectionService._session.SendMessage(new ExitStatusMessage { RecipientChannel = ClientChannelId, ExitStatus = exitCode.Value }); 121 | _connectionService._session.SendMessage(new ChannelCloseMessage { RecipientChannel = ClientChannelId }); 122 | 123 | 124 | CheckBothClosed(); 125 | 126 | 127 | if (CloseReceived != null) 128 | CloseReceived(this, EventArgs.Empty); 129 | } 130 | 131 | internal void OnData(byte[] data) 132 | { 133 | Contract.Requires(data != null); 134 | 135 | ServerAttemptAdjustWindow((uint)data.Length); 136 | 137 | if (DataReceived != null) 138 | DataReceived(this, data); 139 | } 140 | 141 | internal void OnEof() 142 | { 143 | ClientMarkedEof = true; 144 | 145 | if (EofReceived != null) 146 | EofReceived(this, EventArgs.Empty); 147 | } 148 | 149 | internal void OnClose() 150 | { 151 | ClientClosed = true; 152 | 153 | if (CloseReceived != null) 154 | CloseReceived(this, EventArgs.Empty); 155 | 156 | CheckBothClosed(); 157 | } 158 | 159 | internal void ClientAdjustWindow(uint bytesToAdd) 160 | { 161 | ClientWindowSize += bytesToAdd; 162 | 163 | // pulse multithreadings in same time and unsignal until thread switched 164 | // don't try to use AutoResetEvent 165 | _sendingWindowWaitHandle.Set(); 166 | Thread.Sleep(1); 167 | _sendingWindowWaitHandle.Reset(); 168 | } 169 | 170 | private void ServerAttemptAdjustWindow(uint messageLength) 171 | { 172 | ServerWindowSize -= messageLength; 173 | if (ServerWindowSize <= ServerMaxPacketSize) 174 | { 175 | _connectionService._session.SendMessage(new ChannelWindowAdjustMessage 176 | { 177 | RecipientChannel = ClientChannelId, 178 | BytesToAdd = ServerInitialWindowSize - ServerWindowSize 179 | }); 180 | ServerWindowSize = ServerInitialWindowSize; 181 | } 182 | } 183 | 184 | private void CheckBothClosed() 185 | { 186 | if (ClientClosed && ServerClosed) 187 | { 188 | ForceClose(); 189 | } 190 | } 191 | 192 | internal void ForceClose() 193 | { 194 | this.CloseReceived?.Invoke(this, null); 195 | 196 | _connectionService.RemoveChannel(this); 197 | _sendingWindowWaitHandle.Set(); 198 | _sendingWindowWaitHandle.Close(); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /SshServerModule/Services/SftpFileAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SshServerModule.Services; 9 | 10 | /// 11 | /// Contains SFTP file attributes. 12 | /// 13 | public class SftpFileAttributes 14 | { 15 | 16 | 17 | private readonly DateTime _originalLastAccessTimeUtc; 18 | private readonly DateTime _originalLastWriteTimeUtc; 19 | private readonly long _originalSize; 20 | private readonly int _originalUserId; 21 | private readonly int _originalGroupId; 22 | private readonly Permissions _originalPermissions; 23 | private readonly IDictionary _originalExtensions; 24 | 25 | private bool _isBitFiledsBitSet; 26 | private bool _isUIDBitSet; 27 | private bool _isGroupIDBitSet; 28 | private bool _isStickyBitSet; 29 | 30 | internal bool IsLastAccessTimeChanged 31 | { 32 | get { return _originalLastAccessTimeUtc != LastAccessTimeUtc; } 33 | } 34 | 35 | internal bool IsLastWriteTimeChanged 36 | { 37 | get { return _originalLastWriteTimeUtc != LastWriteTimeUtc; } 38 | } 39 | 40 | internal bool IsSizeChanged 41 | { 42 | get { return _originalSize != Size; } 43 | } 44 | 45 | internal bool IsUserIdChanged 46 | { 47 | get { return _originalUserId != UserId; } 48 | } 49 | 50 | internal bool IsGroupIdChanged 51 | { 52 | get { return _originalGroupId != GroupId; } 53 | } 54 | 55 | internal bool IsPermissionsChanged 56 | { 57 | get { return _originalPermissions != Permissions; } 58 | } 59 | 60 | internal bool IsExtensionsChanged 61 | { 62 | get { return _originalExtensions != null && Extensions != null && !_originalExtensions.SequenceEqual(Extensions); } 63 | } 64 | 65 | /// 66 | /// Gets or sets the UTC time the current file or directory was last accessed. 67 | /// 68 | /// 69 | /// The UTC time that the current file or directory was last accessed. 70 | /// 71 | public DateTime LastAccessTimeUtc { get; set; } 72 | 73 | /// 74 | /// Gets or sets the UTC time when the current file or directory was last written to. 75 | /// 76 | /// 77 | /// The UTC time the current file was last written. 78 | /// 79 | public DateTime LastWriteTimeUtc { get; set; } 80 | 81 | /// 82 | /// Gets or sets the size, in bytes, of the current file. 83 | /// 84 | /// 85 | /// The size of the current file in bytes. 86 | /// 87 | public long Size { get; set; } 88 | 89 | /// 90 | /// Gets or sets file user id. 91 | /// 92 | /// 93 | /// File user id. 94 | /// 95 | public int UserId { get; set; } 96 | 97 | /// 98 | /// Gets or sets file group id. 99 | /// 100 | /// 101 | /// File group id. 102 | /// 103 | public int GroupId { get; set; } 104 | internal Permissions Permissions { get; set; } 105 | 106 | /// 107 | /// Gets a value indicating whether file represents a socket. 108 | /// 109 | /// 110 | /// if file represents a socket; otherwise, . 111 | /// 112 | public bool IsSocket { get; private set; } 113 | 114 | /// 115 | /// Gets a value indicating whether file represents a symbolic link. 116 | /// 117 | /// 118 | /// if file represents a symbolic link; otherwise, . 119 | /// 120 | public bool IsSymbolicLink { get; private set; } 121 | 122 | /// 123 | /// Gets a value indicating whether file represents a regular file. 124 | /// 125 | /// 126 | /// if file represents a regular file; otherwise, . 127 | /// 128 | public bool IsRegularFile { get; private set; } 129 | 130 | /// 131 | /// Gets a value indicating whether file represents a block device. 132 | /// 133 | /// 134 | /// if file represents a block device; otherwise, . 135 | /// 136 | public bool IsBlockDevice { get; private set; } 137 | 138 | /// 139 | /// Gets a value indicating whether file represents a directory. 140 | /// 141 | /// 142 | /// if file represents a directory; otherwise, . 143 | /// 144 | public bool IsDirectory { get; private set; } 145 | 146 | /// 147 | /// Gets a value indicating whether file represents a character device. 148 | /// 149 | /// 150 | /// if file represents a character device; otherwise, . 151 | /// 152 | public bool IsCharacterDevice { get; private set; } 153 | 154 | /// 155 | /// Gets a value indicating whether file represents a named pipe. 156 | /// 157 | /// 158 | /// if file represents a named pipe; otherwise, . 159 | /// 160 | public bool IsNamedPipe { get; private set; } 161 | 162 | 163 | 164 | /// 165 | /// Gets the extensions. 166 | /// 167 | /// 168 | /// The extensions. 169 | /// 170 | public IDictionary Extensions { get; private set; } 171 | 172 | private SftpFileAttributes() 173 | { 174 | } 175 | 176 | internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, bool isDirectory, IDictionary extensions) 177 | { 178 | LastAccessTimeUtc = _originalLastAccessTimeUtc = lastAccessTimeUtc; 179 | LastWriteTimeUtc = _originalLastWriteTimeUtc = lastWriteTimeUtc; 180 | Size = _originalSize = size; 181 | UserId = _originalUserId = userId; 182 | GroupId = _originalGroupId = groupId; 183 | Permissions = _originalPermissions = new Permissions(666, isDirectory); 184 | Extensions = _originalExtensions = extensions; 185 | } 186 | 187 | ///// 188 | ///// Sets the permissions. 189 | ///// 190 | ///// The mode. 191 | //public void SetPermissions(short mode) 192 | //{ 193 | // if (mode is < 0 or > 999) 194 | // { 195 | // throw new ArgumentOutOfRangeException(nameof(mode)); 196 | // } 197 | 198 | // var modeBytes = mode.ToString(CultureInfo.InvariantCulture).PadLeft(3, '0').ToCharArray(); 199 | 200 | // var permission = ((modeBytes[0] & 0x0F) * 8 * 8) + ((modeBytes[1] & 0x0F) * 8) + (modeBytes[2] & 0x0F); 201 | 202 | // Permissions.OwnerCanRead = (permission & Permissions.S_IRUSR) == Permissions.S_IRUSR; 203 | // Permissions.OwnerCanWrite = (permission & Permissions.S_IWUSR) == Permissions.S_IWUSR; 204 | // Permissions.OwnerCanExecute = (permission & Permissions.S_IXUSR) == Permissions.S_IXUSR; 205 | 206 | // Permissions.GroupCanRead = (permission & Permissions.S_IRGRP) == Permissions.S_IRGRP; 207 | // Permissions.GroupCanWrite = (permission & Permissions.S_IWGRP) == Permissions.S_IWGRP; 208 | // Permissions.GroupCanExecute = (permission & Permissions.S_IXGRP) == Permissions.S_IXGRP; 209 | 210 | // Permissions.OthersCanRead = (permission & Permissions.S_IROTH) == Permissions.S_IROTH; 211 | // Permissions.OthersCanWrite = (permission & Permissions.S_IWOTH) == Permissions.S_IWOTH; 212 | // Permissions.OthersCanExecute = (permission & Permissions.S_IXOTH) == Permissions.S_IXOTH; 213 | //} 214 | 215 | } 216 | --------------------------------------------------------------------------------