From e2a267d3c12a5e0bb861c5a938df38a925b101a3 Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Sat, 4 Aug 2018 15:55:25 -0700 Subject: [PATCH 1/7] More Swift-y types for HTTP/2 frame, flags, and settings. Motivation: Prior to bringing in my HTTP/2 encoder/decoder, I think my implementations of HTTP/2 frame flags as `OptionSet`s and settings as an enum (i.e. existentials) make for a nicer API, and provide somewhat more information for the compiler to work with when optimizing. Modifications: - `HTTP2Frame.FrameFlags` is now an `OptionSet`, and includes the CACHE_DIGEST `reset` and `complete` flag values. - `HTTP2Frame.FramePayload` now supports all current & proposed HTTP/2 frame types, can return their on-wire identifier, and can return the set of `HTTP2Frame.FrameFlags` that the frame allows. - `HTTP2Setting` is now an enum with associated values, and includes the SETTINGS_ACCEPT_CACHE_DIGEST and SETTINGS_ENABLE_CONNECT_PROTOCOL values. - `HTTP2Setting` already includes code to encode/decode itself, though it's not yet used. - Added `NIOHTTP2Errors.InvalidSettings` error, used when bad settings are encountered by decoder. - NIOHTTP2 now depends on NIOHPACK. - `HTTP2Stream` now maintains a `HPACKEncoder` and `HPACKDecoder`. - Updated `NGHTTP2Session.swift` to handle (i.e. `fatalError("not implemented")`) the additional frame types CONTINUATION, BLOCKED, ORIGIN, and CACHE_DIGEST which were added to `HTTP2Frame.FramePayload`. - Updated tests to use the new `HTTP2Setting` type. Result: Not a lot changes, beyond the in-memory layout of some things. All these types still have nghttp2 conversion methods, and all tests still run successfully. --- Package.swift | 2 +- Sources/NIOHTTP2/HTTP2Error.swift | 18 ++ Sources/NIOHTTP2/HTTP2Frame.swift | 158 ++++++---- Sources/NIOHTTP2/HTTP2Parser.swift | 6 +- Sources/NIOHTTP2/HTTP2Settings.swift | 279 +++++++++++++----- Sources/NIOHTTP2/HTTP2Stream.swift | 12 + Sources/NIOHTTP2/NGHTTP2Session.swift | 8 + Tests/NIOHTTP2Tests/ReentrancyTests.swift | 6 +- .../SimpleClientServerTests.swift | 8 +- 9 files changed, 347 insertions(+), 150 deletions(-) diff --git a/Package.swift b/Package.swift index 535d3313..844bb0f6 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ let package = Package( .target(name: "NIOHTTP2Server", dependencies: ["NIOHTTP2"]), .target(name: "NIOHTTP2", - dependencies: ["NIO", "NIOHTTP1", "NIOTLS", "CNIONghttp2"]), + dependencies: ["NIO", "NIOHTTP1", "NIOTLS", "CNIONghttp2", "NIOHPACK"]), .target(name: "NIOHPACK", dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOHTTP1"]), .testTarget(name: "NIOHTTP2Tests", diff --git a/Sources/NIOHTTP2/HTTP2Error.swift b/Sources/NIOHTTP2/HTTP2Error.swift index 3eed091b..19efc35d 100644 --- a/Sources/NIOHTTP2/HTTP2Error.swift +++ b/Sources/NIOHTTP2/HTTP2Error.swift @@ -59,6 +59,24 @@ public enum NIOHTTP2Errors { self.nghttp2ErrorCode = nghttp2ErrorCode } } + + /// Received/decoded data was invalid. + public struct InvalidSettings: NIOHTTP2Error { + /// The network identifier of the setting being read. + public var settingCode: UInt16 + + /// The offending value. + public var value: Int32 + + /// The error code associated with the error. + public var errorCode: HTTP2ErrorCode + + public init(setting: UInt16, value: Int32, errorCode: HTTP2ErrorCode) { + self.settingCode = setting + self.value = value + self.errorCode = errorCode + } + } } diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index a7c4dfb3..d7804119 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -21,88 +21,45 @@ public struct HTTP2Frame { /// The payload of this HTTP/2 frame. public var payload: FramePayload - /// The frame flags as an 8-bit integer. To set/unset well-defined flags, consider using the - /// other properties on this object (e.g. `endStream`). - public var flags: UInt8 + /// The frame flags. + public var flags: FrameFlags /// The frame stream ID as a 32-bit integer. public var streamID: HTTP2StreamID - // Whether the END_STREAM flag bit is set. - public var endStream: Bool { - get { - switch self.payload { - case .data, .headers: - return (self.flags & UInt8(NGHTTP2_FLAG_END_STREAM.rawValue) != 0) - default: - return false + private func _hasFlag(_ flag: FrameFlags) -> Bool { + return self.flags.contains(flag) } + + private mutating func _setFlagIfValid(_ flag: FrameFlags) { + if self.payload.allowedFlags.contains(flag) { + self.flags.formUnion(flag) } - set { - switch self.payload { - case .data, .headers: - self.flags |= UInt8(NGHTTP2_FLAG_END_STREAM.rawValue) - default: - break } + + // Whether the END_STREAM flag bit is set. + public var endStream: Bool { + get { return self._hasFlag(.endStream) } + set { self._setFlagIfValid(.endStream) } } - } // Whether the PADDED flag bit is set. public var padded: Bool { - get { - switch self.payload { - case .data, .headers, .pushPromise: - return (self.flags & UInt8(NGHTTP2_FLAG_PADDED.rawValue) != 0) - default: - return false - } + get { return self._hasFlag(.padded) } + set { self._setFlagIfValid(.padded) } } - set { - switch self.payload { - case .data, .headers, .pushPromise: - self.flags |= UInt8(NGHTTP2_FLAG_PADDED.rawValue) - default: - break - } - } - } // Whether the PRIORITY flag bit is set. public var priority: Bool { - get { - if case .headers = self.payload { - return (self.flags & UInt8(NGHTTP2_FLAG_PRIORITY.rawValue) != 0) - } else { - return false - } - } - set { - if case .headers = self.payload { - self.flags |= UInt8(NGHTTP2_FLAG_PRIORITY.rawValue) + get { return self._hasFlag(.priority) } + set { self._setFlagIfValid(.priority) } } - } - } // Whether the ACK flag bit is set. public var ack: Bool { - get { - switch self.payload { - case .settings, .ping: - return (self.flags & UInt8(NGHTTP2_FLAG_ACK.rawValue) != 0) - default: - return false + get { return self._hasFlag(.ack) } + set { self._setFlagIfValid(.ack) } } - } - set { - switch self.payload { - case .settings, .ping: - self.flags |= UInt8(NGHTTP2_FLAG_ACK.rawValue) - default: - break - } - } - } public enum FramePayload { case data(IOData) @@ -114,15 +71,86 @@ public struct HTTP2Frame { case ping(HTTP2PingData) case goAway(lastStreamID: HTTP2StreamID, errorCode: HTTP2ErrorCode, opaqueData: ByteBuffer?) case windowUpdate(windowSizeIncrement: Int) - case alternativeService + case continuation(HTTPHeaders) + case alternativeService(origin: String?, field: ByteBuffer?) + case blocked + case origin([String]) + case cacheDigest(origin: String?, digest: ByteBuffer?) + + var code: UInt8 { + switch self { + case .data: return 0x0 + case .headers: return 0x1 + case .priority: return 0x2 + case .rstStream: return 0x3 + case .settings: return 0x4 + case .pushPromise: return 0x5 + case .ping: return 0x6 + case .goAway: return 0x7 + case .windowUpdate: return 0x8 + case .continuation: return 0x9 + case .alternativeService: return 0xa + case .blocked: return 0xb + case .origin: return 0xc + case .cacheDigest: return 0xd + } + } + + var allowedFlags: FrameFlags { + switch self { + case .data: + return [.padded, .endStream] + case .headers: + return [.endStream, .endHeaders, .padded, .priority] + case .pushPromise: + return [.endHeaders, .padded] + case .continuation: + return .endHeaders + case .cacheDigest: + return [.reset, .complete] + + case .settings, .ping: + return .ack + + case .priority, .rstStream, .goAway, .windowUpdate, + .alternativeService, .blocked, .origin: + return [] + } + } + } + + public struct FrameFlags : OptionSet { + public typealias RawValue = UInt8 + + public private(set) var rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let endStream = FrameFlags(rawValue: 0x01) + public static let ack = FrameFlags(rawValue: 0x01) + public static let reset = FrameFlags(rawValue: 0x01) + public static let complete = FrameFlags(rawValue: 0x02) + public static let endHeaders = FrameFlags(rawValue: 0x04) + public static let padded = FrameFlags(rawValue: 0x08) + public static let priority = FrameFlags(rawValue: 0x20) + + // useful for test cases + internal static var allFlags: FrameFlags = [.endStream, .endHeaders, .padded, .priority] } } internal extension HTTP2Frame { + internal init(streamID: HTTP2StreamID, flags: HTTP2Frame.FrameFlags, payload: HTTP2Frame.FramePayload) { + self.streamID = streamID + self.flags = flags.intersection(payload.allowedFlags) + self.payload = payload + } internal init(streamID: HTTP2StreamID, flags: UInt8, payload: HTTP2Frame.FramePayload) { self.streamID = streamID - self.flags = flags + self.flags = FrameFlags(rawValue: flags).intersection(payload.allowedFlags) self.payload = payload } } @@ -131,7 +159,7 @@ public extension HTTP2Frame { /// Constructs a frame header for a given stream ID. All flags are unset. public init(streamID: HTTP2StreamID, payload: HTTP2Frame.FramePayload) { self.streamID = streamID - self.flags = 0 + self.flags = [] self.payload = payload } } diff --git a/Sources/NIOHTTP2/HTTP2Parser.swift b/Sources/NIOHTTP2/HTTP2Parser.swift index f4eb6e08..eab4099e 100644 --- a/Sources/NIOHTTP2/HTTP2Parser.swift +++ b/Sources/NIOHTTP2/HTTP2Parser.swift @@ -18,9 +18,9 @@ import CNIONghttp2 /// NIO's default settings used for initial settings values on HTTP/2 streams, when the user hasn't /// overridden that. This limits the max concurrent streams to 100, and limits the max header list /// size to 16kB, to avoid trivial resource exhaustion on NIO HTTP/2 users. -public let nioDefaultSettings = [ - HTTP2Setting(parameter: .maxConcurrentStreams, value: 100), - HTTP2Setting(parameter: .maxHeaderListSize, value: 1<<16) +public let nioDefaultSettings: [HTTP2Setting] = [ + .maxConcurrentStreams(100), + .maxHeaderListSize(1<<16) ] diff --git a/Sources/NIOHTTP2/HTTP2Settings.swift b/Sources/NIOHTTP2/HTTP2Settings.swift index 8ed3e714..72c3b4ba 100644 --- a/Sources/NIOHTTP2/HTTP2Settings.swift +++ b/Sources/NIOHTTP2/HTTP2Settings.swift @@ -12,101 +12,232 @@ // //===----------------------------------------------------------------------===// import CNIONghttp2 +import NIO -/// A HTTP/2 settings parameter that allows representing both known and unknown HTTP/2 -/// settings parameters. -public struct HTTP2SettingsParameter { - internal let networkRepresentation: UInt16 - - /// Create a `HTTP2SettingsParameter` that is not known to NIO. +/// A single setting for HTTP/2, a combination of parameter identifier and its value. +public enum HTTP2Setting { + /// SETTINGS_HEADER_TABLE_SIZE (0x1) /// - /// If this is a known parameter, use one of the static values. - public init(extensionSetting: Int) { - self.networkRepresentation = UInt16(extensionSetting) - } - - /// Initialize a `HTTP2SettingsParameter` from nghttp2's representation. - internal init(fromNetwork value: Int32) { - self.networkRepresentation = UInt16(value) - } - - /// A helper to initialize the static parameters. - private init(_ value: UInt16) { - self.networkRepresentation = value - } - - /// Corresponds to SETTINGS_HEADER_TABLE_SIZE - public static let headerTableSize = HTTP2SettingsParameter(1) + /// Allows the sender to inform the remote endpoint of the maximum size of + /// the header compression table used to decode header blocks, in octets. The + /// encoder can select any size equal to or less than this value by using + /// signaling specific to the header compression format inside a header block. + /// The initial value is 4,096 octets. + case headerTableSize(Int32) + + /// SETTINGS_ENABLE_PUSH (0x2) + /// + /// This setting can be used to disable server push. The initial value is `true`, + /// which indicates that server push is permitted. + case enablePush(Bool) - /// Corresponds to SETTINGS_ENABLE_PUSH. - public static let enablePush = HTTP2SettingsParameter(2) + /// SETTINGS_MAX_CONCURRENT_STREAMS (0x3) + /// + /// Indicates the maximum number of concurrent streams that the sender will + /// allow. This limit is directional: it applies to the number of streams that + /// the sender permits the receiver to create. Initially, there is no limit to + /// this value. It is recommended that this value be no smaller than 100, so as + /// to not unnecessarily limit parallelism. + /// + /// A value of 0 is legal, and will prevent the creation of new streams. + case maxConcurrentStreams(Int32) - /// Corresponds to SETTINGS_MAX_CONCURRENT_STREAMS - public static let maxConcurrentStreams = HTTP2SettingsParameter(3) + /// SETTINGS_INITIAL_WINDOW_SIZE (0x4) + /// + /// Indicates the sender's initial window size (in octets) for stream-level + /// flow control. The initial value is 2^16-1 (65,535) octets. + /// + /// Values above the maximum flow-control window size of 2^31-1 are illegal, + /// and will result in a connection error. + case initialWindowSize(Int32) - /// Corresponds to SETTINGS_INITIAL_WINDOW_SIZE - public static let initialWindowSize = HTTP2SettingsParameter(4) + /// SETTINGS_MAX_FRAME_SIZE (0x5) + /// + /// Indicates the size of the largest frame payload that the sender is willing + /// to receive, in octets. + /// + /// The initial value is 2^14 (16,384) octets. The value advertised by an + /// endpoint MUST be between this initial value and the maximum allowed frame + /// size (2^24-1 or 16,777,215 octets), inclusive. + case maxFrameSize(Int32) - /// Corresponds to SETTINGS_MAX_FRAME_SIZE - public static let maxFrameSize = HTTP2SettingsParameter(5) + /// SETTINGS_MAX_HEADER_LIST_SIZE (0x6) + /// + /// This advisory setting informs a peer of the maximum size of header list + /// that the sender is prepared to accept, in octets. The value is based on the + /// uncompressed size of header fields, including the length of the name and + /// value in octets plus an overhead of 32 octets for each header field. + case maxHeaderListSize(Int32) - /// Corresponds to SETTINGS_MAX_HEADER_LIST_SIZE - public static let maxHeaderListSize = HTTP2SettingsParameter(6) -} + /// SETTINGS_ACCEPT_CACHE_DIGEST (0x7) + /// + /// A server can notify its support for CACHE_DIGEST frame by sending this + /// parameter with a value of `true`. If the server is tempted to make + /// optimizations based on CACHE_DIGEST frames, it SHOULD send this parameter + /// immediately after the connection is established. + case acceptCacheDigest(Bool) -extension HTTP2SettingsParameter: Equatable { - public static func ==(lhs: HTTP2SettingsParameter, rhs: HTTP2SettingsParameter) -> Bool { - return lhs.networkRepresentation == rhs.networkRepresentation + /// SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) + /// + /// Upon receipt of this parameter with a value of `true`, a client MAY use + /// the Extended CONNECT method when creating new streams, for example to + /// bootstrap a WebSocket connection. Receipt of this parameter by a server + /// does not have any impact. + case enableConnectProtocol(Bool) + + /// The network representation of the identifier for this setting. + internal var identifier: UInt16 { + switch self { + case .headerTableSize: return 1 + case .enablePush: return 2 + case .maxConcurrentStreams: return 3 + case .initialWindowSize: return 4 + case .maxFrameSize: return 5 + case .maxHeaderListSize: return 6 + case .acceptCacheDigest: return 7 + case .enableConnectProtocol: return 8 + } } -} -extension HTTP2SettingsParameter: Hashable { - public var hashValue: Int { - return self.networkRepresentation.hashValue + /// Create a new `HTTP2Setting` from nghttp2's raw representation. + internal init(fromNghttp2 setting: nghttp2_settings_entry) { + switch UInt32(setting.settings_id) { + case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE.rawValue: + self = .headerTableSize(Int32(setting.value)) + case NGHTTP2_SETTINGS_ENABLE_PUSH.rawValue: + self = .enablePush(setting.value != 0) + case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS.rawValue: + self = .maxConcurrentStreams(Int32(setting.value)) + case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE.rawValue: + self = .initialWindowSize(Int32(setting.value)) + case NGHTTP2_SETTINGS_MAX_FRAME_SIZE.rawValue: + self = .maxFrameSize(Int32(setting.value)) + case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE.rawValue: + self = .maxHeaderListSize(Int32(setting.value)) + case 0x7: + self = .acceptCacheDigest(setting.value != 0) + case 0x8: + self = .enableConnectProtocol(setting.value != 0) + default: + // nghttp2 doesn't support any other settings at this point + fatalError("Unrecognised setting from nghttp2: \(setting.settings_id) : \(setting.value)") + } } } -/// A single setting for HTTP/2, a combination of a `HTTP2SettingsParameter` and its value. -public struct HTTP2Setting { - /// The settings parameter for this setting. - public var parameter: HTTP2SettingsParameter - - /// The value of the settings parameter. This must be a 32-bit number. - public var value: Int { - get { - return Int(self._value) - } - set { - self._value = UInt32(newValue) +extension HTTP2Setting: Equatable, Hashable { + public static func == (lhs: HTTP2Setting, rhs: HTTP2Setting) -> Bool { + switch (lhs, rhs) { + case (.headerTableSize(let l), .headerTableSize(let r)), + (.maxConcurrentStreams(let l), .maxConcurrentStreams(let r)), + (.initialWindowSize(let l), .initialWindowSize(let r)), + (.maxFrameSize(let l), .maxFrameSize(let r)), + (.maxHeaderListSize(let l), .maxHeaderListSize(let r)): + return l == r + case (.enablePush(let l), .enablePush(let r)), + (.acceptCacheDigest(let l), .acceptCacheDigest(let r)), + (.enableConnectProtocol(let l), .enableConnectProtocol(let r)): + return l == r + default: + return false } } - - /// The value of the setting. Swift doesn't like using explicitly-sized integers in general, - /// so we use this as an internal implementation detail and expose it via a computed Int - /// property. - internal var _value: UInt32 - - /// Create a new `HTTP2Setting`. - public init(parameter: HTTP2SettingsParameter, value: Int) { - self.parameter = parameter - self._value = UInt32(value) + + public var hashValue: Int { + switch self { + case .headerTableSize(let v), .maxConcurrentStreams(let v), .initialWindowSize(let v), + .maxFrameSize(let v), .maxHeaderListSize(let v): + return Int(self.identifier) * 31 + Int(v) + case .enablePush(let b), .acceptCacheDigest(let b), .enableConnectProtocol(let b): + return Int(self.identifier) & 31 + (b ? 1 : 0) + } } +} - /// Create a new `HTTP2Setting` from nghttp2's raw representation. - internal init(fromNghttp2 setting: nghttp2_settings_entry) { - self.parameter = HTTP2SettingsParameter(fromNetwork: setting.settings_id) - self._value = setting.value +internal extension nghttp2_settings_entry { + internal init(nioSetting: HTTP2Setting) { + switch nioSetting { + case .headerTableSize(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_HEADER_TABLE_SIZE.rawValue), value: UInt32(v)) + case .enablePush(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_ENABLE_PUSH.rawValue), value: v ? 1 : 0) + case .maxConcurrentStreams(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS.rawValue), value: UInt32(v)) + case .initialWindowSize(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE.rawValue), value: UInt32(v)) + case .maxFrameSize(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_FRAME_SIZE.rawValue), value: UInt32(v)) + case .maxHeaderListSize(let v): + self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE.rawValue), value: UInt32(v)) + case .acceptCacheDigest(let v): + self.init(settings_id: 0x7, value: v ? 1 : 0) + case .enableConnectProtocol(let v): + self.init(settings_id: 0x8, value: v ? 1 : 0) + } } } -extension HTTP2Setting: Equatable { - public static func ==(lhs: HTTP2Setting, rhs: HTTP2Setting) -> Bool { - return lhs.parameter == rhs.parameter && lhs._value == rhs._value +extension HTTP2Setting { + // nullable *and* throws? Invalid data causes an error, but unknown setting types return 'nil' quietly. + static func decode(from buffer: inout ByteBuffer) throws -> HTTP2Setting? { + precondition(buffer.readableBytes >= 6) + + let identifier: UInt16 = buffer.readInteger()! + let value: Int32 = buffer.readInteger()! + + switch identifier { + case 0x1: + return .headerTableSize(value) + case 0x2: + guard value == 0 || value == 1 else { + throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) + } + return .enablePush(value != 0) + case 0x3: + return .maxConcurrentStreams(value) + case 0x4: + // yes, this looks weird. Yes, value is an Int32. Yes, this condition is stipulated in the + // protocol specification. + guard value <= UInt32.max else { + throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .flowControlError) + } + return .initialWindowSize(value) + case 0x5: + guard value <= 16_777_215 else { + throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) + } + return .maxFrameSize(value) + case 0x6: + return .maxHeaderListSize(value) + case 0x7: + guard value == 0 || value == 1 else { + throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) + } + return .acceptCacheDigest(value != 0) + case 0x8: + guard value == 0 || value == 1 else { + throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) + } + return .enableConnectProtocol(value != 0) + default: + // ignore any unknown settings + return nil + } } -} -internal extension nghttp2_settings_entry { - internal init(nioSetting: HTTP2Setting) { - self.init(settings_id: Int32(nioSetting.parameter.networkRepresentation), value: nioSetting._value) + func compile(to buffer: inout ByteBuffer) { + buffer.write(integer: self.identifier) + switch self { + case .headerTableSize(let v), + .maxConcurrentStreams(let v), + .initialWindowSize(let v), + .maxFrameSize(let v), + .maxHeaderListSize(let v): + buffer.write(integer: v) + case .enablePush(let b), + .acceptCacheDigest(let b), + .enableConnectProtocol(let b): + buffer.write(integer: Int32(b ? 1 : 0)) + } } } diff --git a/Sources/NIOHTTP2/HTTP2Stream.swift b/Sources/NIOHTTP2/HTTP2Stream.swift index f1849efb..7649d318 100644 --- a/Sources/NIOHTTP2/HTTP2Stream.swift +++ b/Sources/NIOHTTP2/HTTP2Stream.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// +import NIO import NIOHTTP1 +import NIOHPACK private extension HTTPHeaders { /// Whether this `HTTPHeaders` corresponds to a final response or not. @@ -108,6 +110,14 @@ final class HTTP2Stream { /// Whether this stream is still active on the connection. Streams that are not active on the connection are /// safe to prune. var active: Bool = false + + /// An HPACK decoder for this stream, handling integer and Huffman decoding and the management of a dynamic + /// header table. + var decoder: HPACKDecoder + + /// An HPACK encoder, handling integer and huffman encoding, header packing, and dynamic header table + /// management. + var encoder: HPACKEncoder /// The headers state machine for outbound headers. /// @@ -119,6 +129,8 @@ final class HTTP2Stream { self.outboundHeaderStateMachine = HTTP2HeadersStateMachine(mode: mode) self.streamID = streamID self.dataProvider = HTTP2DataProvider() + self.decoder = HPACKDecoder(allocator: ByteBufferAllocator()) + self.encoder = HPACKEncoder(allocator: ByteBufferAllocator()) } /// Called to determine the type of a new outbound header block, so as to manage it appropriately. diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift index 0e7436ee..c13e6cec 100644 --- a/Sources/NIOHTTP2/NGHTTP2Session.swift +++ b/Sources/NIOHTTP2/NGHTTP2Session.swift @@ -626,8 +626,16 @@ class NGHTTP2Session { self.sendGoAway(frame: frame) case .windowUpdate: fatalError("not implemented") + case .continuation(_): + fatalError("shouldn't see this from NGHTTP2") case .alternativeService: fatalError("not implemented") + case .blocked: + fatalError("not implemented") + case .origin(_): + fatalError("not implemented") + case .cacheDigest: + fatalError("not implemented") } } diff --git a/Tests/NIOHTTP2Tests/ReentrancyTests.swift b/Tests/NIOHTTP2Tests/ReentrancyTests.swift index 4115fa3e..654eafd8 100644 --- a/Tests/NIOHTTP2Tests/ReentrancyTests.swift +++ b/Tests/NIOHTTP2Tests/ReentrancyTests.swift @@ -95,7 +95,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] + let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) @@ -133,7 +133,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] + let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) @@ -165,7 +165,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] + let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift index fe830a9a..b3cfd040 100644 --- a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift +++ b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift @@ -526,10 +526,10 @@ class SimpleClientServerTests: XCTestCase { } func testOverridingDefaultSettings() throws { - let initialSettings = [ - HTTP2Setting(parameter: .maxHeaderListSize, value: 1000), - HTTP2Setting(parameter: .initialWindowSize, value: 100), - HTTP2Setting(parameter: .enablePush, value: 0) + let initialSettings: [HTTP2Setting] = [ + .maxHeaderListSize(1000), + .initialWindowSize(100), + .enablePush(false) ] XCTAssertNoThrow(try self.clientChannel.pipeline.add(handler: HTTP2Parser(mode: .client, initialSettings: initialSettings)).wait()) XCTAssertNoThrow(try self.serverChannel.pipeline.add(handler: HTTP2Parser(mode: .server)).wait()) From 4876e014e1e4f7482ac30b9b920383b45271f87d Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Thu, 16 Aug 2018 14:29:15 -0700 Subject: [PATCH 2/7] Should have `.complete` in `FrameFlags.allFlags`. --- Sources/NIOHTTP2/HTTP2Frame.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index d7804119..057b9030 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -137,7 +137,7 @@ public struct HTTP2Frame { public static let priority = FrameFlags(rawValue: 0x20) // useful for test cases - internal static var allFlags: FrameFlags = [.endStream, .endHeaders, .padded, .priority] + internal static var allFlags: FrameFlags = [.endStream, .complete, .endHeaders, .padded, .priority] } } From 874f7e65385c76be7adbdc1c22c2ca372c0b7518 Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Fri, 17 Aug 2018 14:24:23 -0700 Subject: [PATCH 3/7] Updated based on PR feedback. --- Sources/NIOHTTP2/HTTP2Frame.swift | 40 +-- Sources/NIOHTTP2/HTTP2Parser.swift | 6 +- Sources/NIOHTTP2/HTTP2Settings.swift | 282 +++++------------- Sources/NIOHTTP2/HTTP2Stream.swift | 12 - Sources/NIOHTTP2/NGHTTP2Session.swift | 6 - Tests/NIOHTTP2Tests/ReentrancyTests.swift | 6 +- .../SimpleClientServerTests.swift | 8 +- 7 files changed, 101 insertions(+), 259 deletions(-) diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index 057b9030..e2ff5d9b 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -23,43 +23,43 @@ public struct HTTP2Frame { /// The frame flags. public var flags: FrameFlags - + /// The frame stream ID as a 32-bit integer. public var streamID: HTTP2StreamID - + private func _hasFlag(_ flag: FrameFlags) -> Bool { return self.flags.contains(flag) - } + } private mutating func _setFlagIfValid(_ flag: FrameFlags) { if self.payload.allowedFlags.contains(flag) { self.flags.formUnion(flag) } - } - + } + // Whether the END_STREAM flag bit is set. public var endStream: Bool { get { return self._hasFlag(.endStream) } set { self._setFlagIfValid(.endStream) } - } - + } + // Whether the PADDED flag bit is set. public var padded: Bool { get { return self._hasFlag(.padded) } set { self._setFlagIfValid(.padded) } - } - + } + // Whether the PRIORITY flag bit is set. public var priority: Bool { get { return self._hasFlag(.priority) } set { self._setFlagIfValid(.priority) } - } - + } + // Whether the ACK flag bit is set. public var ack: Bool { get { return self._hasFlag(.ack) } set { self._setFlagIfValid(.ack) } - } + } public enum FramePayload { case data(IOData) @@ -71,11 +71,8 @@ public struct HTTP2Frame { case ping(HTTP2PingData) case goAway(lastStreamID: HTTP2StreamID, errorCode: HTTP2ErrorCode, opaqueData: ByteBuffer?) case windowUpdate(windowSizeIncrement: Int) - case continuation(HTTPHeaders) case alternativeService(origin: String?, field: ByteBuffer?) - case blocked case origin([String]) - case cacheDigest(origin: String?, digest: ByteBuffer?) var code: UInt8 { switch self { @@ -88,11 +85,8 @@ public struct HTTP2Frame { case .ping: return 0x6 case .goAway: return 0x7 case .windowUpdate: return 0x8 - case .continuation: return 0x9 case .alternativeService: return 0xa - case .blocked: return 0xb case .origin: return 0xc - case .cacheDigest: return 0xd } } @@ -104,16 +98,12 @@ public struct HTTP2Frame { return [.endStream, .endHeaders, .padded, .priority] case .pushPromise: return [.endHeaders, .padded] - case .continuation: - return .endHeaders - case .cacheDigest: - return [.reset, .complete] case .settings, .ping: return .ack case .priority, .rstStream, .goAway, .windowUpdate, - .alternativeService, .blocked, .origin: + .alternativeService, .origin: return [] } } @@ -130,14 +120,12 @@ public struct HTTP2Frame { public static let endStream = FrameFlags(rawValue: 0x01) public static let ack = FrameFlags(rawValue: 0x01) - public static let reset = FrameFlags(rawValue: 0x01) - public static let complete = FrameFlags(rawValue: 0x02) public static let endHeaders = FrameFlags(rawValue: 0x04) public static let padded = FrameFlags(rawValue: 0x08) public static let priority = FrameFlags(rawValue: 0x20) // useful for test cases - internal static var allFlags: FrameFlags = [.endStream, .complete, .endHeaders, .padded, .priority] + internal static var allFlags: FrameFlags = [.endStream, .endHeaders, .padded, .priority] } } diff --git a/Sources/NIOHTTP2/HTTP2Parser.swift b/Sources/NIOHTTP2/HTTP2Parser.swift index eab4099e..f4eb6e08 100644 --- a/Sources/NIOHTTP2/HTTP2Parser.swift +++ b/Sources/NIOHTTP2/HTTP2Parser.swift @@ -18,9 +18,9 @@ import CNIONghttp2 /// NIO's default settings used for initial settings values on HTTP/2 streams, when the user hasn't /// overridden that. This limits the max concurrent streams to 100, and limits the max header list /// size to 16kB, to avoid trivial resource exhaustion on NIO HTTP/2 users. -public let nioDefaultSettings: [HTTP2Setting] = [ - .maxConcurrentStreams(100), - .maxHeaderListSize(1<<16) +public let nioDefaultSettings = [ + HTTP2Setting(parameter: .maxConcurrentStreams, value: 100), + HTTP2Setting(parameter: .maxHeaderListSize, value: 1<<16) ] diff --git a/Sources/NIOHTTP2/HTTP2Settings.swift b/Sources/NIOHTTP2/HTTP2Settings.swift index 72c3b4ba..afac85cc 100644 --- a/Sources/NIOHTTP2/HTTP2Settings.swift +++ b/Sources/NIOHTTP2/HTTP2Settings.swift @@ -12,232 +12,104 @@ // //===----------------------------------------------------------------------===// import CNIONghttp2 -import NIO -/// A single setting for HTTP/2, a combination of parameter identifier and its value. -public enum HTTP2Setting { - /// SETTINGS_HEADER_TABLE_SIZE (0x1) - /// - /// Allows the sender to inform the remote endpoint of the maximum size of - /// the header compression table used to decode header blocks, in octets. The - /// encoder can select any size equal to or less than this value by using - /// signaling specific to the header compression format inside a header block. - /// The initial value is 4,096 octets. - case headerTableSize(Int32) - - /// SETTINGS_ENABLE_PUSH (0x2) - /// - /// This setting can be used to disable server push. The initial value is `true`, - /// which indicates that server push is permitted. - case enablePush(Bool) +/// A HTTP/2 settings parameter that allows representing both known and unknown HTTP/2 +/// settings parameters. +public struct HTTP2SettingsParameter { + internal let networkRepresentation: UInt16 - /// SETTINGS_MAX_CONCURRENT_STREAMS (0x3) - /// - /// Indicates the maximum number of concurrent streams that the sender will - /// allow. This limit is directional: it applies to the number of streams that - /// the sender permits the receiver to create. Initially, there is no limit to - /// this value. It is recommended that this value be no smaller than 100, so as - /// to not unnecessarily limit parallelism. + /// Create a `HTTP2SettingsParameter` that is not known to NIO. /// - /// A value of 0 is legal, and will prevent the creation of new streams. - case maxConcurrentStreams(Int32) + /// If this is a known parameter, use one of the static values. + public init(extensionSetting: Int) { + self.networkRepresentation = UInt16(extensionSetting) + } - /// SETTINGS_INITIAL_WINDOW_SIZE (0x4) - /// - /// Indicates the sender's initial window size (in octets) for stream-level - /// flow control. The initial value is 2^16-1 (65,535) octets. - /// - /// Values above the maximum flow-control window size of 2^31-1 are illegal, - /// and will result in a connection error. - case initialWindowSize(Int32) + /// Initialize a `HTTP2SettingsParameter` from nghttp2's representation. + internal init(fromNetwork value: Int32) { + self.networkRepresentation = UInt16(value) + } - /// SETTINGS_MAX_FRAME_SIZE (0x5) - /// - /// Indicates the size of the largest frame payload that the sender is willing - /// to receive, in octets. - /// - /// The initial value is 2^14 (16,384) octets. The value advertised by an - /// endpoint MUST be between this initial value and the maximum allowed frame - /// size (2^24-1 or 16,777,215 octets), inclusive. - case maxFrameSize(Int32) + /// A helper to initialize the static parameters. + private init(_ value: UInt16) { + self.networkRepresentation = value + } - /// SETTINGS_MAX_HEADER_LIST_SIZE (0x6) - /// - /// This advisory setting informs a peer of the maximum size of header list - /// that the sender is prepared to accept, in octets. The value is based on the - /// uncompressed size of header fields, including the length of the name and - /// value in octets plus an overhead of 32 octets for each header field. - case maxHeaderListSize(Int32) + /// Corresponds to SETTINGS_HEADER_TABLE_SIZE + public static let headerTableSize = HTTP2SettingsParameter(1) - /// SETTINGS_ACCEPT_CACHE_DIGEST (0x7) - /// - /// A server can notify its support for CACHE_DIGEST frame by sending this - /// parameter with a value of `true`. If the server is tempted to make - /// optimizations based on CACHE_DIGEST frames, it SHOULD send this parameter - /// immediately after the connection is established. - case acceptCacheDigest(Bool) + /// Corresponds to SETTINGS_ENABLE_PUSH. + public static let enablePush = HTTP2SettingsParameter(2) - /// SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) - /// - /// Upon receipt of this parameter with a value of `true`, a client MAY use - /// the Extended CONNECT method when creating new streams, for example to - /// bootstrap a WebSocket connection. Receipt of this parameter by a server - /// does not have any impact. - case enableConnectProtocol(Bool) - - /// The network representation of the identifier for this setting. - internal var identifier: UInt16 { - switch self { - case .headerTableSize: return 1 - case .enablePush: return 2 - case .maxConcurrentStreams: return 3 - case .initialWindowSize: return 4 - case .maxFrameSize: return 5 - case .maxHeaderListSize: return 6 - case .acceptCacheDigest: return 7 - case .enableConnectProtocol: return 8 - } - } + /// Corresponds to SETTINGS_MAX_CONCURRENT_STREAMS + public static let maxConcurrentStreams = HTTP2SettingsParameter(3) - /// Create a new `HTTP2Setting` from nghttp2's raw representation. - internal init(fromNghttp2 setting: nghttp2_settings_entry) { - switch UInt32(setting.settings_id) { - case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE.rawValue: - self = .headerTableSize(Int32(setting.value)) - case NGHTTP2_SETTINGS_ENABLE_PUSH.rawValue: - self = .enablePush(setting.value != 0) - case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS.rawValue: - self = .maxConcurrentStreams(Int32(setting.value)) - case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE.rawValue: - self = .initialWindowSize(Int32(setting.value)) - case NGHTTP2_SETTINGS_MAX_FRAME_SIZE.rawValue: - self = .maxFrameSize(Int32(setting.value)) - case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE.rawValue: - self = .maxHeaderListSize(Int32(setting.value)) - case 0x7: - self = .acceptCacheDigest(setting.value != 0) - case 0x8: - self = .enableConnectProtocol(setting.value != 0) - default: - // nghttp2 doesn't support any other settings at this point - fatalError("Unrecognised setting from nghttp2: \(setting.settings_id) : \(setting.value)") - } - } + /// Corresponds to SETTINGS_INITIAL_WINDOW_SIZE + public static let initialWindowSize = HTTP2SettingsParameter(4) + + /// Corresponds to SETTINGS_MAX_FRAME_SIZE + public static let maxFrameSize = HTTP2SettingsParameter(5) + + /// Corresponds to SETTINGS_MAX_HEADER_LIST_SIZE + public static let maxHeaderListSize = HTTP2SettingsParameter(6) + + /// Corresponds to SETTINGS_ENABLE_CONNECT_PROTOCOL from RFC 8441. + public static let enableConnectProtocol = HTTP2SettingsParameter(8) } -extension HTTP2Setting: Equatable, Hashable { - public static func == (lhs: HTTP2Setting, rhs: HTTP2Setting) -> Bool { - switch (lhs, rhs) { - case (.headerTableSize(let l), .headerTableSize(let r)), - (.maxConcurrentStreams(let l), .maxConcurrentStreams(let r)), - (.initialWindowSize(let l), .initialWindowSize(let r)), - (.maxFrameSize(let l), .maxFrameSize(let r)), - (.maxHeaderListSize(let l), .maxHeaderListSize(let r)): - return l == r - case (.enablePush(let l), .enablePush(let r)), - (.acceptCacheDigest(let l), .acceptCacheDigest(let r)), - (.enableConnectProtocol(let l), .enableConnectProtocol(let r)): - return l == r - default: - return false - } +extension HTTP2SettingsParameter: Equatable { + public static func ==(lhs: HTTP2SettingsParameter, rhs: HTTP2SettingsParameter) -> Bool { + return lhs.networkRepresentation == rhs.networkRepresentation } - +} + +extension HTTP2SettingsParameter: Hashable { public var hashValue: Int { - switch self { - case .headerTableSize(let v), .maxConcurrentStreams(let v), .initialWindowSize(let v), - .maxFrameSize(let v), .maxHeaderListSize(let v): - return Int(self.identifier) * 31 + Int(v) - case .enablePush(let b), .acceptCacheDigest(let b), .enableConnectProtocol(let b): - return Int(self.identifier) & 31 + (b ? 1 : 0) - } + return self.networkRepresentation.hashValue } } -internal extension nghttp2_settings_entry { - internal init(nioSetting: HTTP2Setting) { - switch nioSetting { - case .headerTableSize(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_HEADER_TABLE_SIZE.rawValue), value: UInt32(v)) - case .enablePush(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_ENABLE_PUSH.rawValue), value: v ? 1 : 0) - case .maxConcurrentStreams(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS.rawValue), value: UInt32(v)) - case .initialWindowSize(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE.rawValue), value: UInt32(v)) - case .maxFrameSize(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_FRAME_SIZE.rawValue), value: UInt32(v)) - case .maxHeaderListSize(let v): - self.init(settings_id: Int32(NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE.rawValue), value: UInt32(v)) - case .acceptCacheDigest(let v): - self.init(settings_id: 0x7, value: v ? 1 : 0) - case .enableConnectProtocol(let v): - self.init(settings_id: 0x8, value: v ? 1 : 0) +/// A single setting for HTTP/2, a combination of a `HTTP2SettingsParameter` and its value. +public struct HTTP2Setting { + /// The settings parameter for this setting. + public var parameter: HTTP2SettingsParameter + + /// The value of the settings parameter. This must be a 32-bit number. + public var value: Int { + get { + return Int(self._value) + } + set { + self._value = UInt32(newValue) } } + + /// The value of the setting. Swift doesn't like using explicitly-sized integers in general, + /// so we use this as an internal implementation detail and expose it via a computed Int + /// property. + internal var _value: UInt32 + + /// Create a new `HTTP2Setting`. + public init(parameter: HTTP2SettingsParameter, value: Int) { + self.parameter = parameter + self._value = UInt32(value) + } + + /// Create a new `HTTP2Setting` from nghttp2's raw representation. + internal init(fromNghttp2 setting: nghttp2_settings_entry) { + self.parameter = HTTP2SettingsParameter(fromNetwork: setting.settings_id) + self._value = setting.value + } } -extension HTTP2Setting { - // nullable *and* throws? Invalid data causes an error, but unknown setting types return 'nil' quietly. - static func decode(from buffer: inout ByteBuffer) throws -> HTTP2Setting? { - precondition(buffer.readableBytes >= 6) - - let identifier: UInt16 = buffer.readInteger()! - let value: Int32 = buffer.readInteger()! - - switch identifier { - case 0x1: - return .headerTableSize(value) - case 0x2: - guard value == 0 || value == 1 else { - throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) - } - return .enablePush(value != 0) - case 0x3: - return .maxConcurrentStreams(value) - case 0x4: - // yes, this looks weird. Yes, value is an Int32. Yes, this condition is stipulated in the - // protocol specification. - guard value <= UInt32.max else { - throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .flowControlError) - } - return .initialWindowSize(value) - case 0x5: - guard value <= 16_777_215 else { - throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) - } - return .maxFrameSize(value) - case 0x6: - return .maxHeaderListSize(value) - case 0x7: - guard value == 0 || value == 1 else { - throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) - } - return .acceptCacheDigest(value != 0) - case 0x8: - guard value == 0 || value == 1 else { - throw NIOHTTP2Errors.InvalidSettings(setting: identifier, value: value, errorCode: .protocolError) - } - return .enableConnectProtocol(value != 0) - default: - // ignore any unknown settings - return nil - } +extension HTTP2Setting: Equatable { + public static func ==(lhs: HTTP2Setting, rhs: HTTP2Setting) -> Bool { + return lhs.parameter == rhs.parameter && lhs._value == rhs._value } +} - func compile(to buffer: inout ByteBuffer) { - buffer.write(integer: self.identifier) - switch self { - case .headerTableSize(let v), - .maxConcurrentStreams(let v), - .initialWindowSize(let v), - .maxFrameSize(let v), - .maxHeaderListSize(let v): - buffer.write(integer: v) - case .enablePush(let b), - .acceptCacheDigest(let b), - .enableConnectProtocol(let b): - buffer.write(integer: Int32(b ? 1 : 0)) - } +internal extension nghttp2_settings_entry { + internal init(nioSetting: HTTP2Setting) { + self.init(settings_id: Int32(nioSetting.parameter.networkRepresentation), value: nioSetting._value) } } diff --git a/Sources/NIOHTTP2/HTTP2Stream.swift b/Sources/NIOHTTP2/HTTP2Stream.swift index 7649d318..f1849efb 100644 --- a/Sources/NIOHTTP2/HTTP2Stream.swift +++ b/Sources/NIOHTTP2/HTTP2Stream.swift @@ -12,9 +12,7 @@ // //===----------------------------------------------------------------------===// -import NIO import NIOHTTP1 -import NIOHPACK private extension HTTPHeaders { /// Whether this `HTTPHeaders` corresponds to a final response or not. @@ -110,14 +108,6 @@ final class HTTP2Stream { /// Whether this stream is still active on the connection. Streams that are not active on the connection are /// safe to prune. var active: Bool = false - - /// An HPACK decoder for this stream, handling integer and Huffman decoding and the management of a dynamic - /// header table. - var decoder: HPACKDecoder - - /// An HPACK encoder, handling integer and huffman encoding, header packing, and dynamic header table - /// management. - var encoder: HPACKEncoder /// The headers state machine for outbound headers. /// @@ -129,8 +119,6 @@ final class HTTP2Stream { self.outboundHeaderStateMachine = HTTP2HeadersStateMachine(mode: mode) self.streamID = streamID self.dataProvider = HTTP2DataProvider() - self.decoder = HPACKDecoder(allocator: ByteBufferAllocator()) - self.encoder = HPACKEncoder(allocator: ByteBufferAllocator()) } /// Called to determine the type of a new outbound header block, so as to manage it appropriately. diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift index c13e6cec..9e180ddc 100644 --- a/Sources/NIOHTTP2/NGHTTP2Session.swift +++ b/Sources/NIOHTTP2/NGHTTP2Session.swift @@ -626,16 +626,10 @@ class NGHTTP2Session { self.sendGoAway(frame: frame) case .windowUpdate: fatalError("not implemented") - case .continuation(_): - fatalError("shouldn't see this from NGHTTP2") case .alternativeService: fatalError("not implemented") - case .blocked: - fatalError("not implemented") case .origin(_): fatalError("not implemented") - case .cacheDigest: - fatalError("not implemented") } } diff --git a/Tests/NIOHTTP2Tests/ReentrancyTests.swift b/Tests/NIOHTTP2Tests/ReentrancyTests.swift index 654eafd8..4115fa3e 100644 --- a/Tests/NIOHTTP2Tests/ReentrancyTests.swift +++ b/Tests/NIOHTTP2Tests/ReentrancyTests.swift @@ -95,7 +95,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] + let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) @@ -133,7 +133,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] + let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) @@ -165,7 +165,7 @@ final class ReentrancyTests: XCTestCase { // Here we're going to prepare some frames: specifically, we're going to send a SETTINGS frame and a PING frame at the same time. // We need to send two frames to try to catch any ordering problems we might hit. - let settings: [HTTP2Setting] = [.enablePush(false), .maxConcurrentStreams(5)] + let settings: [HTTP2Setting] = [HTTP2Setting(parameter: .enablePush, value: 0), HTTP2Setting(parameter: .maxConcurrentStreams, value: 5)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(settings)) let pingFrame = HTTP2Frame(streamID: .rootStream, payload: .ping(HTTP2PingData(withInteger: 5))) self.clientChannel.write(settingsFrame, promise: nil) diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift index b3cfd040..fe830a9a 100644 --- a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift +++ b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift @@ -526,10 +526,10 @@ class SimpleClientServerTests: XCTestCase { } func testOverridingDefaultSettings() throws { - let initialSettings: [HTTP2Setting] = [ - .maxHeaderListSize(1000), - .initialWindowSize(100), - .enablePush(false) + let initialSettings = [ + HTTP2Setting(parameter: .maxHeaderListSize, value: 1000), + HTTP2Setting(parameter: .initialWindowSize, value: 100), + HTTP2Setting(parameter: .enablePush, value: 0) ] XCTAssertNoThrow(try self.clientChannel.pipeline.add(handler: HTTP2Parser(mode: .client, initialSettings: initialSettings)).wait()) XCTAssertNoThrow(try self.serverChannel.pipeline.add(handler: HTTP2Parser(mode: .server)).wait()) From cfde0fc8a228f03a758043f46813d7d1f55b4129 Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Fri, 17 Aug 2018 14:29:32 -0700 Subject: [PATCH 4/7] Fixed a nit. --- Sources/NIOHTTP2/HTTP2Frame.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index e2ff5d9b..8a0b1335 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -109,7 +109,7 @@ public struct HTTP2Frame { } } - public struct FrameFlags : OptionSet { + public struct FrameFlags: OptionSet { public typealias RawValue = UInt8 public private(set) var rawValue: UInt8 From 7732becab57469212d06fb061816227add553a6d Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Mon, 20 Aug 2018 11:53:29 -0700 Subject: [PATCH 5/7] Updated based on PR feedback. Also added copious documentation on HTTP/2 frame payload items, because I'm evil. Mwa-ha-haaa. --- Package.swift | 2 +- Sources/NIOHTTP2/HTTP2Error.swift | 18 ------ Sources/NIOHTTP2/HTTP2Frame.swift | 90 ++++++++++++++++++++++++++- Sources/NIOHTTP2/NGHTTP2Session.swift | 2 +- 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/Package.swift b/Package.swift index 844bb0f6..535d3313 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ let package = Package( .target(name: "NIOHTTP2Server", dependencies: ["NIOHTTP2"]), .target(name: "NIOHTTP2", - dependencies: ["NIO", "NIOHTTP1", "NIOTLS", "CNIONghttp2", "NIOHPACK"]), + dependencies: ["NIO", "NIOHTTP1", "NIOTLS", "CNIONghttp2"]), .target(name: "NIOHPACK", dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOHTTP1"]), .testTarget(name: "NIOHTTP2Tests", diff --git a/Sources/NIOHTTP2/HTTP2Error.swift b/Sources/NIOHTTP2/HTTP2Error.swift index 19efc35d..3eed091b 100644 --- a/Sources/NIOHTTP2/HTTP2Error.swift +++ b/Sources/NIOHTTP2/HTTP2Error.swift @@ -59,24 +59,6 @@ public enum NIOHTTP2Errors { self.nghttp2ErrorCode = nghttp2ErrorCode } } - - /// Received/decoded data was invalid. - public struct InvalidSettings: NIOHTTP2Error { - /// The network identifier of the setting being read. - public var settingCode: UInt16 - - /// The offending value. - public var value: Int32 - - /// The error code associated with the error. - public var errorCode: HTTP2ErrorCode - - public init(setting: UInt16, value: Int32, errorCode: HTTP2ErrorCode) { - self.settingCode = setting - self.value = value - self.errorCode = errorCode - } - } } diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index 8a0b1335..7dade574 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -61,19 +61,91 @@ public struct HTTP2Frame { set { self._setFlagIfValid(.ack) } } + /// Frame-type-specific payload data. public enum FramePayload { + /// A DATA frame, containing raw bytes. + /// + /// See [RFC 7540 § 6.1](https://httpwg.org/specs/rfc7540.html#rfc.section.6.1). case data(IOData) + + /// A HEADERS frame, containing all headers or trailers associated with a request + /// or response. + /// + /// Note that swift-nio-http2 automatically coalesces HEADERS and CONTINUATION + /// frames into a single `FramePayload.headers` instance. + /// + /// See [RFC 7540 § 6.2](https://httpwg.org/specs/rfc7540.html#rfc.section.6.2). case headers(HTTPHeaders) + + /// A PRIORITY frame, used to change priority and dependency ordering among + /// streams. + /// + /// See [RFC 7540 § 6.3](https://httpwg.org/specs/rfc7540.html#rfc.section.6.3). case priority + + /// A RST_STREAM (reset stream) frame, sent when a stream has encountered an error + /// condition and needs to be terminated as a result. + /// + /// See [RFC 7540 § 6.4](https://httpwg.org/specs/rfc7540.html#rfc.section.6.4). case rstStream(HTTP2ErrorCode) + + /// A SETTINGS frame, containing various connection--level settings and their + /// desired values. + /// + /// See [RFC 7540 § 6.5](https://httpwg.org/specs/rfc7540.html#rfc.section.6.5). case settings([HTTP2Setting]) - case pushPromise + + /// A PUSH_PROMISE frame, used to notify a peer in advance of streams that a sender + /// intends to initiate. It performs much like a request's HEADERS frame, informing + /// a peer that the response for a theoretical request like the one in the promise + /// will arrive on a new stream. + /// + /// As with the HEADERS frame, swift-nio-http2 will coalesce an initial PUSH_PROMISE + /// frame with any CONTINUATION frames that follow, emitting a single + /// `FramePayload.pushPromise` instance for the complete set. + /// + /// See [RFC 7540 § 6.6](https://httpwg.org/specs/rfc7540.html#rfc.section.6.6). + /// + /// For more information on server push in HTTP/2, see + /// [RFC 7540 § 8.2](https://httpwg.org/specs/rfc7540.html#rfc.section.8.2). + case pushPromise(HTTPHeaders) + + /// A PING frame, used to measure round-trip time between endpoints. + /// + /// See [RFC 7540 § 6.7](https://httpwg.org/specs/rfc7540.html#rfc.section.6.7). case ping(HTTP2PingData) + + /// A GOAWAY frame, used to request that a peer immediately cease communication with + /// the sender. It contains a stream ID indicating the last stream that will be processed + /// by the sender, an error code (if the shutdown was caused by an error), and optionally + /// some additional diagnostic data. + /// + /// See [RFC 7540 § 6.8](https://httpwg.org/specs/rfc7540.html#rfc.section.6.8). case goAway(lastStreamID: HTTP2StreamID, errorCode: HTTP2ErrorCode, opaqueData: ByteBuffer?) + + /// A WINDOW_UPDATE frame. This is used to implement flow control of DATA frames, + /// allowing peers to advertise and update the amount of data they are prepared to + /// process at any given moment. + /// + /// See [RFC 7540 § 6.9](https://httpwg.org/specs/rfc7540.html#rfc.section.6.9). case windowUpdate(windowSizeIncrement: Int) + + /// An ALTSVC frame. This is sent by an HTTP server to indicate alternative origin + /// locations for accessing the same resource, for instance via another protocol, + /// or over TLS. It consists of an origin and a list of alternate protocols and + /// the locations at which they may be addressed. + /// + /// See [RFC 7838 § 4](https://tools.ietf.org/html/rfc7838#section-4). case alternativeService(origin: String?, field: ByteBuffer?) + + /// An ORIGIN frame. This allows servers which allow access to multiple origins + /// via the same socket connection to identify which origins may be accessed in + /// this manner. + /// + /// See [RFC 8336 § 2](https://tools.ietf.org/html/rfc8336#section-2). case origin([String]) + /// The one-byte identifier used to indicate the type of a frame on the wire. var code: UInt8 { switch self { case .data: return 0x0 @@ -90,6 +162,7 @@ public struct HTTP2Frame { } } + /// The set of flags that are permitted in the flags field of a particular frame. var allowedFlags: FrameFlags { switch self { case .data: @@ -98,10 +171,8 @@ public struct HTTP2Frame { return [.endStream, .endHeaders, .padded, .priority] case .pushPromise: return [.endHeaders, .padded] - case .settings, .ping: return .ack - case .priority, .rstStream, .goAway, .windowUpdate, .alternativeService, .origin: return [] @@ -109,6 +180,7 @@ public struct HTTP2Frame { } } + /// The flags supported by the frame types understood by this protocol. public struct FrameFlags: OptionSet { public typealias RawValue = UInt8 @@ -118,10 +190,22 @@ public struct HTTP2Frame { self.rawValue = rawValue } + /// END_STREAM flag. Valid on DATA and HEADERS frames. public static let endStream = FrameFlags(rawValue: 0x01) + + /// ACK flag. Valid on SETTINGS and PING frames. public static let ack = FrameFlags(rawValue: 0x01) + + /// END_HEADERS flag. Valid on HEADERS, CONTINUATION, and PUSH_PROMISE frames. public static let endHeaders = FrameFlags(rawValue: 0x04) + + /// PADDED flag. Valid on DATA, HEADERS, CONTINUATION, and PUSH_PROMISE frames. + /// + /// NB: swift-nio-http2 does not pad outgoing frames. public static let padded = FrameFlags(rawValue: 0x08) + + /// PRIORITY flag. Valid on HEADERS frames, specifically as the first frame sent + /// on a new stream. public static let priority = FrameFlags(rawValue: 0x20) // useful for test cases diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift index 9e180ddc..110a8350 100644 --- a/Sources/NIOHTTP2/NGHTTP2Session.swift +++ b/Sources/NIOHTTP2/NGHTTP2Session.swift @@ -457,7 +457,7 @@ class NGHTTP2Session { } nioFramePayload = .settings(settings) case NGHTTP2_PUSH_PROMISE.rawValue: - nioFramePayload = .pushPromise + nioFramePayload = .pushPromise(self.headersAccumulation) case NGHTTP2_PING.rawValue: nioFramePayload = .ping(HTTP2PingData(withTuple: frame.ping.opaque_data)) case NGHTTP2_GOAWAY.rawValue: From 517e3bb83a195a697b8e8a6e372736a924cb7c8f Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Tue, 21 Aug 2018 11:34:02 -0700 Subject: [PATCH 6/7] Updated based on PR feedback. --- Sources/NIOHTTP2/HTTP2Frame.swift | 2 +- Sources/NIOHTTP2/NGHTTP2Session.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index 7dade574..9e0227d7 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -108,7 +108,7 @@ public struct HTTP2Frame { /// /// For more information on server push in HTTP/2, see /// [RFC 7540 § 8.2](https://httpwg.org/specs/rfc7540.html#rfc.section.8.2). - case pushPromise(HTTPHeaders) + case pushPromise /// A PING frame, used to measure round-trip time between endpoints. /// diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift index 110a8350..9e180ddc 100644 --- a/Sources/NIOHTTP2/NGHTTP2Session.swift +++ b/Sources/NIOHTTP2/NGHTTP2Session.swift @@ -457,7 +457,7 @@ class NGHTTP2Session { } nioFramePayload = .settings(settings) case NGHTTP2_PUSH_PROMISE.rawValue: - nioFramePayload = .pushPromise(self.headersAccumulation) + nioFramePayload = .pushPromise case NGHTTP2_PING.rawValue: nioFramePayload = .ping(HTTP2PingData(withTuple: frame.ping.opaque_data)) case NGHTTP2_GOAWAY.rawValue: From 14d2430b57982e03f245d4b6a439f68793725a31 Mon Sep 17 00:00:00 2001 From: Jim Dovey Date: Wed, 22 Aug 2018 13:06:48 -0700 Subject: [PATCH 7/7] Updated based on PR feedback. --- Sources/NIOHTTP2/HTTP2Frame.swift | 36 +----------- Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift | 12 ++-- Sources/NIOHTTP2/NGHTTP2Session.swift | 4 +- .../HTTP2StreamMultiplexerTests.swift | 18 +++--- .../HTTP2ToHTTP1CodecTests.swift | 14 ++--- .../SimpleClientServerTests.swift | 56 +++++++++---------- Tests/NIOHTTP2Tests/TestUtilities.swift | 16 ++++-- 7 files changed, 63 insertions(+), 93 deletions(-) diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift index 9e0227d7..57313953 100644 --- a/Sources/NIOHTTP2/HTTP2Frame.swift +++ b/Sources/NIOHTTP2/HTTP2Frame.swift @@ -26,40 +26,6 @@ public struct HTTP2Frame { /// The frame stream ID as a 32-bit integer. public var streamID: HTTP2StreamID - - private func _hasFlag(_ flag: FrameFlags) -> Bool { - return self.flags.contains(flag) - } - - private mutating func _setFlagIfValid(_ flag: FrameFlags) { - if self.payload.allowedFlags.contains(flag) { - self.flags.formUnion(flag) - } - } - - // Whether the END_STREAM flag bit is set. - public var endStream: Bool { - get { return self._hasFlag(.endStream) } - set { self._setFlagIfValid(.endStream) } - } - - // Whether the PADDED flag bit is set. - public var padded: Bool { - get { return self._hasFlag(.padded) } - set { self._setFlagIfValid(.padded) } - } - - // Whether the PRIORITY flag bit is set. - public var priority: Bool { - get { return self._hasFlag(.priority) } - set { self._setFlagIfValid(.priority) } - } - - // Whether the ACK flag bit is set. - public var ack: Bool { - get { return self._hasFlag(.ack) } - set { self._setFlagIfValid(.ack) } - } /// Frame-type-specific payload data. public enum FramePayload { @@ -201,7 +167,7 @@ public struct HTTP2Frame { /// PADDED flag. Valid on DATA, HEADERS, CONTINUATION, and PUSH_PROMISE frames. /// - /// NB: swift-nio-http2 does not pad outgoing frames. + /// NB: swift-nio-http2 does not automatically pad outgoing frames. public static let padded = FrameFlags(rawValue: 0x08) /// PRIORITY flag. Valid on HEADERS frames, specifically as the first frame sent diff --git a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift index 0715feae..e19f40c9 100644 --- a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift +++ b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift @@ -94,13 +94,13 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou } else { let respHead = HTTPResponseHead(http2HeaderBlock: headers) ctx.fireChannelRead(self.wrapInboundOut(.head(respHead))) - if frame.endStream { + if frame.flags.contains(.endStream) { ctx.fireChannelRead(self.wrapInboundOut(.end(nil))) } } case .data(.byteBuffer(let b)): ctx.fireChannelRead(self.wrapInboundOut(.body(b))) - if frame.endStream { + if frame.flags.contains(.endStream) { ctx.fireChannelRead(self.wrapInboundOut(.end(nil))) } case .alternativeService, .rstStream, .priority, .windowUpdate: @@ -131,7 +131,7 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou } var frame = HTTP2Frame(streamID: self.streamID, payload: payload) - frame.endStream = true + frame.flags.insert(.endStream) ctx.write(self.wrapOutboundOut(frame), promise: promise) } } @@ -170,13 +170,13 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou } else { let reqHead = HTTPRequestHead(http2HeaderBlock: headers) ctx.fireChannelRead(self.wrapInboundOut(.head(reqHead))) - if frame.endStream { + if frame.flags.contains(.endStream) { ctx.fireChannelRead(self.wrapInboundOut(.end(nil))) } } case .data(.byteBuffer(let b)): ctx.fireChannelRead(self.wrapInboundOut(.body(b))) - if frame.endStream { + if frame.flags.contains(.endStream) { ctx.fireChannelRead(self.wrapInboundOut(.end(nil))) } case .alternativeService, .rstStream, .priority, .windowUpdate: @@ -207,7 +207,7 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou } var frame = HTTP2Frame(streamID: self.streamID, payload: payload) - frame.endStream = true + frame.flags.insert(.endStream) ctx.write(self.wrapOutboundOut(frame), promise: promise) } } diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift index 9e180ddc..54469d1b 100644 --- a/Sources/NIOHTTP2/NGHTTP2Session.swift +++ b/Sources/NIOHTTP2/NGHTTP2Session.swift @@ -677,7 +677,7 @@ class NGHTTP2Session { preconditionFailure("Attempting to send non-headers frame") } - let isEndStream = frame.endStream + let isEndStream = frame.flags.contains(.endStream) let flags = isEndStream ? UInt8(NGHTTP2_FLAG_END_STREAM.rawValue) : UInt8(0) guard let networkStreamID = frame.streamID.networkStreamID else { @@ -744,7 +744,7 @@ class NGHTTP2Session { streamState.dataProvider.bufferWrite(write: data, promise: promise) // If this has END_STREAM set, we do not expect trailers. - if frame.endStream { + if frame.flags.contains(.endStream) { streamState.dataProvider.bufferEOF(trailers: nil) } diff --git a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift index 74c3a1c0..fedc3eb9 100644 --- a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift +++ b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift @@ -179,7 +179,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { let streamIDs = stride(from: 1, to: 100, by: 2).map { HTTP2StreamID(knownID: Int32($0)) } for streamID in streamIDs { var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) } XCTAssertEqual(completedChannelCount, 0) @@ -202,7 +202,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // First, set up the frames we want to send/receive. let streamID = HTTP2StreamID(knownID: Int32(1)) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) let rstStreamFrame = HTTP2Frame(streamID: streamID, payload: .rstStream(.cancel)) let multiplexer = HTTP2StreamMultiplexer { (channel, _) in @@ -236,7 +236,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // First, set up the frames we want to send/receive. let streamID = HTTP2StreamID(knownID: Int32(1)) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) let goAwayFrame = HTTP2Frame(streamID: .rootStream, payload: .goAway(lastStreamID: .rootStream, errorCode: .http11Required, opaqueData: nil)) let multiplexer = HTTP2StreamMultiplexer { (channel, _) in @@ -613,7 +613,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { let secondStreamID = HTTP2StreamID(knownID: 3) for streamID in [firstStreamID, secondStreamID] { var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) } XCTAssertEqual(channels.count, 2) @@ -649,7 +649,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // Let's open a stream. let streamID = HTTP2StreamID(knownID: 1) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) XCTAssertNotNil(channel) @@ -680,7 +680,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // Let's open a stream. let streamID = HTTP2StreamID(knownID: 1) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) XCTAssertNotNil(channel) @@ -711,7 +711,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // Let's open a stream. let streamID = HTTP2StreamID(knownID: 1) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) XCTAssertNotNil(channel) @@ -949,7 +949,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // Let's open a stream. let streamID = HTTP2StreamID(knownID: 1) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) // No handlerRemoved so far. @@ -980,7 +980,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase { // Let's open a stream. let streamID = HTTP2StreamID(knownID: 1) var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders())) - frame.endStream = true + frame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(frame)) // No handlerRemoved so far. diff --git a/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift b/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift index 55bfc39f..550fddfc 100644 --- a/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift +++ b/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift @@ -93,7 +93,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { var bodyData = self.channel.allocator.buffer(capacity: 12) bodyData.write(staticString: "hello, world!") var dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(bodyData))) - dataFrame.endStream = true + dataFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(dataFrame)) self.channel.assertReceivedServerRequestPart(.body(bodyData)) self.channel.assertReceivedServerRequestPart(.end(nil)) @@ -108,7 +108,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { // A basic request. let requestHeaders = HTTPHeaders([(":path", "/get"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")]) var headersFrame = HTTP2Frame(streamID: streamID, payload: .headers(requestHeaders)) - headersFrame.endStream = true + headersFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(headersFrame)) var expectedRequestHead = HTTPRequestHead(version: HTTPVersion(major: 2, minor: 0), method: .GET, uri: "/get") @@ -137,7 +137,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { // Ok, we're going to send trailers. let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "also yes")]) var trailersFrame = HTTP2Frame(streamID: streamID, payload: .headers(trailers)) - trailersFrame.endStream = true + trailersFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(trailersFrame)) self.channel.assertReceivedServerRequestPart(.end(trailers)) @@ -280,7 +280,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { var bodyData = self.channel.allocator.buffer(capacity: 12) bodyData.write(staticString: "hello, world!") var dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(bodyData))) - dataFrame.endStream = true + dataFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(dataFrame)) self.channel.assertReceivedClientResponsePart(.body(bodyData)) self.channel.assertReceivedClientResponsePart(.end(nil)) @@ -295,7 +295,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { // A basic response. let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) var headersFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders)) - headersFrame.endStream = true + headersFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(headersFrame)) var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) @@ -322,7 +322,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { // Ok, we're going to send trailers. let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "also yes")]) var trailersFrame = HTTP2Frame(streamID: streamID, payload: .headers(trailers)) - trailersFrame.endStream = true + trailersFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(trailersFrame)) self.channel.assertReceivedClientResponsePart(.end(trailers)) @@ -406,7 +406,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase { // Now a response. let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")]) var responseFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders)) - responseFrame.endStream = true + responseFrame.flags.insert(.endStream) XCTAssertNoThrow(try self.channel.writeInbound(responseFrame)) var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok) diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift index fe830a9a..5b618332 100644 --- a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift +++ b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift @@ -201,14 +201,14 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID // Let's send a quick response back. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) XCTAssertNoThrow(try self.clientChannel.finish()) @@ -232,7 +232,7 @@ class SimpleClientServerTests: XCTestCase { let streamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: streamID, payload: .headers(requestHeaders)) var reqBodyFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.write(reqFrame, promise: nil) self.clientChannel.write(reqBodyFrame, promise: nil) @@ -303,13 +303,13 @@ class SimpleClientServerTests: XCTestCase { var requestBody = self.clientChannel.allocator.buffer(capacity: 128) requestBody.write(staticString: "A simple HTTP/2 request.") var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel) // The server will respond, closing this stream. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) // The server can now GOAWAY down to stream 1. We evaluate the bytes here ourselves becuase the client won't see this frame. @@ -355,7 +355,7 @@ class SimpleClientServerTests: XCTestCase { // Now we'll try to send this in a DATA frame. var dataFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(buffer))) - dataFrame.endStream = true + dataFrame.flags.insert(.endStream) self.clientChannel.writeAndFlush(dataFrame, promise: nil) self.interactInMemory(self.clientChannel, self.serverChannel) @@ -372,7 +372,7 @@ class SimpleClientServerTests: XCTestCase { // Now send a response from the server and shut things down. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) XCTAssertNoThrow(try self.clientChannel.finish()) @@ -392,14 +392,14 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.fileRegion(region))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID // Let's send a quick response back. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) } @@ -432,7 +432,7 @@ class SimpleClientServerTests: XCTestCase { // Ok, we're gonna send the body here. This should create 4 streams. var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.fileRegion(region))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.writeAndFlush(reqBodyFrame, promise: nil) self.interactInMemory(self.clientChannel, self.serverChannel) @@ -449,7 +449,7 @@ class SimpleClientServerTests: XCTestCase { // Let's send a quick response back. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) // No frames left. @@ -500,12 +500,12 @@ class SimpleClientServerTests: XCTestCase { // Let's complete one of the streams by sending data for the first stream on the client, and responding on the server. var dataFrame = HTTP2Frame(streamID: clientStreamIDs.first!, payload: .data(.byteBuffer(requestBody))) - dataFrame.endStream = true + dataFrame.flags.insert(.endStream) self.clientChannel.writeAndFlush(dataFrame, promise: nil) let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamIDs.first!, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) self.serverChannel.writeAndFlush(respFrame, promise: nil) // Now we expect the following things to have happened: 1) the client will have seen the server's response, @@ -580,7 +580,7 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.write(reqFrame, promise: nil) var receivedError: Error? = nil @@ -610,7 +610,7 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.write(reqFrame, promise: nil) var receivedError: Error? = nil @@ -722,7 +722,7 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.write(reqFrame, promise: nil) self.clientChannel.write(reqBodyFrame, promise: nil) @@ -764,7 +764,7 @@ class SimpleClientServerTests: XCTestCase { for _ in 0..<63 { self.clientChannel.write(reqBodyFrame, promise: nil) } - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) self.clientChannel.writeAndFlush(reqBodyFrame, promise: nil) // Ok, we now want to send this data to the server. @@ -991,7 +991,7 @@ class SimpleClientServerTests: XCTestCase { let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) let reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) var trailerFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(trailers)) - trailerFrame.endStream = true + trailerFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame, trailerFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID @@ -999,7 +999,7 @@ class SimpleClientServerTests: XCTestCase { let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) let respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) var respTrailersFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(trailers)) - respTrailersFrame.endStream = true + respTrailersFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame, respTrailersFrame], sender: self.serverChannel, receiver: self.clientChannel) XCTAssertNoThrow(try self.clientChannel.finish()) @@ -1014,7 +1014,7 @@ class SimpleClientServerTests: XCTestCase { let headers = HTTPHeaders([(":path", "/"), (":method", "GET"), (":scheme", "https"), (":authority", "localhost")]) let clientStreamID = HTTP2StreamID() var reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) - reqFrame.endStream = true + reqFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID @@ -1026,7 +1026,7 @@ class SimpleClientServerTests: XCTestCase { // Now we send the final response back. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) XCTAssertNoThrow(try self.clientChannel.finish()) @@ -1083,7 +1083,7 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID @@ -1094,7 +1094,7 @@ class SimpleClientServerTests: XCTestCase { // Let's send a quick response back. let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")]) var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) // Now the streams are closed, they should have seen user events. @@ -1250,13 +1250,13 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(requestHeaders)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID // Let's send a quick response back. var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) } @@ -1284,13 +1284,13 @@ class SimpleClientServerTests: XCTestCase { let clientStreamID = HTTP2StreamID() let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(requestHeaders)) var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody))) - reqBodyFrame.endStream = true + reqBodyFrame.flags.insert(.endStream) let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID // Let's send a quick response back. var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel) } @@ -1309,7 +1309,7 @@ class SimpleClientServerTests: XCTestCase { // Clean it up with the server now. let serverFrames = serverStreamIDs.map { streamID -> HTTP2Frame in var respFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders)) - respFrame.endStream = true + respFrame.flags.insert(.endStream) return respFrame } try self.assertFramesRoundTrip(frames: serverFrames, sender: self.serverChannel, receiver: self.clientChannel) diff --git a/Tests/NIOHTTP2Tests/TestUtilities.swift b/Tests/NIOHTTP2Tests/TestUtilities.swift index 745726d8..4ac6f733 100644 --- a/Tests/NIOHTTP2Tests/TestUtilities.swift +++ b/Tests/NIOHTTP2Tests/TestUtilities.swift @@ -150,6 +150,10 @@ extension EmbeddedChannel { } extension HTTP2Frame { + var ack: Bool { + return self.flags.contains(.ack) + } + /// Asserts that the given frame is a SETTINGS frame. func assertSettingsFrame(expectedSettings: [HTTP2Setting], ack: Bool, file: StaticString = #file, line: UInt = #line) { guard case .settings(let values) = self.payload else { @@ -196,7 +200,7 @@ extension HTTP2Frame { guard case .headers(let payload) = frame.payload else { preconditionFailure("Headers frames can never match non-headers frames") } - self.assertHeadersFrame(endStream: frame.endStream, + self.assertHeadersFrame(endStream: frame.flags.contains(.endStream), streamID: frame.streamID.networkStreamID!, payload: payload, file: file, @@ -211,8 +215,8 @@ extension HTTP2Frame { return } - XCTAssertEqual(self.endStream, endStream, - "Unexpected endStream: expected \(endStream), got \(self.endStream)", file: file, line: line) + XCTAssertEqual(self.flags.contains(.endStream), endStream, + "Unexpected endStream: expected \(endStream), got \(self.flags.contains(.endStream))", file: file, line: line) XCTAssertEqual(self.streamID.networkStreamID!, streamID, "Unexpected streamID: expected \(streamID), got \(self.streamID.networkStreamID!)", file: file, line: line) XCTAssertEqual(payload, actualPayload, "Non-equal payloads: expected \(payload), got \(actualPayload)", file: file, line: line) @@ -231,7 +235,7 @@ extension HTTP2Frame { preconditionFailure("Data frames can never match non-data frames") } - self.assertDataFrame(endStream: frame.endStream, + self.assertDataFrame(endStream: frame.flags.contains(.endStream), streamID: frame.streamID.networkStreamID!, payload: expectedPayload, file: file, @@ -245,8 +249,8 @@ extension HTTP2Frame { return } - XCTAssertEqual(self.endStream, endStream, - "Unexpected endStream: expected \(endStream), got \(self.endStream)", file: file, line: line) + XCTAssertEqual(self.flags.contains(.endStream), endStream, + "Unexpected endStream: expected \(endStream), got \(self.flags.contains(.endStream))", file: file, line: line) XCTAssertEqual(self.streamID.networkStreamID!, streamID, "Unexpected streamID: expected \(streamID), got \(self.streamID.networkStreamID!)", file: file, line: line) XCTAssertEqual(actualPayload, payload,