Options to send CONNECT request to remote proxy server

Hello!
I have used Connect Proxy with iOS's NEPacketTunnelProvider to set up a local proxy server and it works great!

However, I need some help to achieve my specific requirements. I have a remote proxy server running (e.g., 1.2.3.4:443), and my remote proxy server expects CONNECT requests along with additional headers, such as an auth-token. Upon receiving the CONNECT request with these headers, the remote proxy server responds with 200 OK, and the subsequent flow should continue.

In the current example provided by the plugin, I noticed the glue function is responsible for responding with 200 OK to the CONNECT request. I’d like to override this behavior so that the CONNECT request is sent to my remote proxy server along with the required headers.

Current Implementation

private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) {
        let headers = HTTPHeaders([("Content-Length", "0")])
        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)

        self.removeEncoder(context: context)
        let (localGlue, peerGlue) = GlueHandler.matchedPair()
       // rest of glue functionality
}

Expcted Implementation

private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) {

//update the connect request received in local server with additonal headers 
//send it to remote proxy server
//write the response from remote server to browser 
//receive the client hello from local server and send it to remote proxy server
//receive the server hello from remote server and write it to browser

 // glue local channel and the peer channel together.
// rest of glue functionality
}

I’d appreciate any guidance or pointers on how to accomplish this. Thank you in advance for your support!

This should be very do-able. Once the glue finishes, instead of sending the 200, send the request. You'll need to install a HTTPRequestEncoder, but once you've issued the write-and-flush you can immediately remove it.

Thank you for your response and guidance! @lukasa

I understand your suggestion to issue the CONNECT request instead of sending the 200 response after the glue finishes.

I’ve tried two different approaches to achieve this and am sharing them below to help identify where I might be going wrong:

Approach 1:

In the connectTo method, I created a TLS configuration (my remote proxy server expects TLS—this is based on a working PoC using NWListener, which I’ll include below). Using the TLS configuration, I established a channelFuture. Upon success, I wrote the data to the peer channel and called the glue method once writeAndFlush was completed. Afterward, I cleared the encoder and added a handler.

Here’s the sample code for this approach:

import NIOCore
import NIOPosix
import NIOHTTP1
import Foundation
import NIOSSL


let connectToProxy = "connect-to-Proxy"
let remoteServerIp = "13.212.50.95"

final class ConnectHandler {
    private var upgradeState: State

    private var authorizationToken: String?
    private let repository: IRepository
    private let userSettings: IUserSettings

    init(authorizationToken: String?,repository: IRepository,userSettings: IUserSettings) {
        self.upgradeState = .idle
        self.authorizationToken = authorizationToken
        self.repository = repository
        self.userSettings = userSettings
    }
}


extension ConnectHandler {
    fileprivate enum State {
        case idle
        case beganConnecting
        case awaitingEnd(connectResult: Channel)
        case awaitingConnection(pendingBytes: [NIOAny])
        case upgradeComplete(pendingBytes: [NIOAny])
        case upgradeFailed
    }
}


extension ConnectHandler: ChannelInboundHandler {
    typealias InboundIn = HTTPServerRequestPart
    typealias OutboundOut = HTTPServerResponsePart

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.upgradeState {
        case .idle:
            // Check if the data is a `.head` part of the request
            guard case .head(let originalHead) = self.unwrapInboundIn(data) else {
                // Pass other parts (like body or end) without modification
                context.fireChannelRead(data)
                return
            }
            
           // Modify headers or add custom headers
            var newHeaders = originalHead.headers
            //assuming these domains needs to be sent to remote proxy - customisation needs to be done after POC
            if(originalHead.uri.contains("rozetka.com.ua") || originalHead.uri.contains("pcmag.com")){
                newHeaders.add(name: "Proxy-Authorization", value: "Bearer \(authorizationToken ?? "--default--")")
            }
            // Create a new HTTP request head with the modified headers
            let updatedHead = HTTPRequestHead(
                version: originalHead.version,
                method: originalHead.method,
                uri: originalHead.uri,
                headers: newHeaders
            )
            
            // Pass the modified request head directly to handleInitialMessage
            let updatedRequestPart = HTTPServerRequestPart.head(updatedHead)
            
            self.handleInitialMessage(context: context, data: self.unwrapInboundIn(data),updatedHead: updatedHead)

        case .beganConnecting:
            // We got .end, we're still waiting on the connection
            if case .end = self.unwrapInboundIn(data) {
                self.upgradeState = .awaitingConnection(pendingBytes: [])
                self.removeDecoder(context: context)
            }

        case .awaitingEnd(let peerChannel):
            if case .end = self.unwrapInboundIn(data) {
                // Upgrade has completed!
                self.upgradeState = .upgradeComplete(pendingBytes: [])
                self.removeDecoder(context: context)
                self.glue(peerChannel, context: context)
            }

        case .awaitingConnection(var pendingBytes):
            // We've seen end, this must not be HTTP anymore. Danger, Will Robinson! Do not unwrap.
            self.upgradeState = .awaitingConnection(pendingBytes: [])
            pendingBytes.append(data)
            self.upgradeState = .awaitingConnection(pendingBytes: pendingBytes)

        case .upgradeComplete(pendingBytes: var pendingBytes):
            // We're currently delivering data, keep doing so.
            self.upgradeState = .upgradeComplete(pendingBytes: [])
            pendingBytes.append(data)
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)

        case .upgradeFailed:
            break
        }
    }

    func handlerAdded(context: ChannelHandlerContext) {
    }
}


extension ConnectHandler: RemovableChannelHandler {
    func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
        var didRead = false

        // We are being removed, and need to deliver any pending bytes we may have if we're upgrading.
        while case .upgradeComplete(var pendingBytes) = self.upgradeState, pendingBytes.count > 0 {
            // Avoid a CoW while we pull some data out.
            self.upgradeState = .upgradeComplete(pendingBytes: [])
            let nextRead = pendingBytes.removeFirst()
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)

            context.fireChannelRead(nextRead)
            didRead = true
        }

        if didRead {
            context.fireChannelReadComplete()
        }

//        self.logger.debug("Removing \(self) from pipeline")
        context.leavePipeline(removalToken: removalToken)
    }
}

