Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add a WebSocket client upgrader. #1038

Merged
merged 10 commits into from
Jul 8, 2019
88 changes: 88 additions & 0 deletions Sources/NIOWebSocket/NIOWebSocketClientUpgrader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 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
import NIOHTTP1

/// A `HTTPClientProtocolUpgrader` that knows how to do the WebSocket upgrade dance.
///
/// This upgrader assumes that the `HTTPClientUpgradeHandler` will create and send the upgrade request.
/// This upgrader also assumes that the `HTTPClientUpgradeHandler` will appropriately mutate the
/// pipeline to remove the HTTP `ChannelHandler`s.
public final class NIOWebClientSocketUpgrader: NIOHTTPClientProtocolUpgrader {
Copy link
Contributor

@tanner0101 tanner0101 Oct 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a typo?

- NIOWebClientSocketUpgrader
+ NIOWebSocketClientUpgrader

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tanner0101 omg, yes, this is a typo :|. Mind making PR to rename (alongside a deprecated typealias to have the NIOWebClientSocketUpgrader still valid to not break SemVer).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tanner0101 @weissi Sorry, my bad. Definitely is a typo. I am not sure how that happened. Changing this will also require a change to the WebSocketClientDemo to use the updated class name rather than the old via the type alias.

I am happy to make a PR for one or both of these if you would rather not do it. Let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tigerpixel don't apologise, we all missed it :). We can land these changes in either 1 or 2 PRs, just need to make sure we retain a (deprecated) typealias so the typo'd name stays usable (for SemVer compatibility). NIO's public API breakage checker should however catch any mistakes that we might make.


/// RFC 6455 specs this as the required entry in the Upgrade header.
public var supportedProtocol: String = "websocket"
/// None of the websocket headers are actually defined as 'required'.
public var requiredUpgradeHeaders: [String] = []

private let requestKey: String
private let maxFrameSize: Int
private let automaticErrorHandling: Bool
private let upgradePipelineHandler: (Channel, HTTPResponseHead) -> EventLoopFuture<Void>

public init(requestKey: String,
maxFrameSize: Int = 1 << 14,
automaticErrorHandling: Bool = true,
upgradePipelineHandler: @escaping (Channel, HTTPResponseHead) -> EventLoopFuture<Void>) {

precondition(requestKey != "", "The request key must contain a valid Sec-WebSocket-Key")
precondition(maxFrameSize <= UInt32.max, "invalid overlarge max frame size")
self.requestKey = requestKey
self.upgradePipelineHandler = upgradePipelineHandler
self.maxFrameSize = maxFrameSize
self.automaticErrorHandling = automaticErrorHandling
}

public func addCustom(upgradeRequestHeaders: inout HTTPHeaders) {
upgradeRequestHeaders.add(name: "Sec-WebSocket-Key", value: self.requestKey)
upgradeRequestHeaders.add(name: "Sec-WebSocket-Version", value: "13")
}

public func shouldAllowUpgrade(upgradeResponse: HTTPResponseHead) -> Bool {

let acceptValueHeader = upgradeResponse.headers["Sec-WebSocket-Accept"]

guard acceptValueHeader.count == 1 else {
return false
}

// Validate the response key in 'Sec-WebSocket-Accept'.
let expectedAcceptValue: String
do {
var hasher = SHA1()
hasher.update(string: self.requestKey)
hasher.update(string: magicWebSocketGUID)
expectedAcceptValue = String(base64Encoding: hasher.finish())
}

return expectedAcceptValue == acceptValueHeader[0]
}

public func upgrade(context: ChannelHandlerContext, upgradeResponse: HTTPResponseHead) -> EventLoopFuture<Void> {

var upgradeFuture = context.pipeline.addHandler(WebSocketFrameEncoder()).flatMap {_ in
context.pipeline.addHandler(ByteToMessageHandler(WebSocketFrameDecoder(maxFrameSize: self.maxFrameSize)))
}

if self.automaticErrorHandling {
upgradeFuture = upgradeFuture.flatMap { context.pipeline.addHandler(WebSocketProtocolErrorHandler())
}
}

return upgradeFuture.flatMap { _ in
self.upgradePipelineHandler(context.channel, upgradeResponse)
}
}
}
4 changes: 2 additions & 2 deletions Sources/NIOWebSocket/NIOWebSocketServerUpgrader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Copyright (c) 2017-2019 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -16,7 +16,7 @@ import CNIOSHA1
import NIO
import NIOHTTP1

private let magicWebSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
let magicWebSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

@available(*, deprecated, renamed: "NIOWebSocketServerUpgrader")
public typealias WebSocketUpgrader = NIOWebSocketServerUpgrader
Expand Down
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import XCTest
testCase(ThreadTest.allTests),
testCase(TypeAssistedChannelHandlerTest.allTests),
testCase(UtilitiesTest.allTests),
testCase(WebSocketClientEndToEndTests.allTests),
testCase(WebSocketFrameDecoderTest.allTests),
testCase(WebSocketFrameEncoderTest.allTests),
testCase(WebSocketServerEndToEndTests.allTests),
Expand Down
38 changes: 38 additions & 0 deletions Tests/NIOWebSocketTests/WebSocketClientEndToEndTests+XCTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// WebSocketClientEndToEndTests+XCTest.swift
//
import XCTest

///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///

extension WebSocketClientEndToEndTests {

static var allTests : [(String, (WebSocketClientEndToEndTests) -> () throws -> Void)] {
return [
("testSimpleUpgradeSucceeds", testSimpleUpgradeSucceeds),
("testRejectUpgradeIfMissingAcceptKey", testRejectUpgradeIfMissingAcceptKey),
("testRejectUpgradeIfIncorrectAcceptKey", testRejectUpgradeIfIncorrectAcceptKey),
("testRejectUpgradeIfNotWebsocket", testRejectUpgradeIfNotWebsocket),
("testSendAFewFrames", testSendAFewFrames),
("testReceiveAFewFrames", testReceiveAFewFrames),
]
}
}

Loading