import tls from 'tls';
import https from 'https';
import http from 'http';
import { URL } from 'url';
import { Headers } from '../../../lib/headers/Headers.js';
import { ProxyAuthHandler } from './ProxyAuthHandler.js';
import { HttpEngineErrorFactory } from '../errors/index.js';
/**
 * Handles HTTPS tunneling through an HTTP proxy
 */
export class TunnelConnection {
    options;
    authHandler;
    constructor(options) {
        this.options = options;
        this.authHandler = new ProxyAuthHandler({
            proxyUsername: options.proxyUsername,
            proxyPassword: options.proxyPassword,
            logger: options.logger,
        });
    }
    async connect() {
        const { proxy, proxyIsSsl, targetHost, targetPort, logger, stats } = this.options;
        logger.debug('Creating a tunnel through the proxy...');
        if (!proxy) {
            throw HttpEngineErrorFactory.proxyConfigError('No proxy configuration found.');
        }
        let proxyUrl = proxy;
        if (proxyIsSsl && !proxyUrl.startsWith('https:')) {
            proxyUrl = `https://${proxyUrl}`;
        }
        else if (!proxyIsSsl && !proxyUrl.startsWith('http:')) {
            proxyUrl = `http://${proxyUrl}`;
        }
        const proxyUri = new URL(proxyUrl);
        const proxyPort = proxyUri.port || (proxyIsSsl ? 443 : 80);
        const authority = `${targetHost}:${targetPort}`;
        const connectOptions = {
            host: proxyUri.hostname,
            port: proxyPort,
            method: 'CONNECT',
            path: authority,
            headers: {
                host: authority,
            },
        };
        if (proxyIsSsl) {
            connectOptions.rejectUnauthorized = false;
            // @ts-expect-error This is correct!
            connectOptions.requestOCSP = false;
        }
        const lib = proxyIsSsl ? https : http;
        return new Promise((resolve, reject) => {
            stats.connectionTime = Date.now();
            try {
                // Add proxy authorization if available
                const auth = this.authHandler.generateAuthHeader();
                if (auth) {
                    logger.debug('Adding proxy authorization.');
                    connectOptions.headers['proxy-authorization'] = auth;
                }
                logger.debug('Creating proxy tunnel...');
                const connectRequest = lib.request(connectOptions);
                logger.silly('Proxy tunnel created.');
                connectRequest.once('socket', (socket) => {
                    logger.debug('Proxy tunnel socket created.');
                    socket.on('lookup', () => {
                        logger.debug('Proxy tunnel lookup.');
                        stats.lookupTime = Date.now();
                    });
                });
                connectRequest.on('error', (err) => {
                    logger.error('Error establishing proxy tunnel:', err);
                    reject(HttpEngineErrorFactory.proxyError(`Failed to establish proxy tunnel: ${err.message}`, err.code ? Number(err.code) : 111));
                });
                connectRequest.on('connect', async (res, socket, head) => {
                    const time = Date.now();
                    logger.debug('Proxy tunnel connected.');
                    stats.connectedTime = time;
                    stats.secureStartTime = time;
                    if (typeof stats.lookupTime === 'undefined') {
                        stats.lookupTime = time;
                    }
                    if (res.statusCode === 401) {
                        // Handle proxy authentication required
                        logger.debug('Proxy tunnel not authorized. Closing.');
                        connectRequest.destroy();
                        reject(HttpEngineErrorFactory.proxyAuthRequired(res, head));
                        return;
                    }
                    else if (res.statusCode === 407) {
                        // Handle proxy authentication
                        logger.debug('Proxy tunnel authentication required. Trying to authenticate...');
                        try {
                            const retryResult = await this.handleProxyAuthentication(res);
                            if (retryResult) {
                                // Authentication succeeded and we should retry
                                connectRequest.destroy();
                                socket.destroy();
                                // Retry the connection with new authentication
                                try {
                                    const retrySocket = await this.connect();
                                    resolve(retrySocket);
                                }
                                catch (retryError) {
                                    reject(retryError);
                                }
                                return;
                            }
                            else {
                                // Authentication failed
                                logger.warn('Proxy authentication failed. Closing connection.');
                                connectRequest.destroy();
                                reject(HttpEngineErrorFactory.proxyAuthFailed());
                                return;
                            }
                        }
                        catch (error) {
                            logger.warn('Error handling proxy authentication:', error);
                            connectRequest.destroy();
                            reject(error);
                        }
                        return;
                    }
                    else if (res.statusCode !== 200) {
                        logger.debug(`The proxy tunnel ended with ${res.statusCode} status code. Erroring request.`);
                        connectRequest.destroy();
                        const errorMessage = res.statusMessage || `Proxy tunnel establishment failed with status code ${res.statusCode}`;
                        const e = HttpEngineErrorFactory.proxyError(errorMessage, res.statusCode);
                        reject(e);
                        return;
                    }
                    logger.debug('Established a proxy tunnel.');
                    logger.debug('Upgrading connection to SSL...');
                    const tlsSocket = tls.connect({ socket, rejectUnauthorized: false }, () => {
                        logger.debug('Connection upgraded to SSL.');
                        resolve(tlsSocket);
                    });
                    tlsSocket.once('secureConnect', () => {
                        stats.secureConnectedTime = Date.now();
                    });
                });
                connectRequest.end();
            }
            catch (error) {
                reject(error);
            }
        });
    }
    async handleProxyAuthentication(res) {
        if (res.statusCode === 407) {
            const headers = new Headers(res.headers);
            const authHeader = headers.get('proxy-authenticate');
            if (authHeader) {
                try {
                    const authResponse = this.authHandler.handleChallenge(authHeader);
                    if (authResponse) {
                        this.options.logger.debug('Generated proxy authentication response');
                        this.authHandler.markAuthenticated();
                        return true; // Indicate successful authentication
                    }
                }
                catch (error) {
                    this.options.logger.error('Failed to handle proxy authentication:', error);
                    throw error;
                }
            }
            else {
                throw HttpEngineErrorFactory.proxyError('Proxy authentication required but no challenge provided', 407);
            }
        }
        return false; // Indicate failed authentication
    }
}
//# sourceMappingURL=TunnelConnection.js.map