extension ConnectHandler {
    private func handleInitialMessage(context: ChannelHandlerContext, data: InboundIn,updatedHead:HTTPRequestHead?) {
            guard case .head(let head) = data else {
                self.httpErrorAndClose(context: context)
                return
            }

            print("\(head.method) \(head.uri) \(head.version)")

            guard head.method == .CONNECT else {
                self.httpErrorAndClose(context: context)
                return
            }

            let components = head.uri.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
//            let host = components.first!  // There will always be a first.
//            let port = components.last.flatMap { Int($0, radix: 10) } ?? 80  // Port 80 if not
        //update host to remote Proxy serverIp
            let host = remoteServerIp
            let port = 443

            self.upgradeState = .beganConnecting
            self.connectTo(host: String(host), port: port, context: context,updatedHead: updatedHead)
        }
    
   
    private func connectTo(host: String, port: Int, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        // Create TLS configuration
        let tlsConfiguration = TLSConfiguration.makeClientConfiguration()
        var customTLSConfig = tlsConfiguration
        customTLSConfig.certificateVerification = .none  // Equivalent to allowInsecure: true

        // Create SSL Context
        let sslContext = try! NIOSSLContext(configuration: customTLSConfig)

        
        let channelFuture = ClientBootstrap(group: context.eventLoop)
            .channelInitializer { channel in
                        // Add HTTP encoder and decoder to the channel pipeline
//                       return channel.pipeline.addHTTPClientHandlers()
                do {
                    let sslHandler = try NIOSSLClientHandler(
                                context: sslContext,
                                serverHostname: "*.proxy.mydomain.net"  // SNI is set here instead
                            )
                            return channel.pipeline.addHandler(sslHandler).flatMap {
                                channel.pipeline.addHTTPClientHandlers()
                            }
                } catch {
                    return channel.eventLoop.makeFailedFuture(error)
                }
                    }
            .connectTimeout(.seconds(120))
            .connect(host: String(host), port: port)

        channelFuture.whenSuccess { peerChannel in
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Connected to remote server: \(host):\(port)", saveLog: true,tag: .swg)
            self.connectSucceeded(peerChannel: peerChannel, context: context,updatedHead: updatedHead)
        }
        channelFuture.whenFailure { error in
            self.connectFailed(error: error, context: context)
        }
    }

    private func connectSucceeded(peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        switch self.upgradeState {
        case .beganConnecting:
            // Ok, we have a channel, let's wait for end.
            self.upgradeState = .awaitingEnd(connectResult: peerChannel)

        case .awaitingConnection(pendingBytes: let pendingBytes):
            // Upgrade complete! Begin gluing the connection together.
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
            
                // Write the CONNECT request to the remote Proxy server
            if let updatedHead = updatedHead{
                peerChannel.write(NIOAny(HTTPClientRequestPart.head(updatedHead))).whenComplete { result in
                    switch result {
                    case .success:
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "CONNECT request sent successfully to \(updatedHead.uri) with data \(updatedHead.headers.description)", saveLog: true,tag: .swg)
                    case .failure(let error):
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to send CONNECT request: \(error)", saveLog: true,tag: .swg)
                    }
                }
                peerChannel.writeAndFlush(NIOAny(HTTPClientRequestPart.end(nil))).whenComplete { result in
                    switch result {
                    case .success:
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "CONNECT request completed successfully.", saveLog: true,tag: .swg)
                        //glue the part
                        self.glue(peerChannel, context: context)
                    case .failure(let error):
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to complete CONNECT request: \(error)", saveLog: true,tag: .swg)
                    }
                }
            }
//            self.glue(peerChannel, context: context)

        case .awaitingEnd(let peerChannel):
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)

        case .idle, .upgradeFailed, .upgradeComplete:
            // These cases are logic errors, but let's be careful and just shut the connection.
            context.close(promise: nil)
        }
    }

    private func connectFailed(error: Error, context: ChannelHandlerContext) {

        switch self.upgradeState {
        case .beganConnecting, .awaitingConnection:
            // We still have a somewhat active connection here in HTTP mode, and can report failure.
            self.httpErrorAndClose(context: context)

        case .awaitingEnd(let peerChannel):
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)

        case .idle, .upgradeFailed, .upgradeComplete:
            // Most of these cases are logic errors, but let's be careful and just shut the connection.
            context.close(promise: nil)
        }

        context.fireErrorCaught(error)
    }

    private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) {
        print("Gluing together \(ObjectIdentifier(context.channel)) and \(ObjectIdentifier(peerChannel))")

        // Ok, upgrade has completed! We now need to begin the upgrade process.
        // First, send the 200 message.
        // This content-length header is MUST NOT, but we need to workaround NIO's insistence that we set one.
//        let headers = HTTPHeaders([("Content-Length", "0")])
//        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
//        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
//        context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
        
        // Now remove the HTTP encoder.
        self.removeEncoder(peerChannel: peerChannel,context: context)

        
        // Now we need to glue our channel and the peer channel together.
        let (localGlue, peerGlue) = GlueHandler.matchedPair()
        context.channel.pipeline.addHandler(localGlue).and(peerChannel.pipeline.addHandler(peerGlue)).whenComplete { result in
            switch result {
            case .success(_):
                context.pipeline.removeHandler(self, promise: nil)
            case .failure(_):
                // Close connected peer channel before closing our channel.
                peerChannel.close(mode: .all, promise: nil)
                context.close(promise: nil)
            }
        }
    }
    
    private func httpErrorAndClose(context: ChannelHandlerContext) {
        self.upgradeState = .upgradeFailed

        let headers = HTTPHeaders([("Content-Length", "0"), ("Connection", "close")])
        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .badRequest, headers: headers)
        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil))).whenComplete { (_: Result<Void, Error>) in
            context.close(mode: .output, promise: nil)
        }
    }

    private func removeDecoder(context: ChannelHandlerContext) {
        // We drop the future on the floor here as these handlers must all be in our own pipeline, and this should
        // therefore succeed fast.
        context.pipeline.context(handlerType: ByteToMessageHandler<HTTPRequestDecoder>.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
    }

    private func removeEncoder(peerChannel: Channel,context: ChannelHandlerContext) {
        context.pipeline.context(handlerType: HTTPRequestEncoder.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
        
        // Remove HTTP handlers since we'll be dealing with raw bytes after CONNECT
        peerChannel.pipeline.removeHandler(name: "HTTPRequestEncoder",promise: nil)
        peerChannel.pipeline.removeHandler(name: "HTTPResponseDecoder", promise: nil)
        
        context.pipeline.context(handlerType: HTTPResponseEncoder.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
    }
}



Approach 2:

I added the HTTPRequestEncoder to the pipeline, sent the request using writeAndFlush, and immediately removed the encoder. However, this approach fails and enters the error block of peerChannel.pipeline.addHandler.

Here’s the sample code for this approach:

private func connectSucceeded(peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        switch self.upgradeState {
        case .beganConnecting:
            // Ok, we have a channel, let's wait for end.
            self.upgradeState = .awaitingEnd(connectResult: peerChannel)

        case .awaitingConnection(pendingBytes: let pendingBytes):
            // Upgrade complete! Begin gluing the connection together.
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
             self.glue(peerChannel, context: context,updatedHead: updatedHead)

        case .awaitingEnd(let peerChannel):
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)

        case .idle, .upgradeFailed, .upgradeComplete:
            // These cases are logic errors, but let's be careful and just shut the connection.
            context.close(promise: nil)
        }
    }

private func glue(_ peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        print("Gluing together \(ObjectIdentifier(context.channel)) and \(ObjectIdentifier(peerChannel))")
        if let updatedHead = updatedHead{
        var encoder: HTTPRequestEncoder? = nil
        
        // First, add the HTTP request encoder temporarily
        peerChannel.pipeline.addHandler(HTTPRequestEncoder()).flatMap { handler -> EventLoopFuture<Void> in
            encoder = handler as? HTTPRequestEncoder  // Store reference to the encoder
            
            // Write the request
            return peerChannel.write(NIOAny(HTTPClientRequestPart.head(updatedHead)))
                .flatMap {
                    peerChannel.writeAndFlush(HTTPClientRequestPart.end(nil))
                }.flatMap {
                    // Remove the HTTP encoder after sending the request
                    if let encoder = encoder {
                        return peerChannel.pipeline.removeHandler(encoder)
                    }
                    return peerChannel.eventLoop.makeSucceededFuture(())
                }
        }.flatMap { _ in
            // Now glue the channels together
            let (localGlue, peerGlue) = GlueHandler.matchedPair()
            return context.channel.pipeline.addHandler(localGlue)
                .and(peerChannel.pipeline.addHandler(peerGlue))
        }.whenComplete { result in
            switch result {
            case .success(_):
                context.pipeline.removeHandler(self, promise: nil)
            case .failure(let error):
                // Close connected peer channel before closing our channel
                peerChannel.close(mode: .all, promise: nil)
                context.close(promise: nil)
            }
        }
        }else{
            print("no head")
        }
    }

Unfortunately, neither approach results in the webpage loading successfully.

For reference, here’s the working implementation I have using NWListener:

func startLocalServer() {
    let parameters = NWParameters.tcp
    let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(integerLiteral: port))
    listener?.stateUpdateHandler = { state in
            self.customLogging(location: "LocalServer", info: "running on port \(self.port) \(state)", saveLog: true,tag: .eaa)
        }
        listener?.newConnectionHandler = { connection in
            self.handleNewConnection(connection)
            print("New connection: \(connection)")
        }
        listener?.start(queue: .main)
    }

