diff --git a/Sources/NIOCore/ChannelPipeline.swift b/Sources/NIOCore/ChannelPipeline.swift index 7c4b0853a4..6b63c5e2bb 100644 --- a/Sources/NIOCore/ChannelPipeline.swift +++ b/Sources/NIOCore/ChannelPipeline.swift @@ -176,10 +176,14 @@ public final class ChannelPipeline: ChannelInvoker { let future: EventLoopFuture if self.eventLoop.inEventLoop { - future = self.eventLoop.makeCompletedFuture(self.addHandlerSync(handler, name: name, position: position)) + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + future = self.eventLoop.makeCompletedFuture( + self.addHandlerSync(handler, name: name, position: syncPosition) + ) } else { future = self.eventLoop.submit { - try self.addHandlerSync(handler, name: name, position: position).get() + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self.addHandlerSync(handler, name: name, position: syncPosition).get() } } @@ -198,7 +202,7 @@ public final class ChannelPipeline: ChannelInvoker { fileprivate func addHandlerSync( _ handler: ChannelHandler, name: String? = nil, - position: ChannelPipeline.Position = .last + position: ChannelPipeline.SynchronousOperations.Position = .last ) -> Result { self.eventLoop.assertInEventLoop() @@ -1122,11 +1126,12 @@ extension ChannelPipeline { _ handlers: [ChannelHandler & Sendable], position: ChannelPipeline.Position ) -> Result { - switch position { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + switch syncPosition { case .first, .after: - return self._addHandlersSync(handlers.reversed(), position: position) + return self._addHandlersSync(handlers.reversed(), position: syncPosition) case .last, .before: - return self._addHandlersSync(handlers, position: position) + return self._addHandlersSync(handlers, position: syncPosition) } } @@ -1143,7 +1148,7 @@ extension ChannelPipeline { /// - Returns: A result representing whether the handlers were added or not. fileprivate func addHandlersSyncNotSendable( _ handlers: [ChannelHandler], - position: ChannelPipeline.Position + position: ChannelPipeline.SynchronousOperations.Position ) -> Result { switch position { case .first, .after: @@ -1162,7 +1167,7 @@ extension ChannelPipeline { /// - Returns: A result representing whether the handlers were added or not. private func _addHandlersSync( _ handlers: Handlers, - position: ChannelPipeline.Position + position: ChannelPipeline.SynchronousOperations.Position ) -> Result where Handlers.Element == ChannelHandler & Sendable { self.eventLoop.assertInEventLoop() @@ -1191,7 +1196,7 @@ extension ChannelPipeline { /// - Returns: A result representing whether the handlers were added or not. private func _addHandlersSyncNotSendable( _ handlers: Handlers, - position: ChannelPipeline.Position + position: ChannelPipeline.SynchronousOperations.Position ) -> Result where Handlers.Element == ChannelHandler { self.eventLoop.assertInEventLoop() @@ -1238,20 +1243,67 @@ extension ChannelPipeline { public func addHandler( _ handler: ChannelHandler, name: String? = nil, - position: ChannelPipeline.Position = .last + position: ChannelPipeline.SynchronousOperations.Position = .last ) throws { try self._pipeline.addHandlerSync(handler, name: name, position: position).get() } + /// Add a handler to the pipeline. + /// + /// - Important: This *must* be called on the event loop. + /// - Parameters: + /// - handler: The handler to add. + /// - name: The name to use for the `ChannelHandler` when it's added. If no name is specified the one will be generated. + /// - position: The position in the `ChannelPipeline` to add `handler`. Defaults to `.last`. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload + public func addHandler( + _ handler: ChannelHandler, + name: String? = nil, + position: ChannelPipeline.Position = .last + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._pipeline.addHandlerSync(handler, name: name, position: syncPosition).get() + } + + /// Add an array of handlers to the pipeline. + /// + /// - Important: This *must* be called on the event loop. + /// - Parameters: + /// - handlers: The handlers to add. + /// - position: The position in the `ChannelPipeline` to add `handlers`. Defaults to `.last`. + public func addHandlers( + _ handlers: [ChannelHandler], + position: ChannelPipeline.SynchronousOperations.Position = .last + ) throws { + try self._pipeline.addHandlersSyncNotSendable(handlers, position: position).get() + } + /// Add an array of handlers to the pipeline. /// /// - Important: This *must* be called on the event loop. /// - Parameters: /// - handlers: The handlers to add. /// - position: The position in the `ChannelPipeline` to add `handlers`. Defaults to `.last`. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func addHandlers( _ handlers: [ChannelHandler], position: ChannelPipeline.Position = .last + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._pipeline.addHandlersSyncNotSendable(handlers, position: syncPosition).get() + } + + /// Add one or more handlers to the pipeline. + /// + /// - Important: This *must* be called on the event loop. + /// - Parameters: + /// - handlers: The handlers to add. + /// - position: The position in the `ChannelPipeline` to add `handlers`. Defaults to `.last`. + public func addHandlers( + _ handlers: ChannelHandler..., + position: ChannelPipeline.SynchronousOperations.Position = .last ) throws { try self._pipeline.addHandlersSyncNotSendable(handlers, position: position).get() } @@ -1262,11 +1314,14 @@ extension ChannelPipeline { /// - Parameters: /// - handlers: The handlers to add. /// - position: The position in the `ChannelPipeline` to add `handlers`. Defaults to `.last`. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func addHandlers( _ handlers: ChannelHandler..., position: ChannelPipeline.Position = .last ) throws { - try self._pipeline.addHandlersSyncNotSendable(handlers, position: position).get() + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._pipeline.addHandlersSyncNotSendable(handlers, position: syncPosition).get() } /// Remove a `ChannelHandler` from the `ChannelPipeline`. @@ -1574,6 +1629,41 @@ extension ChannelPipeline { } } +extension ChannelPipeline.SynchronousOperations { + /// A `Position` within the `ChannelPipeline`'s `SynchronousOperations` used to insert non-sendable handlers + /// into the `ChannelPipeline` at a certain position. + public enum Position { + /// The first `ChannelHandler` -- the front of the `ChannelPipeline`. + case first + + /// The last `ChannelHandler` -- the back of the `ChannelPipeline`. + case last + + /// Before the given `ChannelHandler`. + case before(ChannelHandler) + + /// After the given `ChannelHandler`. + case after(ChannelHandler) + + public init(_ position: ChannelPipeline.Position) { + switch position { + case .first: + self = .first + case .last: + self = .last + case .before(let handler): + self = .before(handler) + case .after(let handler): + self = .after(handler) + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(*, unavailable) +extension ChannelPipeline.SynchronousOperations.Position: Sendable {} + /// Special `ChannelHandler` that forwards all events to the `Channel.Unsafe` implementation. final class HeadChannelHandler: _ChannelOutboundHandler, Sendable { diff --git a/Sources/NIOHTTP1/HTTPPipelineSetup.swift b/Sources/NIOHTTP1/HTTPPipelineSetup.swift index a789b8f6a1..5e935123ce 100644 --- a/Sources/NIOHTTP1/HTTPPipelineSetup.swift +++ b/Sources/NIOHTTP1/HTTPPipelineSetup.swift @@ -84,9 +84,10 @@ extension ChannelPipeline { let future: EventLoopFuture if self.eventLoop.inEventLoop { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) let result = Result { try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, withClientUpgrade: upgrade ) @@ -94,8 +95,9 @@ extension ChannelPipeline { future = self.eventLoop.makeCompletedFuture(result) } else { future = self.eventLoop.submit { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, withClientUpgrade: upgrade ) @@ -127,9 +129,10 @@ extension ChannelPipeline { let future: EventLoopFuture if self.eventLoop.inEventLoop { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) let result = Result { try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, withClientUpgrade: upgrade @@ -138,8 +141,9 @@ extension ChannelPipeline { future = self.eventLoop.makeCompletedFuture(result) } else { future = self.eventLoop.submit { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, withClientUpgrade: upgrade @@ -174,9 +178,10 @@ extension ChannelPipeline { let future: EventLoopFuture if self.eventLoop.inEventLoop { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) let result = Result { try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, encoderConfiguration: encoderConfiguration, @@ -186,8 +191,9 @@ extension ChannelPipeline { future = self.eventLoop.makeCompletedFuture(result) } else { future = self.eventLoop.submit { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self.syncOperations.addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, encoderConfiguration: encoderConfiguration, @@ -343,9 +349,10 @@ extension ChannelPipeline { let future: EventLoopFuture if self.eventLoop.inEventLoop { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) let result = Result { try self.syncOperations.configureHTTPServerPipeline( - position: position, + position: syncPosition, withPipeliningAssistance: pipelining, withServerUpgrade: upgrade, withErrorHandling: errorHandling, @@ -356,8 +363,9 @@ extension ChannelPipeline { future = self.eventLoop.makeCompletedFuture(result) } else { future = self.eventLoop.submit { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self.syncOperations.configureHTTPServerPipeline( - position: position, + position: syncPosition, withPipeliningAssistance: pipelining, withServerUpgrade: upgrade, withErrorHandling: errorHandling, @@ -384,15 +392,69 @@ extension ChannelPipeline.SynchronousOperations { /// the upgrade completion handler. See the documentation on `HTTPClientUpgradeHandler` /// for more details. /// - Throws: If the pipeline could not be configured. + public func addHTTPClientHandlers( + position: ChannelPipeline.SynchronousOperations.Position = .last, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil + ) throws { + try self._addHTTPClientHandlers( + position: position, + leftOverBytesStrategy: leftOverBytesStrategy, + withClientUpgrade: upgrade + ) + } + + /// Configure a `ChannelPipeline` for use as a HTTP client with a client upgrader configuration. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: The position in the `ChannelPipeline` where to add the HTTP client handlers. Defaults to `.last`. + /// - leftOverBytesStrategy: The strategy to use when dealing with leftover bytes after removing the `HTTPDecoder` + /// from the pipeline. + /// - upgrade: Add a `HTTPClientUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Should be a tuple of an array of `HTTPClientProtocolUpgrader` and + /// the upgrade completion handler. See the documentation on `HTTPClientUpgradeHandler` + /// for more details. + /// - Throws: If the pipeline could not be configured. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload @preconcurrency public func addHTTPClientHandlers( position: ChannelPipeline.Position = .last, leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._addHTTPClientHandlers( + position: syncPosition, + leftOverBytesStrategy: leftOverBytesStrategy, + withClientUpgrade: upgrade + ) + } + + /// Configure a `ChannelPipeline` for use as a HTTP client. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: The position in the `ChannelPipeline` where to add the HTTP client handlers. Defaults to `.last`. + /// - leftOverBytesStrategy: The strategy to use when dealing with leftover bytes after removing the `HTTPDecoder` + /// from the pipeline. + /// - enableOutboundHeaderValidation: Whether or not request header validation is enforced. + /// - upgrade: Add a ``NIOHTTPClientUpgradeHandler`` to the pipeline, configured for + /// HTTP upgrade. Should be a tuple of an array of ``NIOHTTPClientProtocolUpgrader`` and + /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` + /// for more details. + /// - Throws: If the pipeline could not be configured. + public func addHTTPClientHandlers( + position: ChannelPipeline.SynchronousOperations.Position = .last, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + enableOutboundHeaderValidation: Bool = true, + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil ) throws { try self._addHTTPClientHandlers( position: position, leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, withClientUpgrade: upgrade ) } @@ -410,16 +472,49 @@ extension ChannelPipeline.SynchronousOperations { /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` /// for more details. /// - Throws: If the pipeline could not be configured. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func addHTTPClientHandlers( position: ChannelPipeline.Position = .last, leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, enableOutboundHeaderValidation: Bool = true, withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._addHTTPClientHandlers( + position: syncPosition, + leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, + withClientUpgrade: upgrade + ) + } + + /// Configure a `ChannelPipeline` for use as a HTTP client. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: The position in the `ChannelPipeline` where to add the HTTP client handlers. Defaults to `.last`. + /// - leftOverBytesStrategy: The strategy to use when dealing with leftover bytes after removing the `HTTPDecoder` + /// from the pipeline. + /// - enableOutboundHeaderValidation: Whether or not request header validation is enforced. + /// - encoderConfiguration: The configuration for the ``HTTPRequestEncoder``. + /// - upgrade: Add a ``NIOHTTPClientUpgradeHandler`` to the pipeline, configured for + /// HTTP upgrade. Should be a tuple of an array of ``NIOHTTPClientProtocolUpgrader`` and + /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` + /// for more details. + /// - Throws: If the pipeline could not be configured. + public func addHTTPClientHandlers( + position: ChannelPipeline.SynchronousOperations.Position = .last, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + enableOutboundHeaderValidation: Bool = true, + encoderConfiguration: HTTPRequestEncoder.Configuration = .init(), + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil ) throws { try self._addHTTPClientHandlers( position: position, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, + encoderConfiguration: encoderConfiguration, withClientUpgrade: upgrade ) } @@ -438,6 +533,8 @@ extension ChannelPipeline.SynchronousOperations { /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` /// for more details. /// - Throws: If the pipeline could not be configured. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func addHTTPClientHandlers( position: ChannelPipeline.Position = .last, leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, @@ -445,8 +542,9 @@ extension ChannelPipeline.SynchronousOperations { encoderConfiguration: HTTPRequestEncoder.Configuration = .init(), withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self._addHTTPClientHandlers( - position: position, + position: syncPosition, leftOverBytesStrategy: leftOverBytesStrategy, enableOutboundHeaderValidation: enableOutboundHeaderValidation, encoderConfiguration: encoderConfiguration, @@ -455,7 +553,7 @@ extension ChannelPipeline.SynchronousOperations { } private func _addHTTPClientHandlers( - position: ChannelPipeline.Position = .last, + position: ChannelPipeline.SynchronousOperations.Position = .last, leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, enableOutboundHeaderValidation: Bool = true, encoderConfiguration: HTTPRequestEncoder.Configuration = .init(), @@ -481,7 +579,7 @@ extension ChannelPipeline.SynchronousOperations { } private func _addHTTPClientHandlers( - position: ChannelPipeline.Position, + position: ChannelPipeline.SynchronousOperations.Position, leftOverBytesStrategy: RemoveAfterUpgradeStrategy, encoderConfiguration: HTTPRequestEncoder.Configuration ) throws { @@ -496,7 +594,7 @@ extension ChannelPipeline.SynchronousOperations { } private func _addHTTPClientHandlersFallback( - position: ChannelPipeline.Position, + position: ChannelPipeline.SynchronousOperations.Position, leftOverBytesStrategy: RemoveAfterUpgradeStrategy, enableOutboundHeaderValidation: Bool, encoderConfiguration: HTTPRequestEncoder.Configuration, @@ -522,6 +620,47 @@ extension ChannelPipeline.SynchronousOperations { try self.addHandlers(handlers, position: position) } + + /// Configure a `ChannelPipeline` for use as a HTTP server. + /// + /// This function knows how to set up all first-party HTTP channel handlers appropriately + /// for server use. It supports the following features: + /// + /// 1. Providing assistance handling clients that pipeline HTTP requests, using the + /// `HTTPServerPipelineHandler`. + /// 2. Supporting HTTP upgrade, using the `HTTPServerUpgradeHandler`. + /// + /// This method will likely be extended in future with more support for other first-party + /// features. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: Where in the pipeline to add the HTTP server handlers, defaults to `.last`. + /// - pipelining: Whether to provide assistance handling HTTP clients that pipeline + /// their requests. Defaults to `true`. If `false`, users will need to handle + /// clients that pipeline themselves. + /// - upgrade: Whether to add a `HTTPServerUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Defaults to `nil`, which will not add the handler to the pipeline. If + /// provided should be a tuple of an array of `HTTPServerProtocolUpgrader` and the upgrade + /// completion handler. See the documentation on `HTTPServerUpgradeHandler` for more + /// details. + /// - errorHandling: Whether to provide assistance handling protocol errors (e.g. + /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. + /// - Throws: If the pipeline could not be configured. + public func configureHTTPServerPipeline( + position: ChannelPipeline.SynchronousOperations.Position = .last, + withPipeliningAssistance pipelining: Bool = true, + withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, + withErrorHandling errorHandling: Bool = true + ) throws { + try self._configureHTTPServerPipeline( + position: position, + withPipeliningAssistance: pipelining, + withServerUpgrade: upgrade, + withErrorHandling: errorHandling + ) + } + /// Configure a `ChannelPipeline` for use as a HTTP server. /// /// This function knows how to set up all first-party HTTP channel handlers appropriately @@ -549,14 +688,17 @@ extension ChannelPipeline.SynchronousOperations { /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. /// - Throws: If the pipeline could not be configured. @preconcurrency + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func configureHTTPServerPipeline( position: ChannelPipeline.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, withErrorHandling errorHandling: Bool = true ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) try self._configureHTTPServerPipeline( - position: position, + position: syncPosition, withPipeliningAssistance: pipelining, withServerUpgrade: upgrade, withErrorHandling: errorHandling @@ -594,7 +736,7 @@ extension ChannelPipeline.SynchronousOperations { /// spec compliance. Defaults to `true`. /// - Throws: If the pipeline could not be configured. public func configureHTTPServerPipeline( - position: ChannelPipeline.Position = .last, + position: ChannelPipeline.SynchronousOperations.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, withErrorHandling errorHandling: Bool = true, @@ -638,13 +780,62 @@ extension ChannelPipeline.SynchronousOperations { /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. /// - headerValidation: Whether to validate outbound request headers to confirm that they meet /// spec compliance. Defaults to `true`. - /// - encoderConfiguration: The configuration for the ``HTTPRequestEncoder``. /// - Throws: If the pipeline could not be configured. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload public func configureHTTPServerPipeline( position: ChannelPipeline.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._configureHTTPServerPipeline( + position: syncPosition, + withPipeliningAssistance: pipelining, + withServerUpgrade: upgrade, + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation + ) + } + + /// Configure a `ChannelPipeline` for use as a HTTP server. + /// + /// This function knows how to set up all first-party HTTP channel handlers appropriately + /// for server use. It supports the following features: + /// + /// 1. Providing assistance handling clients that pipeline HTTP requests, using the + /// `HTTPServerPipelineHandler`. + /// 2. Supporting HTTP upgrade, using the `HTTPServerUpgradeHandler`. + /// 3. Providing assistance handling protocol errors. + /// 4. Validating outbound header fields to protect against response splitting attacks. + /// + /// This method will likely be extended in future with more support for other first-party + /// features. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: Where in the pipeline to add the HTTP server handlers, defaults to `.last`. + /// - pipelining: Whether to provide assistance handling HTTP clients that pipeline + /// their requests. Defaults to `true`. If `false`, users will need to handle + /// clients that pipeline themselves. + /// - upgrade: Whether to add a `HTTPServerUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Defaults to `nil`, which will not add the handler to the pipeline. If + /// provided should be a tuple of an array of `HTTPServerProtocolUpgrader` and the upgrade + /// completion handler. See the documentation on `HTTPServerUpgradeHandler` for more + /// details. + /// - errorHandling: Whether to provide assistance handling protocol errors (e.g. + /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. + /// - headerValidation: Whether to validate outbound request headers to confirm that they meet + /// spec compliance. Defaults to `true`. + /// - encoderConfiguration: The configuration for the ``HTTPRequestEncoder``. + /// - Throws: If the pipeline could not be configured. + public func configureHTTPServerPipeline( + position: ChannelPipeline.SynchronousOperations.Position = .last, + withPipeliningAssistance pipelining: Bool = true, + withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, + withErrorHandling errorHandling: Bool = true, withOutboundHeaderValidation headerValidation: Bool = true, withEncoderConfiguration encoderConfiguration: HTTPResponseEncoder.Configuration ) throws { @@ -658,12 +849,64 @@ extension ChannelPipeline.SynchronousOperations { ) } - private func _configureHTTPServerPipeline( + /// Configure a `ChannelPipeline` for use as a HTTP server. + /// + /// This function knows how to set up all first-party HTTP channel handlers appropriately + /// for server use. It supports the following features: + /// + /// 1. Providing assistance handling clients that pipeline HTTP requests, using the + /// `HTTPServerPipelineHandler`. + /// 2. Supporting HTTP upgrade, using the `HTTPServerUpgradeHandler`. + /// 3. Providing assistance handling protocol errors. + /// 4. Validating outbound header fields to protect against response splitting attacks. + /// + /// This method will likely be extended in future with more support for other first-party + /// features. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - Parameters: + /// - position: Where in the pipeline to add the HTTP server handlers, defaults to `.last`. + /// - pipelining: Whether to provide assistance handling HTTP clients that pipeline + /// their requests. Defaults to `true`. If `false`, users will need to handle + /// clients that pipeline themselves. + /// - upgrade: Whether to add a `HTTPServerUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Defaults to `nil`, which will not add the handler to the pipeline. If + /// provided should be a tuple of an array of `HTTPServerProtocolUpgrader` and the upgrade + /// completion handler. See the documentation on `HTTPServerUpgradeHandler` for more + /// details. + /// - errorHandling: Whether to provide assistance handling protocol errors (e.g. + /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. + /// - headerValidation: Whether to validate outbound request headers to confirm that they meet + /// spec compliance. Defaults to `true`. + /// - encoderConfiguration: The configuration for the ``HTTPRequestEncoder``. + /// - Throws: If the pipeline could not be configured. + @available(*, deprecated, message: "Use ChannelPipeline.SynchronousOperations.Position instead") + @_disfavoredOverload + public func configureHTTPServerPipeline( position: ChannelPipeline.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, withErrorHandling errorHandling: Bool = true, withOutboundHeaderValidation headerValidation: Bool = true, + withEncoderConfiguration encoderConfiguration: HTTPResponseEncoder.Configuration + ) throws { + let syncPosition = ChannelPipeline.SynchronousOperations.Position(position) + try self._configureHTTPServerPipeline( + position: syncPosition, + withPipeliningAssistance: pipelining, + withServerUpgrade: upgrade, + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation, + withEncoderConfiguration: encoderConfiguration + ) + } + + private func _configureHTTPServerPipeline( + position: ChannelPipeline.SynchronousOperations.Position = .last, + withPipeliningAssistance pipelining: Bool = true, + withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, + withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true, withEncoderConfiguration encoderConfiguration: HTTPResponseEncoder.Configuration = .init() ) throws { self.eventLoop.assertInEventLoop() diff --git a/Tests/NIOPosixTests/ChannelPipelineTest.swift b/Tests/NIOPosixTests/ChannelPipelineTest.swift index 130e39fd2c..d00f460d05 100644 --- a/Tests/NIOPosixTests/ChannelPipelineTest.swift +++ b/Tests/NIOPosixTests/ChannelPipelineTest.swift @@ -2693,6 +2693,132 @@ class ChannelPipelineTest: XCTestCase { XCTAssertTrue(try channel.finish().isClean) } + + func testAddAfterForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertNoThrow(try channel.finish()) + } + + let firstHandler = IndexWritingHandler(1) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(firstHandler)) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(IndexWritingHandler(2))) + XCTAssertNoThrow( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(3), + position: .after(firstHandler) + ) + ) + + channel.assertReadIndexOrder([1, 3, 2]) + channel.assertWriteIndexOrder([2, 3, 1]) + } + + func testAddBeforeForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertNoThrow(try channel.finish()) + } + + let secondHandler = IndexWritingHandler(2) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(IndexWritingHandler(1))) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(secondHandler)) + XCTAssertNoThrow( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(3), + position: .before(secondHandler) + ) + ) + + channel.assertReadIndexOrder([1, 3, 2]) + channel.assertWriteIndexOrder([2, 3, 1]) + } + + func testAddAfterLastForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertNoThrow(try channel.finish()) + } + + let secondHandler = IndexWritingHandler(2) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(IndexWritingHandler(1))) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(secondHandler)) + XCTAssertNoThrow( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(3), + position: .after(secondHandler) + ) + ) + + channel.assertReadIndexOrder([1, 2, 3]) + channel.assertWriteIndexOrder([3, 2, 1]) + } + + func testAddBeforeFirstForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertNoThrow(try channel.finish()) + } + + let firstHandler = IndexWritingHandler(1) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(firstHandler)) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(IndexWritingHandler(2))) + XCTAssertNoThrow( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(3), + position: .before(firstHandler) + ) + ) + + channel.assertReadIndexOrder([3, 1, 2]) + channel.assertWriteIndexOrder([2, 1, 3]) + } + + func testAddAfterWhileClosedForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertThrowsError(try channel.finish()) { error in + XCTAssertEqual(.alreadyClosed, error as? ChannelError) + } + } + + let handler = IndexWritingHandler(1) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(handler)) + XCTAssertNoThrow(try channel.close().wait()) + channel.embeddedEventLoop.run() + + XCTAssertThrowsError( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(2), + position: .after(handler) + ) + ) { error in + XCTAssertEqual(.ioOnClosedChannel, error as? ChannelError) + } + } + + func testAddBeforeWhileClosedForSynchronousPosition() { + let channel = EmbeddedChannel() + defer { + XCTAssertThrowsError(try channel.finish()) { error in + XCTAssertEqual(.alreadyClosed, error as? ChannelError) + } + } + + let handler = IndexWritingHandler(1) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(handler)) + XCTAssertNoThrow(try channel.close().wait()) + channel.embeddedEventLoop.run() + + XCTAssertThrowsError( + try channel.pipeline.syncOperations.addHandler( + IndexWritingHandler(2), + position: .before(handler) + ) + ) { error in + XCTAssertEqual(.ioOnClosedChannel, error as? ChannelError) + } + } } // this should be within `testAddMultipleHandlers` but https://bugs.swift.org/browse/SR-9956