"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.containsHeaderOverLimit = exports.getServerHeapLimit = exports.getHandlerHeapLimit = exports.spawnServer = exports.clearServer = exports.handleServerError = exports.handleTimeoutError = exports.handleServerResponse = exports.handleError = exports.shapeResponseForEvent = exports.addMetrics = exports.handler = void 0;
const ports_1 = require("../utils/ports");
const http_1 = __importDefault(require("http"));
const constants_1 = require("../constants");
const v8_1 = require("v8");
const log_1 = __importDefault(require("../log"));
const errors_1 = require("../errors");
const eventUtils_1 = require("./eventUtils");
const handlerConstants_1 = require("./handlerConstants");
const child_process_1 = require("child_process");
const path_1 = require("path");
const handlerMetrics_1 = require("./handlerMetrics");
const environment_1 = require("../environment");
/**
 * Following variables needs to be global,
 * so we can still hold reference to the server child process
 * between lambda invocations.
 */
let server;
let timeout;
/**
 * The handler function is the main entry point in the lambda.
 * It's responsible for:
 * - Spawning the server process
 * - Managing the system resources in way that should prevent the crash of whole lambda
 * - Converting the lambda event to node req/res and proxying it to the server
 * - Catching top-level lambda errors and restarting the server if it crashes
 *
 * The reserves the necessary system resources for itself first,
 * and then dedicates the rest of the resources to the server process.
 *
 * This handler is expected to be run in Linux environment
 * only and runs only in AWS Lambda as it's using the native OS APIs.
 * Therefore, it's excluded from the local simulation,
 * that starts the server directly in the main process.
 * @param event
 * @param context
 */
const handler = async (event, context) => {
    let response;
    handlerMetrics_1.metrics.handlerReqStartedAt = Date.now();
    try {
        // The context is restored on each invocation,
        // so we need to reset the timeout right away on each invocation.
        clearTimeout(timeout);
        // Don't wait for the event loop to be empty.
        // The lambda would timeout without this config,
        // because we're running server in the background.
        context.callbackWaitsForEmptyEventLoop = false;
        // We need to keep the server process running between lambda invocations.
        // Spawn server if it's not already running.
        server = server || (await spawnServer());
        // Remove all existing event listeners from the previous invocation.
        server.removeAllListeners();
        response = await Promise.race([
            handleTimeoutError(context),
            handleServerError(server),
            handleServerResponse(event),
        ]);
        // Added to make the tests exit cleanly and fast.
        timeout === null || timeout === void 0 ? void 0 : timeout.unref();
        // Check the size of every individual header value
        // before returning the response.
        if (containsHeaderOverLimit(response.headers)) {
            throw new errors_1.EdgioHeadersOverflowError(`Headers contain value over the limit of ${handlerConstants_1.HANDLER_MAX_HEADER_VALUE_SIZE}B`);
        }
        // Convert the response body string/buffer to base64, to correctly return binary data.
        response.base64EncodedBody = Buffer.from(response.unencodedBody).toString('base64');
        // The 6277476 bytes (around 6MB ≃ 5.9MiB) is the maximum allowed response size for Buffered Lambda Responses.
        // If the response is larger than this, AWS returns 502 Bad Gateway error.
        // We need to check the size of the final base64 encoded response and return a custom error ahead of time.
        if (response.base64EncodedBody.length > handlerConstants_1.RESPONSE_SIZE_LIMIT) {
            throw new errors_1.EdgioResponseTooLargeError(`Received base64 encoded body with size ${response.base64EncodedBody.length}B`);
        }
    }
    catch (e) {
        handlerMetrics_1.metrics.handlerErrorCount++;
        response = await handleError(e, event);
    }
    handlerMetrics_1.metrics.handlerReqCount++;
    handlerMetrics_1.metrics.handlerReqFinishedAt = Date.now();
    return shapeResponseForEvent(event, addMetrics(response));
};
exports.handler = handler;
/**
 * Injects the metrics headers into the response
 * or appends the metrics to the existing header.
 * @param response The lambda response
 */
