此项目包含使用 SwiftNIO 的 SSH 支持。
SwiftNIO SSH 是 SSH 的程序化实现:也就是说,它是一组 API,允许程序员实现 SSH 通信端点。 关键在于,它更像是 libssh2 而不是 openssh。 SwiftNIO SSH 不提供生产就绪的 SSH 客户端和服务器,而是为构建此类客户端和服务器提供构建块。
提供程序化 SSH 实现有许多原因。 其中之一是 SSH 与用户交互的独特关系。 技术用户非常习惯于以交互方式与 SSH 交互,无论是在远程计算机上运行命令还是运行交互式 shell。 能够以编程方式响应这些请求,可以实现有趣的替代交互模式。 作为先前的示例,我们可以指出 Twisted 的 Manhole,它使用 一个名为 conch
的程序化 SSH 实现,以在运行中的 Python 服务器中提供交互式 Python 解释器,或者 ssh-chat,一个提供聊天室而不是常规 SSH shell 功能的 SSH 服务器。 还可以为 TCP 转发设想创新用途。
提供程序化 SSH 的另一个充分理由是,服务通常需要以涉及运行命令的方式与其他服务交互。 虽然 Process
解决了本地用例的问题,但有时需要调用的命令是远程的。 虽然 Process
可以启动 ssh
客户端作为子进程来运行此调用,但直接调用 SSH 可以更直接。 这是 libssh2
的目标用例。 SwiftNIO SSH 提供了 libssh2 的网络和密码学层的等价物,允许有动力的用户直接从 Swift 服务中驱动 SSH 会话。
SwiftNIO SSH 需要 Swift 5.4 及更高版本。 0.3.x 支持 Swift 5.2 和 5.3,0.2.x 支持 Swift 5.1。
SwiftNIO SSH 支持具有以下功能的 SSHv2
SwiftNIO SSH 提供了一个 SwiftNIO ChannelHandler
,NIOSSHHandler
。 此处理程序直接实现 SSH 协议的大部分。 用户不应直接生成 SSH 消息:相反,他们通过子通道和委托与 NIOSSHHandler
交互。
SSH 是一种多路复用协议:每个 SSH 连接都细分为多个双向通信通道,恰如其分地称为通道。 SwiftNIO SSH 通过使用“子通道”抽象来反映这种结构。 当对等方创建新的 SSH 通道时,SwiftNIO SSH 将创建一个新的 NIO Channel
,用于表示该 SSH 通道上的所有流量。 在此子 Channel
中,所有事件都彼此严格排序:但是,不同 Channel
中的事件可能会被实现自由地交错。
因此,活动的 SSH 连接看起来像这样
┌ ─ NIO Channel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌────────────────────────────────┐ │
│ │
│ │ │ │
│ │
│ │ │ │
│ NIOSSHHandler │───────────────────────┐
│ │ │ │ │
│ │ │
│ │ │ │ │
│ │ │
│ └────────────────────────────────┘ │ │
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│
│
│
│
▼
┌── SSH Child Channel ─────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ├───┐
│ │ │ │ │ │ │
│ │ │ │ │ │ ├───┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ User Handler │ │ User Handler │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │
│ │ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │
└───┬──────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
SSH 通道通过通道类型调用。 NIOSSH 支持三种:session
、directTCPIP
和 forwardedTCPIP
。 最常见的通道类型是 session
:session
用于表示程序的调用,无论是特定的命名程序还是 shell。 其他两种通道类型与 TCP 端口转发有关,将在稍后讨论。
SSH 通道对单个数据类型进行操作:SSHChannelData
。 此结构封装了 SSH 支持常规和“扩展”通道数据的事实。 常规通道数据 (SSHChannelData.DataType.channel
) 用于绝大多数核心数据。 在 session
通道中,.channel
数据类型用于标准输入和标准输出:.stdErr
数据类型自然用于标准错误。 在 TCP 转发通道中,.channel
数据类型是唯一使用的数据类型,表示转发的数据。
session
通道表示命令的调用。 通道的具体操作方式通过许多入站用户事件进行通信。 以下事件很重要
SSHChannelRequestEvent.PseudoTerminalRequest
:请求分配伪终端。SSHChannelRequestEvent.EnvironmentRequest
:请求命令调用单个环境变量。 始终在命令本身之前发送。SSHChannelRequestEvent.ShellRequest
:请求要调用的命令是经过身份验证的用户的 shell。SSHChannelRequestEvent.ExecRequest
:请求调用特定命令。SSHChannelRequestEvent.ExitStatus
:用于指示远程命令已退出,并传达退出代码。SSHChannelRequestEvent.ExitSignal
:用于指示远程命令因信号而终止,以及该信号是什么。SSHChannelRequestEvent.SignalRequest
:用于向远程命令发送信号。SSHChannelRequestEvent.LocalFlowControlRequest
:用于指示客户端是否能够自行执行 Ctrl-Q/Ctrl-S 流控制。SSHChannelRequestEvent.WindowChangeRequest
:用于将客户端终端窗口大小的更改传达给分配的伪终端。SSHChannelRequestEvent.SubsystemRequest
:用于请求调用特定子系统。 这的含义特定于各个用例。这些事件在端口转发消息中未使用。 支持 .session
类型通道的 SSH 实现需要准备好以各种方式处理大多数或所有这些事件。
这些事件中的每一个事件也都有一个 wantReply
字段。 这表示请求是否需要回复以指示成功或失败。 如果需要,则使用以下两个事件
ChannelSuccessEvent
,用于传达成功。ChannelFailureEvent
,用于传达失败。SSH 网络协议在其子通道中普遍使用半关闭。 NIO Channel
默认情况下通常禁用半关闭支持,SwiftNIO SSH 也尊重其子通道中的此默认设置。 但是,如果您将此设置保留为默认值,则 SSH 子通道的行为将非常出乎意料。 因此,强烈建议所有子通道都启用半关闭支持
channel.setOption(ChannelOptions.allowRemoteHalfClosure, true)
然后,这使用标准的 NIO 半关闭支持。 远程对等方发送 EOF 将通过入站用户事件 ChannelEvent.inputClosed
进行通信。 要自己发送 EOF,请调用 close(mode: .output)
。
用户身份验证是 SSH 的重要组成部分。 为了管理它,SwiftNIO SSH 使用一对委托协议:NIOSSHClientUserAuthenticationDelegate
和 NIOSSHServerUserAuthenticationDelegate
。 客户端和服务器应提供这些委托协议的实现来管理用户身份验证。
客户端协议很简单:SwiftNIO SSH 将在委托上调用方法 nextAuthenticationType(availableMethods:nextChallengePromise:)
。 availableMethods
将是 NIOSSHAvailableUserAuthenticationMethods
的实例,用于传达服务器已建议哪些身份验证方法是可接受的。 然后,委托可以使用新的身份验证请求或 nil
完成 nextChallengePromise
,以指示客户端已用尽尝试次数。
服务器协议更复杂。 委托必须提供一个 supportedAuthenticationMethods
属性,该属性传达委托支持哪些身份验证方法。 然后,每次客户端发送用户身份验证请求时,都会调用 requestReceived(request:responsePromise:)
方法。 这可能会并行调用多次,因为允许客户端并行发出身份验证请求。 responsePromise
应该以身份验证结果成功完成。 有三个结果:.success
和 .failure
很简单,但原则上服务器可以使用 .partialSuccess(remainingMethods:)
要求多次质询。
直接端口转发是从客户端到服务器的端口转发。 在这种模式下,传统上客户端将侦听本地端口,并将入站连接转发到服务器。 它将要求服务器将这些连接作为出站连接转发到特定的主机和端口。
客户端可以使用 .directTCPIP
通道类型直接打开这些通道。
远程端口转发是一种不太常见的情况,其中客户端要求服务器侦听特定的地址和端口,并将所有入站连接转发到客户端。 由于客户端需要请求此行为,因此它使用全局请求来执行此操作。
全局请求使用 NIOSSHHandler.sendGlobalRequest
发起,并通过 GlobalRequestDelegate
接收和处理。 今天支持两个全局请求
GlobalRequest.TCPForwardingRequest.listen(host:port:)
:请求服务器侦听给定的主机和端口。GlobalRequest.TCPForwardingRequest.cancel(host:port:)
:请求取消侦听给定的主机和端口。服务器可以使用 GlobalRequestDelegate
通知并响应这些请求。 此处要实现的方法是 tcpForwardingRequest(_:handler:promise:)
。 每当收到全局请求时,都会调用此委托方法。 对请求的响应将传递到 promise
中。
然后,使用 .forwardedTCPIP
通道类型从服务器发送到客户端转发的通道。