/**
 * Copyright 2017 Google Inc. All rights reserved.
 * Modifications 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 fs from 'fs';
import mime from 'mime';
import path from 'path';
import util from 'util';
import { WebSocketServer } from 'ws';
import zlib, { gzip } from 'zlib';
import { utils } from '../../../packages/playwright-core/lib/coreBundle';
const { createHttpServer, createHttpsServer } = utils;
const fulfillSymbol = Symbol('fulfil callback');
const rejectSymbol = Symbol('reject callback');
const gzipAsync = util.promisify(gzip.bind(zlib));
export class TestServer {
    _server;
    _wsServer;
    _dirPath;
    debugServer;
    _startTime;
    _cachedPathPrefix;
    _routes = new Map();
    _auths = new Map();
    _csp = new Map();
    _extraHeaders = new Map();
    _gzipRoutes = new Set();
    _requestSubscribers = new Map();
    _upgradeCallback;
    PORT;
    PREFIX;
    CROSS_PROCESS_PREFIX;
    EMPTY_PAGE;
    HOST;
    HOSTNAME;
    HELLO_WORLD;
    static async create(dirPath, port, loopback) {
        const server = new TestServer(dirPath, port, loopback);
        await server.waitUntilReady();
        return server;
    }
    static async certOptions() {
        return {
            key: await fs.promises.readFile(path.join(__dirname, 'key.pem')),
            cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')),
            passphrase: 'aaaa',
        };
    }
    static async createHTTPS(dirPath, port, loopback) {
        const server = new TestServer(dirPath, port, loopback, await this.certOptions());
        await server.waitUntilReady();
        return server;
    }
    constructor(dirPath, port, loopback, sslOptions) {
        if (sslOptions)
            this._server = createHttpsServer(sslOptions, this._onRequest.bind(this));
        else
            this._server = createHttpServer(this._onRequest.bind(this));
        this._server.on('connection', socket => this._onSocket(socket));
        this._wsServer = new WebSocketServer({ noServer: true });
        this._server.on('upgrade', async (request, socket, head) => {
            const doUpgrade = () => {
                this._wsServer.handleUpgrade(request, socket, head, ws => {
                    // Next emit is only for our internal 'connection' listeners.
                    this._wsServer.emit('connection', ws, request);
                });
            };
            if (this._upgradeCallback) {
                this._upgradeCallback({ doUpgrade, socket });
                return;
            }
            const pathname = new URL(request.url, 'http://localhost').pathname;
            if (pathname === '/ws-401') {
                socket.write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized body');
                socket.destroy();
                return;
            }
            if (pathname === '/ws-slow')
                await new Promise(f => setTimeout(f, 2000));
            if (!['/ws', '/ws-slow'].includes(pathname)) {
                socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
                socket.destroy();
                return;
            }
            doUpgrade();
        });
        this._server.listen(port);
        this._dirPath = dirPath;
        this.debugServer = require('debug')('pw:testserver');
        this._startTime = new Date();
        this._cachedPathPrefix = null;
        const cross_origin = loopback || '127.0.0.1';
        const same_origin = loopback || 'localhost';
        const protocol = sslOptions ? 'https' : 'http';
        this.PORT = port;
        this.PREFIX = `${protocol}://${same_origin}:${port}`;
        this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}`;
        this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`;
        this.HOST = new URL(this.EMPTY_PAGE).host;
        this.HOSTNAME = new URL(this.EMPTY_PAGE).hostname;
        this.HELLO_WORLD = `${this.PREFIX}/hello-world`;
    }
    async waitUntilReady() {
        await new Promise(x => this._server.once('listening', x));
    }
    _onSocket(socket) {
        // ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given
        // that tab closing aborts outgoing connections to the server.
        // HPE_INVALID_METHOD is a legit error when a client (e.g. Chromium which
        // makes https requests to http sites) makes a https connection to a http server.
        socket.on('error', error => {
            if (!['ECONNRESET', 'HPE_INVALID_EOF_STATE', 'HPE_INVALID_METHOD'].includes(error.code))
                throw error;
        });
    }
    enableHTTPCache(pathPrefix) {
        this._cachedPathPrefix = pathPrefix;
    }
    setAuth(path, username, password) {
        this.debugServer(`set auth for ${path} to ${username}:${password}`);
        this._auths.set(path, { username, password });
    }
    enableGzip(path) {
        this._gzipRoutes.add(path);
    }
    setCSP(path, csp) {
        this._csp.set(path, csp);
    }
    setExtraHeaders(path, object) {
        this._extraHeaders.set(path, object);
    }
    async stop() {
        this.reset();
        await new Promise(x => this._server.close(x));
    }
    setContent(path, content, mimeType) {
        this.setRoute(path, (req, res) => {
            res.writeHead(200, { 'Content-Type': mimeType });
            res.end(mimeType === 'text/html' ? `<!DOCTYPE html>${content}` : content);
        });
    }
    setRoute(path, handler) {
        this._routes.set(path, handler);
    }
    setRedirect(from, to) {
        this.setRoute(from, (req, res) => {
            const headers = this._extraHeaders.get(req.url) || {};
            res.writeHead(302, { ...headers, location: to });
            res.end();
        });
    }
    waitForRequest(path) {
        let promise = this._requestSubscribers.get(path);
        if (promise)
            return promise;
        let fulfill;
        let reject;
        promise = new Promise((f, r) => {
            fulfill = f;
            reject = r;
        });
        promise[fulfillSymbol] = fulfill;
        const error = new Error(`Request ${path} was not received before the test finished.`);
        promise[rejectSymbol] = () => reject(error);
        this._requestSubscribers.set(path, promise);
        return promise;
    }
    reset() {
        this._routes.clear();
        this._auths.clear();
        this._csp.clear();
        this._extraHeaders.clear();
        this._gzipRoutes.clear();
        this._upgradeCallback = undefined;
        this._wsServer.removeAllListeners('connection');
        this._server.closeAllConnections();
        for (const subscriber of this._requestSubscribers.values())
            subscriber[rejectSymbol].call(null);
        this._requestSubscribers.clear();
    }
    _onRequest(request, response) {
        request.on('error', error => {
            if (error.code === 'ECONNRESET')
                response.end();
            else
                throw error;
        });
        request.postBody = new Promise(resolve => {
            const chunks = [];
            request.on('data', chunk => {
                chunks.push(chunk);
            });
            request.on('end', () => resolve(Buffer.concat(chunks)));
        });
        const url = new URL(request.url, 'http://localhost');
        const pathWithSearch = url.pathname + url.search;
        this.debugServer(`request ${request.method} ${pathWithSearch}`);
        if (this._auths.has(pathWithSearch)) {
            const auth = this._auths.get(pathWithSearch);
            const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString();
            this.debugServer(`request credentials ${credentials}`);
            this.debugServer(`actual credentials ${auth.username}:${auth.password}`);
            if (credentials !== `${auth.username}:${auth.password}`) {
                this.debugServer(`request write www-auth`);
                response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' });
                response.end('HTTP Error 401 Unauthorized: Access is denied');
                return;
            }
        }
        // Notify request subscriber.
        if (this._requestSubscribers.has(pathWithSearch)) {
            this._requestSubscribers.get(pathWithSearch)[fulfillSymbol].call(null, request);
            this._requestSubscribers.delete(pathWithSearch);
        }
        const handler = this._routes.get(pathWithSearch);
        if (handler)
            handler.call(null, request, response);
        else
            this.serveFile(request, response);
    }
    serveFile(request, response, filePath) {
        this._serveFile(request, response, filePath).catch(e => {
            this.debugServer(`error: ${e}`);
        });
    }
    async _serveFile(request, response, filePath) {
        let pathName = new URL(request.url, 'http://localhost').pathname;
        if (!filePath) {
            if (pathName === '/')
                pathName = '/index.html';
            filePath = path.join(this._dirPath, pathName.substring(1));
        }
        if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
            if (request.headers['if-modified-since']) {
                response.statusCode = 304; // not modified
                response.end();
                return;
            }
            response.setHeader('Cache-Control', 'public, max-age=31536000, no-cache');
            response.setHeader('Last-Modified', this._startTime.toISOString());
        }
        else {
            response.setHeader('Cache-Control', 'no-cache, no-store');
        }
        if (this._csp.has(pathName))
            response.setHeader('Content-Security-Policy', this._csp.get(pathName));
        if (this._extraHeaders.has(pathName)) {
            const object = this._extraHeaders.get(pathName);
            for (const key in object)
                response.setHeader(key, object[key]);
        }
        const { err, data } = await fs.promises.readFile(filePath).then(data => ({ data, err: undefined })).catch(err => ({ data: undefined, err }));
        // The HTTP transaction might be already terminated after async hop here - do nothing in this case.
        if (response.writableEnded)
            return;
        if (err) {
            response.statusCode = 404;
            response.setHeader('Content-Type', 'text/plain');
            response.end(request.method !== 'HEAD' ? `File not found: ${filePath}` : null);
            return;
        }
        const extension = filePath.substring(filePath.lastIndexOf('.') + 1);
        const mimeType = mime.getType(extension) || 'application/octet-stream';
        const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(mimeType);
        const contentType = isTextEncoding ? `${mimeType}; charset=utf-8` : mimeType;
        response.setHeader('Content-Type', contentType);
        if (this._gzipRoutes.has(pathName)) {
            response.setHeader('Content-Encoding', 'gzip');
            const result = await gzipAsync(data);
            // The HTTP transaction might be already terminated after async hop here.
            if (!response.writableEnded)
                response.end(request.method !== 'HEAD' ? result : null);
        }
        else {
            response.end(request.method !== 'HEAD' ? data : null);
        }
    }
    onceWebSocketConnection(handler) {
        this._wsServer.once('connection', handler);
    }
    waitForWebSocketConnectionRequest() {
        return new Promise(fulfil => {
            this._wsServer.once('connection', (ws, req) => fulfil(req));
        });
    }
    waitForUpgrade() {
        return new Promise(fulfill => this._upgradeCallback = fulfill);
    }
    waitForWebSocket() {
        return new Promise(fulfill => this._wsServer.once('connection', (ws, req) => fulfill(ws)));
    }
    sendOnWebSocketConnection(data) {
        this.onceWebSocketConnection(ws => ws.send(data));
    }
}
//# sourceMappingURL=index.js.map