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
The peer connection becomes active, and we are able to write data, but the website remains blank.
customTLSConfig.certificateVerification = .fullVerification
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)