{"id":13496793,"url":"https://github.com/orlandos-nl/Citadel","last_synced_at":"2025-03-28T19:31:07.499Z","repository":{"id":39657129,"uuid":"257619637","full_name":"orlandos-nl/Citadel","owner":"orlandos-nl","description":"SSH Client \u0026 Server framework in Swift","archived":false,"fork":false,"pushed_at":"2024-12-23T12:15:23.000Z","size":571,"stargazers_count":274,"open_issues_count":21,"forks_count":45,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-25T17:20:03.182Z","etag":null,"topics":["server-side-swift","sftp","sftp-client","ssh","ssh-client","ssh-server","ssh-tunnel","swift","swift-nio"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/orlandos-nl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["joannis"]}},"created_at":"2020-04-21T14:20:07.000Z","updated_at":"2025-03-18T07:57:26.000Z","dependencies_parsed_at":"2023-11-18T18:22:06.519Z","dependency_job_id":"773a781e-c0f3-4d86-a824-643ceee4414e","html_url":"https://github.com/orlandos-nl/Citadel","commit_stats":{"total_commits":145,"total_committers":11,"mean_commits":"13.181818181818182","dds":"0.24827586206896557","last_synced_commit":"bbe2e6570782efa430c35f57dc810f982748b913"},"previous_names":[],"tags_count":39,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orlandos-nl%2FCitadel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orlandos-nl%2FCitadel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orlandos-nl%2FCitadel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/orlandos-nl%2FCitadel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/orlandos-nl","download_url":"https://codeload.github.com/orlandos-nl/Citadel/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245507063,"owners_count":20626537,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["server-side-swift","sftp","sftp-client","ssh","ssh-client","ssh-server","ssh-tunnel","swift","swift-nio"],"created_at":"2024-07-31T19:01:59.934Z","updated_at":"2025-03-28T19:31:07.492Z","avatar_url":"https://github.com/orlandos-nl.png","language":"Swift","funding_links":["https://github.com/sponsors/joannis"],"categories":["Swift","HarmonyOS"],"sub_categories":["Windows Manager"],"readme":"\u003ca href=\"https://unbeatable.software\"\u003e\u003cimg src=\"./assets/Citadel.png\" /\u003e\u003c/a\u003e\n\nCitadel is a high level API around [NIOSSH](https://github.com/apple/swift-nio-ssh). It makes NIOSSH accessible and easy to adopt, while providing tools otherwise out-of-scope for NIOSSH.\n\nCitadel is in active development by our team or Swift experts. Get in touch with our [Discord Community](https://discord.gg/H6799jh).\n\nDo you need professional support? We're available at [joannis@unbeatable.software](mailto:joannis@unbeatable.software)\n\n## Client Usage\n\nCitadel's `SSHClient` needs a connection to a SSH server first:\n\n```swift\nlet client = try await SSHClient.connect(\n    host: \"example.com\",\n    authenticationMethod: .passwordBased(username: \"joannis\", password: \"s3cr3t\"),\n    hostKeyValidator: .acceptAnything(), // Please use another validator if at all possible, it's insecure\n    reconnect: .never\n)\n```\n\nUsing that client, we support a couple types of operations:\n\n### Executing Commands\n\nYou can execute a command through SSH using the following code:\n\n```swift\nlet stdout = try await client.executeCommand(\"ls -la ~\")\n```\n\nAdditionally, a maximum responsive response size can be set, and `stderr` can be merged with `stdout` so that the answer contains the content of both streams:\n\n```swift\nlet stdoutAndStderr = try await client.executeCommand(\"ls -la ~\", maxResponseSize: 42, mergeStreams: true)\n```\n\nThe `executeCommand` function accumulated information into a contiguous `ByteBuffer`. This is useful for non-interactive commands such as `cat` and `ls`.\n\nThe `executeCommandPair` function or `executeCommandStream` function can be used to access `stdout` and `stderr` independently. Both functions also accumulate information into contiguous separate `ByteBuffers`.\n\nAn example of how executeCommandPair can be used:\n\n```swift\nlet streams = try await client.executeCommandPair(\"cat /foo/bar.log\")\n\nfor try await blob in answer.stdout {\n    // do something with blob\n}\n\nfor try await blob in answer.stderr {\n    // do something with blob\n}\n```\n\nAn example of how executeCommandStream can be used:\n\n```swift\nlet streams = try await client.executeCommandStream(\"cat /foo/bar.log\")\n\nfor try await event in streams {\n    switch event {\n    case .stdout(let stdout):\n        // do something with stdout\n    case .stderr(let stderr):\n        // do something with stderr\n    }\n}\n```\n\nCitadel currently  expose APIs for streaming into a process' `stdin`. only  withPTY and withTTY.\n\nAn example of how pty model can be used:\n\n```swift\ntry await client.withPTY(\n        SSHChannelRequestEvent.PseudoTerminalRequest(\n            wantReply: true,\n            term: \"xterm\",\n            terminalCharacterWidth: 80,\n            terminalRowHeight: 24,\n            terminalPixelWidth: 0,\n            terminalPixelHeight: 0,\n            terminalModes: .init([.ECHO: 1])\n        ),\n        environment: [SSHChannelRequestEvent.EnvironmentRequest(wantReply: true, name: \"LANG\", value: \"en_US.UTF-8\")]) {\n        \n        ttyOutput, ttyStdinWriter in \n        \n        ...do something...\n}\n```\n\n\n### SFTP Client\n\nTo begin with SFTP, you must instantiate an SFTPClient based on your SSHClient:\n\n```swift\n// Open an SFTP session on the SSH client\nlet sftp = try await client.openSFTP()\n\n// Get the current working directory\nlet cwd = try await sftp.getRealPath(atPath: \".\")\n//Obtain the real path of the directory eg \"/opt/vulscan/.. -\u003e /opt\"\nlet truePath = try await sftp.getRealPath(atPath: \"/opt/vulscan/..\")\n// List the contents of the /etc directory\nlet directoryContents = try await sftp.listDirectory(atPath: \"/etc\")\n\n// Create a directory\ntry await sftp.createDirectory(atPath: \"/etc/custom-folder\")\n\n// Write to a file (using a helper that cleans up the file automatically)\ntry await sftp.withFile(\n    filePath: \"/etc/resolv.conf\",\n    flags: [.read, .write, .forceCreate]\n) { file in\n    try await file.write(ByteBuffer(string: \"Hello, world\", at: 0))\n}\n\n// Read a file\nlet data = try await sftp.withFile(\n    filePath: \"/etc/resolv.conf\",\n    flags: .read\n) { file in\n    try await file.readAll()\n}\n\n// Close the SFTP session\ntry await sftp.close()\n```\n\n### TCP-IP Forwarding (Proxying)\n\n```swift\n// The address that is presented as the locally exposed interface\n// This is purely communicated to the SSH server\nlet address = try SocketAddress(ipAddress: \"fe80::1\", port: 27017)\nlet configuredProxyChannel = try await client.createDirectTCPIPChannel(\n    using: SSHChannelType.DirectTCPIP(\n        targetHost: \"localhost\", // MongoDB host \n        targetPort: 27017, // MongoDB port\n        originatorAddress: address\n    )\n) { proxyChannel in\n  proxyChannel.pipeline.addHandlers(...)\n}\n```\n\nThis will create a channel that is connected to the SSH server, and then forwarded to the target host. This is useful for proxying TCP-IP connections, such as MongoDB, Redis, MySQL, etc.\n\n## Servers\n\nTo use Citadel, first you need to create \u0026 start an SSH server, using your own authentication delegate:\n\n```swift\nimport NIOSSH\nimport Citadel\n\n// Create a custom authentication delegate that uses MongoDB to authenticate users\n// This is just an example, you can use any database you want\n// You can use public key authentication, password authentication, or both.\nstruct MyCustomMongoDBAuthDelegate: NIOSSHServerUserAuthenticationDelegate {\n    let db: MongoKitten.Database\n\n    let supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods = [.password, .publicKey]\n    \n    func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise\u003cNIOSSHUserAuthenticationOutcome\u003e) {\n        responsePromise.completeWithTask {\n            // Authenticate the user\n            guard let user = try await db[User.self].findOne(matching: { user in\n                user.$username == username\n            }) else {\n                // User does not exist\n                return .failure\n            }\n\n            switch request.request {\n            case .hostBased. none:\n                // Not supported\n                return .failure\n            case .publicKey(let publicKey):\n                // Check if the public key is correct\n                guard publicKey.publicKey == user.publicKey else {\n                    return .failure\n                }\n\n                return .success\n            case .password(let request):\n                // Uses Vapor's Bcrypt library to verify the password\n                guard try Bcrypt.verify(request.password, created: user.password) else {\n                    return .failure\n                }\n                \n                return .success\n            }\n        }\n    }\n}\n```\n\nThen, create the server:\n\n```swift\nlet server = try await SSHServer.host(\n    host: \"0.0.0.0\",\n    port: 22,\n    hostKeys: [\n        // This hostkey changes every app boot, it's more practical to use a pre-generated one\n        NIOSSHPrivateKey(ed25519Key: .init())\n    ],\n    authenticationDelegate: MyCustomMongoDBAuthDelegate(db: mongokitten)\n)\n```\n\nThen, enable the SFTP server or allow executing commands. Don't worry, these commands do not target the host system. You can implement filsystem and shell access yourself! So you get to dictate permissions, where it's actually stored, and do any shenanigans you need:\n\n```swift\nserver.enableExec(withDelegate: MyExecDelegate())\nserver.enableSFTP(withDelegate: MySFTPDelegate())\n```\n\nIf you're running the SSHServer from `main.swift` or an `@main` annotated type, make sure that Swift doesn't exit or `deinit` the server.\nA simple solution that is applicable most of the time is to use the server's `closeFuture`.\n\n```swift\ntry await server.closeFuture.get()\n```\n\n### Exec Server\n\nWhen creating a command execution delegate, simply implement the `ExecDelegate` protocol and the following functions:\n\n```swift\nfunc setEnvironmentValue(_ value: String, forKey key: String) async throws\nfunc start(command: String, outputHandler: ExecOutputHandler) async throws -\u003e ExecCommandContext\n```\n\nThe `setEnvironmentValue` function adds an environment variable, which you can pass onto child processes. The `start` command simply executed the command \"in the shell\". How and if you process that command is up to you. The executed `command` is inputted as the first argument, and the second argument (the `ExecOutputHandler`), contains the authenticated user, Pipes for `stdin`, `stdout` and `stderr` as well as some function calls for indicating a process has exited.\n\nWhether you simulate a process, or hook up a real child-process, the requirements are the same. You **must** provide an exit code or throw an error out of the executing function. You can also `fail` on the outputHandler the process using an error. Finally, you'll have to return an `ExecCommandContext` that represents your process. This can receive remote `terminate` signals, or receive a notification that `stdin` was closed through `inputClosed`.\n\n```swift\nimport Foundation\n\n/// A context that represents a process that is being executed.\n/// This can receive remote `terminate` signals, or receive a notification that `stdin` was closed through `inputClosed`.\nstruct ExecProcessContext: ExecCommandContext {\n    let process: Process\n    \n    func terminate() async throws {\n        process.terminate()\n    }\n    \n    func inputClosed() async throws {\n        try process.stdin.close()\n    }\n}\n\n/// An example of a custom ExecDelegate that uses bash as the shell to execute commands\npublic final class MyExecDelegate: ExecDelegate {\n    var environment: [String: String] = [:]\n\n    public func setEnvironmentValue(_ value: String, forKey key: String) async throws {\n        // Set the environment variable\n        environment[key] = value\n    }\n\n    public func start(command: String, outputHandler: ExecOutputHandler) async throws -\u003e ExecCommandContext {\n        // Start the command\n        let process = Process()\n\n        // This uses bash as the shell to execute the command\n        // You can use any shell you want, or even a custom one\n        // This is just an example, you can do whatever you want\n        // as long as you provide an exit code\n        process.executableURL = URL(fileURLWithPath: \"/bin/bash\")\n        process.arguments = [\"-c\", command]\n        process.environment = environment\n        process.standardInput = outputHandler.stdin\n        process.standardOutput = outputHandler.stdout\n        process.standardError = outputHandler.stderr\n        process.terminationHandler = { process in\n            // Send the exit code\n            outputHandler.exit(code: Int(process.terminationStatus))\n        }\n\n        // Start the process\n        try process.run()\n        return ExecProcessContext(process: process)\n    }\n}\n```\n\n### SFTP Servers\n\nWhen you implement SFTP in Citadel, you're responsible for taking care of logistics. Be it through a backing MongoDB store, a real filesystem, or your S3 bucket.\n\n## Helpers\n\nThe most important helper most people need is OpenSSH key parsing. We support extensions on PrivateKey types such as our own `Insecure.RSA.PrivateKey`, as well as existing SwiftCrypto types like `Curve25519.Signing.PrivateKey`:\n\n```swift\n// Parse an OpenSSH RSA private key. This is the same format as the one used by OpenSSH\nlet sshFile = try String(contentsOf: ..)\nlet privateKey = try Insecure.RSA.PrivateKey(sshRsa: sshFile)\n```\n\n## FAQ\n\nIf you can't connect to a server, it's likely that your server uses a deprecated set of algorithms that NIOSSH doesn't support. No worries though, as Citadel does implement these! Don't use these if you don't have to, as they're deprecated for good (security) reasons.\n\n```swift\n// Create a new set of algorithms\nvar algorithms = SSHAlgorithms()\n\nalgorithms.transportProtectionSchemes = .add([\n    AES128CTR.self\n])\n\nalgorithms.keyExchangeAlgorithms = .add([\n    DiffieHellmanGroup14Sha1.self,\n    DiffieHellmanGroup14Sha256.self\n])\n```\n\nYou can then use these in an SSHClient, together with any other potential protocol configuration options:\n\n```swift\n// Connect to the server using the new algorithms and a password-based authentication method\nlet client = try await SSHClient.connect(\n    host: \"example.com\",\n    authenticationMethod: .passwordBased(username: \"joannis\", password: \"s3cr3t\"),\n    hostKeyValidator: .acceptAnything(), // Please use another validator if at all possible, it's insecure\n    reconnect: .never,\n    algorithms: algorithms,\n    protocolOptions: [\n        .maximumPacketSize(1 \u003c\u003c 20)\n    ]\n)\n```\n\nYou can also use `SSHAlgorithms.all` to enable all supported algorithms.\n\n## TODO\n\nA couple of code is held back until further work in SwiftNIO SSH is completed. We're currently working with Apple to resolve these.\n\n- [ ] RSA Authentication (implemented \u0026 supported, but in a [fork of NIOSSH](https://github.com/Joannis/swift-nio-ssh-1/pull/1))\n\n## Contributing\n\nI'm happy to accept ideas and PRs for new API's.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forlandos-nl%2FCitadel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forlandos-nl%2FCitadel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forlandos-nl%2FCitadel/lists"}