More featureful error handling options for WebSockets (#528)

Motivation:

Currently if you hit a parse error in the WebSocketFrameDecoder, that
handler takes it upon itself to return the error code and shut the connection
down. That's not really its job: it's not a pattern that we use anywhere
else, and it prevents users from overriding this handling with their own
choices.

We should stop doing that. Unfortunately, we can't totally stop it because
it's a supported feature in the current release of NIO, but we can give
users the option to opt-out, and also build our future solution at the same
time.

Modifications:

1. Added support for disabling the automatic error handling in
    WebSocketFrameDecoder.
2. Created a new WebSocketProtocolErrorHandler class that will be used for
    default error handling in 2.0.
3. Added support to the WebSocketUpgrader to disable automatic error handling
    altogether.
4. Changed the WebSocketUpgrader to use the WebSocketProtocolErrorHandler
    for default error handling when necessary.

Result:

Better separation of concerns, and users can override NIO's default
error handling behaviour.
This commit is contained in:
Cory Benfield 2018-07-31 10:27:12 +01:00 committed by GitHub
parent d31f4a249a
commit 3803d9b096
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 438 additions and 33 deletions

View File

@ -229,6 +229,9 @@ public final class WebSocketFrameDecoder: ByteToMessageDecoder {
/// Whether we should continue to parse.
private var shouldKeepParsing = true
/// Whether this `ChannelHandler` should be performing automatic error handling.
private let automaticErrorHandling: Bool
/// Construct a new `WebSocketFrameDecoder`
///
/// - parameters:
@ -241,9 +244,13 @@ public final class WebSocketFrameDecoder: ByteToMessageDecoder {
/// Users are strongly encouraged not to increase this value unless they absolutely
/// must, as the decoder will not produce partial frames, meaning that it will hold
/// on to data until the *entire* body is received.
public init(maxFrameSize: Int = 1 << 14) {
/// - automaticErrorHandling: Whether this `ChannelHandler` should automatically handle
/// protocol errors in frame serialization, or whether it should allow the pipeline
/// to handle them.
public init(maxFrameSize: Int = 1 << 14, automaticErrorHandling: Bool = true) {
precondition(maxFrameSize <= UInt32.max, "invalid overlarge max frame size")
self.maxFrameSize = maxFrameSize
self.automaticErrorHandling = automaticErrorHandling
}
public func decode(ctx: ChannelHandlerContext, buffer: inout ByteBuffer) -> DecodingState {
@ -307,14 +314,19 @@ public final class WebSocketFrameDecoder: ByteToMessageDecoder {
}
self.shouldKeepParsing = false
var data = ctx.channel.allocator.buffer(capacity: 2)
data.write(webSocketErrorCode: WebSocketErrorCode(error))
let frame = WebSocketFrame(fin: true,
opcode: .connectionClose,
data: data)
ctx.writeAndFlush(self.wrapOutboundOut(frame)).whenComplete {
ctx.close(promise: nil)
// If we've been asked to handle the errors here, we should.
// TODO(cory): Remove this in 2.0, in favour of `WebSocketProtocolErrorHandler`.
if self.automaticErrorHandling {
var data = ctx.channel.allocator.buffer(capacity: 2)
data.write(webSocketErrorCode: WebSocketErrorCode(error))
let frame = WebSocketFrame(fin: true,
opcode: .connectionClose,
data: data)
ctx.writeAndFlush(self.wrapOutboundOut(frame)).whenComplete {
ctx.close(promise: nil)
}
}
ctx.fireErrorCaught(error)
}
}

View File

@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// 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
/// A simple `ChannelHandler` that catches protocol errors emitted by the
/// `WebSocketFrameDecoder` and automatically generates protocol error responses.
///
/// This `ChannelHandler` provides default error handling for basic errors in the
/// WebSocket protocol, and can be used by users when custom behaviour is not required.
public final class WebSocketProtocolErrorHandler: ChannelInboundHandler {
public typealias InboundIn = Never
public typealias OutboundOut = WebSocketFrame
public init() { }
public func errorCaught(ctx: ChannelHandlerContext, error: Error) {
if let error = error as? NIOWebSocketError {
var data = ctx.channel.allocator.buffer(capacity: 2)
data.write(webSocketErrorCode: WebSocketErrorCode(error))
let frame = WebSocketFrame(fin: true,
opcode: .connectionClose,
data: data)
ctx.writeAndFlush(self.wrapOutboundOut(frame)).whenComplete {
ctx.close(promise: nil)
}
}
// Regardless of whether this is an error we want to handle or not, we always
// forward the error on to let others see it.
ctx.fireErrorCaught(error)
}
}

View File

@ -60,10 +60,14 @@ public final class WebSocketUpgrader: HTTPProtocolUpgrader {
private let shouldUpgrade: (HTTPRequestHead) -> HTTPHeaders?
private let upgradePipelineHandler: (Channel, HTTPRequestHead) -> EventLoopFuture<Void>
private let maxFrameSize: Int
private let automaticErrorHandling: Bool
/// Create a new `WebSocketUpgrader`.
///
/// - 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`,
@ -74,18 +78,10 @@ public final class WebSocketUpgrader: HTTPProtocolUpgrader {
/// websocket protocol. This only needs to add the user handlers: the
/// `WebSocketFrameEncoder` and `WebSocketFrameDecoder` will have been added to the
/// pipeline automatically.
/// - 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 allocate a buffer large enough to handle this size), so we
/// set a lower one. The default value is the same as the default HTTP/2 max frame
/// size, `2**14` bytes. Users may override this to any value up to `UInt32.max`.
/// Users are strongly encouraged not to increase this value unless they absolutely
/// must, as the decoder will not produce partial frames, meaning that it will hold
/// on to data until the *entire* body is received.
public convenience init(shouldUpgrade: @escaping (HTTPRequestHead) -> HTTPHeaders?,
public convenience init(automaticErrorHandling: Bool = true, shouldUpgrade: @escaping (HTTPRequestHead) -> HTTPHeaders?,
upgradePipelineHandler: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<Void>) {
self.init(maxFrameSize: 1 << 14, shouldUpgrade: shouldUpgrade, upgradePipelineHandler: upgradePipelineHandler)
self.init(maxFrameSize: 1 << 14, automaticErrorHandling: automaticErrorHandling,
shouldUpgrade: shouldUpgrade, upgradePipelineHandler: upgradePipelineHandler)
}
@ -96,6 +92,9 @@ public final class WebSocketUpgrader: HTTPProtocolUpgrader {
/// 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`,
@ -106,12 +105,13 @@ public final class WebSocketUpgrader: HTTPProtocolUpgrader {
/// websocket protocol. This only needs to add the user handlers: the
/// `WebSocketFrameEncoder` and `WebSocketFrameDecoder` will have been added to the
/// pipeline automatically.
public init(maxFrameSize: Int, shouldUpgrade: @escaping (HTTPRequestHead) -> HTTPHeaders?,
public init(maxFrameSize: Int, automaticErrorHandling: Bool = true, shouldUpgrade: @escaping (HTTPRequestHead) -> HTTPHeaders?,
upgradePipelineHandler: @escaping (Channel, HTTPRequestHead) -> EventLoopFuture<Void>) {
precondition(maxFrameSize <= UInt32.max, "invalid overlarge max frame size")
self.shouldUpgrade = shouldUpgrade
self.upgradePipelineHandler = upgradePipelineHandler
self.maxFrameSize = maxFrameSize
self.automaticErrorHandling = automaticErrorHandling
}
public func buildUpgradeResponse(upgradeRequest: HTTPRequestHead, initialResponseHeaders: HTTPHeaders) throws -> HTTPHeaders {
@ -145,9 +145,17 @@ public final class WebSocketUpgrader: HTTPProtocolUpgrader {
}
public func upgrade(ctx: ChannelHandlerContext, upgradeRequest: HTTPRequestHead) -> EventLoopFuture<Void> {
return ctx.pipeline.add(handler: WebSocketFrameEncoder()).then {
ctx.pipeline.add(handler: WebSocketFrameDecoder(maxFrameSize: self.maxFrameSize))
}.then {
/// We never use the automatic error handling feature of the WebSocketFrameDecoder: we always use the separate channel
/// handler.
var upgradeFuture = ctx.pipeline.add(handler: WebSocketFrameEncoder()).then {
ctx.pipeline.add(handler: WebSocketFrameDecoder(maxFrameSize: self.maxFrameSize, automaticErrorHandling: false))
}
if self.automaticErrorHandling {
upgradeFuture = upgradeFuture.then { ctx.pipeline.add(handler: WebSocketProtocolErrorHandler())}
}
return upgradeFuture.then {
self.upgradePipelineHandler(ctx.channel, upgradeRequest)
}
}

View File

@ -36,6 +36,8 @@ extension EndToEndTests {
("testMayRegisterMultipleWebSocketEndpoints", testMayRegisterMultipleWebSocketEndpoints),
("testSendAFewFrames", testSendAFewFrames),
("testMaxFrameSize", testMaxFrameSize),
("testAutomaticErrorHandling", testAutomaticErrorHandling),
("testNoAutomaticErrorHandling", testNoAutomaticErrorHandling),
]
}
}

View File

@ -26,6 +26,14 @@ extension EmbeddedChannel {
return buffer
}
func finishAcceptingAlreadyClosed() throws {
do {
try self.finish()
} catch ChannelError.alreadyClosed {
// ok
}
}
}
extension ByteBuffer {
@ -42,7 +50,7 @@ extension EmbeddedChannel {
}
}
private func interactInMemory(_ first: EmbeddedChannel, _ second: EmbeddedChannel) {
private func interactInMemory(_ first: EmbeddedChannel, _ second: EmbeddedChannel) throws {
var operated: Bool
repeat {
@ -50,11 +58,11 @@ private func interactInMemory(_ first: EmbeddedChannel, _ second: EmbeddedChanne
if case .some(.byteBuffer(let data)) = first.readOutbound() {
operated = true
XCTAssertNoThrow(try second.writeInbound(data))
try second.writeInbound(data)
}
if case .some(.byteBuffer(let data)) = second.readOutbound() {
operated = true
XCTAssertNoThrow(try first.writeInbound(data))
try first.writeInbound(data)
}
} while operated
}
@ -88,11 +96,17 @@ private class WebSocketRecorderHandler: ChannelInboundHandler {
typealias OutboundOut = WebSocketFrame
public var frames: [WebSocketFrame] = []
public var errors: [Error] = []
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let frame = self.unwrapInboundIn(data)
self.frames.append(frame)
}
func errorCaught(ctx: ChannelHandlerContext, error: Error) {
self.errors.append(error)
ctx.fireErrorCaught(error)
}
}
class EndToEndTests: XCTestCase {
@ -122,7 +136,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -142,7 +156,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="], protocolName: "WebSocket")
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -282,7 +296,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -313,7 +327,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(path: "/third", extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -337,7 +351,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -356,7 +370,7 @@ class EndToEndTests: XCTestCase {
let pingFrame = WebSocketFrame(fin: true, opcode: .ping, data: client.allocator.buffer(capacity: 0))
XCTAssertNoThrow(try client.writeAndFlush(pingFrame).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
XCTAssertEqual(recorder.frames, [dataFrame, pingFrame])
}
@ -375,7 +389,7 @@ class EndToEndTests: XCTestCase {
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
interactInMemory(client, server)
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
@ -385,4 +399,95 @@ class EndToEndTests: XCTestCase {
let decoder = (try server.pipeline.context(handlerType: WebSocketFrameDecoder.self).wait()).handler as! WebSocketFrameDecoder
XCTAssertEqual(16, decoder.maxFrameSize)
}
func testAutomaticErrorHandling() throws {
let recorder = WebSocketRecorderHandler()
let basicUpgrader = WebSocketUpgrader(shouldUpgrade: { head in HTTPHeaders() },
upgradePipelineHandler: { (channel, req) in
channel.pipeline.add(handler: recorder)
})
let (loop, server, client) = createTestFixtures(upgraders: [basicUpgrader])
defer {
XCTAssertNoThrow(try client.finish())
XCTAssertNoThrow(try server.finishAcceptingAlreadyClosed())
XCTAssertNoThrow(try loop.syncShutdownGracefully())
}
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
expectedResponseLine: "HTTP/1.1 101 Switching Protocols",
expectedResponseHeaders: ["Upgrade: websocket", "Sec-WebSocket-Accept: OfS0wDaT5NoxF2gqm7Zj2YtetzM=", "Connection: upgrade"])
// Send a fake frame header that claims this is a ping frame with 126 bytes of data.
var data = client.allocator.buffer(capacity: 12)
data.write(bytes: [0x89, 0x7E, 0x00, 0x7E])
XCTAssertNoThrow(try client.writeAndFlush(data).wait())
do {
try interactInMemory(client, server)
XCTFail("Did not throw")
} catch NIOWebSocketError.multiByteControlFrameLength {
// ok
} catch {
XCTFail("Unexpected error: \(error)")
}
XCTAssertEqual(recorder.errors.count, 1)
XCTAssertEqual(recorder.errors.first as? NIOWebSocketError, .some(.multiByteControlFrameLength))
// The client should have received a close frame, if we'd continued interacting.
let errorFrame = server.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [0x88, 0x02, 0x03, 0xEA])
}
func testNoAutomaticErrorHandling() throws {
let recorder = WebSocketRecorderHandler()
let basicUpgrader = WebSocketUpgrader(automaticErrorHandling: false,
shouldUpgrade: { head in HTTPHeaders() },
upgradePipelineHandler: { (channel, req) in
channel.pipeline.add(handler: recorder)
})
let (loop, server, client) = createTestFixtures(upgraders: [basicUpgrader])
defer {
XCTAssertNoThrow(try client.finish())
XCTAssertNoThrow(try server.finishAcceptingAlreadyClosed())
XCTAssertNoThrow(try loop.syncShutdownGracefully())
}
let upgradeRequest = self.upgradeRequest(extraHeaders: ["Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "AQIDBAUGBwgJCgsMDQ4PEC=="])
XCTAssertNoThrow(try client.writeString(upgradeRequest).wait())
XCTAssertNoThrow(try interactInMemory(client, server))
let receivedResponse = client.readAllInboundBuffers().allAsString()
assertResponseIs(response: receivedResponse,
expectedResponseLine: "HTTP/1.1 101 Switching Protocols",
expectedResponseHeaders: ["Upgrade: websocket", "Sec-WebSocket-Accept: OfS0wDaT5NoxF2gqm7Zj2YtetzM=", "Connection: upgrade"])
// Send a fake frame header that claims this is a ping frame with 126 bytes of data.
var data = client.allocator.buffer(capacity: 12)
data.write(bytes: [0x89, 0x7E, 0x00, 0x7E])
XCTAssertNoThrow(try client.writeAndFlush(data).wait())
do {
try interactInMemory(client, server)
XCTFail("Did not throw")
} catch NIOWebSocketError.multiByteControlFrameLength {
// ok
} catch {
XCTFail("Unexpected error: \(error)")
}
XCTAssertEqual(recorder.errors.count, 1)
XCTAssertEqual(recorder.errors.first as? NIOWebSocketError, .some(.multiByteControlFrameLength))
// The client should not have received a close frame, if we'd continued interacting.
let errorFrame = server.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [])
}
}

View File

@ -39,6 +39,14 @@ extension WebSocketFrameDecoderTest {
("testDecoderRejectsMultibyteControlFrameLengths", testDecoderRejectsMultibyteControlFrameLengths),
("testIgnoresFurtherDataAfterRejectedFrame", testIgnoresFurtherDataAfterRejectedFrame),
("testClosingSynchronouslyOnChannelRead", testClosingSynchronouslyOnChannelRead),
("testDecoderRejectsOverlongFramesWithNoAutomaticErrorHandling", testDecoderRejectsOverlongFramesWithNoAutomaticErrorHandling),
("testDecoderRejectsFragmentedControlFramesWithNoAutomaticErrorHandling", testDecoderRejectsFragmentedControlFramesWithNoAutomaticErrorHandling),
("testDecoderRejectsMultibyteControlFrameLengthsWithNoAutomaticErrorHandling", testDecoderRejectsMultibyteControlFrameLengthsWithNoAutomaticErrorHandling),
("testIgnoresFurtherDataAfterRejectedFrameWithNoAutomaticErrorHandling", testIgnoresFurtherDataAfterRejectedFrameWithNoAutomaticErrorHandling),
("testDecoderRejectsOverlongFramesWithSeparateErrorHandling", testDecoderRejectsOverlongFramesWithSeparateErrorHandling),
("testDecoderRejectsFragmentedControlFramesWithSeparateErrorHandling", testDecoderRejectsFragmentedControlFramesWithSeparateErrorHandling),
("testDecoderRejectsMultibyteControlFrameLengthsWithSeparateErrorHandling", testDecoderRejectsMultibyteControlFrameLengthsWithSeparateErrorHandling),
("testIgnoresFurtherDataAfterRejectedFrameWithSeparateErrorHandling", testIgnoresFurtherDataAfterRejectedFrameWithSeparateErrorHandling),
]
}
}

View File

@ -100,6 +100,16 @@ public class WebSocketFrameDecoderTest: XCTestCase {
XCTAssertNotEqual(frameForFrame(frame), frame)
}
private func swapDecoder(for handler: ChannelHandler) {
// We need to insert a decoder that doesn't do error handling. We still insert
// an encoder because we want to fail gracefully if a frame is written.
XCTAssertNoThrow(try self.decoderChannel.pipeline.context(handlerType: WebSocketFrameDecoder.self).then {
self.decoderChannel.pipeline.remove(handler: $0.handler)
}.then { (_: Bool) in
self.decoderChannel.pipeline.add(handler: handler)
}.wait())
}
public func testFramesWithoutBodies() throws {
let frame = WebSocketFrame(fin: true, opcode: .ping, data: self.buffer)
assertFrameRoundTrips(frame: frame)
@ -330,4 +340,220 @@ public class WebSocketFrameDecoderTest: XCTestCase {
XCTAssertNil(self.decoderChannel.readOutbound())
XCTAssertNil(self.decoderChannel.readInbound() as WebSocketFrame?)
}
public func testDecoderRejectsOverlongFramesWithNoAutomaticErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling. We still insert
// an encoder because we want to fail gracefully if a frame is written.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
// A fake frame header that claims that the length of the frame is 16385 bytes,
// larger than the frame max.
self.buffer.write(bytes: [0x81, 0xFE, 0x40, 0x01])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.invalidFrameLength {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// No error frame should be written.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [])
}
public func testDecoderRejectsFragmentedControlFramesWithNoAutomaticErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling. We still insert
// an encoder because we want to fail gracefully if a frame is written.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
// A fake frame header that claims this is a fragmented ping frame.
self.buffer.write(bytes: [0x09, 0x00])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.fragmentedControlFrame {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// No error frame should be written.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [])
}
public func testDecoderRejectsMultibyteControlFrameLengthsWithNoAutomaticErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling. We still insert
// an encoder because we want to fail gracefully if a frame is written.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
// A fake frame header that claims this is a ping frame with 126 bytes of data.
self.buffer.write(bytes: [0x89, 0x7E, 0x00, 0x7E])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.multiByteControlFrameLength {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// No error frame should be written.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [])
}
func testIgnoresFurtherDataAfterRejectedFrameWithNoAutomaticErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling. We still insert
// an encoder because we want to fail gracefully if a frame is written.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
// A fake frame header that claims this is a fragmented ping frame.
self.buffer.write(bytes: [0x09, 0x00])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.fragmentedControlFrame {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// No error frame should be written.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [])
// Now write another broken frame, this time an overlong frame.
// No error should occur here.
self.buffer.clear()
self.buffer.write(bytes: [0x81, 0xFE, 0x40, 0x01])
XCTAssertNoThrow(try self.decoderChannel.writeInbound(self.buffer))
// No extra data should have been sent.
XCTAssertNil(self.decoderChannel.readOutbound())
}
public func testDecoderRejectsOverlongFramesWithSeparateErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling, and then a separate error
// handler.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketProtocolErrorHandler()).wait())
// A fake frame header that claims that the length of the frame is 16385 bytes,
// larger than the frame max.
self.buffer.write(bytes: [0x81, 0xFE, 0x40, 0x01])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.invalidFrameLength {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// We expect that an error frame will have been written out.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [0x88, 0x02, 0x03, 0xF1])
}
public func testDecoderRejectsFragmentedControlFramesWithSeparateErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling, and then a separate error
// handler.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketProtocolErrorHandler()).wait())
// A fake frame header that claims this is a fragmented ping frame.
self.buffer.write(bytes: [0x09, 0x00])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.fragmentedControlFrame {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// We expect that an error frame will have been written out.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [0x88, 0x02, 0x03, 0xEA])
}
public func testDecoderRejectsMultibyteControlFrameLengthsWithSeparateErrorHandling() throws {
// We need to insert a decoder that doesn't do error handling, and then a separate error
// handler.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketProtocolErrorHandler()).wait())
// A fake frame header that claims this is a ping frame with 126 bytes of data.
self.buffer.write(bytes: [0x89, 0x7E, 0x00, 0x7E])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.multiByteControlFrameLength {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// We expect that an error frame will have been written out.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [0x88, 0x02, 0x03, 0xEA])
}
func testIgnoresFurtherDataAfterRejectedFrameWithSeparateErrorHandling() throws {
let swallower = CloseSwallower()
// We need to insert a decoder that doesn't do error handling, and then a separate error
// handler.
self.swapDecoder(for: WebSocketFrameDecoder(automaticErrorHandling: false))
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketFrameEncoder(), first: true).wait())
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: WebSocketProtocolErrorHandler()).wait())
XCTAssertNoThrow(try self.decoderChannel.pipeline.add(handler: swallower, first: true).wait())
// A fake frame header that claims this is a fragmented ping frame.
self.buffer.write(bytes: [0x09, 0x00])
do {
try self.decoderChannel.writeInbound(self.buffer)
XCTFail("did not throw")
} catch NIOWebSocketError.fragmentedControlFrame {
// OK
} catch {
XCTFail("Unexpected error: \(error)")
}
// We expect that an error frame will have been written out.
let errorFrame = self.decoderChannel.readAllOutboundBytes()
XCTAssertEqual(errorFrame, [0x88, 0x02, 0x03, 0xEA])
// Now write another broken frame, this time an overlong frame.
// No error should occur here.
self.buffer.clear()
self.buffer.write(bytes: [0x81, 0xFE, 0x40, 0x01])
XCTAssertNoThrow(try self.decoderChannel.writeInbound(self.buffer))
// No extra data should have been sent.
XCTAssertNil(self.decoderChannel.readOutbound())
// Allow the channel to close.
swallower.allowClose()
// Take the handler out for cleanliness.
XCTAssertNoThrow(try self.decoderChannel.pipeline.remove(handler: swallower).wait())
}
}