Skip to content

Commit

Permalink
Merge pull request #92 from orlandos-nl/jo/update-docs
Browse files Browse the repository at this point in the history
Add more docs
  • Loading branch information
Joannis authored Dec 23, 2024
2 parents be1a5bc + 863408c commit a889a05
Show file tree
Hide file tree
Showing 5 changed files with 437 additions and 134 deletions.
82 changes: 35 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,6 @@ let client = try await SSHClient.connect(

Using that client, we support a couple types of operations:

### TCP-IP Forwarding (Proxying)

```swift
// The address that is presented as the locally exposed interface
// This is purely communicated to the SSH server
let address = try SocketAddress(ipAddress: "fe80::1", port: 27017)
let configuredProxyChannel = try await client.createDirectTCPIPChannel(
using: SSHChannelType.DirectTCPIP(
targetHost: "localhost", // MongoDB host
targetPort: 27017, // MongoDB port
originatorAddress: address
)
) { proxyChannel in
proxyChannel.pipeline.addHandlers(...)
}
```

This will create a channel that is connected to the SSH server, and then forwarded to the target host. This is useful for proxying TCP-IP connections, such as MongoDB, Redis, MySQL, etc.

### Executing Commands

You can execute a command through SSH using the following code:
Expand Down Expand Up @@ -76,14 +57,13 @@ An example of how executeCommandStream can be used:

```swift
let streams = try await client.executeCommandStream("cat /foo/bar.log")
var asyncStreams = streams.makeAsyncIterator()

while let blob = try await asyncStreams.next() {
switch blob {
case .stdout(let stdout):
// do something with stdout
case .stderr(let stderr):
// do something with stderr

for try await event in streams {
switch event {
case .stdout(let stdout):
// do something with stdout
case .stderr(let stderr):
// do something with stderr
}
}
```
Expand Down Expand Up @@ -130,25 +110,15 @@ let directoryContents = try await sftp.listDirectory(atPath: "/etc")
// Create a directory
try await sftp.createDirectory(atPath: "/etc/custom-folder")

// Open a file
let resolv = try await sftp.openFile(filePath: "/etc/resolv.conf", flags: .read)

// Read a file in bulk
let resolvContents: ByteBuffer = try await resolv.readAll()

// Read a file in chunks
let chunk: ByteBuffer = try await resolv.read(from: index, length: maximumByteCount)

// Close a file
try await resolv.close()

// Write to a file
let file = try await sftp.openFile(filePath: "/etc/resolv.conf", flags: [.read, .write, .forceCreate])
let fileWriterIndex = 0
try await file.write(ByteBuffer(string: "Hello, world", at: fileWriterIndex)
try await file.close()
// Write to a file (using a helper that cleans up the file automatically)
try await sftp.withFile(
filePath: "/etc/resolv.conf",
flags: [.read, .write, .forceCreate]
) { file in
try await file.write(ByteBuffer(string: "Hello, world", at: 0))
}

// Read a file using a helper. This closes the file automatically
// Read a file
let data = try await sftp.withFile(
filePath: "/etc/resolv.conf",
flags: .read
Expand All @@ -160,6 +130,25 @@ let data = try await sftp.withFile(
try await sftp.close()
```

### TCP-IP Forwarding (Proxying)

```swift
// The address that is presented as the locally exposed interface
// This is purely communicated to the SSH server
let address = try SocketAddress(ipAddress: "fe80::1", port: 27017)
let configuredProxyChannel = try await client.createDirectTCPIPChannel(
using: SSHChannelType.DirectTCPIP(
targetHost: "localhost", // MongoDB host
targetPort: 27017, // MongoDB port
originatorAddress: address
)
) { proxyChannel in
proxyChannel.pipeline.addHandlers(...)
}
```

This will create a channel that is connected to the SSH server, and then forwarded to the target host. This is useful for proxying TCP-IP connections, such as MongoDB, Redis, MySQL, etc.

## Servers

To use Citadel, first you need to create & start an SSH server, using your own authentication delegate:
Expand Down Expand Up @@ -303,7 +292,7 @@ public final class MyExecDelegate: ExecDelegate {
}
```

### SFTP Server
### SFTP Servers

When you implement SFTP in Citadel, you're responsible for taking care of logistics. Be it through a backing MongoDB store, a real filesystem, or your S3 bucket.

Expand Down Expand Up @@ -358,7 +347,6 @@ You can also use `SSHAlgorithms.all` to enable all supported algorithms.
A couple of code is held back until further work in SwiftNIO SSH is completed. We're currently working with Apple to resolve these.

- [ ] RSA Authentication (implemented & supported, but in a [fork of NIOSSH](https://github.com/Joannis/swift-nio-ssh-1/pull/1))
- [ ] Much more documentation & tutorials

## Contributing

Expand Down
36 changes: 31 additions & 5 deletions Sources/Citadel/Exec/Client/ExecClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ import Foundation
import NIO
import NIOSSH

/// A channel handler that manages TTY (terminal) input/output for SSH command execution.
/// This handler processes both incoming and outgoing data through the SSH channel.
final class TTYHandler: ChannelDuplexHandler {
typealias InboundIn = SSHChannelData
typealias InboundOut = ByteBuffer
typealias OutboundIn = ByteBuffer
typealias OutboundOut = SSHChannelData

/// Maximum allowed size for command response data
let maxResponseSize: Int
/// Flag to indicate if input should be ignored (e.g., when response size exceeds limit)
var isIgnoringInput = false
/// Buffer to store the command's response data
var response = ByteBuffer()
/// Promise that will be fulfilled with the final response
let done: EventLoopPromise<ByteBuffer>
/// Buffer to store error messages from stderr
private var errorBuffer = ByteBuffer()

init(
maxResponseSize: Int,
Expand Down Expand Up @@ -39,7 +47,11 @@ final class TTYHandler: ChannelDuplexHandler {
}

func handlerRemoved(context: ChannelHandlerContext) {
done.succeed(response)
if errorBuffer.readableBytes > 0 {
done.fail(TTYSTDError(message: errorBuffer))
} else {
done.succeed(response)
}
}

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
Expand All @@ -63,7 +75,7 @@ final class TTYHandler: ChannelDuplexHandler {
response.writeBuffer(&bytes)
return
case .stdErr:
done.fail(TTYSTDError(message: bytes))
errorBuffer.writeBuffer(&bytes)
default:
()
}
Expand All @@ -76,10 +88,24 @@ final class TTYHandler: ChannelDuplexHandler {
}

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.
/// Executes a command on the remote SSH server and returns its output.
///
/// This method establishes a new channel, executes the specified command, and collects
/// its output. The command execution is handled asynchronously and includes timeout protection
/// for channel creation.
///
/// - Parameters:
/// - command: The command to execute.
/// - maxResponseSize: The maximum size of the response. If the response is larger, the command will fail.
/// - command: The shell command to execute on the remote server
/// - maxResponseSize: Maximum allowed size for the command's output in bytes.
/// If exceeded, throws `CitadelError.commandOutputTooLarge`
///
/// - Returns: A ByteBuffer containing the command's output
///
/// - Throws:
/// - `CitadelError.channelCreationFailed` if the channel cannot be created within 15 seconds
/// - `CitadelError.commandOutputTooLarge` if the response exceeds maxResponseSize
/// - `SSHClient.CommandFailed` if the command returns a non-zero exit status
/// - `TTYSTDError` if there was output to stderr
public func executeCommand(_ command: String, maxResponseSize: Int = .max) async throws -> ByteBuffer {
let promise = eventLoop.makePromise(of: ByteBuffer.self)

Expand Down
Loading

0 comments on commit a889a05

Please sign in to comment.