/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import http from 'http';
import http2 from 'http2';
import https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { getProxyForUrl } from 'proxy-from-env';
import { ManualPromise } from '@isomorphic/manualPromise';
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happyEyeballs';
export const NET_DEFAULT_TIMEOUT = 30_000;
export function httpRequest(params, onResponse, onError) {
    let url = new URL(params.url);
    const options = {
        method: params.method || 'GET',
        headers: params.headers,
    };
    if (params.rejectUnauthorized !== undefined)
        options.rejectUnauthorized = params.rejectUnauthorized;
    const proxyURL = getProxyForUrl(params.url);
    if (proxyURL) {
        const parsedProxyURL = normalizeProxyURL(proxyURL);
        if (params.url.startsWith('http:')) {
            options.path = url.toString();
            url = parsedProxyURL;
        }
        else {
            options.agent = new HttpsProxyAgent(parsedProxyURL);
        }
    }
    options.agent ??= url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent;
    let cancelRequest;
    const requestCallback = (res) => {
        const statusCode = res.statusCode || 0;
        if (statusCode >= 300 && statusCode < 400 && res.headers.location) {
            // Close the original socket before following the redirect. Otherwise
            // it may stay idle and cause a timeout error.
            request.destroy();
            cancelRequest = httpRequest({ ...params, url: new URL(res.headers.location, params.url).toString() }, onResponse, onError).cancel;
        }
        else {
            onResponse(res);
        }
    };
    const request = url.protocol === 'https:' ?
        https.request(url, options, requestCallback) :
        http.request(url, options, requestCallback);
    request.on('error', onError);
    if (params.socketTimeout !== undefined) {
        request.setTimeout(params.socketTimeout, () => {
            onError(new Error(`Request to ${params.url} timed out after ${params.socketTimeout}ms`));
            request.abort();
        });
    }
    cancelRequest = e => {
        try {
            request.destroy(e);
        }
        catch {
        }
    };
    request.end(params.data);
    return { cancel: e => cancelRequest(e) };
}
function shouldBypassProxy(url, bypass) {
    if (!bypass)
        return false;
    const domains = bypass.split(',').map(s => {
        s = s.trim();
        if (!s.startsWith('.'))
            s = '.' + s;
        return s;
    });
    const domain = '.' + url.hostname;
    return domains.some(d => domain.endsWith(d));
}
function normalizeProxyURL(proxy) {
    proxy = proxy.trim();
    // Browsers allow to specify proxy without a protocol, defaulting to http.
    if (!/^\w+:\/\//.test(proxy))
        proxy = 'http://' + proxy;
    return new URL(proxy);
}
export function createProxyAgent(proxy, forUrl) {
    if (!proxy)
        return;
    if (forUrl && proxy.bypass && shouldBypassProxy(forUrl, proxy.bypass))
        return;
    const proxyURL = normalizeProxyURL(proxy.server);
    if (proxyURL.protocol?.startsWith('socks')) {
        // SocksProxyAgent distinguishes between socks5 and socks5h.
        // socks5h is what we want, it means that hostnames are resolved by the proxy.
        // browsers behave the same way, even if socks5 is specified.
        if (proxyURL.protocol === 'socks5:')
            proxyURL.protocol = 'socks5h:';
        else if (proxyURL.protocol === 'socks4:')
            proxyURL.protocol = 'socks4a:';
        return new SocksProxyAgent(proxyURL);
    }
    if (proxy.username) {
        proxyURL.username = proxy.username;
        proxyURL.password = proxy.password || '';
    }
    if (forUrl && ['ws:', 'wss:'].includes(forUrl.protocol)) {
        // Force CONNECT method for WebSockets.
        return new HttpsProxyAgent(proxyURL);
    }
    // TODO: This branch should be different from above. We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
    return new HttpsProxyAgent(proxyURL);
}
export function createHttpServer(...args) {
    const server = http.createServer(...args);
    decorateServer(server);
    return server;
}
export function createHttpsServer(...args) {
    const server = https.createServer(...args);
    decorateServer(server);
    return server;
}
export function createHttp2Server(...args) {
    const server = http2.createSecureServer(...args);
    decorateServer(server);
    return server;
}
export async function startHttpServer(server, options) {
    const { host = 'localhost', port = 0 } = options;
    const errorPromise = new ManualPromise();
    const errorListener = (error) => errorPromise.reject(error);
    server.on('error', errorListener);
    try {
        server.listen(port, host);
        await Promise.race([
            new Promise(cb => server.once('listening', cb)),
            errorPromise,
        ]);
    }
    finally {
        server.removeListener('error', errorListener);
    }
}
export async function isURLAvailable(url, ignoreHTTPSErrors, onLog, onStdErr) {
    let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onLog, onStdErr);
    if (statusCode === 404 && url.pathname === '/') {
        const indexUrl = new URL(url);
        indexUrl.pathname = '/index.html';
        statusCode = await httpStatusCode(indexUrl, ignoreHTTPSErrors, onLog, onStdErr);
    }
    return statusCode >= 200 && statusCode < 404;
}
async function httpStatusCode(url, ignoreHTTPSErrors, onLog, onStdErr) {
    return new Promise(resolve => {
        onLog?.(`HTTP GET: ${url}`);
        httpRequest({
            url: url.toString(),
            headers: { Accept: '*/*' },
            rejectUnauthorized: !ignoreHTTPSErrors
        }, res => {
            res.resume();
            const statusCode = res.statusCode ?? 0;
            onLog?.(`HTTP Status: ${statusCode}`);
            resolve(statusCode);
        }, error => {
            if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
                onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
            onLog?.(`Error while checking if ${url} is available: ${error.message}`);
            resolve(0);
        });
    });
}
export function decorateServer(server) {
    const sockets = new Set();
    server.on('connection', socket => {
        sockets.add(socket);
        socket.once('close', () => sockets.delete(socket));
    });
    const close = server.close;
    server.close = (callback) => {
        for (const socket of sockets)
            socket.destroy();
        sockets.clear();
        return close.call(server, callback);
    };
}
//# sourceMappingURL=network.js.map