Skip to content

Fix import guards in NIOSSHServer #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/NIOSSH/GlobalRequestDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import NIOCore
public protocol GlobalRequestDelegate {
/// The client wants to manage TCP port forwarding.
///
/// The eventLoop associated with the promise passed in must be the same as the one used to create the handler.
///
/// The default implementation rejects all requests to establish TCP port forwarding.
func tcpForwardingRequest(
_: GlobalRequest.TCPForwardingRequest,
Expand Down
9 changes: 5 additions & 4 deletions Sources/NIOSSHServer/DataToBufferCodec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand All @@ -21,14 +21,15 @@ import NIOFoundationCompat
import NIOPosix
import NIOSSH

final class DataToBufferCodec: ChannelDuplexHandler {
final class DataToBufferCodec: ChannelDuplexHandler, Sendable {
typealias InboundIn = SSHChannelData
typealias InboundOut = ByteBuffer
typealias OutboundIn = ByteBuffer
typealias OutboundOut = SSHChannelData

func handlerAdded(context: ChannelHandlerContext) {
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).assumeIsolated().whenFailure {
error in
context.fireErrorCaught(error)
}
}
Expand Down Expand Up @@ -60,4 +61,4 @@ func createOutboundConnection(targetHost: String, targetPort: Int, loop: EventLo
}.connect(host: targetHost, port: targetPort)
}

#endif // canImport(Foundation.Process)
#endif
63 changes: 33 additions & 30 deletions Sources/NIOSSHServer/ExecHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand Down Expand Up @@ -40,7 +40,8 @@ final class ExampleExecHandler: ChannelDuplexHandler {
var environment: [String: String] = [:]

func handlerAdded(context: ChannelHandlerContext) {
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).assumeIsolated().whenFailure {
error in
context.fireErrorCaught(error)
}
}
Expand Down Expand Up @@ -90,39 +91,42 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}

private func exec(_ event: SSHChannelRequestEvent.ExecRequest, channel: Channel) {
// Kick this off to a background queue
self.queue.async {
do {
// We're not a shell, so we just do our "best".
let executable = URL(fileURLWithPath: "/usr/local/bin/bash")
let process = Process()
process.executableURL = executable
process.arguments = ["-c", event.command]
process.terminationHandler = { process in
// The process terminated. Check its return code, fire it, and then move on.
let rcode = process.terminationStatus
channel.triggerUserOutboundEvent(SSHChannelRequestEvent.ExitStatus(exitStatus: Int(rcode)))
.whenComplete { _ in
channel.close(promise: nil)
}
// We're not a shell, so we just do our "best".
let executable = URL(fileURLWithPath: "/usr/local/bin/bash")
let process = Process()
process.executableURL = executable
process.arguments = ["-c", event.command]
process.terminationHandler = { process in
// The process terminated. Check its return code, fire it, and then move on.
let rcode = process.terminationStatus
channel.triggerUserOutboundEvent(SSHChannelRequestEvent.ExitStatus(exitStatus: Int(rcode)))
.whenComplete { _ in
channel.close(promise: nil)
}
}

let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()

process.standardInput = inPipe
process.standardOutput = outPipe
process.standardError = errPipe
process.environment = self.environment
let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()

let (ours, theirs) = GlueHandler.matchedPair()
try channel.pipeline.addHandler(ours).wait()
process.standardInput = inPipe
process.standardOutput = outPipe
process.standardError = errPipe
process.environment = self.environment
self.process = process

// Kick this off to a background queue
self.queue.sync {
do {
_ = try NIOPipeBootstrap(group: channel.eventLoop)
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.channelInitializer { pipeChannel in
pipeChannel.pipeline.addHandler(theirs)
pipeChannel.eventLoop.makeCompletedFuture {
let (ours, theirs) = GlueHandler.matchedPair()
try channel.pipeline.syncOperations.addHandler(ours)
try pipeChannel.pipeline.syncOperations.addHandler(theirs)
}

}.takingOwnershipOfDescriptors(
input: dup(outPipe.fileHandleForReading.fileDescriptor),
output: dup(inPipe.fileHandleForWriting.fileDescriptor)
Expand All @@ -149,7 +153,6 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}

try process.run()
self.process = process
} catch {
if event.wantReply {
channel.triggerUserOutboundEvent(ChannelFailureEvent()).whenComplete { _ in
Expand All @@ -163,4 +166,4 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}
}

#endif // canImport(Foundation.Process)
#endif
4 changes: 2 additions & 2 deletions Sources/NIOSSHServer/GlueHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import NIOCore

Expand Down Expand Up @@ -125,4 +125,4 @@ extension GlueHandler: ChannelDuplexHandler {
}
}

#endif // canImport(Foundation.Process)
#endif
57 changes: 32 additions & 25 deletions Sources/NIOSSHServer/RemotePortForwarding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand All @@ -26,16 +26,19 @@ import NIOSSH
// Please note that, as with the rest of this example, there are important security features missing from
// this demo.
final class RemotePortForwarder {
private var serverChannel: Channel?
private let serverChannel: Channel?

private var inboundSSHHandler: NIOSSHHandler
private let inboundSSHHandler: NIOSSHHandler

init(inboundSSHHandler: NIOSSHHandler) {
self.inboundSSHHandler = inboundSSHHandler
self.serverChannel = nil
}

func beginListening(on host: String, port: Int, loop: EventLoop) -> EventLoopFuture<Int?> {
ServerBootstrap(group: loop).serverChannelOption(
let loopBoundHandler = NIOLoopBound(inboundSSHHandler, eventLoop: loop)

return ServerBootstrap(group: loop).serverChannelOption(
ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SocketOptionName(SO_REUSEADDR)),
value: 1
)
Expand All @@ -45,29 +48,33 @@ final class RemotePortForwarder {
value: 1
)
.childChannelInitializer { childChannel in
let (ours, theirs) = GlueHandler.matchedPair()

// Ok, ask for the remote channel to be created. This needs remote half closure turned on and to be
// set up for data I/O.
let promise = loop.makePromise(of: Channel.self)
self.inboundSSHHandler.createChannel(
promise,
channelType: .forwardedTCPIP(
.init(
listeningHost: host,
listeningPort: childChannel.localAddress!.port!,
originatorAddress: childChannel.remoteAddress!
// Great, now we add the glue handler to the newly-accepted channel, and then we don't allow this channel to go
// active until the SSH channel has. Both should go active at once.
childChannel.eventLoop.makeCompletedFuture {
let (ours, theirs) = GlueHandler.matchedPair()

// Ok, ask for the remote channel to be created. This needs remote half closure turned on and to be
// set up for data I/O.
let promise = loop.makePromise(of: Channel.self)
loopBoundHandler.value.createChannel(
promise,
channelType: .forwardedTCPIP(
.init(
listeningHost: host,
listeningPort: childChannel.localAddress!.port!,
originatorAddress: childChannel.remoteAddress!
)
)
)
) { sshChildChannel, _ in
sshChildChannel.pipeline.addHandlers([DataToBufferCodec(), theirs]).flatMap {
sshChildChannel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
) { sshChildChannel, _ in
sshChildChannel.eventLoop.makeCompletedFuture {
try sshChildChannel.pipeline.syncOperations.addHandlers([DataToBufferCodec(), theirs])
_ = sshChildChannel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
}

}
}

// Great, now we add the glue handler to the newly-accepted channel, and then we don't allow this channel to go
// active until the SSH channel has. Both should go active at once.
return childChannel.pipeline.addHandler(ours).flatMap { _ in promise.futureResult }.map { _ in () }
try childChannel.pipeline.syncOperations.addHandler(ours)
}
}
.bind(host: host, port: port).map { channel in
if port == 0 {
Expand Down Expand Up @@ -119,4 +126,4 @@ final class RemotePortForwarderGlobalRequestDelegate: GlobalRequestDelegate {
}
}

#endif // canImport(Foundation.Process)
#endif
61 changes: 34 additions & 27 deletions Sources/NIOSSHServer/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Crypto
import Dispatch
Expand All @@ -22,7 +22,7 @@ import NIOSSH

// This file contains an example NIO SSH server. It's not intended for production use, it's not secure,
// but it's a good example of how to
final class ErrorHandler: ChannelInboundHandler {
final class ErrorHandler: ChannelInboundHandler, Sendable {
typealias InboundIn = Any

func errorCaught(context: ChannelHandlerContext, error: Error) {
Expand Down Expand Up @@ -62,18 +62,26 @@ defer {
func sshChildChannelInitializer(_ channel: Channel, _ channelType: SSHChannelType) -> EventLoopFuture<Void> {
switch channelType {
case .session:
return channel.pipeline.addHandler(ExampleExecHandler())
return channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(ExampleExecHandler())
}
case .directTCPIP(let target):
let (ours, theirs) = GlueHandler.matchedPair()
return channel.eventLoop.makeCompletedFuture {
let (ours, theirs) = GlueHandler.matchedPair()
_ = channel.pipeline.addHandler(DataToBufferCodec())
try channel.pipeline.syncOperations.addHandler(ours)

let loopBoundHandler = NIOLoopBound(theirs, eventLoop: channel.eventLoop)

return channel.pipeline.addHandlers([DataToBufferCodec(), ours]).flatMap {
createOutboundConnection(
_ = createOutboundConnection(
targetHost: target.targetHost,
targetPort: target.targetPort,
loop: channel.eventLoop
)
}.flatMap { targetChannel in
targetChannel.pipeline.addHandler(theirs)
).flatMap { targetChannel in
targetChannel.eventLoop.makeCompletedFuture {
try targetChannel.pipeline.syncOperations.addHandler(loopBoundHandler.value)
}
}
}
case .forwardedTCPIP:
return channel.eventLoop.makeFailedFuture(SSHServerError.invalidChannelType)
Expand All @@ -85,19 +93,22 @@ let hostKey = NIOSSHPrivateKey(ed25519Key: .init())

let bootstrap = ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandlers([
NIOSSHHandler(
role: .server(
.init(
hostKeys: [hostKey],
userAuthDelegate: HardcodedPasswordDelegate(),
globalRequestDelegate: RemotePortForwarderGlobalRequestDelegate()
)
),
allocator: channel.allocator,
inboundChildChannelInitializer: sshChildChannelInitializer(_:_:)
), ErrorHandler(),
])
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandlers([
NIOSSHHandler(
role: .server(
.init(
hostKeys: [hostKey],
userAuthDelegate: HardcodedPasswordDelegate(),
globalRequestDelegate: RemotePortForwarderGlobalRequestDelegate()
)
),
allocator: channel.allocator,
inboundChildChannelInitializer: sshChildChannelInitializer(_:_:)
), ErrorHandler(),
])
}

}
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
Expand All @@ -107,8 +118,4 @@ let channel = try bootstrap.bind(host: "0.0.0.0", port: 2222).wait()
// Run forever
try channel.closeFuture.wait()

#else // canImport(Foundation.Process)

fatalError("NIOSSHServer is only supported on platforms with Foundation.Process")

#endif // !canImport(Foundation.Process)
#endif