-
-
Notifications
You must be signed in to change notification settings - Fork 36
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
Development of EchoShell
- A simple shell emulator
#39
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import Foundation | ||
import NIO | ||
import NIOSSH | ||
|
||
final class TTYHandler: ChannelDuplexHandler { | ||
typealias InboundIn = SSHChannelData | ||
typealias InboundOut = ByteBuffer | ||
typealias OutboundIn = ByteBuffer | ||
typealias OutboundOut = SSHChannelData | ||
|
||
let maxResponseSize: Int | ||
var isIgnoringInput = false | ||
var response = ByteBuffer() | ||
let done: EventLoopPromise<ByteBuffer> | ||
|
||
init( | ||
maxResponseSize: Int, | ||
done: EventLoopPromise<ByteBuffer> | ||
) { | ||
self.maxResponseSize = maxResponseSize | ||
self.done = done | ||
} | ||
|
||
func handlerAdded(context: ChannelHandlerContext) { | ||
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in | ||
context.fireErrorCaught(error) | ||
} | ||
} | ||
|
||
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { | ||
switch event { | ||
case is SSHChannelRequestEvent.ExitStatus: | ||
() | ||
default: | ||
context.fireUserInboundEventTriggered(event) | ||
} | ||
} | ||
|
||
func handlerRemoved(context: ChannelHandlerContext) { | ||
done.succeed(response) | ||
} | ||
|
||
func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
let data = self.unwrapInboundIn(data) | ||
|
||
guard case .byteBuffer(var bytes) = data.data, !isIgnoringInput else { | ||
return | ||
} | ||
|
||
switch data.type { | ||
case .channel: | ||
if | ||
response.readableBytes + bytes.readableBytes > maxResponseSize | ||
{ | ||
isIgnoringInput = true | ||
done.fail(CitadelError.commandOutputTooLarge) | ||
return | ||
} | ||
|
||
// Channel data is forwarded on, the pipe channel will handle it. | ||
response.writeBuffer(&bytes) | ||
return | ||
case .stdErr: | ||
done.fail(TTYSTDError(message: bytes)) | ||
default: | ||
() | ||
} | ||
} | ||
|
||
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { | ||
let data = self.unwrapOutboundIn(data) | ||
context.write(self.wrapOutboundOut(SSHChannelData(type: .channel, data: .byteBuffer(data))), promise: promise) | ||
} | ||
} | ||
|
||
extension SSHClient { | ||
/// Executes a command on the remote server. This will return the output of the command. If the command fails, the error will be thrown. If the output is too large, the command will fail. | ||
/// - Parameters: | ||
/// - command: The command to execute. | ||
/// - maxResponseSize: The maximum size of the response. If the response is larger, the command will fail. | ||
public func executeCommand(_ command: String, maxResponseSize: Int = .max) async throws -> ByteBuffer { | ||
let promise = eventLoop.makePromise(of: ByteBuffer.self) | ||
|
||
let channel: Channel | ||
|
||
do { | ||
channel = try await eventLoop.flatSubmit { | ||
let createChannel = self.eventLoop.makePromise(of: Channel.self) | ||
self.session.sshHandler.createChannel(createChannel) { channel, _ in | ||
channel.pipeline.addHandlers( | ||
TTYHandler( | ||
maxResponseSize: maxResponseSize, | ||
done: promise | ||
) | ||
) | ||
} | ||
|
||
self.eventLoop.scheduleTask(in: .seconds(15)) { | ||
createChannel.fail(CitadelError.channelCreationFailed) | ||
} | ||
|
||
return createChannel.futureResult | ||
}.get() | ||
} catch { | ||
promise.fail(error) | ||
throw error | ||
} | ||
|
||
// We need to exec a thing. | ||
let execRequest = SSHChannelRequestEvent.ExecRequest( | ||
command: command, | ||
wantReply: true | ||
) | ||
|
||
return try await eventLoop.flatSubmit { | ||
channel.triggerUserOutboundEvent(execRequest).whenFailure { [channel] error in | ||
channel.close(promise: nil) | ||
promise.fail(error) | ||
} | ||
|
||
return promise.futureResult | ||
}.get() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Citadel open source and the ClamShell project | ||
// | ||
// Copyright (c) 2023 Gregor Feigel and the Citadel project authors | ||
// Licensed under MIT License | ||
// | ||
// See LICENSE.txt for license information | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Foundation | ||
import Crypto | ||
import NIOSSH | ||
|
||
public extension NIOSSHPrivateKey { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These helpers cannot go in the library. But you can add them to your extension. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you consider to add this as an default directly to |
||
init(file: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("citadel_ssh_host_key")) throws { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The URL should not use this default. It's best to leave the defaults empty There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
let hostKeyFile = HostKey(file: file) | ||
try self.init(ed25519Key: .init(rawRepresentation: hostKeyFile.key)) | ||
} | ||
} | ||
|
||
public struct HostKey { | ||
|
||
init(file: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("citadel_ssh_host_key")) { | ||
self.file = file | ||
} | ||
|
||
let file: URL | ||
|
||
var key: Data { | ||
get throws { | ||
if FileManager.default.fileExists(atPath: file.path) { | ||
return try Data(contentsOf: file) | ||
} else { | ||
// generate, store and return new key | ||
let key: Curve25519.Signing.PrivateKey = .init() | ||
try key.rawRepresentation.write(to: file) | ||
return key.rawRepresentation | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I understand the desire to import this library - I don't want to add a dependency for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, then I'll embed the necessary code parts of the library and mention the author above?