function addMetrics(response) {
    // Metrics for x-edg-t header
    // x-edg-t: hid=3rYwt1zjg2,hlt=69906,slt=69899,hrc=4,src=4,hec=4 etc...
    const xEdgeT = response.headers[constants_1.HTTP_HEADERS.xEdgeT] || '';
    const previousXEdgeT = xEdgeT ? `,${xEdgeT}` : '';
    response.headers[constants_1.HTTP_HEADERS.xEdgeT] = `${handlerMetrics_1.metrics.serialize()}${previousXEdgeT}`;
    // Status code for x-edg-status header
    // x-edg-status: 200
    const xEdgeStatus = response.headers[constants_1.HTTP_HEADERS.xEdgeStatus] || '';
    const previousXEdgeStatus = xEdgeStatus ? `,${xEdgeStatus}` : '';
    response.headers[constants_1.HTTP_HEADERS.xEdgeStatus] = `h=${response.statusCode}${previousXEdgeStatus}`;
    return response;
}
exports.addMetrics = addMetrics;
/**
 * Converts universal BufferedResponse to the specific response object
 * based on the lambda event type.
 * @param event The lambda event
 * @param response The universal BufferedResponse
 * @returns HandlerResponse The specific response object
 */
function shapeResponseForEvent(event, response) {
    var _a, _b;
    // Sanitize the headers before returning them to the client.
    response.headers = (0, eventUtils_1.sanitizeHeaders)(response.headers);
    // Encode the body to base64 if it's not already provided.
    if (!response.base64EncodedBody) {
        response.base64EncodedBody = Buffer.from(response.unencodedBody).toString('base64');
    }
    // Return caught errors to the console as object,
    // if invoked from the console, so we can fail deployment
    // and show the error message in the logs.
    if ((0, eventUtils_1.isConsoleEvent)(event) && ((_a = response.statusCode) !== null && _a !== void 0 ? _a : 0) >= 500) {
        const errorMessage = JSON.parse(response.unencodedBody);
        return {
            statusCode: response.statusCode,
            errorMessage: errorMessage,
        };
    }
    // Return success responses in plain text format for the console
    if ((0, eventUtils_1.isConsoleEvent)(event)) {
        return response.unencodedBody.toString();
    }
    if ((0, eventUtils_1.isApiGatewayV2Event)(event)) {
        const lowerCaseHeaders = Object.fromEntries(Object.entries(response.headers).map(([key, value]) => [key.toLowerCase(), value]));
        const cookies = (_b = lowerCaseHeaders['set-cookie']) !== null && _b !== void 0 ? _b : [];
        delete lowerCaseHeaders['set-cookie'];
        return {
            statusCode: response.statusCode,
            headers: lowerCaseHeaders,
            cookies,
            body: response.base64EncodedBody,
            isBase64Encoded: true,
        };
    }
    else {
        return {
            statusCode: response.statusCode,
            multiValueHeaders: Object.entries(response.headers).reduce((accumulatedHeaders, [key, value]) => ({
                ...accumulatedHeaders,
                // the set-cookie header is multi-value
                [key]: Array.isArray(value) ? value : [value],
            }), {}),
            body: response.base64EncodedBody.toString(),
            isBase64Encoded: true,
        };
    }
}
exports.shapeResponseForEvent = shapeResponseForEvent;
/**
 * Handles any error, converts it to EdgioError if necessary,
 * renders error page and returns it as the lambda response.
 * @param e The error
 * @param event The lambda event
 * @returns The lambda response
 */
