Hi, I am trying to implement command execution to a SSH server framework (Citadel) based on swift-nio.
I have wrapped Process()
to pass user input to bash and added a method to capture the output. This allows commands to be executed, but all shell functions like coloring, escape/control characters, input echoing, etc are missing.
My goal is to directly forward the input received and forward the output of the shell. Just like any other SSH server.
I have read that a pseudo-terminal could fix this. But as I don't have much experience with PTYs, I don't know how to implement it. I also noticed that swift-nio provides a SSHChannelRequestEvent.PseudoTerminalRequest
, could that already meet my needs?
I tinkered a bit with the openpty
and received some more information, but it was printed to the standard output of the application and not to my output pipe. Also text colouring and echoing is missing. I guess I am overlooking something here.
Any help is very much appreciated.
// process.standardOutput = FileHandle(fileDescriptor: master)
var master: Int32 = 0
var slave: Int32 = 0
if openpty(&master, &slave, nil, nil, nil) != 0 {
perror("openpty")
return -1
}
@main struct ExampleSSHServer {
static func main() async throws {
// File stored host key
let hostKey = HostKey()
let server = try await SSHServer.host(
host: "localhost",
port: 2222,
hostKeys: [
try .init(ed25519Key: .init(rawRepresentation: hostKey.key))
],
authenticationDelegate: LoginHandler(username: "usr", password: "test")
)
server.enableShell(withDelegate: ExecShell())
try await server.closeFuture.get()
}
}
public struct ExecShell: ShellDelegate {
public func startShell(reading stream: AsyncStream<ShellClientEvent>, context: SSHContext) async throws -> AsyncThrowingStream<ShellServerEvent, Error> {
AsyncThrowingStream { continuation in
Task {
let prcs = Shell()
try prcs.start()
try prcs._readOutput { output in
continuation.yield(.stdout(ByteBuffer(bytes: output)))
}
for await message in stream {
if case .stdin(let input) = message {
let bytes = input.getBytes(at: input.readerIndex, length: input.readableBytes)!
prcs.write(Data(bytes))
}
}
continuation.finish()
}
}
}
}
class Shell {
deinit {
process.terminate()
}
private let process: Process = Process()
private let inputPipe: Pipe = Pipe()
private var outputPipe: Pipe = Pipe()
func start() throws {
process.standardInput = inputPipe
process.standardOutput = outputPipe
process.standardError = outputPipe
process.launchPath = "/bin/bash"
process.arguments = ["-i"]
process.environment = ["TERM": "xterm-256color"]
process.launch()
}
func write(_ command: Data) {
inputPipe.fileHandleForWriting.write(command)
}
// sketchy read out
func _readOutput(handler: @escaping (Data) -> Void) throws {
let outputHandle = outputPipe.fileHandleForReading
Task {
while true {
let data = outputHandle.availableData
if data.isEmpty {
break
}
handler(data)
}
}
}
}