Skip to content

Commit 4c8e7e2

Browse files
committed
Fix client auth using a timeout - now correctly rethrows an error. Added example client + server
1 parent cd96d0e commit 4c8e7e2

13 files changed

+148
-41
lines changed

Examples/ExampleClient/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configurations": [
3+
{
4+
"type": "lldb",
5+
"request": "launch",
6+
"sourceLanguages": ["swift"],
7+
"args": [],
8+
"cwd": "${workspaceFolder:ExampleClient}",
9+
"name": "Debug ExampleClient",
10+
"program": "${workspaceFolder:ExampleClient}/.build/debug/ExampleClient",
11+
"preLaunchTask": "swift: Build Debug ExampleClient"
12+
},
13+
{
14+
"type": "lldb",
15+
"request": "launch",
16+
"sourceLanguages": ["swift"],
17+
"args": [],
18+
"cwd": "${workspaceFolder:ExampleClient}",
19+
"name": "Release ExampleClient",
20+
"program": "${workspaceFolder:ExampleClient}/.build/release/ExampleClient",
21+
"preLaunchTask": "swift: Build Release ExampleClient"
22+
}
23+
]
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM ubuntu:latest
2+
3+
RUN apt update && apt install openssh-server sudo -y
4+
5+
RUN useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u 1000 test
6+
7+
RUN echo 'test:test' | chpasswd
8+
9+
RUN service ssh start
10+
11+
EXPOSE 22
12+
13+
CMD ["/usr/sbin/sshd","-D"]

Examples/ExampleClient/Package.swift

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "ExampleClient",
8+
platforms: [
9+
.macOS(.v12),
10+
],
11+
dependencies: [
12+
.package(path: "../../"),
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.executableTarget(
18+
name: "ExampleClient",
19+
dependencies: [
20+
.product(name: "Citadel", package: "Citadel"),
21+
]
22+
),
23+
]
24+
)

Examples/ExampleClient/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
1. Build the dockerfile using `docker build --file ExampleServer.dockerfile --tag sshd-example .`
2+
2. Run the docker image using `docker run -p 2323:22 sshd-example`
3+
3. Run this Swift code to connect tom the server and run a command using `swift run`
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Citadel
2+
3+
let client = try await SSHClient.connect(
4+
host: "localhost",
5+
port: 2323,
6+
authenticationMethod: .passwordBased(username: "test", password: "test"),
7+
hostKeyValidator: .acceptAnything(),
8+
reconnect: .never
9+
)
10+
11+
let result = try await client.executeCommand("echo 'Hello, World!'")
12+
print(String(buffer: result))

Package.resolved

+15-6
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
"repositoryURL": "https://github.com/apple/swift-atomics.git",
1616
"state": {
1717
"branch": null,
18-
"revision": "ff3d2212b6b093db7f177d0855adbc4ef9c5f036",
19-
"version": "1.0.3"
18+
"revision": "cd142fd2f64be2100422d658e7411e39489da985",
19+
"version": "1.2.0"
2020
}
2121
},
2222
{
2323
"package": "swift-collections",
2424
"repositoryURL": "https://github.com/apple/swift-collections.git",
2525
"state": {
2626
"branch": null,
27-
"revision": "937e904258d22af6e447a0b72c0bc67583ef64a2",
28-
"version": "1.0.4"
27+
"revision": "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
28+
"version": "1.1.0"
2929
}
3030
},
3131
{
@@ -51,8 +51,8 @@
5151
"repositoryURL": "https://github.com/apple/swift-nio.git",
5252
"state": {
5353
"branch": null,
54-
"revision": "45167b8006448c79dda4b7bd604e07a034c15c49",
55-
"version": "2.48.0"
54+
"revision": "359c461e5561d22c6334828806cc25d759ca7aa6",
55+
"version": "2.65.0"
5656
}
5757
},
5858
{
@@ -63,6 +63,15 @@
6363
"revision": "01e03b888734b03f1005b0ca329d7b5af50208e7",
6464
"version": "0.3.2"
6565
}
66+
},
67+
{
68+
"package": "swift-system",
69+
"repositoryURL": "https://github.com/apple/swift-system.git",
70+
"state": {
71+
"branch": null,
72+
"revision": "025bcb1165deab2e20d4eaba79967ce73013f496",
73+
"version": "1.2.1"
74+
}
6675
}
6776
]
6877
},