private func handleNewConnection(_ connection: NWConnection) {
        let remoteConnection = NWConnection(to: .url(url), using: createSelfSignedTLSParameters(allowInsecure: true, queue: connectionQueue))
        connection.start(queue: .main)
        remoteConnection.start(queue: connectionQueue)
}

func createSelfSignedTLSParameters(allowInsecure: Bool, queue: DispatchQueue) -> NWParameters {
        print("***** inside sec_protocol_options_set_verify_block *****")
      
        
        let options = NWProtocolTLS.Options()
        let securityProtocol = options.securityProtocolOptions

        sec_protocol_options_set_tls_server_name(securityProtocol, "*.proxy.mydomain.net")
        sec_protocol_options_set_verify_block(securityProtocol, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
            let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()

            // Print detailed certificate information for debugging
            let certificateCount = SecTrustGetCertificateCount(trust)
            for index in 0..<certificateCount {
                if let certificate = SecTrustGetCertificateAtIndex(trust, index){
                    let summary = SecCertificateCopySubjectSummary(certificate)
                    print("Certificate \(index + 1): \(summary.debugDescription)")
                }else{
                    print("No Certificate")
                }
            }
            
            var error: CFError?
            if SecTrustEvaluateWithError(trust, &error) {
                print("***** server trusted ***** with error = \(error.debugDescription)")
                sec_protocol_verify_complete(true)
            } else {
                print("***** server Not trusted ***** with error \(error.debugDescription)")
                if allowInsecure == true {
                    print("***** server Not trusted => allowInsecure *****")
    //                    sec_protocol_verify_complete(true)
                    sec_protocol_verify_complete(false)
                } else {
                    print("***** server NOT trusted  *****")
                    sec_protocol_verify_complete(false)
                }
            }
        }, queue)

        return NWParameters(tls: options)
    }

I’m aiming to replicate this behavior using Swift NIO. Any insights or corrections would be greatly appreciated!

I think there are some bugs in approach 1. However, can you clarify what error you saw in approach 2?

Thank you for your response and suggestions!

I’m encountering a typecast crash during execution for approach-2. Here’s the specific error:

NIO-ELT-1-#1 (8): Fatal error: tried to decode as type HTTPPart<HTTPRequestHead, IOData> but found IOData with contents ioData(IOData { [434f4e4e4543542070636d61672e636f6d3a34343320485454502f312e310d0a...4554502d4f726967696e2d49703a203139342e36302e36392e3131320d0a0d0a](688 bytes) })

Implementation Details

This is how I update the headers for the received CONNECT request:


// Check if the data is a `.head` part of the request
            guard case .head(let originalHead) = self.unwrapInboundIn(data) else {
                // Pass other parts (like body or end) without modification
                context.fireChannelRead(data)
                return
            }
            
           // Modify headers or add custom headers
            var newHeaders = originalHead.headers
// Create a new HTTP request head with the modified headers
            let updatedHead = HTTPRequestHead(
                version: originalHead.version,
                method: originalHead.method,
                uri: originalHead.uri,
                headers: newHeaders
            )
            
            // Pass the modified request head directly to handleInitialMessage
            let updatedRequestPart = HTTPServerRequestPart.head(updatedHead)
            self.handleInitialMessage(context: context, data: updatedRequestPart,updatedHead: updatedHead)

Even after updating the headers and forwarding the request to handleInitialMessage and to the glue method, I still run into this typecast issue.

Please let me know if more details or sample code snippets are required from my side to troubleshoot this issue further.

Are you removing the HTTP handlers on the listening side? Your code shows you removing the HTTPRequestEncoder, but have you removed the HTTPRequestDecoder and HTTPResponseEncoder from the other channel?

I did tried removing the encoder and decoder as used in connect-proxy example
but even then I'm facing the typecast issue.

peerChannel.pipeline.addHandler( HTTPRequestEncoder()).flatMap { handler -> EventLoopFuture<Void> in
                encoder = handler as? HTTPRequestEncoder  // Store reference to the encoder
                
                // Write the request
                return peerChannel.write(NIOAny(HTTPClientRequestPart.head(updatedHead)))
                    .flatMap {
                        peerChannel.writeAndFlush(HTTPClientRequestPart.end(nil))
                    }.flatMap {
                        self.removeEncoder(context: context)
                        self.removeDecoder(context: context)
                        
                        return peerChannel.eventLoop.makeSucceededFuture(())
                    }
            }.flatMap { _ in
                // Now glue the channels together
                let (localGlue, peerGlue) = GlueHandler.matchedPair()
                return context.channel.pipeline.addHandler(localGlue)
                    .and(peerChannel.pipeline.addHandler(peerGlue))
            }.whenComplete { result in
                switch result {
                case .success(_):
                    context.pipeline.removeHandler(self, promise: nil)
                case .failure(let error):
                    // Close connected peer channel before closing our channel
                    peerChannel.close(mode: .all, promise: nil)
                    context.close(promise: nil)
                }
            }

