Skip to content

Commit

Permalink
Add window change requests to server shell, and add a shell based on …
Browse files Browse the repository at this point in the history
…SwiftTUI
  • Loading branch information
Joannis committed May 1, 2024
1 parent 0c6b5d7 commit d311fe9
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Examples/TerminalAppServer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
26 changes: 26 additions & 0 deletions Examples/TerminalAppServer/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TerminalAppServer",
platforms: [
.macOS(.v12),
],
dependencies: [
.package(url: "https://github.com/joannis/SwiftTUI.git", branch: "jo/allow-use-with-concurrency"),
.package(path: "/Users/joannisorlandos/git/joannis/SwiftTUI"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "TerminalAppServer",
dependencies: [
.product(name: "Citadel", package: "Citadel"),
.product(name: "SwiftTUI", package: "SwiftTUI"),
]
),
]
)
105 changes: 105 additions & 0 deletions Examples/TerminalAppServer/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Citadel
import Crypto
import Foundation
import NIO
import NIOFoundationCompat
import NIOSSH
import SwiftTUI

@main struct ExampleSSHServer {
static func main() async throws {
let privateKey: Curve25519.Signing.PrivateKey
let privateKeyURL = URL(fileURLWithPath: "./citadel_host_key_ed25519")

// Read or create a private key
if let file = try? Data(contentsOf: privateKeyURL) {
// File exists, read it into a Curve25519 private key
privateKey = try Curve25519.Signing.PrivateKey(sshEd25519: file)
} else {
// File does not exist, create a new Curve25519 private
privateKey = Curve25519.Signing.PrivateKey()

// Write the private key to a file
try privateKey.makeSSHRepresentation().write(to: privateKeyURL, atomically: true, encoding: .utf8)
}

let server = try await SSHServer.host(
host: "localhost",
port: 2323,
hostKeys: [
NIOSSHPrivateKey(ed25519Key: privateKey)
],
authenticationDelegate: LoginHandler(username: "joannis", password: "test")
)

server.enableShell(withDelegate: CustomAppShell())

try await server.closeFuture.get()
}
}

final class CustomAppShell: ShellDelegate {
@MainActor public func startShell(
inbound: AsyncStream<ShellClientEvent>,
outbound: ShellOutboundWriter,
context: SSHShellContext
) async throws {
struct MyTerminalView: View {
var body: some View {
VStack {
Text("Hello, world!")
Button("Click me") {
print("clicked")
}
Button("Don't click") {
print("Clicked anyways")
}
}
}
}

let app = Application(rootView: MyTerminalView()) { string in
outbound.write(ByteBuffer(string: string))
}

await withTaskGroup(of: Void.self) { group in
group.addTask { @MainActor in
for await message in inbound {
if case .stdin(let input) = message {
app.handleInput(Data(buffer: input))
}
}
}
group.addTask { @MainActor in
for await windowSize in context.windowSize {
app.changeWindosSize(to: Size(
width: Extended(windowSize.columns),
height: Extended(windowSize.rows)
))
}
}

app.draw()
}
}
}

struct LoginHandler: NIOSSHServerUserAuthenticationDelegate {
let username: String
let password: String

var supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods {
.password
}

func requestReceived(
request: NIOSSHUserAuthenticationRequest,
responsePromise: EventLoopPromise<NIOSSHUserAuthenticationOutcome>
) {
if case .password(.init(password: password)) = request.request, request.username == username {
return responsePromise.succeed(.success)
}

return responsePromise.succeed(.failure)
}
}
6 changes: 6 additions & 0 deletions Sources/Citadel/SFTP/Server/SFTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ public struct SSHContext {
}

public struct SSHShellContext {
public struct WindowSize {
public let columns: Int
public let rows: Int
}

public let session: SSHContext
internal let channel: Channel
public let windowSize: AsyncStream<WindowSize>

public var isClosed: Bool {
!channel.isActive
Expand Down
6 changes: 5 additions & 1 deletion Sources/Citadel/Shell/Server/ShellDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class ShellServerInboundHandler: ChannelInboundHandler {
let eventLoop: EventLoop
let inbound = AsyncStream<ShellClientEvent>.makeStream()
let outbound = AsyncThrowingStream<ShellServerEvent, Error>.makeStream()
let windowSize = AsyncStream<SSHShellContext.WindowSize>.makeStream()

init(logger: Logger, delegate: ShellDelegate, eventLoop: EventLoop, username: String?) {
self.logger = logger
Expand All @@ -64,7 +65,8 @@ final class ShellServerInboundHandler: ChannelInboundHandler {

let shellContext = SSHShellContext(
session: SSHContext(username: self.username),
channel: channel
channel: channel,
windowSize: windowSize.stream
)

let done = context.eventLoop.makePromise(of: Void.self)
Expand Down Expand Up @@ -106,6 +108,8 @@ final class ShellServerInboundHandler: ChannelInboundHandler {

func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
switch event {
case let event as SSHChannelRequestEvent.WindowChangeRequest:
windowSize.continuation.yield(.init(columns: event.terminalCharacterWidth, rows: event.terminalRowHeight))
default:
context.fireUserInboundEventTriggered(event)
}
Expand Down

0 comments on commit d311fe9

Please sign in to comment.