async function handleError(e, event) {
    // Here we catch all server-level errors (Error Level: 1)
    // and render nice looking error page with error details.
    // ERROR LEVELS overview:
    // Error Level 0 is handled by the Edge and means that both lambdas failed or are not available.
    // Error Level 1 is handled by the Lambda itself and is for fatal server errors, like timeouts, out of memory etc...
    // Error Level 2 is handled by the Lambda itself and is for project errors, like uncaught user-errors in compute, malformed responses, etc...
    // Wrap all errors into EdgioError instance
    // NOTE: instance of EdgioError doesn't work here.
    const error = (0, errors_1.isEdgioError)(e) ? e : new errors_1.EdgioInternalError(e.message, e.stack);
    const requestId = (0, eventUtils_1.requestIdFromEvent)(event);
    const method = (0, eventUtils_1.methodFromEvent)(event) || 'GET';
    const url = (0, eventUtils_1.urlFromEvent)(event);
    // Set correct error type based on request accept header
    const acceptHeader = (0, eventUtils_1.headerValueFromEvent)(event, constants_1.HTTP_HEADERS.accept);
    const type = (acceptHeader === null || acceptHeader === void 0 ? void 0 : acceptHeader.includes('html')) ? 'html' : 'json';
    const contentType = type === 'html' ? 'text/html' : 'application/json';
    const invokeSource = (0, eventUtils_1.invokeSourceFromEvent)(event);
    // Log the error for debugging with the details from event
    log_1.default.error(`[Edgio Error][Level 1]: ${method} ${url} - ${e === null || e === void 0 ? void 0 : e.stack}`);
    // If error is caused by one of the following errors,
    // we need to clear the server process, so it's restarted on the next request.
    // 539 - EdgioProjectTimeoutError - Server can be stuck in infinite loop or heavy computation and won't respond to any other requests.
    // 540 - EdgioOutOfResourcesError - Server consumed all the memory and crashed.
    // 549 - EdgioProjectCrashedError - Server crashed for some other reason.
    if ([539, 540, 549].includes(error.statusCode)) {
        clearServer();
    }
    // Render the EdgioError to HTML or JSON response
    return {
        statusCode: error.statusCode,
        headers: {
            [constants_1.HTTP_HEADERS.contentType]: contentType,
            [constants_1.HTTP_HEADERS.xEdgErrorMessage]: encodeURI(error.message),
            [constants_1.HTTP_HEADERS.xEdgErrorLevel]: error.level.toString(),
            [constants_1.HTTP_HEADERS.xEdgErrorDetails]: encodeURI(error.details || ''),
        },
        unencodedBody: error.render({
            type,
            requestId,
            includeStack: (0, environment_1.isLocal)() || invokeSource === constants_1.INVOKE_SOURCES.console,
        }),
    };
}
exports.handleError = handleError;
/**
 * Converts the lambda event to node req/res
 * and proxies it to the server.
 * @param event The lambda event
 */
