Search code examples
iosswiftproxyhttp2swift-nio

Swift-NIO based proxy reconfigures pipeline on every incoming request


I'm using Swift-NIO to create a Http/2 proxy with TLS, for iOS/tvOS. My proxy startup:

var tlsConfiguration = ...
tlsConfiguration.applicationProtocols = NIOHTTP2SupportedALPNProtocols

let bootstrap = NIOTSListenerBootstrap(group: loopGroup)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1)
.childChannelInitializer { channel in
    let sslContext: NIOSSLContext
    let tlsHandler: NIOSSLServerHandler
    do {
         sslContext = try NIOSSLContext(configuration: tlsConfiguration)
         tlsHandler = NIOSSLServerHandler(context: sslContext)
    } catch {
         print("[HTTP2PROXY] Could not configure TLS")
         return channel.close(mode: .all)
    }
    
    return channel.pipeline.addHandler(tlsHandler, name: "TLS_Handler").flatMap {
        print("[HTTP2PROXY] TLSHandler added to pipeline")
        print("[HTTP2PROXY] Configuring pipeline for Http/1.1 and Http/2")
        return channel.configureCommonHTTPServerPipeline(h2ConnectionChannelConfigurator: nil) { streamChannel in
            return streamChannel.pipeline.addHandlers([DebugInboundEventsHandler(), DebugOutboundEventsHandler()]).flatMap {
                print("[HTTP2PROXY] Event debugger handlers added")
                return streamChannel.pipeline.addHandler(HTTPResponseCompressor(), name: "ResponseCompressor")
                }.flatMap {
                     print("[HTTP2PROXY] HTTPResponseCompressor added to pipeline")
                     return streamChannel.pipeline.addHandler(CustomHttp1Handler(hlsRequestHandler: self.hlsRequestHandler), name: "Custom_Http1")
                }.flatMap {
                     print("[HTTP2PROXY] Custom Http1Handler added to pipeline")
                     return streamChannel.pipeline.addHandler(ErrorHandler())
                }
            }
        }
    }
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1)
do {
    let serverChannel = try bootstrap.bind(host: Http2Proxy.host, port: Http2Proxy.port).wait()
    print("[HTTP2PROXY] Server Channel bound to: \(serverChannel.localAddress!)")
}
catch {
    try! loopGroup.syncShutdownGracefully()
    print("[HTTP2PROXY] Failed to start channel: \(error)")
}

The server works and handles requests correctly using Https over Http/2 and even gzips responses when asked. So that's great. But in the logs I see that for every incoming request, the pipeline is being reconfigured (i.e. escapes for attaching the handlers are being called over and over again). Is that the way it is supposed to work? As far as I know I'm not closing the context/channel anywhere. Is this Proxy setting up a new pipeline for every request and am I therefor missing out on the Http/2 approach of sending a lot of requests over the same channel (and pipeline)? Or is this actuallty the way it is supposed to work? It feels like a none optimal result...

LOGS:

10:21:15.760 [HTTP2PROXY] Server Channel bound to: [IPv4]127.0.0.1/127.0.0.1:50001
10:22:47.813 [HTTP2PROXY] TLSHandler added to pipeline
10:22:47.813 [HTTP2PROXY] Configuring pipeline for Http/1.1 and Http/2
10:22:47.837 [HTTP2PROXY] Event debugger handlers added
10:22:47.838 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.838 [HTTP2PROXY] Custom Http1Handler added to pipeline
10:22:47.853 [HTTP2PROXY] Event debugger handlers added
10:22:47.853 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.853 [HTTP2PROXY] Custom Http1Handler added to pipeline
10:22:47.854 [HTTP2PROXY] Event debugger handlers added
10:22:47.854 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.854 [HTTP2PROXY] Custom Http1Handler added to pipeline
10:22:47.860 [HTTP2PROXY] Event debugger handlers added
10:22:47.860 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.860 [HTTP2PROXY] Custom Http1Handler added to pipeline
10:22:47.861 [HTTP2PROXY] Event debugger handlers added
10:22:47.861 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.861 [HTTP2PROXY] Custom Http1Handler added to pipeline
10:22:47.927 [HTTP2PROXY] Event debugger handlers added
10:22:47.927 [HTTP2PROXY] HTTPResponseCompressor added to pipeline
10:22:47.927 [HTTP2PROXY] Custom Http1Handler added to pipeline

Logging the context.channel.pipeline.debugDescription gives:

10:30:41.083 [HTTP2PROXY] Pipeline config: 
ChannelPipeline[ObjectIdentifier(0x00000002804d6fd0)]:
                                 [I] ↓↑ [O]
 HTTP2FramePayloadToHTTP1ServerCodec ↓↑ HTTP2FramePayloadToHTTP1ServerCodec [handler0]
              HTTPResponseCompressor ↓↑ HTTPResponseCompressor              [ResponseCompressor]
                  CustomHttp1Handler ↓↑                                     [Custom_Http1]
                        ErrorHandler ↓↑                                     [handler1]
10:30:41.087 [HTTP2PROXY] Pipeline config: 
ChannelPipeline[ObjectIdentifier(0x00000002804d7160)]:
                                 [I] ↓↑ [O]
 HTTP2FramePayloadToHTTP1ServerCodec ↓↑ HTTP2FramePayloadToHTTP1ServerCodec [handler0]
              HTTPResponseCompressor ↓↑ HTTPResponseCompressor              [ResponseCompressor]
                  CustomHttp1Handler ↓↑                                     [Custom_Http1]
                        ErrorHandler ↓↑                                     [handler1]
10:30:41.090 [HTTP2PROXY] Pipeline config: 
ChannelPipeline[ObjectIdentifier(0x00000002804d7610)]:
                                 [I] ↓↑ [O]
 HTTP2FramePayloadToHTTP1ServerCodec ↓↑ HTTP2FramePayloadToHTTP1ServerCodec [handler0]
              HTTPResponseCompressor ↓↑ HTTPResponseCompressor              [ResponseCompressor]
                  CustomHttp1Handler ↓↑                                     [Custom_Http1]
                        ErrorHandler ↓↑                                     [handler1]
10:30:41.100 [HTTP2PROXY] Pipeline config: 
ChannelPipeline[ObjectIdentifier(0x00000002804d71b0)]:
                                 [I] ↓↑ [O]
 HTTP2FramePayloadToHTTP1ServerCodec ↓↑ HTTP2FramePayloadToHTTP1ServerCodec [handler0]
              HTTPResponseCompressor ↓↑ HTTPResponseCompressor              [ResponseCompressor]
                  CustomHttp1Handler ↓↑                                     [Custom_Http1]
                        ErrorHandler ↓↑                                     [handler1]

So the objectIdentifier of the pipeline is different every time...


Solution

  • This question was handled on the swift Forums https://forums.swift.org/t/swift-nio-based-proxy-reconfigures-pipeline-on-every-incoming-request/52043

    Answer by Lukasa:

    This is expected behaviour. HTTP/2 is multiplexed: this means you can run multiple request/response sequences over the same TCP connection. This manifests in SwiftNIO HTTP/2 in the form of the "stream channel initializer": this is called once per stream creation. The stream channel initializer here is the trailing closure being passed to configureCommonHTTPServerPipeline.

    If you would like to create the handlers only once, you can do that. But now your handlers need to support being involved in multiple concurrent requests and responses.