Package.swift

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let package = Package(
1616
),
1717
],
1818
dependencies: [
19+
// .package(path: "/Users/joannisorlandos/git/joannis/swift-nio-ssh"),
1920
.package(name: "swift-nio-ssh", url: "https://github.com/Joannis/swift-nio-ssh.git", "0.3.2" ..< "0.4.0"),
2021
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2122
.package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"),

Sources/Citadel/Algorithms/AES.swift

-16
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,6 @@ import Crypto
44
import NIO
55
import NIOSSH
66

7-
enum CitadelError: Error {
8-
case invalidKeySize
9-
case invalidEncryptedPacketLength
10-
case invalidDecryptedPlaintextLength
11-
case insufficientPadding, excessPadding
12-
case invalidMac
13-
case cryptographicError
14-
case invalidSignature
15-
case signingError
16-
case unsupported
17-
case unauthorized
18-
case commandOutputTooLarge
19-
case channelCreationFailed
20-
case channelFailure
21-
}
22-
237
public final class AES128CTR: NIOSSHTransportProtection {
248
private enum Mac {
259
case sha1, sha256, sha512

Sources/Citadel/Client.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public struct SSHAlgorithms {
6565
public final class SSHClient {
6666
private(set) var session: SSHClientSession
6767
private var userInitiatedClose = false
68-
let authenticationMethod: SSHAuthenticationMethod
68+
let authenticationMethod: () -> SSHAuthenticationMethod
6969
let hostKeyValidator: SSHHostKeyValidator
7070
internal var connectionSettings = SSHConnectionSettings()
7171
private let algorithms: SSHAlgorithms
@@ -83,7 +83,7 @@ public final class SSHClient {
8383

8484
init(
8585
session: SSHClientSession,
86-
authenticationMethod: SSHAuthenticationMethod,
86+
authenticationMethod: @escaping @autoclosure () -> SSHAuthenticationMethod,
8787
hostKeyValidator: SSHHostKeyValidator,
8888
algorithms: SSHAlgorithms = SSHAlgorithms(),
8989
protocolOptions: Set<SSHProtocolOption>
@@ -111,21 +111,21 @@ public final class SSHClient {
111111
/// - Returns: An SSH client.
112112
public static func connect(
113113
on channel: Channel,
114-
authenticationMethod: SSHAuthenticationMethod,
114+
authenticationMethod: @escaping @autoclosure () -> SSHAuthenticationMethod,
115115
hostKeyValidator: SSHHostKeyValidator,
116116
algorithms: SSHAlgorithms = SSHAlgorithms(),
117117
protocolOptions: Set<SSHProtocolOption> = []
118118
) async throws -> SSHClient {
119119
let session = try await SSHClientSession.connect(
120120
on: channel,
121-
authenticationMethod: authenticationMethod,
121+
authenticationMethod: authenticationMethod(),
122122
hostKeyValidator: hostKeyValidator,
123123
protocolOptions: protocolOptions
124124
)
125125

126126
return SSHClient(
127127
session: session,
128-
authenticationMethod: authenticationMethod,
128+
authenticationMethod: authenticationMethod(),
129129
hostKeyValidator: hostKeyValidator,
130130
algorithms: algorithms,
131131
protocolOptions: protocolOptions
@@ -222,7 +222,7 @@ public final class SSHClient {
222222
self.session = try await SSHClientSession.connect(
223223
host: host,
224224
port: port,
225-
authenticationMethod: authenticationMethod,
225+
authenticationMethod: self.authenticationMethod(),
226226
hostKeyValidator: self.hostKeyValidator,
227227
protocolOptions: protocolOptions,
228228
group: session.channel.eventLoop

Sources/Citadel/ClientSession.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import NIO
22
import NIOSSH
33

4-
struct AuthenticationFailed: Error, Equatable {}
54
final class ClientHandshakeHandler: ChannelInboundHandler {
65
typealias InboundIn = Any
76

@@ -15,17 +14,18 @@ final class ClientHandshakeHandler: ChannelInboundHandler {
1514
init(eventLoop: EventLoop) {
1615
let promise = eventLoop.makePromise(of: Void.self)
1716
self.promise = promise
18-
19-
eventLoop.scheduleTask(in: .seconds(10)) {
20-
promise.fail(AuthenticationFailed())
21-
}
2217
}
2318

2419
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
20+
print(event)
2521
if event is UserAuthSuccessEvent {
2622
self.promise.succeed(())
2723
}
2824
}
25+
26+
func errorCaught(context: ChannelHandlerContext, error: any Error) {
27+
self.promise.fail(error)
28+
}
2929

3030
deinit {
3131
struct Disconnected: Error {}
@@ -50,14 +50,14 @@ final class SSHClientSession {
5050
/// - group: The event loop group to use, will use a new group with one thread if not specified.
5151
public static func connect(
5252
on channel: Channel,
53-
authenticationMethod: SSHAuthenticationMethod,
53+
authenticationMethod: @escaping @autoclosure () -> SSHAuthenticationMethod,
5454
hostKeyValidator: SSHHostKeyValidator,
5555
algorithms: SSHAlgorithms = SSHAlgorithms(),
5656
protocolOptions: Set<SSHProtocolOption> = []
5757
) async throws -> SSHClientSession {
5858
let handshakeHandler = ClientHandshakeHandler(eventLoop: channel.eventLoop)
5959
var clientConfiguration = SSHClientConfiguration(
60-
userAuthDelegate: authenticationMethod,
60+
userAuthDelegate: authenticationMethod(),
6161
serverAuthDelegate: hostKeyValidator
6262
)
6363

@@ -95,7 +95,7 @@ final class SSHClientSession {
9595
public static func connect(
9696
host: String,
9797
port: Int = 22,
98-
authenticationMethod: SSHAuthenticationMethod,
98+
authenticationMethod: @escaping @autoclosure () -> SSHAuthenticationMethod,
9999
hostKeyValidator: SSHHostKeyValidator,
100100
algorithms: SSHAlgorithms = SSHAlgorithms(),
101101
protocolOptions: Set<SSHProtocolOption> = [],
@@ -104,7 +104,7 @@ final class SSHClientSession {
104104
) async throws -> SSHClientSession {
105105
let handshakeHandler = ClientHandshakeHandler(eventLoop: group.next())
106106
var clientConfiguration = SSHClientConfiguration(
107-
userAuthDelegate: authenticationMethod,
107+
userAuthDelegate: authenticationMethod(),
108108
serverAuthDelegate: hostKeyValidator
109109
)
110110

Sources/Citadel/Errors.swift

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
enum SSHClientError: Error {
22
case unsupportedPasswordAuthentication, unsupportedPrivateKeyAuthentication, unsupportedHostBasedAuthentication
33
case channelCreationFailed
4+
case allAuthenticationOptionsFailed
45
}
56

67
enum SSHChannelError: Error {
@@ -24,3 +25,21 @@ enum SFTPError: Error {
2425
case errorStatus(SFTPMessage.Status)
2526
case unsupportedVersion(SFTPProtocolVersion)
2627
}
28+
29+
enum CitadelError: Error {
30+
case invalidKeySize
31+
case invalidEncryptedPacketLength
32+
case invalidDecryptedPlaintextLength
33+
case insufficientPadding, excessPadding
34+
case invalidMac
35+
case cryptographicError
36+
case invalidSignature
37+
case signingError
38+
case unsupported
39+
case unauthorized
40+
case commandOutputTooLarge
41+
case channelCreationFailed
42+
case channelFailure
43+
}
44+
45+
public struct AuthenticationFailed: Error, Equatable {}

Sources/Citadel/SSHAuthenticationMethod.swift

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,28 @@ import NIOSSH
33
import Crypto
44

55
/// Represents an authentication method.
6-
public struct SSHAuthenticationMethod: NIOSSHClientUserAuthenticationDelegate {
6+
public final class SSHAuthenticationMethod: NIOSSHClientUserAuthenticationDelegate {
77
private enum Implementation {
88
case custom(NIOSSHClientUserAuthenticationDelegate)
99
case user(String, offer: NIOSSHUserAuthenticationOffer.Offer)
1010
}
1111

12-
private let implementation: Implementation
12+
private let allImplementations: [Implementation]
13+
private var implementations: [Implementation]
1314

1415
internal init(
1516
username: String,
1617
offer: NIOSSHUserAuthenticationOffer.Offer
1718
) {
18-
self.implementation = .user(username, offer: offer)
19+
self.allImplementations = [.user(username, offer: offer)]
20+
self.implementations = allImplementations
1921
}
2022

2123
internal init(
2224
custom: NIOSSHClientUserAuthenticationDelegate
2325
) {
24-
self.implementation = .custom(custom)
26+
self.allImplementations = [.custom(custom)]
27+
self.implementations = allImplementations
2528
}
2629

2730
/// Creates a password based authentication method.
@@ -80,6 +83,13 @@ public struct SSHAuthenticationMethod: NIOSSHClientUserAuthenticationDelegate {
8083
availableMethods: NIOSSHAvailableUserAuthenticationMethods,
8184
nextChallengePromise: EventLoopPromise<NIOSSHUserAuthenticationOffer?>
8285
) {
86+
if implementations.isEmpty {
87+
nextChallengePromise.fail(SSHClientError.allAuthenticationOptionsFailed)
88+
return
89+
}
90+
91+
let implementation = implementations.removeFirst()
92+
8393
switch implementation {
8494
case .user(let username, offer: let offer):
8595
switch offer {

0 commit comments

Comments
 (0)