@@ -92,6 +92,13 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
92
92
/// which can cause an infinite recursion.
93
93
private var isUnbufferingAndFlushingAutomaticFrames = false
94
94
95
+ /// In some cases channelInactive can be fired while channelActive is still running.
96
+ private var activationState = ActivationState . idle
97
+
98
+ /// Whether we should tolerate "impossible" state transitions in debug mode. Only true in tests specifically trying to
99
+ /// trigger them.
100
+ private let tolerateImpossibleStateTransitionsInDebugMode : Bool
101
+
95
102
/// The mode for this parser to operate in: client or server.
96
103
public enum ParserMode {
97
104
/// Client mode
@@ -152,6 +159,36 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
152
159
self . initialSettings = initialSettings
153
160
self . outboundBuffer = CompoundOutboundBuffer ( mode: mode, initialMaxOutboundStreams: 100 , maxBufferedControlFrames: maximumBufferedControlFrames)
154
161
self . denialOfServiceValidator = DOSHeuristics ( maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames)
162
+ self . tolerateImpossibleStateTransitionsInDebugMode = false
163
+ }
164
+
165
+ /// Constructs a ``NIOHTTP2Handler``.
166
+ ///
167
+ /// - parameters:
168
+ /// - mode: The mode for this handler, client or server.
169
+ /// - initialSettings: The settings that will be advertised to the peer in the preamble. Defaults to ``nioDefaultSettings``.
170
+ /// - headerBlockValidation: Whether to validate sent and received HTTP/2 header blocks. Defaults to ``ValidationState/enabled``.
171
+ /// - contentLengthValidation: Whether to validate the content length of sent and received streams. Defaults to ``ValidationState/enabled``.
172
+ /// - maximumSequentialEmptyDataFrames: Controls the number of empty data frames this handler will tolerate receiving in a row before DoS protection
173
+ /// is triggered and the connection is terminated. Defaults to 1.
174
+ /// - maximumBufferedControlFrames: Controls the maximum buffer size of buffered outbound control frames. If we are unable to send control frames as
175
+ /// fast as we produce them we risk building up an unbounded buffer and exhausting our memory. To protect against this DoS vector, we put an
176
+ /// upper limit on the depth of this queue. Defaults to 10,000.
177
+ /// - tolerateImpossibleStateTransitionsInDebugMode: Whether impossible state transitions should be tolerated
178
+ /// in debug mode.
179
+ internal init ( mode: ParserMode ,
180
+ initialSettings: HTTP2Settings = nioDefaultSettings,
181
+ headerBlockValidation: ValidationState = . enabled,
182
+ contentLengthValidation: ValidationState = . enabled,
183
+ maximumSequentialEmptyDataFrames: Int = 1 ,
184
+ maximumBufferedControlFrames: Int = 10000 ,
185
+ tolerateImpossibleStateTransitionsInDebugMode: Bool = false ) {
186
+ self . stateMachine = HTTP2ConnectionStateMachine ( role: . init( mode) , headerBlockValidation: . init( headerBlockValidation) , contentLengthValidation: . init( contentLengthValidation) )
187
+ self . mode = mode
188
+ self . initialSettings = initialSettings
189
+ self . outboundBuffer = CompoundOutboundBuffer ( mode: mode, initialMaxOutboundStreams: 100 , maxBufferedControlFrames: maximumBufferedControlFrames)
190
+ self . denialOfServiceValidator = DOSHeuristics ( maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames)
191
+ self . tolerateImpossibleStateTransitionsInDebugMode = tolerateImpossibleStateTransitionsInDebugMode
155
192
}
156
193
157
194
public func handlerAdded( context: ChannelHandlerContext ) {
@@ -160,6 +197,11 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
160
197
self . writeBuffer = context. channel. allocator. buffer ( capacity: 128 )
161
198
162
199
if context. channel. isActive {
200
+ // We jump immediately to activated here, as channelActive has probably already passed.
201
+ if self . activationState != . idle {
202
+ self . impossibleActivationStateTransition ( state: self . activationState, activating: true , context: context)
203
+ }
204
+ self . activationState = . activated
163
205
self . writeAndFlushPreamble ( context: context)
164
206
}
165
207
}
@@ -170,13 +212,80 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
170
212
}
171
213
172
214
public func channelActive( context: ChannelHandlerContext ) {
215
+ // Check our activation state.
216
+ switch self . activationState {
217
+ case . idle:
218
+ // This is our first channelActive. We're now activating.
219
+ self . activationState = . activating
220
+ case . activated:
221
+ // This is a weird one, but it implies we "beat" the channelActive
222
+ // call down the pipeline when we added in handlerAdded. That's ok!
223
+ // We can safely ignore this.
224
+ return
225
+ case . activating, . inactiveWhileActivating, . inactive:
226
+ // All of these states are channel pipeline invariant violations, but conceptually possible.
227
+ //
228
+ // - .activating implies we got another channelActive while we were handling one. That would be
229
+ // a violation of pipeline invariants.
230
+ // - .inactiveWhileActivating implies a sequence of channelActive, channelInactive, channelActive
231
+ // synchronously. This is not just unlikely, but also misuse of the handler or violation of
232
+ // channel pipeline invariants.
233
+ // - .inactive implies we received channelInactive and then got another active. This is almost certainly
234
+ // misuse of the handler.
235
+ //
236
+ // We'll throw an error and then close. In debug builds, we crash.
237
+ self . impossibleActivationStateTransition (
238
+ state: self . activationState, activating: true , context: context
239
+ )
240
+ return
241
+ }
242
+
173
243
self . writeAndFlushPreamble ( context: context)
174
- context. fireChannelActive ( )
244
+
245
+ // Ok, we progressed. Now we check our state again.
246
+ switch self . activationState {
247
+ case . activating:
248
+ // This is the easy case: nothing exciting happened. We can activate and notify the pipeline.
249
+ self . activationState = . activated
250
+ context. fireChannelActive ( )
251
+ case . inactiveWhileActivating:
252
+ // This is awkward: we got a channelInactive during the above operation. We need to fire channelActive
253
+ // and then re-issue the channelInactive call.
254
+ self . activationState = . activated
255
+ context. fireChannelActive ( )
256
+ self . channelInactive ( context: context)
257
+ case . idle, . activated, . inactive:
258
+ // These three states should be impossible.
259
+ //
260
+ // - .idle somehow implies we didn't execute the code above.
261
+ // - .activated implies that the above code didn't prevent us re-entrantly getting to this point.
262
+ // - .inactive implies that somehow we hit channelInactive but didn't enter .inactiveWhileActivating.
263
+ self . impossibleActivationStateTransition (
264
+ state: self . activationState, activating: true , context: context
265
+ )
266
+ }
175
267
}
176
268
177
269
public func channelInactive( context: ChannelHandlerContext ) {
178
- self . channelClosed = true
179
- context. fireChannelInactive ( )
270
+ switch self . activationState {
271
+ case . activated:
272
+ // This is the easy one. We were active, now we aren't.
273
+ self . activationState = . inactive
274
+ self . channelClosed = true
275
+ context. fireChannelInactive ( )
276
+ case . activating:
277
+ // Huh, we got channelInactive during activation. We need to maintain
278
+ // ordering, so we'll temporarily delay this.
279
+ self . activationState = . inactiveWhileActivating
280
+ case . idle, . inactiveWhileActivating, . inactive:
281
+ // This is weird.
282
+ //
283
+ // .idle implies that somehow we got channelInactive before channelActive, which is probably an error.
284
+ // .inactiveWhileActivating and .inactive make this a second channelInactive call, which is also probably
285
+ // an error.
286
+ self . impossibleActivationStateTransition ( state: self . activationState, activating: false , context: context)
287
+ }
288
+
180
289
}
181
290
182
291
public func channelRead( context: ChannelHandlerContext , data: NIOAny ) {
@@ -699,8 +808,38 @@ NIOHTTP2Handler(
699
808
mode: \( String ( describing: self . mode) ) ,
700
809
initialSettings: \( String ( describing: self . initialSettings) ) ,
701
810
channelClosed: \( String ( describing: self . channelClosed) ) ,
702
- channelWritable: \( String ( describing: self . channelWritable) )
811
+ channelWritable: \( String ( describing: self . channelWritable) ) ,
812
+ activationState: \( String ( describing: self . activationState) )
703
813
)
704
814
"""
705
815
}
706
816
}
817
+
818
+ extension NIOHTTP2Handler {
819
+ /// Tracks the state of activation of the handler.
820
+ enum ActivationState {
821
+ /// The handler hasn't been activated yet.
822
+ case idle
823
+
824
+ /// The handler has received channelActive, but hasn't yet fired it on.
825
+ case activating
826
+
827
+ /// The handler was activating when it received channelInactive. The channel
828
+ /// must go inactive after firing channel active.
829
+ case inactiveWhileActivating
830
+
831
+ /// The channel has received and fired channelActive.
832
+ case activated
833
+
834
+ /// The channel has received and fired channelActive and channelInactive.
835
+ case inactive
836
+ }
837
+
838
+ fileprivate func impossibleActivationStateTransition(
839
+ state: ActivationState , activating: Bool , context: ChannelHandlerContext
840
+ ) {
841
+ assert ( self . tolerateImpossibleStateTransitionsInDebugMode, " Unexpected channelActive in state \( state) " )
842
+ context. fireErrorCaught ( NIOHTTP2Errors . ActivationError ( state: state, activating: activating) )
843
+ context. close ( promise: nil )
844
+ }
845
+ }
0 commit comments