private func removeDecoder(context: ChannelHandlerContext) {
        // We drop the future on the floor here as these handlers must all be in our own pipeline, and this should
        // therefore succeed fast.
        context.pipeline.context(handlerType: ByteToMessageHandler<HTTPRequestDecoder>.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
    }

    private func removeEncoder(context: ChannelHandlerContext) {
        context.pipeline.context(handlerType: HTTPResponseEncoder.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
    }

It would be helpful if you can help with pointing out whats issue with the code or please provide with relevant code snippet to address this?

So you need to remove all three handlers. The code above has removed the removal of the HTTPRequestEncoder (called encoder). All three have to be removed for correct function.

Relatedly, you want any other HTTP-related handlers gone. What's your ServerBootstrap code look like?

A useful thing to do, if you don't want to go around in circles, would be to post the entire sample here so I can check it myself.

Thank you for your guidance!
please find the code for reference.

Server-start Code

import Foundation
import NIOHTTP1
import NIO
import os.log

final class Server {
    // MARK: - Private properties
    let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
    private var host: String
    private var port: Int
    private let proxyToken: String?
    private let repository: IRepository
    private let userSettings: IUserSettings

    // MARK: - Initializers
    init(host: String, port: Int, proxyToken: String?,repository: IRepository,userSettings: IUserSettings) {
        self.host = host
        self.port = port
        self.proxyToken = proxyToken
        self.repository = repository
        self.userSettings = userSettings
    }
    
    // MARK: - Public functions
    func start() {
        
        defer {
            try! group.syncShutdownGracefully()
        }
        
        do {
            let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
            
            let bootstrap = ServerBootstrap(group: group)
                .childChannelInitializer { channel in
                    
                    channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))).flatMap {
                        
                        return channel.pipeline.addHandler(HTTPResponseEncoder()).flatMap {
                            channel.pipeline.addHandler(ConnectHandler(authorizationToken: self.proxyToken,repository: self.repository,userSettings: self.userSettings))
                        }
                    }
                }
            
            let channel = try! bootstrap.bind(host: host, port: port).whenComplete { result in
                // Need to create this here for thread-safety purposes
                switch result {
                case .success(let channel):
                    self.customLogging(location: "NioLocalServer", info: "Listening on \(channel.localAddress.debugDescription)", saveLog: true)
                case .failure(let error):
                    self.customLogging(location: "NioLocalServer", info: "Failed to bind \(self.host) \(self.port) with error \(error.localizedDescription)", saveLog: true)
                    self.stop()
                }
            }
            
//            try! channel.closeFuture.wait() // wait forever as we never close the Channel
            
        } catch {
            print("An error happed \(error.localizedDescription)")
            exit(0)
        }
    }
    
    func stop() {
        do {
            try group.syncShutdownGracefully()
        } catch {
            print("An error happed \(error.localizedDescription)")
            exit(0)
        }
    }
    
    func customLogging(location:String,info:String,saveLog:Bool){
        if UserDefaults.shared.debugLoggingEnabled() { // If the user turns off toggle Debug logging, Diagnostic Data is not recorded.
            Logger.tunnel.info("********** \(location,privacy: .public) => \(info,privacy: .public)")
        }
    }
}

ConnectHandler Code


import NIOCore
import NIOPosix
import NIOHTTP1
import Foundation
import NIOSSL


let remoteServerIp = "13.22.50.195"

final class ConnectHandler {
    private var upgradeState: State

    private var authorizationToken: String?
    private let repository: IRepository
    private let userSettings: IUserSettings

    init(authorizationToken: String?,repository: IRepository,userSettings: IUserSettings) {
        self.upgradeState = .idle
        self.authorizationToken = authorizationToken
        self.repository = repository
        self.userSettings = userSettings
    }
}


extension ConnectHandler {
    fileprivate enum State {
        case idle
        case beganConnecting
        case awaitingEnd(connectResult: Channel)
        case awaitingConnection(pendingBytes: [NIOAny])
        case upgradeComplete(pendingBytes: [NIOAny])
        case upgradeFailed
    }
}


extension ConnectHandler: ChannelInboundHandler {
    typealias InboundIn = HTTPServerRequestPart
    typealias OutboundOut = HTTPServerResponsePart

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.upgradeState {
        case .idle:
            // Check if the data is a `.head` part of the request
            guard case .head(let originalHead) = self.unwrapInboundIn(data) else {
                // Pass other parts (like body or end) without modification
                context.fireChannelRead(data)
                return
            }
            
           // Modify headers or add custom headers
            var newHeaders = originalHead.headers
            if(originalHead.uri.contains("rozetka.com.ua") || originalHead.uri.contains("pcmag.com")){
                newHeaders.add(name: "Proxy-Authorization", value: "Bearer \(authorizationToken ?? "--default--")")
                let updatedHead = HTTPRequestHead(
                    version: originalHead.version,
                    method: originalHead.method,
                    uri: originalHead.uri,
                    headers: newHeaders
                )
                
                // Pass the modified request head directly to handleInitialMessage
                let updatedRequestPart = HTTPServerRequestPart.head(updatedHead)
                self.handleInitialMessage(context: context, data: updatedRequestPart,updatedHead: updatedHead)
            }else{
                self.handleInitialMessage(context: context, data: self.unwrapInboundIn(data),updatedHead:nil)
            }

        case .beganConnecting:
            // We got .end, we're still waiting on the connection
            if case .end = self.unwrapInboundIn(data) {
                self.upgradeState = .awaitingConnection(pendingBytes: [])
                self.removeDecoder(context: context)
            }

        case .awaitingEnd(let peerChannel):
            if case .end = self.unwrapInboundIn(data) {
                // Upgrade has completed!
                self.upgradeState = .upgradeComplete(pendingBytes: [])
                self.removeDecoder(context: context)
                self.glue(peerChannel, context: context,updatedHead: nil)
            }

        case .awaitingConnection(var pendingBytes):
            // We've seen end, this must not be HTTP anymore. Danger, Will Robinson! Do not unwrap.
            self.upgradeState = .awaitingConnection(pendingBytes: [])
            pendingBytes.append(data)
            self.upgradeState = .awaitingConnection(pendingBytes: pendingBytes)

        case .upgradeComplete(pendingBytes: var pendingBytes):
            // We're currently delivering data, keep doing so.
            self.upgradeState = .upgradeComplete(pendingBytes: [])
            pendingBytes.append(data)
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)

        case .upgradeFailed:
            break
        }
    }
}


extension ConnectHandler: RemovableChannelHandler {
    func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
        var didRead = false

