1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 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
/// The result of an ALPN negotiation.
///
/// In a system expecting an ALPN negotiation to occur, a wide range of
/// possible things can happen. In the best case scenario it is possible for
/// the server and client to agree on a protocol to speak, in which case this
/// will be `.negotiated` with the relevant protocol provided as the associated
/// value. However, if for any reason it was not possible to negotiate a
/// protocol, whether because one peer didn't support ALPN or because there was no
/// protocol overlap, we should `fallback` to a default choice of some kind.
///
/// Exactly what to do when falling back is the responsibility of a specific
/// implementation.
public enum ALPNResult: Equatable {
/// ALPN negotiation succeeded. The associated value is the ALPN token that
/// was negotiated.
case negotiated(String)
/// ALPN negotiation either failed, or never took place. The application
/// should fall back to a default protocol choice or close the connection.
case fallback
}
/// A helper `ChannelInboundHandler` that makes it easy to swap channel pipelines
/// based on the result of an ALPN negotiation.
///
/// The standard pattern used by applications that want to use ALPN is to select
/// an application protocol based on the result, optionally falling back to some
/// default protocol. To do this in SwiftNIO requires that the channel pipeline be
/// reconfigured based on the result of the ALPN negotiation. This channel handler
/// encapsulates that logic in a generic form that doesn't depend on the specific
/// TLS implementation in use by using `TLSUserEvent`
///
/// The user of this channel handler provides a single closure that is called with
/// an `ALPNResult` when the ALPN negotiation is complete. Based on that result
/// the user is free to reconfigure the `ChannelPipeline` as required, and should
/// return an `EventLoopFuture` that will complete when the pipeline is reconfigured.
///
/// Until the `EventLoopFuture` completes, this channel handler will buffer inbound
/// data. When the `EventLoopFuture` completes, the buffered data will be replayed
/// down the channel. Then, finally, this channel handler will automatically remove
/// itself from the channel pipeline, leaving the pipeline in its final
/// configuration.
public final class ApplicationProtocolNegotiationHandler: ChannelInboundHandler, RemovableChannelHandler {
public typealias InboundIn = Any
public typealias InboundOut = Any
private let completionHandler: (ALPNResult, Channel) -> EventLoopFuture<Void>
private var waitingForUser: Bool
private var eventBuffer: [NIOAny]
/// Create an `ApplicationProtocolNegotiationHandler` with the given completion
/// callback.
///
/// - Parameter alpnCompleteHandler: The closure that will fire when ALPN
/// negotiation has completed.
public init(alpnCompleteHandler: @escaping (ALPNResult, Channel) -> EventLoopFuture<Void>) {
self.completionHandler = alpnCompleteHandler
self.waitingForUser = false
self.eventBuffer = []
}
/// Create an `ApplicationProtocolNegotiationHandler` with the given completion
/// callback.
///
/// - Parameter alpnCompleteHandler: The closure that will fire when ALPN
/// negotiation has completed.
public convenience init(alpnCompleteHandler: @escaping (ALPNResult) -> EventLoopFuture<Void>) {
self.init { result, _ in
alpnCompleteHandler(result)
}
}
public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
guard let tlsEvent = event as? TLSUserEvent else {
context.fireUserInboundEventTriggered(event)
return
}
if case .handshakeCompleted(let p) = tlsEvent {
handshakeCompleted(context: context, negotiatedProtocol: p)
} else {
context.fireUserInboundEventTriggered(event)
}
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
if waitingForUser {
eventBuffer.append(data)
} else {
context.fireChannelRead(data)
}
}
private func handshakeCompleted(context: ChannelHandlerContext, negotiatedProtocol: String?) {
waitingForUser = true
let result: ALPNResult
if let negotiatedProtocol = negotiatedProtocol {
result = .negotiated(negotiatedProtocol)
} else {
result = .fallback
}
let switchFuture = self.completionHandler(result, context.channel)
switchFuture.whenComplete { (_: Result<Void, Error>) in
self.unbuffer(context: context)
context.pipeline.removeHandler(self, promise: nil)
}
}
private func unbuffer(context: ChannelHandlerContext) {
for datum in eventBuffer {
context.fireChannelRead(datum)
}
let buffer = eventBuffer
eventBuffer = []
waitingForUser = false
if buffer.count > 0 {
context.fireChannelReadComplete()
}
}
}
|