Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ struct SSHConnectionStateMachine {
}

mutating func processInboundMessage(allocator: ByteBufferAllocator,
loop: EventLoop,
userAuthDelegate: UserAuthDelegate) throws -> StateMachineInboundProcessResult? {
loop: EventLoop) throws -> StateMachineInboundProcessResult? {
switch self.state {
case .idle:
preconditionFailure("Received messages before sending our first message.")
Expand Down Expand Up @@ -131,7 +130,7 @@ struct SSHConnectionStateMachine {
return result
case .newKeys:
try state.receiveNewKeysMessage()
let newState = ReceivedNewKeysState(keyExchangeState: state, delegate: userAuthDelegate, loop: loop)
let newState = ReceivedNewKeysState(keyExchangeState: state, loop: loop)
let possibleMessage = newState.userAuthStateMachine.beginAuthentication()
self.state = .receivedNewKeys(newState)

Expand Down Expand Up @@ -337,8 +336,7 @@ struct SSHConnectionStateMachine {
mutating func processOutboundMessage(_ message: SSHMessage,
buffer: inout ByteBuffer,
allocator: ByteBufferAllocator,
loop: EventLoop,
userAuthDelegate: UserAuthDelegate) throws {
loop: EventLoop) throws {
switch self.state {
case .idle(var state):
switch message {
Expand Down Expand Up @@ -371,7 +369,7 @@ struct SSHConnectionStateMachine {
self.state = .keyExchange(kex)
case .newKeys:
try kex.writeNewKeysMessage(into: &buffer)
self.state = .sentNewKeys(.init(keyExchangeState: kex, delegate: userAuthDelegate, loop: loop))
self.state = .sentNewKeys(.init(keyExchangeState: kex, loop: loop))

case .disconnect:
try kex.serializer.serialize(message: message, to: &buffer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ extension SSHConnectionStateMachine {
var userAuthStateMachine: UserAuthenticationStateMachine

init(keyExchangeState state: KeyExchangeState,
delegate: UserAuthDelegate,
loop: EventLoop) {
self.role = state.role
self.parser = state.parser
Expand All @@ -43,7 +42,6 @@ extension SSHConnectionStateMachine {

// We force unwrap the session ID because it's programmer error to not have it at this time.
self.userAuthStateMachine = UserAuthenticationStateMachine(role: self.role,
delegate: delegate,
loop: loop,
sessionID: state.keyExchangeStateMachine.sessionID!)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ extension SSHConnectionStateMachine {
var userAuthStateMachine: UserAuthenticationStateMachine

init(keyExchangeState state: KeyExchangeState,
delegate: UserAuthDelegate,
loop: EventLoop) {
self.role = state.role
self.parser = state.parser
Expand All @@ -43,7 +42,6 @@ extension SSHConnectionStateMachine {

// We force unwrap the session ID here because it's programmer error to not have it at this stage.
self.userAuthStateMachine = UserAuthenticationStateMachine(role: self.role,
delegate: delegate,
loop: loop,
sessionID: self.keyExchangeStateMachine.sessionID!)
}
Expand Down
51 changes: 51 additions & 0 deletions Sources/NIOSSH/GlobalRequestDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO

/// A `GlobalRequestDelegate` is used by an SSH server to handle SSH global requests.
///
/// These are requests for connection-wide SSH resources. Today the only global requests
/// available are for managing TCP port forwarding: specifically, they allow clients to
/// request that the server listen on a port for it.
///
/// All delegate methods for this delegate are optional: if not implemented, they default to rejecting
/// all requests of a given type.
public protocol GlobalRequestDelegate {
/// The client wants to manage TCP port forwarding.
func tcpForwardingRequest(_: GlobalRequest.TCPForwardingRequest, promise: EventLoopPromise<Void>)
}

extension GlobalRequestDelegate {
func tcpForwardingRequest(_ request: GlobalRequest.TCPForwardingRequest, promise: EventLoopPromise<Void>) {
// The default implementation rejects all requests.
promise.fail(NIOSSHError.unsupportedGlobalRequest)
}
}

/// A namespace of `GlobalRequest` objects that delegates may be asked to handle.
public enum GlobalRequest {
/// A request from a client to a server for the server to listen on a port on the client's behalf. If accepted,
/// the server will listen on a port, and will forward accepted connections to the client using the "forwarded-tcpip"
/// channel type.
public enum TCPForwardingRequest: Equatable {
/// A request to listen on a given address.
case listen(SocketAddress)

/// A request to stop listening on a given address.
case cancel(SocketAddress)
}
}

/// The internal default global request delegate rejects all requests.
internal struct DefaultGlobalRequestDelegate: GlobalRequestDelegate {}
8 changes: 4 additions & 4 deletions Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ struct SSHKeyExchangeStateMachine {
switch self.role {
case .client:
throw SSHKeyExchangeError.unexpectedMessage
case .server(let keys):
case .server(let configuration):
let (result, reply) = try exchanger.completeKeyExchangeServerSide(
clientKeyExchangeMessage: message,
serverHostKey: negotiated.negotiatedHostKey(keys),
serverHostKey: negotiated.negotiatedHostKey(configuration.hostKeys),
initialExchangeBytes: &self.initialExchangeBytes,
allocator: self.allocator, expectedKeySizes: AES256GCMOpenSSHTransportProtection.keySizes
)
Expand Down Expand Up @@ -351,8 +351,8 @@ struct SSHKeyExchangeStateMachine {
switch self.role {
case .client:
return Self.supportedServerHostKeyAlgorithms
case .server(let keys):
return keys.flatMap { $0.hostKeyAlgorithms }
case .server(let configuration):
return configuration.hostKeys.flatMap { $0.hostKeyAlgorithms }
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/NIOSSH/NIOSSHError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ extension NIOSSHError {
internal static let tcpShutdown = NIOSSHError(type: .tcpShutdown, diagnostics: nil)

internal static let invalidUserAuthSignature = NIOSSHError(type: .invalidUserAuthSignature, diagnostics: nil)

@inline(never)
internal static func unknownPacketType(diagnostic: String) -> NIOSSHError {
NIOSSHError(type: .unknownPacketType, diagnostics: diagnostic)
}

internal static let unsupportedGlobalRequest = NIOSSHError(type: .unsupportedGlobalRequest, diagnostics: nil)
}

// MARK: - NIOSSHError CustomStringConvertible conformance.
Expand Down Expand Up @@ -137,6 +144,8 @@ extension NIOSSHError {
case creatingChannelAfterClosure
case tcpShutdown
case invalidUserAuthSignature
case unknownPacketType
case unsupportedGlobalRequest
}

private var base: Base
Expand Down Expand Up @@ -207,6 +216,12 @@ extension NIOSSHError {

/// The signature provided in user authentication was invalid.
public static let invalidUserAuthSignature: ErrorType = .init(.invalidUserAuthSignature)

/// An packet type that we don't recognise was received.
public static let unknownPacketType: ErrorType = .init(.unknownPacketType)

/// A global request was made and rejected due to being unsupported.
public static let unsupportedGlobalRequest: ErrorType = .init(.unsupportedGlobalRequest)
}
}

Expand Down
10 changes: 3 additions & 7 deletions Sources/NIOSSH/NIOSSHHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ public final class NIOSSHHandler {
/// Whether there's a pending unflushed write.
private var pendingWrite: Bool

/// The user-auth delegate for this connection.
private let authDelegate: UserAuthDelegate

private var context: ChannelHandlerContext?

// Must be optional as we need to pass it a reference to self.
Expand All @@ -55,11 +52,10 @@ public final class NIOSSHHandler {
// we're attempting to initialize a channel before user auth is complete.
private var pendingChannelInitializations: CircularBuffer<(promise: EventLoopPromise<Channel>?, initializer: ((Channel) -> EventLoopFuture<Void>)?)>

public init(role: SSHConnectionRole, allocator: ByteBufferAllocator, clientUserAuthDelegate: NIOSSHClientUserAuthenticationDelegate?, serverUserAuthDelegate: NIOSSHServerUserAuthenticationDelegate?, inboundChildChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?) {
public init(role: SSHConnectionRole, allocator: ByteBufferAllocator, inboundChildChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?) {
self.stateMachine = SSHConnectionStateMachine(role: role)
self.pendingWrite = false
self.outboundFrameBuffer = allocator.buffer(capacity: 1024)
self.authDelegate = UserAuthDelegate(role: role, client: clientUserAuthDelegate, server: serverUserAuthDelegate)
self.pendingChannelInitializations = CircularBuffer(initialCapacity: 4)
self.multiplexer = SSHChannelMultiplexer(delegate: self, allocator: allocator, childChannelInitializer: inboundChildChannelInitializer)
}
Expand Down Expand Up @@ -109,7 +105,7 @@ extension NIOSSHHandler: ChannelDuplexHandler {
self.stateMachine.bufferInboundData(&data)

do {
while let result = try self.stateMachine.processInboundMessage(allocator: context.channel.allocator, loop: context.eventLoop, userAuthDelegate: self.authDelegate) {
while let result = try self.stateMachine.processInboundMessage(allocator: context.channel.allocator, loop: context.eventLoop) {
try self.processInboundMessageResult(result, context: context)
}
} catch {
Expand All @@ -133,7 +129,7 @@ extension NIOSSHHandler: ChannelDuplexHandler {
self.outboundFrameBuffer.clear()

for message in multiMessage {
try self.stateMachine.processOutboundMessage(message, buffer: &self.outboundFrameBuffer, allocator: context.channel.allocator, loop: context.eventLoop, userAuthDelegate: self.authDelegate)
try self.stateMachine.processOutboundMessage(message, buffer: &self.outboundFrameBuffer, allocator: context.channel.allocator, loop: context.eventLoop)
self.pendingWrite = true
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/NIOSSH/Role.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

/// The role of a given party in an SSH connection.
public enum SSHConnectionRole {
case client
case server([NIOSSHPrivateKey])
case client(SSHClientConfiguration)
case server(SSHServerConfiguration)

internal var isClient: Bool {
switch self {
Expand Down
27 changes: 27 additions & 0 deletions Sources/NIOSSH/SSHClientConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Configuration for an SSH client.
public struct SSHClientConfiguration {
/// The user authentication delegate to be used with this client.
public var userAuthDelegate: NIOSSHClientUserAuthenticationDelegate

/// The global request delegate to be used with this client.
public var globalRequestDelegate: GlobalRequestDelegate

public init(userAuthDelegate: NIOSSHClientUserAuthenticationDelegate, globalRequestDelegate: GlobalRequestDelegate? = nil) {
self.userAuthDelegate = userAuthDelegate
self.globalRequestDelegate = globalRequestDelegate ?? DefaultGlobalRequestDelegate()
}
}
61 changes: 49 additions & 12 deletions Sources/NIOSSH/SSHMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,13 @@ extension SSHMessage {
// SSH_MSG_GLOBAL_REQUEST
static let id: UInt8 = 80

var name: String
enum RequestType: Equatable {
case tcpipForward(String, UInt32)
case cancelTcpipForward(String, UInt32)
}

var wantReply: Bool
var bytes: ByteBuffer?
var type: RequestType
}

struct RequestSuccessMessage: Equatable {
Expand Down Expand Up @@ -392,7 +396,7 @@ extension ByteBuffer {
}
return .userAuthPKOK(message)
case SSHMessage.GlobalRequestMessage.id:
guard let message = self.readGlobalRequestMessage() else {
guard let message = try self.readGlobalRequestMessage() else {
return nil
}
return .globalRequest(message)
Expand Down Expand Up @@ -697,23 +701,43 @@ extension ByteBuffer {
}
}

mutating func readGlobalRequestMessage() -> SSHMessage.GlobalRequestMessage? {
self.rewindReaderOnNil { `self` in
mutating func readGlobalRequestMessage() throws -> SSHMessage.GlobalRequestMessage? {
try self.rewindOnNilOrError { `self` in
guard
let name = self.readSSHStringAsString(),
let wantReply = self.readSSHBoolean()
else {
return nil
}

let bytes: ByteBuffer?
if self.readableBytes > 0 {
bytes = self.readSlice(length: self.readableBytes)
} else {
bytes = nil
let type: SSHMessage.GlobalRequestMessage.RequestType

switch name {
case "tcpip-forward":
guard
let addressToBind = self.readSSHStringAsString(),
let port = self.readInteger(as: UInt32.self)
else {
return nil
}

type = .tcpipForward(addressToBind, port)

case "cancel-tcpip-forward":
guard
let addressToBind = self.readSSHStringAsString(),
let port = self.readInteger(as: UInt32.self)
else {
return nil
}

type = .cancelTcpipForward(addressToBind, port)

default:
throw NIOSSHError.unknownPacketType(diagnostic: "global request, name \(name)")
}

return SSHMessage.GlobalRequestMessage(name: name, wantReply: wantReply, bytes: bytes)
return SSHMessage.GlobalRequestMessage(wantReply: wantReply, type: type)
}
}

Expand Down Expand Up @@ -1178,9 +1202,22 @@ extension ByteBuffer {
mutating func writeGlobalRequestMessage(_ message: SSHMessage.GlobalRequestMessage) -> Int {
var writtenBytes = 0

writtenBytes += self.writeSSHString(message.name.utf8)
switch message.type {
case .tcpipForward:
writtenBytes += self.writeSSHString("tcpip-forward".utf8)
case .cancelTcpipForward:
writtenBytes += self.writeSSHString("cancel-tcpip-forward".utf8)
}

writtenBytes += self.writeSSHBoolean(message.wantReply)

switch message.type {
case .tcpipForward(let addressToBind, let port),
.cancelTcpipForward(let addressToBind, let port):
writtenBytes += self.writeSSHString(addressToBind.utf8)
writtenBytes += self.writeInteger(port)
}

return writtenBytes
}

Expand Down
31 changes: 31 additions & 0 deletions Sources/NIOSSH/SSHServerConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Configuration for an SSH server.
public struct SSHServerConfiguration {
/// The user authentication delegate to be used with this server.
public var userAuthDelegate: NIOSSHServerUserAuthenticationDelegate

/// The global request delegate to be used with this server.
public var globalRequestDelegate: GlobalRequestDelegate

/// The host keys for this server.
public var hostKeys: [NIOSSHPrivateKey]

public init(hostKeys: [NIOSSHPrivateKey], userAuthDelegate: NIOSSHServerUserAuthenticationDelegate, globalRequestDelegate: GlobalRequestDelegate? = nil) {
self.hostKeys = hostKeys
self.userAuthDelegate = userAuthDelegate
self.globalRequestDelegate = globalRequestDelegate ?? DefaultGlobalRequestDelegate()
}
}
Loading