        // We are being removed, and need to deliver any pending bytes we may have if we're upgrading.
        while case .upgradeComplete(var pendingBytes) = self.upgradeState, pendingBytes.count > 0 {
            // Avoid a CoW while we pull some data out.
            self.upgradeState = .upgradeComplete(pendingBytes: [])
            let nextRead = pendingBytes.removeFirst()
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)

            context.fireChannelRead(nextRead)
            didRead = true
        }

        if didRead {
            context.fireChannelReadComplete()
        }

//        self.logger.debug("Removing \(self) from pipeline")
        context.leavePipeline(removalToken: removalToken)
    }
}

extension ConnectHandler {
    private func handleInitialMessage(context: ChannelHandlerContext, data: InboundIn,updatedHead:HTTPRequestHead?) {
            guard case .head(let head) = data else {
                self.httpErrorAndClose(context: context)
                return
            }

            print("\(head.method) \(head.uri) \(head.version)")

            guard head.method == .CONNECT else {
                self.httpErrorAndClose(context: context)
                return
            }

            let components = head.uri.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
            let hostName = components.first!  // There will always be a first.
//            let port = components.last.flatMap { Int($0, radix: 10) } ?? 80  // Port 80 if not
        let host = remoteServerIp  // There will always be a first.
            let port = 443

            self.upgradeState = .beganConnecting
        self.connectTo(hostName:String(hostName),host: String(host), port: port, context: context,updatedHead: updatedHead)
        }
    
    private func connectTo(hostName:String,host: String, port: Int, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
//         Create TLS configuration
        let tlsConfiguration = TLSConfiguration.makeClientConfiguration()
        var customTLSConfig = tlsConfiguration
        customTLSConfig.certificateVerification = .none  // Equivalent to allowInsecure: true

        // Create SSL Context
        let sslContext = try! NIOSSLContext(configuration: customTLSConfig)

        
        let channelFuture = ClientBootstrap(group: context.eventLoop)
            .channelInitializer { channel in
                        // Add HTTP encoder and decoder to the channel pipeline
//                        channel.pipeline.addHTTPClientHandlers()
                do {
                    // Create SSL handler with error handling
                    let sslHandler = try NIOSSLClientHandler(
                        context: sslContext,
//                        serverHostname: hostName
                        serverHostname: "*.proxy.domain.net"
                    )
                    return channel.pipeline.addHandler(sslHandler).flatMap {
                        channel.pipeline.addHTTPClientHandlers()
                    }
                } catch {
                    return channel.eventLoop.makeFailedFuture(error)
                }
                    }
            .connectTimeout(.seconds(120))
            .connect(host: String(host), port: port)

        channelFuture.whenSuccess { peerChannel in
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Connected to remote server: \(host):\(port)", saveLog: true,tag: .swg)

            // Add response handler to capture server responses
            peerChannel.pipeline.addHandler(HTTPResponseHandler(repository: self.repository,peerChannel: peerChannel)).whenComplete { result in
                   switch result {
                   case .success:
                       CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "HTTPResponseHandler added successfully.", saveLog: true,tag: .swg)
                   case .failure(let error):
                       CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to add HTTPResponseHandler: \(error)", saveLog: true,tag: .swg)
                   }
               }
            self.connectSucceeded(peerChannel: peerChannel, context: context,updatedHead: updatedHead)
        }
        channelFuture.whenFailure { error in
            self.connectFailed(error: error, context: context)
        }
    }

    private func connectSucceeded(peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        switch self.upgradeState {
        case .beganConnecting:
            // Ok, we have a channel, let's wait for end.
            self.upgradeState = .awaitingEnd(connectResult: peerChannel)

        case .awaitingConnection(pendingBytes: let pendingBytes):
            // Upgrade complete! Begin gluing the connection together.
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
            self.glue(peerChannel, context: context,updatedHead: updatedHead)

        case .awaitingEnd(let peerChannel):
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)

        case .idle, .upgradeFailed, .upgradeComplete:
            // These cases are logic errors, but let's be careful and just shut the connection.
            context.close(promise: nil)
        }
    }
    
    private func glue(_ peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
            print("Gluing together \(ObjectIdentifier(context.channel)) and \(ObjectIdentifier(peerChannel))")
            if let updatedHead = updatedHead{
//            var encoder: HTTPRequestEncoder? = nil
                let encoder = HTTPRequestEncoder()
                let resEncoder = HTTPResponseEncoder()

            
            // First, add the HTTP request encoder temporarily
            peerChannel.pipeline.addHandler(encoder).flatMap { handler -> EventLoopFuture<Void> in
//                encoder = handler as? HTTPRequestEncoder  // Store reference to the encoder
                
                // Write the request
                return peerChannel.write(NIOAny(HTTPClientRequestPart.head(updatedHead)))
                    .flatMap {
                        peerChannel.writeAndFlush(HTTPClientRequestPart.end(nil))
                    }.flatMap {
                        peerChannel.pipeline.removeHandler(encoder)
                        peerChannel.pipeline.removeHandler(resEncoder)
                        self.removeEncoder(context: context)
                        self.removeDecoder(context: context)
                        
//                        if let encoder = encoder {
//                            return peerChannel.pipeline.removeHandler(encoder)
//                        }
                        return peerChannel.eventLoop.makeSucceededFuture(())
                    }
            }.flatMap { _ in
                // Now glue the channels together
                let (localGlue, peerGlue) = GlueHandler.matchedPair()
                return context.channel.pipeline.addHandler(localGlue)
                    .and(peerChannel.pipeline.addHandler(peerGlue))
            }.whenComplete { result in
                switch result {
                case .success(_):
                    context.pipeline.removeHandler(self, promise: nil)
                case .failure(let error):
                    // Close connected peer channel before closing our channel
                    peerChannel.close(mode: .all, promise: nil)
                    context.close(promise: nil)
                }
            }
            }else{
                print("no head")
            }
        }
    
    private func write200Response(context: ChannelHandlerContext) {
        let headers = HTTPHeaders([("Content-Length", "0")])
        let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers)
        context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
    }
    
    private func connectFailed(error: Error, context: ChannelHandlerContext) {

        switch self.upgradeState {
        case .beganConnecting, .awaitingConnection:
            // We still have a somewhat active connection here in HTTP mode, and can report failure.
            self.httpErrorAndClose(context: context)

        case .awaitingEnd(let peerChannel):
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)

        case .idle, .upgradeFailed, .upgradeComplete:
            // Most of these cases are logic errors, but let's be careful and just shut the connection.
            context.close(promise: nil)
        }

        context.fireErrorCaught(error)
    }
    
    private func httpErrorAndClose(context: ChannelHandlerContext) {
        self.upgradeState = .upgradeFailed

        let headers = HTTPHeaders([("Content-Length", "0"), ("Connection", "close")])
        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .badRequest, headers: headers)
        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil))).whenComplete { (_: Result<Void, Error>) in
            context.close(mode: .output, promise: nil)
        }
    }

    private func removeDecoder(context: ChannelHandlerContext) {
        // We drop the future on the floor here as these handlers must all be in our own pipeline, and this should
        // therefore succeed fast.
        context.pipeline.context(handlerType: ByteToMessageHandler<HTTPRequestDecoder>.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
        }
    }

    private func removeEncoder(context: ChannelHandlerContext) {
            context.pipeline.context(handlerType: HTTPResponseEncoder.self).whenSuccess {
                context.pipeline.removeHandler(context: $0, promise: nil)
            }
        }
}



