Citadel 是围绕 NIOSSH 构建的高级 API。它使 NIOSSH 更易于访问和采用,同时提供了 NIOSSH 范围之外的工具。
Citadel 正由我们的团队或 Swift 专家积极开发中。请通过我们的 Discord 社区 与我们联系。
您需要专业的支持吗?请通过 joannis@unbeatable.software 联系我们
Citadel 的 SSHClient
需要首先连接到 SSH 服务器
let client = try await SSHClient.connect(
host: "example.com",
authenticationMethod: .passwordBased(username: "joannis", password: "s3cr3t"),
hostKeyValidator: .acceptAnything(), // Please use another validator if at all possible, it's insecure
reconnect: .never
)
使用该客户端,我们支持几种类型的操作
您可以使用以下代码通过 SSH 执行命令
let stdout = try await client.executeCommand("ls -la ~")
此外,可以设置最大响应大小,并且 stderr
可以与 stdout
合并,以便答案包含两个流的内容
let stdoutAndStderr = try await client.executeCommand("ls -la ~", maxResponseSize: 42, mergeStreams: true)
executeCommand
函数将信息累积到连续的 ByteBuffer
中。这对于非交互式命令(如 cat
和 ls
)很有用。
executeCommandPair
函数或 executeCommandStream
函数可用于独立访问 stdout
和 stderr
。这两个函数还将信息累积到单独的连续 ByteBuffer
中。
如何使用 executeCommandPair 的示例
let streams = try await client.executeCommandPair("cat /foo/bar.log")
for try await blob in answer.stdout {
// do something with blob
}
for try await blob in answer.stderr {
// do something with blob
}
如何使用 executeCommandStream 的示例
let streams = try await client.executeCommandStream("cat /foo/bar.log")
for try await event in streams {
switch event {
case .stdout(let stdout):
// do something with stdout
case .stderr(let stderr):
// do something with stderr
}
}
Citadel 目前公开了用于流式传输到进程 stdin
的 API。仅适用于 withPTY 和 withTTY。
如何使用 pty 模型的示例
try await client.withPTY(
SSHChannelRequestEvent.PseudoTerminalRequest(
wantReply: true,
term: "xterm",
terminalCharacterWidth: 80,
terminalRowHeight: 24,
terminalPixelWidth: 0,
terminalPixelHeight: 0,
terminalModes: .init([.ECHO: 1])
),
environment: [SSHChannelRequestEvent.EnvironmentRequest(wantReply: true, name: "LANG", value: "en_US.UTF-8")]) {
ttyOutput, ttyStdinWriter in
...do something...
}
要开始使用 SFTP,您必须基于您的 SSHClient 实例化一个 SFTPClient
// Open an SFTP session on the SSH client
let sftp = try await client.openSFTP()
// Get the current working directory
let cwd = try await sftp.getRealPath(atPath: ".")
//Obtain the real path of the directory eg "/opt/vulscan/.. -> /opt"
let truePath = try await sftp.getRealPath(atPath: "/opt/vulscan/..")
// List the contents of the /etc directory
let directoryContents = try await sftp.listDirectory(atPath: "/etc")
// Create a directory
try await sftp.createDirectory(atPath: "/etc/custom-folder")
// Write to a file (using a helper that cleans up the file automatically)
try await sftp.withFile(
filePath: "/etc/resolv.conf",
flags: [.read, .write, .forceCreate]
) { file in
try await file.write(ByteBuffer(string: "Hello, world", at: 0))
}
// Read a file
let data = try await sftp.withFile(
filePath: "/etc/resolv.conf",
flags: .read
) { file in
try await file.readAll()
}
// Close the SFTP session
try await sftp.close()
// The address that is presented as the locally exposed interface
// This is purely communicated to the SSH server
let address = try SocketAddress(ipAddress: "fe80::1", port: 27017)
let configuredProxyChannel = try await client.createDirectTCPIPChannel(
using: SSHChannelType.DirectTCPIP(
targetHost: "localhost", // MongoDB host
targetPort: 27017, // MongoDB port
originatorAddress: address
)
) { proxyChannel in
proxyChannel.pipeline.addHandlers(...)
}
这将创建一个连接到 SSH 服务器的通道,然后转发到目标主机。这对于代理 TCP-IP 连接(例如 MongoDB、Redis、MySQL 等)非常有用。
要使用 Citadel,首先您需要创建并启动一个 SSH 服务器,使用您自己的身份验证委托
import NIOSSH
import Citadel
// Create a custom authentication delegate that uses MongoDB to authenticate users
// This is just an example, you can use any database you want
// You can use public key authentication, password authentication, or both.
struct MyCustomMongoDBAuthDelegate: NIOSSHServerUserAuthenticationDelegate {
let db: MongoKitten.Database
let supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods = [.password, .publicKey]
func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise<NIOSSHUserAuthenticationOutcome>) {
responsePromise.completeWithTask {
// Authenticate the user
guard let user = try await db[User.self].findOne(matching: { user in
user.$username == username
}) else {
// User does not exist
return .failure
}
switch request.request {
case .hostBased. none:
// Not supported
return .failure
case .publicKey(let publicKey):
// Check if the public key is correct
guard publicKey.publicKey == user.publicKey else {
return .failure
}
return .success
case .password(let request):
// Uses Vapor's Bcrypt library to verify the password
guard try Bcrypt.verify(request.password, created: user.password) else {
return .failure
}
return .success
}
}
}
}
然后,创建服务器
let server = try await SSHServer.host(
host: "0.0.0.0",
port: 22,
hostKeys: [
// This hostkey changes every app boot, it's more practical to use a pre-generated one
NIOSSHPrivateKey(ed25519Key: .init())
],
authenticationDelegate: MyCustomMongoDBAuthDelegate(db: mongokitten)
)
然后,启用 SFTP 服务器或允许执行命令。不用担心,这些命令不会针对主机系统。您可以自己实现文件系统和 shell 访问!因此您可以决定权限、实际存储位置,并进行您需要的任何操作
server.enableExec(withDelegate: MyExecDelegate())
server.enableSFTP(withDelegate: MySFTPDelegate())
如果您从 main.swift
或 @main
注释类型运行 SSHServer,请确保 Swift 不会退出或 deinit
服务器。一个适用于大多数情况的简单解决方案是使用服务器的 closeFuture
。
try await server.closeFuture.get()
当创建命令执行委托时,只需实现 ExecDelegate
协议和以下函数
func setEnvironmentValue(_ value: String, forKey key: String) async throws
func start(command: String, outputHandler: ExecOutputHandler) async throws -> ExecCommandContext
setEnvironmentValue
函数添加一个环境变量,您可以将其传递给子进程。start
命令只是“在 shell 中”执行命令。如何以及是否处理该命令取决于您。执行的 command
作为第一个参数输入,第二个参数(ExecOutputHandler
)包含经过身份验证的用户、用于 stdin
、stdout
和 stderr
的管道,以及一些指示进程已退出的函数调用。
无论您是模拟进程还是连接真实的子进程,要求都是相同的。您必须提供退出代码或从执行函数中抛出错误。您也可以使用错误在 outputHandler 上 fail
进程。最后,您必须返回一个代表您的进程的 ExecCommandContext
。它可以接收远程 terminate
信号,或接收 stdin
通过 inputClosed
关闭的通知。
import Foundation
/// A context that represents a process that is being executed.
/// This can receive remote `terminate` signals, or receive a notification that `stdin` was closed through `inputClosed`.
struct ExecProcessContext: ExecCommandContext {
let process: Process
func terminate() async throws {
process.terminate()
}
func inputClosed() async throws {
try process.stdin.close()
}
}
/// An example of a custom ExecDelegate that uses bash as the shell to execute commands
public final class MyExecDelegate: ExecDelegate {
var environment: [String: String] = [:]
public func setEnvironmentValue(_ value: String, forKey key: String) async throws {
// Set the environment variable
environment[key] = value
}
public func start(command: String, outputHandler: ExecOutputHandler) async throws -> ExecCommandContext {
// Start the command
let process = Process()
// This uses bash as the shell to execute the command
// You can use any shell you want, or even a custom one
// This is just an example, you can do whatever you want
// as long as you provide an exit code
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", command]
process.environment = environment
process.standardInput = outputHandler.stdin
process.standardOutput = outputHandler.stdout
process.standardError = outputHandler.stderr
process.terminationHandler = { process in
// Send the exit code
outputHandler.exit(code: Int(process.terminationStatus))
}
// Start the process
try process.run()
return ExecProcessContext(process: process)
}
}
当您在 Citadel 中实现 SFTP 时,您负责处理后勤。无论是通过后端的 MongoDB 存储、真实的文件系统还是您的 S3 存储桶。
大多数人最需要的助手是 OpenSSH 密钥解析。我们支持 PrivateKey 类型的扩展,例如我们自己的 Insecure.RSA.PrivateKey
,以及现有的 SwiftCrypto 类型,如 Curve25519.Signing.PrivateKey
// Parse an OpenSSH RSA private key. This is the same format as the one used by OpenSSH
let sshFile = try String(contentsOf: ..)
let privateKey = try Insecure.RSA.PrivateKey(sshRsa: sshFile)
如果您无法连接到服务器,则可能是您的服务器使用了 NIOSSH 不支持的已弃用算法集。不过不用担心,因为 Citadel 确实实现了这些!如果可以避免,请不要使用这些,因为它们因(安全)原因而被弃用。
// Create a new set of algorithms
var algorithms = SSHAlgorithms()
algorithms.transportProtectionSchemes = .add([
AES128CTR.self
])
algorithms.keyExchangeAlgorithms = .add([
DiffieHellmanGroup14Sha1.self,
DiffieHellmanGroup14Sha256.self
])
然后您可以在 SSHClient 中使用这些算法,以及任何其他可能的协议配置选项
// Connect to the server using the new algorithms and a password-based authentication method
let client = try await SSHClient.connect(
host: "example.com",
authenticationMethod: .passwordBased(username: "joannis", password: "s3cr3t"),
hostKeyValidator: .acceptAnything(), // Please use another validator if at all possible, it's insecure
reconnect: .never,
algorithms: algorithms,
protocolOptions: [
.maximumPacketSize(1 << 20)
]
)
您还可以使用 SSHAlgorithms.all
来启用所有支持的算法。
部分代码被保留,直到 SwiftNIO SSH 中的进一步工作完成。我们目前正在与 Apple 合作解决这些问题。
我很高兴接受关于新 API 的想法和 PR。