2018-03-14 00:24:54 +08:00
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
//
|
|
|
|
// This source file is part of the SwiftNIO open source project
|
|
|
|
//
|
2021-08-12 20:49:46 +08:00
|
|
|
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
|
2018-03-14 00:24:54 +08:00
|
|
|
// 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 CNIOSHA1
|
2021-08-12 20:49:46 +08:00
|
|
|
import NIOCore
|
2018-03-14 00:24:54 +08:00
|
|
|
import NIOHTTP1
|
|
|
|
|
2019-07-08 15:55:02 +08:00
|
|
|
let magicWebSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
2018-03-14 00:24:54 +08:00
|
|
|
|
2019-05-02 18:02:48 +08:00
|
|
|
@available(*, deprecated, renamed: "NIOWebSocketServerUpgrader")
|
|
|
|
public typealias WebSocketUpgrader = NIOWebSocketServerUpgrader
|
|
|
|
|
2018-03-14 00:24:54 +08:00
|
|
|
/// Errors that can be thrown by `NIOWebSocket` during protocol upgrade.
|
2019-03-04 22:19:18 +08:00
|
|
|
public struct NIOWebSocketUpgradeError: Error, Equatable {
|
|
|
|
private enum ActualError {
|
|
|
|
case invalidUpgradeHeader
|
|
|
|
case unsupportedWebSocketTarget
|
|
|
|
}
|
|
|
|
|
|
|
|
private let actualError: ActualError
|
|
|
|
|
|
|
|
private init(actualError: ActualError) {
|
|
|
|
self.actualError = actualError
|
|
|
|
}
|
2018-03-14 00:24:54 +08:00
|
|
|
/// A HTTP header on the upgrade request was invalid.
|
2019-03-04 22:19:18 +08:00
|
|
|
public static let invalidUpgradeHeader = NIOWebSocketUpgradeError(actualError: .invalidUpgradeHeader)
|
2018-03-14 00:24:54 +08:00
|
|
|
|
|
|
|
/// The HTTP request targets a websocket pipeline that does not support
|
|
|
|
/// it in some way.
|
2019-03-04 22:19:18 +08:00
|
|
|
public static let unsupportedWebSocketTarget = NIOWebSocketUpgradeError(actualError: .unsupportedWebSocketTarget)
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate extension HTTPHeaders {
|
2018-11-01 17:50:55 +08:00
|
|
|
func nonListHeader(_ name: String) throws -> String {
|
2018-04-09 15:18:48 +08:00
|
|
|
let fields = self[canonicalForm: name]
|
2018-03-14 00:24:54 +08:00
|
|
|
guard fields.count == 1 else {
|
|
|
|
throw NIOWebSocketUpgradeError.invalidUpgradeHeader
|
|
|
|
}
|
2019-03-07 02:11:40 +08:00
|
|
|
return String(fields.first!)
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-31 19:36:55 +08:00
|
|
|
/// A `HTTPServerProtocolUpgrader` that knows how to do the WebSocket upgrade dance.
|
2018-03-14 00:24:54 +08:00
|
|
|
///
|
|
|
|
/// Users may frequently want to offer multiple websocket endpoints on the same port. For this
|
2019-05-02 18:02:48 +08:00
|
|
|
/// reason, this `WebServerSocketUpgrader` only knows how to do the required parts of the upgrade and to
|
2018-03-14 00:24:54 +08:00
|
|
|
/// complete the handshake. Users are expected to provide a callback that examines the HTTP headers
|
|
|
|
/// (including the path) and determines whether this is a websocket upgrade request that is acceptable
|
|
|
|
/// to them.
|
|
|
|
///
|
|
|
|
/// This upgrader assumes that the `HTTPServerUpgradeHandler` will appropriately mutate the pipeline to
|
|
|
|
/// remove the HTTP `ChannelHandler`s.
|
2022-10-28 23:46:41 +08:00
|
|
|
public final class NIOWebSocketServerUpgrader: HTTPServerProtocolUpgrader, @unchecked Sendable {
|
|
|
|
// This type *is* Sendable but we can't express that properly until Swift 5.7. In the meantime
|
|
|
|
// the conformance is `@unchecked`.
|
|
|
|
|
2018-03-14 00:24:54 +08:00
|
|
|
/// RFC 6455 specs this as the required entry in the Upgrade header.
|
|
|
|
public let supportedProtocol: String = "websocket"
|
|
|
|
|
|
|
|
/// We deliberately do not actually set any required headers here, because the websocket
|
|
|
|
/// spec annoyingly does not actually force the client to send these in the Upgrade header,
|
|
|
|
/// which NIO requires. We check for these manually.
|
|
|
|
public let requiredUpgradeHeaders: [String] = []
|
|
|
|
|
2022-08-26 23:59:34 +08:00
|
|
|
private let shouldUpgrade: (Channel, HTTPRequestHead) -> EventLoopFuture<HTTPHeaders?>
|
|
|
|
private let upgradePipelineHandler: (Channel, HTTPRequestHead) -> EventLoopFuture<Void>
|
2018-04-17 02:13:20 +08:00
|
|
|
private let maxFrameSize: Int
|
2018-07-31 17:27:12 +08:00
|
|
|
private let automaticErrorHandling: Bool
|
2018-03-14 00:24:54 +08:00
|
|
|
|
2022-07-05 19:07:44 +08:00
|
|
|
#if swift(>=5.7)
|
2022-10-28 23:46:41 +08:00
|
|
|
// FIXME: remove @unchecked when 5.7 is the minimum supported version.
|
2019-05-02 18:02:48 +08:00
|
|
|
/// Create a new `NIOWebSocketServerUpgrader`.
|
2018-03-14 00:24:54 +08:00
|
|
|
///
|
|
|
|
/// - parameters:
|
2018-07-31 17:27:12 +08:00
|
|
|
/// - automaticErrorHandling: Whether the pipeline should automatically handle protocol
|
|
|
|
/// errors by sending error responses and closing the connection. Defaults to `true`,
|
|
|
|
/// may be set to `false` if the user wishes to handle their own errors.
|
2018-03-14 00:24:54 +08:00
|
|
|
/// - shouldUpgrade: A callback that determines whether the websocket request should be
|
|
|
|
/// upgraded. This callback is responsible for creating a `HTTPHeaders` object with
|
|
|
|
/// any headers that it needs on the response *except for* the `Upgrade`, `Connection`,
|
|
|
|
/// and `Sec-WebSocket-Accept` headers, which this upgrader will handle. Should return
|
2019-03-02 02:24:59 +08:00
|
|
|
/// an `EventLoopFuture` containing `nil` if the upgrade should be refused.
|
2018-03-14 00:24:54 +08:00
|
|
|
/// - upgradePipelineHandler: A function that will be called once the upgrade response is
|
|
|
|
/// flushed, and that is expected to mutate the `Channel` appropriately to handle the
|
|
|
|
/// websocket protocol. This only needs to add the user handlers: the
|
|
|
|
/// `WebSocketFrameEncoder` and `WebSocketFrameDecoder` will have been added to the
|
|
|
|
/// pipeline automatically.
|
2022-07-05 19:07:44 +08:00
|
|
|
@preconcurrency
|
|
|
|
public convenience init(
|
|
|
|
automaticErrorHandling: Bool = true,
|
|
|
|
shouldUpgrade: @escaping @Sendable (Channel, HTTPRequestHead) -> EventLoopFuture<HTTPHeaders?>,
|
|
|
|
upgradePipelineHandler: @escaping @Sendable (Channel, HTTPRequestHead) -> EventLoopFuture<Void>
|
|
|
|
) {
|
2018-07-31 17:27:12 +08:00
|
|
|
self.init(maxFrameSize: 1 << 14, automaticErrorHandling: automaticErrorHandling,
|
|
|
|
shouldUpgrade: shouldUpgrade, upgradePipelineHandler: upgradePipelineHandler)
|
2018-04-17 02:13:20 +08:00
|
|
|
}
|
2022-07-05 19:07:44 +08:00
|
|
|
#else
|
|
|
|
/// Create a new `NIOWebSocketServerUpgrader`.
|
|
|
|
///
|
|
|
|
/// - parameters:
|
|
|
|
/// - automaticErrorHandling: Whether the pipeline should automatically handle protocol
|
|
|
|
/// errors by sending error responses and closing the connection. Defaults to `true`,
|
|
|
|
/// may be set to `false` if the user wishes to handle their own errors.
|
|
|
|
/// - shouldUpgrade: A callback that determines whether the websocket request should be
|
|
|
|
/// upgraded. This callback is responsible for creating a `HTTPHeaders` object with
|
|
|
|
/// any headers that it needs on the response *except for* the `Upgrade`, `Connection`,
|
|
|
|
/// and `Sec-WebSocket-Accept` headers, which this upgrader will handle. Should return
|
|
|
|
/// an `EventLoopFuture` containing `nil` if the upgrade should be refused.
|
|
|
|
/// - upgradePipelineHandler: A function that will be called once the upgrade response is
|
|
|
|
/// flushed, and that is expected to mutate the `Channel` appropriately to handle the
|
|
|
|
/// websocket protocol. This only needs to add the user handlers: the
|
|
|
|
/// `WebSocketFrameEncoder` and `WebSocketFrameDecoder` will have been added to the
|
|
|
|
/// pipeline automatically.
|
|
|
|
public convenience init(
|
|
|
|
automaticErrorHandling: Bool = true,
|
|
|
|
shouldUpgrade: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<HTTPHeaders?>,
|
|
|
|
upgradePipelineHandler: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<Void>
|
|
|
|
) {
|
|
|
|
self.init(maxFrameSize: 1 << 14, automaticErrorHandling: automaticErrorHandling,
|
|
|
|
shouldUpgrade: shouldUpgrade, upgradePipelineHandler: upgradePipelineHandler)
|
|
|
|
}
|
|
|
|
#endif
|
2018-04-17 02:13:20 +08:00
|
|
|
|
2022-07-05 19:07:44 +08:00
|
|
|
/// Create a new `NIOWebSocketServerUpgrader`.
|
|
|
|
///
|
|
|
|
/// - parameters:
|
|
|
|
/// - maxFrameSize: The maximum frame size the decoder is willing to tolerate from the
|
|
|
|
/// remote peer. WebSockets in principle allows frame sizes up to `2**64` bytes, but
|
|
|
|
/// this is an objectively unreasonable maximum value (on AMD64 systems it is not
|
|
|
|
/// possible to even. Users may set this to any value up to `UInt32.max`.
|
|
|
|
/// - automaticErrorHandling: Whether the pipeline should automatically handle protocol
|
|
|
|
/// errors by sending error responses and closing the connection. Defaults to `true`,
|
|
|
|
/// may be set to `false` if the user wishes to handle their own errors.
|
|
|
|
/// - shouldUpgrade: A callback that determines whether the websocket request should be
|
|
|
|
/// upgraded. This callback is responsible for creating a `HTTPHeaders` object with
|
|
|
|
/// any headers that it needs on the response *except for* the `Upgrade`, `Connection`,
|
|
|
|
/// and `Sec-WebSocket-Accept` headers, which this upgrader will handle. Should return
|
|
|
|
/// an `EventLoopFuture` containing `nil` if the upgrade should be refused.
|
|
|
|
/// - upgradePipelineHandler: A function that will be called once the upgrade response is
|
|
|
|
/// flushed, and that is expected to mutate the `Channel` appropriately to handle the
|
|
|
|
/// websocket protocol. This only needs to add the user handlers: the
|
|
|
|
/// `WebSocketFrameEncoder` and `WebSocketFrameDecoder` will have been added to the
|
|
|
|
/// pipeline automatically.
|
2022-08-26 23:59:34 +08:00
|
|
|
public init(
|
2022-07-05 19:07:44 +08:00
|
|
|
maxFrameSize: Int,
|
|
|
|
automaticErrorHandling: Bool = true,
|
|
|
|
shouldUpgrade: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<HTTPHeaders?>,
|
|
|
|
upgradePipelineHandler: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<Void>
|
|
|
|
) {
|
2018-04-17 02:13:20 +08:00
|
|
|
precondition(maxFrameSize <= UInt32.max, "invalid overlarge max frame size")
|
2018-03-14 00:24:54 +08:00
|
|
|
self.shouldUpgrade = shouldUpgrade
|
|
|
|
self.upgradePipelineHandler = upgradePipelineHandler
|
2018-04-17 02:13:20 +08:00
|
|
|
self.maxFrameSize = maxFrameSize
|
2018-07-31 17:27:12 +08:00
|
|
|
self.automaticErrorHandling = automaticErrorHandling
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
|
2019-03-02 02:24:59 +08:00
|
|
|
public func buildUpgradeResponse(channel: Channel, upgradeRequest: HTTPRequestHead, initialResponseHeaders: HTTPHeaders) -> EventLoopFuture<HTTPHeaders> {
|
|
|
|
let key: String
|
|
|
|
let version: String
|
2018-03-14 00:24:54 +08:00
|
|
|
|
2019-03-02 02:24:59 +08:00
|
|
|
do {
|
|
|
|
key = try upgradeRequest.headers.nonListHeader("Sec-WebSocket-Key")
|
|
|
|
version = try upgradeRequest.headers.nonListHeader("Sec-WebSocket-Version")
|
|
|
|
} catch {
|
|
|
|
return channel.eventLoop.makeFailedFuture(error)
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
|
2019-03-02 02:24:59 +08:00
|
|
|
// The version must be 13.
|
|
|
|
guard version == "13" else {
|
|
|
|
return channel.eventLoop.makeFailedFuture(NIOWebSocketUpgradeError.invalidUpgradeHeader)
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
|
2019-03-02 02:24:59 +08:00
|
|
|
return self.shouldUpgrade(channel, upgradeRequest).flatMapThrowing { extraHeaders in
|
|
|
|
guard let extraHeaders = extraHeaders else {
|
|
|
|
throw NIOWebSocketUpgradeError.unsupportedWebSocketTarget
|
|
|
|
}
|
|
|
|
return extraHeaders
|
|
|
|
}.map { (extraHeaders: HTTPHeaders) in
|
|
|
|
var extraHeaders = extraHeaders
|
|
|
|
|
|
|
|
// Cool, we're good to go! Let's do our upgrade. We do this by concatenating the magic
|
|
|
|
// GUID to the base64-encoded key and taking a SHA1 hash of the result.
|
|
|
|
let acceptValue: String
|
|
|
|
do {
|
|
|
|
var hasher = SHA1()
|
|
|
|
hasher.update(string: key)
|
|
|
|
hasher.update(string: magicWebSocketGUID)
|
|
|
|
acceptValue = String(base64Encoding: hasher.finish())
|
|
|
|
}
|
|
|
|
|
|
|
|
extraHeaders.replaceOrAdd(name: "Upgrade", value: "websocket")
|
|
|
|
extraHeaders.add(name: "Sec-WebSocket-Accept", value: acceptValue)
|
|
|
|
extraHeaders.replaceOrAdd(name: "Connection", value: "upgrade")
|
|
|
|
|
|
|
|
return extraHeaders
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-26 02:20:22 +08:00
|
|
|
public func upgrade(context: ChannelHandlerContext, upgradeRequest: HTTPRequestHead) -> EventLoopFuture<Void> {
|
2018-07-31 17:27:12 +08:00
|
|
|
/// We never use the automatic error handling feature of the WebSocketFrameDecoder: we always use the separate channel
|
|
|
|
/// handler.
|
2019-02-26 02:20:22 +08:00
|
|
|
var upgradeFuture = context.pipeline.addHandler(WebSocketFrameEncoder()).flatMap {
|
2019-03-09 03:11:39 +08:00
|
|
|
context.pipeline.addHandler(ByteToMessageHandler(WebSocketFrameDecoder(maxFrameSize: self.maxFrameSize)))
|
2018-07-31 17:27:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.automaticErrorHandling {
|
2019-07-08 15:55:02 +08:00
|
|
|
upgradeFuture = upgradeFuture.flatMap {
|
|
|
|
context.pipeline.addHandler(WebSocketProtocolErrorHandler())
|
|
|
|
}
|
2018-07-31 17:27:12 +08:00
|
|
|
}
|
|
|
|
|
2019-01-22 00:41:04 +08:00
|
|
|
return upgradeFuture.flatMap {
|
2019-02-26 02:20:22 +08:00
|
|
|
self.upgradePipelineHandler(context.channel, upgradeRequest)
|
2018-03-14 00:24:54 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|