final class HTTPResponseHandler: ChannelInboundHandler, RemovableChannelHandler {
    func removeHandler(context: NIOCore.ChannelHandlerContext, removalToken: NIOCore.ChannelHandlerContext.RemovalToken) {
        
    }
    
    typealias InboundIn = HTTPClientResponsePart
    private let repository: IRepository
    private let peerChannel: Channel

    init(repository: IRepository,peerChannel: Channel) {
        self.repository = repository
        self.peerChannel = peerChannel
    }
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let responsePart = self.unwrapInboundIn(data)

        switch responsePart {
        case .head(let responseHead):
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response head: \(responseHead.status)", saveLog: true,tag: .swg)
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Headers: \(responseHead.headers)", saveLog: true,tag: .swg)
        case .body(let byteBuffer):
            if let responseBody = byteBuffer.getString(at: 0, length: byteBuffer.readableBytes) {
                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response body: \(responseBody)", saveLog: true,tag: .swg)
            }
        case .end:
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response end.", saveLog: true,tag: .swg)
            // Optionally close the channel or proceed with further logic.
            context.close(promise: nil)
        }
    }
    
    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("Error in HTTP response handling: \(error)")
        context.close(promise: nil)
    }
}


Let me know if anything needed from my side.

Thanks for your response! @lukasa

We have managed to fix the type cast crash, and we are now receiving the Client Hello message on the local server after manually writing a 200 OK response. We also glue the local and peer connections after the remote proxy server responds with 200 OK.

However, even after successfully gluing the connection, the website does not load and stays blank.

Observations:

  • Our remote proxy server expects a TLS connection, so we have configured TLSConfiguration accordingly.
  • If we set:
customTLSConfig.certificateVerification = .none  

:white_check_mark: The peer connection becomes active, and we are able to write data, but the website remains blank.

  • If we set:
customTLSConfig.certificateVerification = .fullVerification  

:x: The peer connection does not become active, and we get an I/O error when writing data.

Questions:

  • What could be causing the blank page issue despite the connection being successfully established?
  • Why does enabling fullVerification result in an I/O error when writing data to the peer connection?
  • Is there something missing in our setup after gluing the connections together?

Would appreciate any insights or debugging tips! Thanks for your help.

Attached my connect handler for your reference and my server start code is posted in the last comment

import NIOCore
import NIOPosix
import NIOHTTP1
import Foundation
import NIOSSL

