orlandos-nl / citadel Goto Github PK
View Code? Open in Web Editor NEWSSH Client & Server in Swift
License: MIT License
SSH Client & Server in Swift
License: MIT License
As discussed on Slack, there seems to be an issue around buffering or back pressure support when logging output from Citadel.
I'm running a task as follows:
let stream = try await executeCommandStream("script.sh", inShell: true)
for try await output in stream {
switch output {
case let .stdout(buffer):
print(String(buffer: buffer), terminator: "")
case let .stderr(buffer):
print(String(buffer: buffer), terminator: "")
}
}
where "script.sh" is a simple
echo "❌ this must fail"
exit 1
but the echo isn't showing up in the logs.
An ugly fix for this is to introduce a delay before exiting:
echo "❌ this must fail"
sleep 0.1
exit 1
Best I can tell, there's no way on the consumer side of the AsyncThorwingStream
to ensure all messages have been delivered before the stream terminates.
There is requirement in our project to catch script output it prints out to its STDOUT/STDERR and send response back to STDIN when it prompts for it.
Any solution allowing to catch STDOUT/STDERR stream output in realtime in couple with ability to immediately send data to ssh STDIN will satisfy us.
For now we implemented alternative ssh interaction based on low-level swift Process class. We transferring ssh command with arguments as Process.arguments then interacting with it like any other child UNIX process.
task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/sshpass")
// https://github.com/hudochenkov/homebrew-sshpass
// brew install hudochenkov/sshpass/sshpass
task.arguments = [
"-p", password,
"ssh",
"-o", "StrictHostKeyChecking no",
"-o", "UserKnownHostsFile=/dev/null",
"-q",
"\(login)@\(host)",
]
We found this solution horrible especially in contrast with nice Citadel interface and still use Citadel for sftp, so we very appreciate you to implement real-time interaction in citadel.
I would like to suggest adding support for custom SSHAuthenticationMethod
. Currently, I need to provide multiple authentication methods for one SSH connection, such as auto-attempting all available keys or implementing two-factor login. It would be great if SSHClient.connect
could use NIOSSHClientUserAuthenticationDelegate
as a generic parameter so that users can implement their own authentication logic.
In OpenSSHKey init(string key: String, decryptionKey: Data? = nil)
there is an assumption that there will be padding. However generating an RSA key using ssh-keygen -t rsa -b 4096
produces a private key without padding and the below for loop results in a RangeException as paddingLength is 0
for i in 1..<paddingLength { guard padding[i - 1] == UInt8(i) else { throw InvalidKey() } }
Trying to run unit test Citadel2Tests.testSFTPUpload and it fails with:
Test Case '-[CitadelTests.Citadel2Tests testSFTPUpload]' started.
2023-07-14 09:46:50.680986+1000 xctest[82987:3661392] [si_destination_compare] send failed: Invalid argument
2023-07-14 09:46:50.681103+1000 xctest[82987:3661392] [si_destination_compare] send failed: Undefined error: 0
NIOSSH/SSHKeyExchangeStateMachine.swift:299: Assertion failed: Negotiated MAC was not supported by negotiated transport protection
2023-07-14 09:46:50.688181+1000 xctest[82987:3661397] NIOSSH/SSHKeyExchangeStateMachine.swift:299: Assertion failed: Negotiated MAC was not supported by negotiated transport protection
I've tried fiddling with the algorithms for both server and client along the lines suggested in the project README but without success
Hi, Does Citadel support ProxyCommand or ProxyJum?
Hi Joannis, great job on this library! I'm wondering why you set the iOS 15 as minimum deployment target, I tried to change it to iOS 14 and it works as expected on iOS 14.
TTY added ";exit\n" in its code. However, if a command has already ended with ";", a parse error of ";;" is risen and the in shell can't quit.
Since the in shell is not quit, the app calls this api won't get the buffer and the app waits forever.
related function:
public func executeCommand(_ command: String, maxResponseSize: Int = .max, mergeStreams: Bool = false, inShell: Bool = false) async throws -> ByteBuffer
Try: "ls abc;" as command.
product '_CryptoExtras' required by package 'citadel' target 'Citadel' not found in package 'swift-crypto'
Thats the error I get. I just added version version 0.7.0 as dependency to my package. I tried 0.6.0 as well, but the error persists
Describe the bug
The write file example produces an error:
2024-04-16T13:21:15+0400 info nl.orlandos.citadel.sftp : [Citadel] SFTP connection opened and ready
2024-04-16T13:21:15+0400 info nl.orlandos.citadel.sftp : [Citadel] SFTP requesting to open file at '/flightbriefing/file.txt' with flags 0x00000023
(lldb) po error
▿ {1}(code: SSH_FX_NO_SUCH_FILE, #'No such file')
Reproducer Sample
func testDirectWriting() async throws {
guard let data = "Hello world".data(using: .utf8) else {
XCTFail()
return
}
do {
let file = try await sut?.sftp?.openFile(filePath: "/flightbriefing/file.txt", flags: [.read, .write, .forceCreate])
try await file?.write(ByteBuffer(data: data), at: 0)
try await file?.close()
} catch {
print(error)
XCTFail()
}
}
(Where sftp is an SSHClient.SFTPClient)
Expected behavior
The file should be written (even if it does not exist as the .forceCreate flag is there). No error should be thrown.
Client (please complete the following information):
Great news! I have tested the branch, and it works perfectly. However, I did encounter an issue when testing 2-factor login, as `swift-nio-ssh` currently does not support keyboard-interactive authentication. If I have the time, I will attempt to make a pull request to address this issue. Thank you for your efforts in developing this feature!
Originally posted by @waylybaye in #28 (comment)
Hi Joannis, Do you have any plans to support a shell channel? I noticed that you already support streaming in executeCommand, and it would be really cool if you could also support a shell channel. Thank you!
Hi, I'm using Citadel as SSH Client to connect a remote server, just want to know if there is anyway or API to get notified/callback when the connection was closed(by remote/network environment changed)?
Describe the bug
I am currently trying to get the direct TCP tunnel from the readme to work. Unfortunately I am getting the following error:
Cannot convert value of type '(UnboundedRange_) -> ()' to expected argument type '[any ChannelHandler]'
in this line:
proxyChannel.pipeline.addHandlers(...)
Reproducer Sample
Task {
let client = try? await SSHClient.connect(
host: "example.com",
authenticationMethod: .passwordBased(username: "root", password: "password"),
hostKeyValidator: .acceptAnything(),
reconnect: .never
)
let address = try SocketAddress(ipAddress: "fe80::1", port: 27017)
let configuredProxyChannel = try await client?.createDirectTCPIPChannel(
using: SSHChannelType.DirectTCPIP(
targetHost: "localhost",
targetPort: 27017,
originatorAddress: address
)
) { proxyChannel in
proxyChannel.pipeline.addHandlers(...)
}
}
I would really appreciate your help!
P.S. Also I would actually be interested in setting up a forwardedTCPIP tunnel instead of a direct one. Am I correct that Citadel currently doesn't support this?
I have to connect to an external FTP server to search for and download files. I do not control this server, I just need to get the files to put them in S3 for future use. Up until now I’ve used a wrapper around curl that I wrote with my Vapor server, but I’d love to replace this. Can I leverage citadel to do this instead?
For example, could I use citadel’s SFTP client in an plain FTP mode? Or, extend the package to support plain FTP?
The
executeCommand
function accumulated information into a contiguousByteBuffer
. This is useful for non-interactive commands such ascat
andls
. Citadel currently does not expose APIs for streaming into a process'stdin
or streaming thestdout
elsewhere. If you want this, please create an issue.
We are planning to work on adding streaming support for Citadel as our app requires the use of interactive commands (a shell). Would you shed some lights on how we might approach this? :)
Hi
Is it possible to upload a specific file from iOS app to a ssh server? If yes, how?
Thank you very much
Alex
Is it possible to store and retrieve an RSA Private key in Keychain? I have successfully gotten the raw data or the private key and stored it as Data, but when I try to recreate the key with Insecure.RSA.PrivateKey(sshRsa: [data])
, Citadel throws an InvalidKey
exception. I'm sure I've missed something obvious - thank you for any help!
Hi Orlandos,
Will it be possible to introduce dynamic port forwarding aka socks proxy over ssh tunnel?
The aim is to route all macOS or iOS traffic over a configured socks5 port using swift nio ssh.
Any guidance on how to implement this functionality would be very helpful.
Thanks
Trying to connect to an SFTP server with the following config:
let ssh = try await SSHClient.connect (
host: "sftp012.successfactors.eu",
authenticationMethod: .passwordBased(username: "UUUU", password: "PPPP"),
hostKeyValidator: .acceptAnything(),
reconnect: .never )
Receiving the following error:
Citadel.ClientHandshakeHandler.(unknown context at $10122d328).(unknown context at $10122d3a4).AuthenticationFailed error 1.
Connection to this server via an SFTP client like CyberDuck works fine.
Connecting via Citadel to this sftp Testserver works fine https://www.sftp.net/public-online-sftp-servers
Help would be much appreciated :)
Am I missing something? I can't find how to remove a file or directory, or rename a file in SFTPClient
Adding an exec handler to an SFTP server causes this crash:
NIOCore/NIOAny.swift:200: Fatal error: tried to decode as type SSHChannelData but found IOData with contents ioData(IOData { ByteBuffer { readerIndex: 0, writerIndex: 9, readableBytes: 9, capacity: 9, storageCapacity: 32, slice: _ByteBufferSlice { 9..<18 }, storage: 0x00006000002058c0 (32 bytes) } })
2023-10-01 15:55:39.527637-0400 citadel-backend[53048:39683831] NIOCore/NIOAny.swift:200: Fatal error: tried to decode as type SSHChannelData but found IOData with contents ioData(IOData { ByteBuffer { readerIndex: 0, writerIndex: 9, readableBytes: 9, capacity: 9, storageCapacity: 32, slice: _ByteBufferSlice { 9..<18 }, storage: 0x00006000002058c0 (32 bytes) } })
CC @Joannis
Describe the bug
Trying to connect to a server using a RSA private key throws a invalidOpenSSHBoundary error without any other details.
Reproducer Sample
import Citadel
import CryptoKit
let host:String = "...server ip address..."
let port:Int = 22
let username: String = "root"
let sshFile = try String(contentsOfFile: "path_to_key")
let privateKey = try Insecure.RSA.PrivateKey(sshRsa: sshFile)
let client = try await SSHClient.connect(
host: host,
port: port,
authenticationMethod: .rsa(username: username, privateKey: privateKey),
hostKeyValidator: .acceptAnything(),
reconnect: .never
)
let result = try await client.executeCommand("ls -l /")
print(String(buffer: result))
Expected behavior
It should connect to the server.
Client (please complete the following information):
Server (please complete the following information):
Additional context
Using the same key from the command line with "ssh -i key root@server" works.
The key starts with "-----BEGIN RSA PRIVATE KEY-----" and ends with "-----END RSA PRIVATE KEY-----"
Not sure if related (feel free to delete this part if not): on another server I get a Citadel.SSHClientError.allAuthenticationOptionsFailed error.
That key uses "-----BEGIN OPENSSH PRIVATE KEY-----" and "-----END OPENSSH PRIVATE KEY-----" as bondaries. It was generated on macOs 14.4.1 with this comand: "ssh-keygen -t rsa -b 4096". When connecting from the command line it works.
My recent addition #53 is failing with a channelFailure
. As discussed, I'm fairly sure this worked initially but I'm certainly now hitting the channelFailure
very reliably.
The error can be avoided by sending the env request with wantReply: false
but in that case the env variable isn't actually being set:
try await executeCommandStream("printing", inShell: true, environment: ["FOO": "bar"])
→
USER=admin
LOGNAME=admin
HOME=/Users/admin
PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin
SHELL=/bin/zsh
SSH_CLIENT=172.29.29.8 53247 22
SSH_CONNECTION=172.29.29.8 53247 192.168.64.5 22
TMPDIR=/var/folders/fs/0w3t9t1d28xc3bj0tgb8hcrw0000gn/T/
SHLVL=1
PWD=/Users/admin
OLDPWD=/Users/admin
HOMEBREW_PREFIX=/opt/homebrew
HOMEBREW_CELLAR=/opt/homebrew/Cellar
HOMEBREW_REPOSITORY=/opt/homebrew
MANPATH=/opt/homebrew/share/man::
INFOPATH=/opt/homebrew/share/info:
_=/usr/bin/printenv
Also, inShell
true or false does not make a difference.
I've tried moving the env request's triggerUserOutboundEvent
into createChannel
but also to no effect.
I'm not sure what else to try 🤔
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.