-
-
Save sarensw/7016a2271b504dfd8518d7ecd1227ced to your computer and use it in GitHub Desktop.
let ssh = SwiftNioSshWrapper(host: "...", port: 22, user: "...", password: "...") | |
do { | |
try ssh.run(command: "pwd") | |
} catch { | |
print(error) | |
} |
import Foundation | |
import Dispatch | |
import NIO | |
import NIOSSH | |
public class SwiftNioSshWrapper { | |
let host: String | |
let port: Int | |
let user: String | |
let password: String | |
public init(host: String, port: Int, user: String, password: String) { | |
print("Initializing the wrapper") | |
self.host = host | |
self.port = port | |
self.user = user | |
self.password = password | |
} | |
// Just prints the config | |
public func printConfig() { | |
print("Config: \(host), \(port), \(user), \(password)") | |
} | |
// This file contains an example NIO SSH client. As NIO SSH is currently under active | |
// development this file doesn't currently do all that much, but it does provide a binary you | |
// can kick off to get a feel for how NIO SSH drives the connection live. As the feature set of | |
// NIO SSH increases we'll be adding to this client to try to make it a better example of what you | |
// can do with NIO SSH. | |
final class ErrorHandler: ChannelInboundHandler { | |
typealias InboundIn = Any | |
func errorCaught(context: ChannelHandlerContext, error: Error) { | |
print("Error in pipeline: \(error)") | |
context.close(promise: nil) | |
} | |
} | |
final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate { | |
func validateHostKey(hostKey: NIOSSHPublicKey, validationCompletePromise: EventLoopPromise<Void>) { | |
// Do not replicate this in your own code: validate host keys! This is a | |
// choice made for expedience, not for any other reason. | |
validationCompletePromise.succeed(()) | |
} | |
} | |
public func run(command: String) throws -> Void { | |
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) | |
defer { | |
try! group.syncShutdownGracefully() | |
} | |
let bootstrap = ClientBootstrap(group: group) | |
.channelInitializer { channel in | |
channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: InteractivePasswordPromptDelegate(username: self.user, password: self.password), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) | |
} | |
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) | |
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) | |
do { | |
print("connect via ssh") | |
let channel = try bootstrap.connect(host: self.host, port: self.port).wait() | |
// We've been asked to exec. | |
print("channel created") | |
let exitStatusPromise = channel.eventLoop.makePromise(of: Int.self) | |
let childChannel: Channel = try! channel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in | |
let promise = channel.eventLoop.makePromise(of: Channel.self) | |
sshHandler.createChannel(promise) { childChannel, channelType in | |
guard channelType == .session else { | |
return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType) | |
} | |
return childChannel.pipeline.addHandlers([ExampleExecHandler(command: command, completePromise: exitStatusPromise), ErrorHandler()]) | |
} | |
return promise.futureResult | |
}.wait() | |
// Wait for the connection to close | |
print("closing") | |
try! childChannel.closeFuture.wait() | |
print("child channel closed") | |
let exitStatus = try! exitStatusPromise.futureResult.wait() | |
print("exist status promise awaited") | |
try! channel.close().wait() | |
print("all closed") | |
print(exitStatus) | |
// Exit like we're the command. | |
// exit(Int32(exitStatus)) | |
} catch { | |
print("could not connect via SSH") | |
return | |
} | |
} | |
} |
@t089 when I try this, I'm getting an exception at the line try! childChannel.close().wait()
MBeat/SwiftNioSshWrapper.swift:90: Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.alreadyClosed
2022-08-30 09:29:16.087351+0200 MBeat[13289:5465932] MBeat/SwiftNioSshWrapper.swift:90: Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.alreadyClosed
So I commented out try! childChannel.close().wait()
, and at least, it runs through once. But when I call a second command, I get an exception in the line let childChannel: Channel = try! ... .wait()
MBeat/SwiftNioSshWrapper.swift:74: Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.eof
2022-08-30 09:32:14.539830+0200 MBeat[13485:5469283] MBeat/SwiftNioSshWrapper.swift:74: Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.eof
It looks like that the channel doesn't reset the stream.
The reason for my assumption is purely missing knowledge. I tried to understand ExampleExecHandler but failed. So I assume that I'm my wrapper has a few more issues. (apart from the main issue that I can only run it once 🤣 )
hm, ok, I will try to find some time to take a closer look
Any way to upload file to ssh server from iOS app?
import Foundation
import Dispatch
import NIO
import NIOSSH
import NIOCore
import NIOPosix
public class SshClient {
let host: String
let port: Int
var user: String {
hardcodedClientPasswordDelegate.user
}
var password: String {
hardcodedClientPasswordDelegate.password
}
let hardcodedClientPasswordDelegate : HardcodedClientPasswordDelegate
public init(host: String, port: Int = 22, user: String, password: String) {
print("Initializing the wrapper")
self.host = host
self.port = port
self.hardcodedClientPasswordDelegate = .init(user: user, password: password)
}
// Just prints the config
public func printConfig() {
print("Config: \(host), \(port), \(user)")
}
// This file contains an example NIO SSH client. As NIO SSH is currently under active
// development this file doesn't currently do all that much, but it does provide a binary you
// can kick off to get a feel for how NIO SSH drives the connection live. As the feature set of
// NIO SSH increases we'll be adding to this client to try to make it a better example of what you
// can do with NIO SSH.
final class ErrorHandler: ChannelInboundHandler {
typealias InboundIn = Any
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("Error in pipeline: \(error)")
context.close(promise: nil)
}
}
final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate {
func validateHostKey(hostKey: NIOSSHPublicKey, validationCompletePromise: EventLoopPromise<Void>) {
// Do not replicate this in your own code: validate host keys! This is a
// choice made for expedience, not for any other reason.
validationCompletePromise.succeed(())
}
}
struct HardcodedClientPasswordDelegate: NIOSSHClientUserAuthenticationDelegate {
var user: String
var password: String
init(user:String, password:String) {
self.user = user
self.password = password
}
func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise<NIOSSHUserAuthenticationOffer?>) {
precondition(availableMethods.contains(.password))
nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: user,
serviceName: "",
offer: .password(.init(password: password))))
}
}
public func run(command: String) throws -> Void {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
let bootstrap = ClientBootstrap(group: group)
.channelInitializer { channel in
channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: self.hardcodedClientPasswordDelegate, serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()])
}
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
print("connecting via ssh ...")
let channel = try bootstrap.connect(host: self.host, port: self.port).wait()
// We've been asked to exec.
print("connected: channel created.")
let exitStatusPromise = channel.eventLoop.makePromise(of: Int.self)
let childChannel: Channel = try! channel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in
let promise = channel.eventLoop.makePromise(of: Channel.self)
sshHandler.createChannel(promise) { childChannel, channelType in
guard channelType == .session else {
return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType)
}
return childChannel.pipeline.addHandlers([
ExampleExecHandler(command: command,
completePromise: exitStatusPromise),
ErrorHandler(),
])
}
return promise.futureResult
}.wait()
// Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.eof
// Wait for the connection to close
print("closing")
try! childChannel.closeFuture.wait()
Swift.print("child channel closed")
let exitStatus = try exitStatusPromise.futureResult.wait()
print("exist status promise awaited")
try! channel.close().wait()
print("all closed")
print(exitStatus)
}
enum SSHClientError: Swift.Error {
case passwordAuthenticationNotSupported
case commandExecFailed
case invalidChannelType
case invalidData
}
}
I am also getting the error Fatal error: 'try!' expression unexpectedly raised an error: NIOCore.ChannelError.eof
Why do you assume the child channel is closed after sending the command? Looking at
ExampleExecHandler
it does not seem like that.Have you tried first waiting on the exit status and then actively closing the channel?