let connectToProxy = "connect-to-Proxy"
let remoteIp = "213.121.25.24"
final class ConnectHandler {
    private var upgradeState: State
    private var authorizationToken: String?
    private let repository: IRepository
    private let userSettings: IUserSettings
    init(authorizationToken: String?,repository: IRepository,userSettings: IUserSettings) {
        self.upgradeState = .idle
        self.authorizationToken = authorizationToken
        self.repository = repository
        self.userSettings = userSettings
    }
}
extension ConnectHandler {
    fileprivate enum State {
        case idle
        case beganConnecting
        case awaitingEnd(connectResult: Channel)
        case awaitingConnection(pendingBytes: [NIOAny])
        case upgradeComplete(pendingBytes: [NIOAny])
        case upgradeFailed
extension ConnectHandler: ChannelInboundHandler {
    typealias InboundIn = HTTPServerRequestPart
    typealias OutboundOut = HTTPServerResponsePart
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.upgradeState {
        case .idle:
//            self.handleInitialMessage(context: context, data: self.unwrapInboundIn(data))
            // Check if the data is a `.head` part of the request
            guard case .head(let originalHead) = self.unwrapInboundIn(data) else {
                // Pass other parts (like body or end) without modification
                context.fireChannelRead(data)
                return
            }
        
           // Modify headers or add custom headers
            var newHeaders = originalHead.headers
            if(originalHead.uri.contains("rozetka.com.ua") || originalHead.uri.contains("pcmag.com")){
//                newHeaders.replaceOrAdd(name: "Host", value: "pcmag.com:443")
                newHeaders.add(name: "Proxy-Authorization", value: "Bearer \(authorizationToken ?? "--default--")")
                // Create a new HTTP request head with the modified headers
                let updatedHead = HTTPRequestHead(
                    version: originalHead.version,
                    method: originalHead.method,
                    uri: originalHead.uri,
                    headers: newHeaders
                )
                
                // Pass the modified request head directly to handleInitialMessage
                let updatedRequestPart = HTTPServerRequestPart.head(updatedHead)
                self.handleInitialMessage(context: context, data: updatedRequestPart,updatedHead: updatedHead)
            }else{
                self.handleInitialMessage(context: context, data: self.unwrapInboundIn(data),updatedHead:nil)
        case .beganConnecting:
            // We got .end, we're still waiting on the connection
            if case .end = self.unwrapInboundIn(data) {
                self.upgradeState = .awaitingConnection(pendingBytes: [])
                self.removeDecoder(context: context)
        case .awaitingEnd(let peerChannel):
                // Upgrade has completed!
                self.upgradeState = .upgradeComplete(pendingBytes: [])
                self.glue(peerChannel, context: context)
        case .awaitingConnection(var pendingBytes):
            // We've seen end, this must not be HTTP anymore. Danger, Will Robinson! Do not unwrap.
            self.upgradeState = .awaitingConnection(pendingBytes: [])
            pendingBytes.append(data)
            self.upgradeState = .awaitingConnection(pendingBytes: pendingBytes)
        case .upgradeComplete(pendingBytes: var pendingBytes):
            // We're currently delivering data, keep doing so.
            self.upgradeState = .upgradeComplete(pendingBytes: [])
            self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
        case .upgradeFailed:
            break
        }
    func handlerAdded(context: ChannelHandlerContext) {
        // Add logger metadata.
//        self.logger[metadataKey: "localAddress"] = "\(String(describing: context.channel.localAddress))"
//        self.logger[metadataKey: "remoteAddress"] = "\(String(describing: context.channel.remoteAddress))"
//        self.logger[metadataKey: "channel"] = "\(ObjectIdentifier(context.channel))"
extension ConnectHandler: RemovableChannelHandler {
    func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
        var didRead = false
        // We are being removed, and need to deliver any pending bytes we may have if we're upgrading.
        while case .upgradeComplete(var pendingBytes) = self.upgradeState, pendingBytes.count > 0 {
            // Avoid a CoW while we pull some data out.
            let nextRead = pendingBytes.removeFirst()
            context.fireChannelRead(nextRead)
            didRead = true
        if didRead {
            context.fireChannelReadComplete()
//        self.logger.debug("Removing \(self) from pipeline")
        context.leavePipeline(removalToken: removalToken)
    private func handleInitialMessage(context: ChannelHandlerContext, data: InboundIn,updatedHead:HTTPRequestHead?) {
            guard case .head(let head) = data else {
                self.httpErrorAndClose(context: context)
            print("\(head.method) \(head.uri) \(head.version)")
            guard head.method == .CONNECT else {
            let components = head.uri.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
            let hostName = components.first!  // There will always be a first.
//            let port = components.last.flatMap { Int($0, radix: 10) } ?? 80  // Port 80 if not
        let host = remoteIp  // There will always be a first.
            let port = 443
            self.upgradeState = .beganConnecting
        self.connectTo(hostName:String(hostName),host: String(host), port: port, context: context,updatedHead: updatedHead)
    
    private func connectTo(hostName:String,host: String, port: Int, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
        // Create TLS configuration
        let tlsConfiguration = TLSConfiguration.makeClientConfiguration()
        var customTLSConfig = tlsConfiguration
        customTLSConfig.certificateVerification = .fullVerification 
        customTLSConfig.trustRoots = .default
        // Create SSL Context
            let sslContext = try! NIOSSLContext(configuration: customTLSConfig)
            let channelFuture = ClientBootstrap(group: context.eventLoop)
                .channelInitializer { channel in
                            // Add HTTP encoder and decoder to the channel pipeline
    //                       return channel.pipeline.addHTTPClientHandlers()
                    do {
                        let sslHandler = try NIOSSLClientHandler(
                                    context: sslContext,
                                    serverHostname: nil
                                )
                                return channel.pipeline.addHandlers([
                                    sslHandler,
                                    SSLDebugHandler(repository: self.repository)
//                                    ByteToMessageHandler(HTTPRequestDecoder())
                                ]).flatMap {
                                    channel.pipeline.addHTTPClientHandlers()
                                }
                    } catch {
                        return channel.eventLoop.makeFailedFuture(error)
                    }
                        }
                .connectTimeout(.seconds(120))
                .connect(host: String(host), port: port).whenComplete{ result in
                    switch result{
                    case .success(let peerChannel):
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Connected to remote server: \(host):\(port)", saveLog: true,tag: .swg)
                        peerChannel.pipeline.addHandler(PeerChannelStateHandler(repository: self.repository)).whenComplete { result in
                                switch result {
                                case .success:
                                    CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "PeerChannelStateHandler added successfully", saveLog: true, tag: .swg)
                                case .failure(let error):
                                    CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to add PeerChannelStateHandler: \(error)", saveLog: true, tag: .swg)
                            }
                        peerChannel.pipeline.addHandler(HTTPResponseHandler(repository: self.repository, peerChannel: peerChannel)).whenComplete { result in
                            switch result {
                            case .success:
                                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "HTTPResponseHandler added successfully.", saveLog: true,tag: .swg)
                            case .failure(let error):
                                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to add HTTPResponseHandler: \(error)", saveLog: true,tag: .swg)
                        self.connectSucceeded(peerChannel: peerChannel, context: context,updatedHead: updatedHead)
                        
                    case .failure(let error):
                        CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "channelFuture connection Failed: \(error)", saveLog: true,tag: .swg)
                        self.connectFailed(error: error, context: context)
                }
    private func connectSucceeded(peerChannel: Channel, context: ChannelHandlerContext,updatedHead: HTTPRequestHead?) {
            switch self.upgradeState {
            case .beganConnecting:
                // Ok, we have a channel, let's wait for end.
                self.upgradeState = .awaitingEnd(connectResult: peerChannel)
            case .awaitingConnection(pendingBytes: let pendingBytes):
                // Upgrade complete! Begin gluing the connection together.
                self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
                    // Write the CONNECT request to the remote Proxy server
                if let updatedHead = updatedHead{
                    peerChannel.write(NIOAny(HTTPClientRequestPart.head(updatedHead))).whenComplete { result in
                        switch result {
                        case .success:
                            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "CONNECT request sent successfully to \(updatedHead.uri) with data \(updatedHead.headers.description)", saveLog: true,tag: .swg)
                        case .failure(let error):
                            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to send CONNECT request: \(error)", saveLog: true,tag: .swg)
                    peerChannel.writeAndFlush(NIOAny(HTTPClientRequestPart.end(nil))).whenComplete { result in
                            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "CONNECT request completed successfully.", saveLog: true,tag: .swg)
                            //glue the part
//                            self.glue(peerChannel, context: context)
                            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Failed to complete CONNECT request: \(error)", saveLog: true,tag: .swg)
                    
                    // Ok, upgrade has completed! We now need to begin the upgrade process.
                    // First, send the 200 message.
                    // This content-length header is MUST NOT, but we need to workaround NIO's insistence that we set one.
                    let headers = HTTPHeaders([("Content-Length", "0")])
                    let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
                    context.write(self.wrapOutboundOut(.head(head)), promise: nil)
                    context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
//                     Remove HTTP Request Encoder
                    peerChannel.pipeline.context(handlerType: HTTPRequestEncoder.self).flatMap { context in
                        peerChannel.pipeline.removeHandler(context.handler as! RemovableChannelHandler)
//                    // Remove HTTP Response Decoder (Wrapped inside ByteToMessageHandler)
//                    peerChannel.pipeline.context(handlerType: ByteToMessageHandler<HTTPResponseDecoder>.self).flatMap { context in
//                        peerChannel.pipeline.removeHandler(context.handler as! RemovableChannelHandler)
//                    }
                    // Now remove the HTTP encoder.
                    self.removeEncoder(context: context)
//                    self.glue(peerChannel, context: context)
            case .awaitingEnd(let peerChannel):
                // This case is a logic error, close already connected peer channel.
                peerChannel.close(mode: .all, promise: nil)
                context.close(promise: nil)
            case .idle, .upgradeFailed, .upgradeComplete:
                // These cases are logic errors, but let's be careful and just shut the connection.
    private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) {
            print("Gluing together \(ObjectIdentifier(context.channel)) and \(ObjectIdentifier(peerChannel))")
            /*
            // Ok, upgrade has completed! We now need to begin the upgrade process.
            // First, send the 200 message.
            // This content-length header is MUST NOT, but we need to workaround NIO's insistence that we set one.
    //        let headers = HTTPHeaders([("Content-Length", "0")])
    //        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
    //        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
    //        context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
            
            // Now remove the HTTP encoder.
            self.removeEncoder(context: context)
            // Now we need to glue our channel and the peer channel together.
            let (localGlue, peerGlue) = GlueHandler.matchedPair()
            context.channel.pipeline.addHandler(localGlue).and(peerChannel.pipeline.addHandler(peerGlue)).whenComplete { result in
                switch result {
                case .success(_):
                    context.pipeline.removeHandler(self, promise: nil)
                case .failure(_):
                    // Close connected peer channel before closing our channel.
                    peerChannel.close(mode: .all, promise: nil)
                    context.close(promise: nil)
            }*/
    private func write200Response(context: ChannelHandlerContext) {
        let headers = HTTPHeaders([("Content-Length", "0")])
        let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers)
        context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
    private func connectFailed(error: Error, context: ChannelHandlerContext) {
        case .beganConnecting, .awaitingConnection:
            // We still have a somewhat active connection here in HTTP mode, and can report failure.
            self.httpErrorAndClose(context: context)
            // This case is a logic error, close already connected peer channel.
            peerChannel.close(mode: .all, promise: nil)
            context.close(promise: nil)
        case .idle, .upgradeFailed, .upgradeComplete:
            // Most of these cases are logic errors, but let's be careful and just shut the connection.
        context.fireErrorCaught(error)
    private func httpErrorAndClose(context: ChannelHandlerContext) {
        self.upgradeState = .upgradeFailed
        let headers = HTTPHeaders([("Content-Length", "0"), ("Connection", "close")])
        let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .badRequest, headers: headers)
        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.end(nil))).whenComplete { (_: Result<Void, Error>) in
            context.close(mode: .output, promise: nil)
    private func removeDecoder(context: ChannelHandlerContext) {
        // We drop the future on the floor here as these handlers must all be in our own pipeline, and this should
        // therefore succeed fast.
        context.pipeline.context(handlerType: ByteToMessageHandler<HTTPRequestDecoder>.self).whenSuccess {
            context.pipeline.removeHandler(context: $0, promise: nil)
    private func removeEncoder(context: ChannelHandlerContext) {
            context.pipeline.context(handlerType: HTTPResponseEncoder.self).whenSuccess {
                context.pipeline.removeHandler(context: $0, promise: nil)
final class HTTPResponseHandler: ChannelInboundHandler, RemovableChannelHandler {
    func removeHandler(context: NIOCore.ChannelHandlerContext, removalToken: NIOCore.ChannelHandlerContext.RemovalToken) {
    typealias InboundIn = HTTPClientResponsePart
    typealias OutboundOut = HTTPServerResponsePart  // ✅ FIX: Correct outbound type
    private let peerChannel: Channel
    init(repository: IRepository,peerChannel: Channel) {
        self.peerChannel = peerChannel
        let responsePart = self.unwrapInboundIn(data)
        switch responsePart {
        case .head(let responseHead):
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response head: \(responseHead.status)", saveLog: true,tag: .swg)
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Headers: \(responseHead.headers)", saveLog: true,tag: .swg)
            if(responseHead.status == .ok){
//                // Remove HTTP Request Encoder
//                peerChannel.pipeline.context(handlerType: HTTPRequestEncoder.self).flatMap { context in
//                    self.peerChannel.pipeline.removeHandler(context.handler as! RemovableChannelHandler)
//                }
                // Remove HTTP Response Decoder (Wrapped inside ByteToMessageHandler)
                peerChannel.pipeline.context(handlerType: ByteToMessageHandler<HTTPResponseDecoder>.self).flatMap { context in
                    self.peerChannel.pipeline.removeHandler(context.handler as! RemovableChannelHandler)
                self.setupGlue(localChannel: context.channel, peerChannel: self.peerChannel)
//            }
        case .body(let byteBuffer):
            if let responseBody = byteBuffer.getString(at: 0, length: byteBuffer.readableBytes) {
                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response body: \(responseBody)", saveLog: true,tag: .swg)
        case .end:
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "Received response end.", saveLog: true,tag: .swg)
            // Optionally close the channel or proceed with further logic.
    private func setupGlue(localChannel: Channel, peerChannel: Channel) {
        let (localGlue, peerGlue) = GlueHandler.matchedPair()
//        localChannel.pipeline.addHandler(localGlue).and(peerChannel.pipeline.addHandler(peerGlue)).whenComplete { result in
        localChannel.pipeline.addHandlers([LoggingHandler(repository: repository), localGlue]).and(peerChannel.pipeline.addHandler(peerGlue)).whenComplete { result in
            switch result {
            case .success:
                print("Successfully set up channel forwarding.")
            case .failure(let error):
                print("Failed to set up glue: \(error)")
                localChannel.close(promise: nil)
                peerChannel.close(promise: nil)
    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("Error in HTTP response handling: \(error)")
        context.close(promise: nil)
final class LoggingHandler: ChannelDuplexHandler {
    typealias InboundIn = ByteBuffer
    typealias OutboundIn = ByteBuffer
    typealias OutboundOut = ByteBuffer
    init(repository: IRepository) {
    // Log incoming data
        let buffer = unwrapInboundIn(data)
        if let receivedString = buffer.getString(at: 0, length: buffer.readableBytes) {
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "🔹 Received Data: \(receivedString)", saveLog: true,tag: .swg)
        context.fireChannelRead(data) // Forward to next handler
    // Log outgoing data
    func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
        let buffer = unwrapOutboundIn(data)
        if let sentString = buffer.getString(at: 0, length: buffer.readableBytes) {
            CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "🚀 Sent Data: \(sentString)", saveLog: true,tag: .swg)
        context.write(data, promise: promise) // Forward to next handler
class SSLDebugHandler: ChannelInboundHandler {
    typealias InboundIn = Any
        if let sslError = error as? NIOSSLError {
            switch sslError {
            case .handshakeFailed:
                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "🚀 SSL Handshake failed - certificate verification error", saveLog: true,tag: .swg)
            case .noCertificateToValidate:
                print("No certificate to validate")
            default:
                CommonHelper.customLogging(repository: self.repository, location: "NioLocalServer", info: "🚀 SSL error: \(sslError)", saveLog: true,tag: .swg)
    func channelActive(context: ChannelHandlerContext) {
        print("SSL Connection established successfully")
        context.fireChannelActive()
final class PeerChannelStateHandler: ChannelInboundHandler {
    typealias InboundIn = Never  // We only care about state changes, so we ignore inbound data.
    private let repository: IRepository  // Your logging or tracking service
        CommonHelper.customLogging(repository: self.repository, location: "PeerChannel", info: "PeerChannel is now active", saveLog: true, tag: .swg)
    func channelInactive(context: ChannelHandlerContext) {
        CommonHelper.customLogging(repository: self.repository, location: "PeerChannel", info: "PeerChannel became inactive", saveLog: true, tag: .swg)
    func channelUnregistered(context: ChannelHandlerContext) {
        CommonHelper.customLogging(repository: self.repository, location: "PeerChannel", info: "PeerChannel is unregistered", saveLog: true, tag: .swg)
        CommonHelper.customLogging(repository: self.repository, location: "PeerChannel", info: "Error in PeerChannel: \(error)", saveLog: true, tag: .swg)