async function handleServerResponse(event) {
    const method = (0, eventUtils_1.methodFromEvent)(event) || 'GET';
    const path = (0, eventUtils_1.pathFromEvent)(event) || '/';
    const search = (0, eventUtils_1.searchFromEvent)(event) || '';
    const pathWithSearch = path + search;
    const headers = (0, eventUtils_1.sanitizeHeaders)((0, eventUtils_1.singleValueHeadersFromEvent)(event) || {});
    const cookies = (0, eventUtils_1.cookiesFromEvent)(event);
    // Cookie is the only header that can be multi-value.
    // @ts-ignore
    headers.cookie = cookies;
    const invokeSource = (0, eventUtils_1.invokeSourceFromEvent)(event);
    const invokeAction = (0, eventUtils_1.invokeActionFromEvent)(event);
    // Pass invoke source and action to server as reserved headers
    // that users can't override or set through the Function URL request.
    // The source and action are later checked in the dispatch function.
    // See: packages/core/src/lambda/dispatch.ts
    headers[constants_1.HTTP_HEADERS.xEdgInvokeSource] = invokeSource;
    headers[constants_1.HTTP_HEADERS.xEdgInvokeAction] = invokeAction;
    // Pass original rawUrl (without encoded/decoded query) in as request header,
    // so we can expose it under req.rawUrl property in server
    headers[constants_1.HTTP_HEADERS.xEdgRawUrl] = (0, eventUtils_1.urlFromEvent)(event) || '/';
    const body = event.body
        ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : undefined)
        : undefined;
    const requestOptions = {
        hostname: ports_1.localhost,
        port: ports_1.port,
        path: pathWithSearch,
        method,
        headers,
        timeout: handlerConstants_1.HANDLER_REQUEST_TIMEOUT,
    };
    return new Promise((resolve, reject) => {
        handlerMetrics_1.metrics.serverReqCount++;
        handlerMetrics_1.metrics.serverReqStartedAt = Date.now();
        handlerMetrics_1.metrics.serverReqFinishedAt = undefined;
        const req = http_1.default.request(requestOptions, res => {
            let body = Buffer.from('');
            res.on('data', chunk => {
                body = Buffer.concat([body, chunk]);
                // Check the response body size before the encoding to base64.
                // We destroy the request if it's too large
                // and return the error page immediately,
                // so we don't even waste resources to encoding it to base64.
                // Even when the response is less than 6MB here,
                // it can still be larger after the base64 encoding.
                // That's why we have second check in the handler function.
                if (body.length > handlerConstants_1.RESPONSE_SIZE_LIMIT) {
                    req.destroy();
                    reject(new errors_1.EdgioResponseTooLargeError(`Received body with size ${body.length}B`));
                }
            });
            res.on('error', (e) => {
                // Return original error
                reject(e);
            });
            res.on('end', () => {
                handlerMetrics_1.metrics.serverReqFinishedAt = Date.now();
                resolve({
                    statusCode: res.statusCode || 200,
                    // NOTE: Duplicate set-cookie headers are always in single array
                    headers: res.headers,
                    unencodedBody: body,
                });
            });
        });
        req.on('timeout', () => {
            req.destroy();
            reject(new errors_1.EdgioProjectTimeoutError('The req to the server timed out'));
        });
        req.on('error', (e) => {
            // When the server crash or is killed,
            // the socket is closed. These kind of errors are expected
            // and caught by the handleServerError function.
            // In this case we care more about the server crash reason than the req error.
            if (e.code === 'ECONNRESET')
                return;
            if (e.code === 'HPE_HEADER_OVERFLOW') {
                reject(new errors_1.EdgioHeadersOverflowError('HPE_HEADER_OVERFLOW - The server response headers are too large'));
            }
            // Return original error
            return reject(e);
        });
        req.end(body);
    });
}
exports.handleServerResponse = handleServerResponse;
/**
 * Measures the time of the lambda invocation
 * and returns timeout error page a few milliseconds
 * before the lambda would actually time out.
 * @param context The lambda context
 */
async function handleTimeoutError(context) {
    return new Promise((_resolve, reject) => {
        // We need some reserved time to handle the error and return the response
        // before the lambda actually times out.
        const timeoutMilliseconds = context.getRemainingTimeInMillis() - handlerConstants_1.HANDLER_RESERVED_TIME;
        timeout = setTimeout(() => {
            clearTimeout(timeout);
            reject(new errors_1.EdgioProjectTimeoutError(`The project's code was terminated after ${timeoutMilliseconds}ms`));
        }, timeoutMilliseconds);
    });
}
exports.handleTimeoutError = handleTimeoutError;
/**
 * Handles any server error and returns
 * error page with error details as the lambda response.
 * @param child The server process
 */
async function handleServerError(child) {
    return new Promise((_resolve, reject) => {
        child === null || child === void 0 ? void 0 : child.on('exit', code => {
            if (code === null || code === undefined) {
                // When the server crash for memory consumption,
                // the exit code is null, and we need to handle it as OutOfResourcesError.
                const handlerHeapLimit = getHandlerHeapLimit();
                return reject(new errors_1.EdgioOutOfResourcesError(`Total configured memory ${handlerHeapLimit}MB was exceeded`));
            }
            // When the server crash for other reasons, like uncaught exception,
            // we need to handle it as CrashedError.
            // For example: 0,1 etc...
            reject(new errors_1.EdgioProjectCrashedError(`Server exited with code ${code}`));
        });
    });
}
exports.handleServerError = handleServerError;
/**
 * Immediately terminates the server process
 * and clears the reference to it.
 */
function clearServer() {
    // Terminate the process immediately.
    // The server process doesn't do/have write access to the file system anyway,
    // so it cannot get into inconsistent state when killed abruptly.
    server === null || server === void 0 ? void 0 : server.kill('SIGKILL');
    server = undefined;
    handlerMetrics_1.metrics.serverStartedAt = undefined;
    handlerMetrics_1.metrics.serverReadyAt = undefined;
    handlerMetrics_1.metrics.serverReqCount = 0;
}
exports.clearServer = clearServer;
/**
 * Spawns the server as a separate process,
 * waits for the ready message and resolves the promise
 */
async function spawnServer() {
    var _a, _b;
    const serverHeapLimit = getServerHeapLimit();
    const serverPath = (0, path_1.join)(__dirname, 'server.cjs');
    handlerMetrics_1.metrics.serverStartedAt = Date.now();
    handlerMetrics_1.metrics.serverReadyAt = undefined;
    // NOTE: fork() cold startup time of server here is by 300-400ms faster than spawn().
    const child = (0, child_process_1.fork)(serverPath, [
        `--max-old-space-size=${serverHeapLimit}`,
        // Pass the handlerId to the server process,
        // so we can put in the DRI logs.
        `--handler-id=${handlerMetrics_1.metrics.handlerId}`,
    ], {
        cwd: process.cwd(),
        stdio: 'pipe',
        env: process.env,
    });
    // The main handler.ts process has default priority 0.
    // The server child process has lower priority 3 (the higher number, the lower CPU priority).
    // The main process idles most of the time, but we need it to react quickly when necessary
    // and don't let the server child process to consume all the CPU time.
    // Priority can be from -20 (highest priority) to 19 (lowest priority).
    // os.setPriority(child.pid!, 3)
    // Pipe stdout and stderr to the main process
    (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
    (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
    return new Promise(function (resolve, reject) {
        let onReady;
        let onError;
        let onExit;
        let addListeners;
        let removeListeners;
        let stderr = [];
        onReady = () => {
            handlerMetrics_1.metrics.serverReadyAt = Date.now();
            removeListeners();
            resolve(child);
        };
        onError = data => stderr.push(data);
        onExit = async (code) => {
            removeListeners();
            reject(new errors_1.EdgioProjectCrashedError(`Server process exited immediately after startup with code ${code}`, Buffer.concat(stderr).toString()));
        };
        removeListeners = () => {
            var _a, _b;
            (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.off('data', onReady);
            (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.off('data', onError);
            child.off('exit', onExit);
        };
        addListeners = () => {
            var _a, _b;
            (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onReady);
            (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', onError);
            child.on('exit', onExit);
        };
        addListeners();
    });
}
exports.spawnServer = spawnServer;
/**
 * The total heap memory limit for main process 'handler.ts' in MiB.
 */
function getHandlerHeapLimit() {
    return Math.floor((0, v8_1.getHeapStatistics)().heap_size_limit / 1024 / 1024);
}
exports.getHandlerHeapLimit = getHandlerHeapLimit;
/**
 * Heap memory limit for child process 'server.ts' in MiB.
 * 50MiB are reserved for the main process 'handler.ts'.
 * NOTE: fremem() and totalmem() are reporting system memory,
 * that is higher than the configured memory limit of the main process.
 */
function getServerHeapLimit() {
    return getHandlerHeapLimit() - handlerConstants_1.HANDLER_RESERVED_MEMORY;
}
exports.getServerHeapLimit = getServerHeapLimit;
/**
 * Returns true if the headers object
 * contains a header with value over the limit.
 * @param headers
 */
function containsHeaderOverLimit(headers) {
    var _a;
    for (const key in headers) {
        if (((_a = headers[key]) === null || _a === void 0 ? void 0 : _a.toString().length) > handlerConstants_1.HANDLER_MAX_HEADER_VALUE_SIZE) {
            return true;
        }
    }
    return false;
}
exports.containsHeaderOverLimit = containsHeaderOverLimit;
