"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.hdbServer = operationsServer;
exports.start = operationsServer;
exports.calculateRestHttpURL = calculateRestHttpURL;
const cluster_1 = __importDefault(require("cluster"));
const node_zlib_1 = __importDefault(require("node:zlib"));
const environmentManager_js_1 = __importDefault(require("../utility/environment/environmentManager.js"));
environmentManager_js_1.default.initSync();
const terms = __importStar(require("../utility/hdbTerms.js"));
const harper_logger_js_1 = __importDefault(require("../utility/logging/harper_logger.js"));
const fastify_1 = __importDefault(require("fastify"));
const cors_1 = __importDefault(require("@fastify/cors"));
const compress_1 = __importDefault(require("@fastify/compress"));
const static_1 = __importDefault(require("@fastify/static"));
const requestTimePlugin_js_1 = __importDefault(require("./serverHelpers/requestTimePlugin.js"));
const path_1 = __importDefault(require("path"));
const packageUtils_js_1 = require("../utility/packageUtils.js");
const globalSchema_js_1 = __importDefault(require("../utility/globalSchema.js"));
const common_utils_js_1 = __importDefault(require("../utility/common_utils.js"));
const userSchema = __importStar(require("../security/user.js"));
const Server_ts_1 = require("../server/Server.js");
const serverHandlers_js_1 = require("./serverHelpers/serverHandlers.js");
const contentTypes_ts_1 = require("./serverHelpers/contentTypes.js");
const openApi_ts_1 = require("../resources/openApi.js");
const hdbError_js_1 = require("../utility/errors/hdbError.js");
const DEFAULT_HEADERS_TIMEOUT = 60000;
const REQ_MAX_BODY_SIZE = environmentManager_js_1.default.get(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_MAXREQUESTBODYSIZE) ?? 1024 * 1024 * 1024; //this defaults to 1GB in bytes
const TRUE_COMPARE_VAL = 'TRUE';
const { CONFIG_PARAMS } = terms;
let server;
/**
 * Builds a Harper server.
 */
async function operationsServer(options) {
    try {
        harper_logger_js_1.default.debug('In Fastify server' + process.cwd());
        harper_logger_js_1.default.debug(`Running with NODE_ENV set as: ${process.env.NODE_ENV}`);
        harper_logger_js_1.default.debug(`Harper server process ${process.pid} starting up.`);
        global.clustering_on = false;
        global.isMaster = cluster_1.default.isMaster;
        await setUp();
        // if we have a secure port, need to use the secure HTTP server for fastify (it can be used for HTTP as well)
        const isHttps = options.securePort > 0;
        //generate a Fastify server instance
        server = buildServer(isHttps, options.resources);
        //make sure the process waits for the server to be fully instantiated before moving forward
        await server.ready();
        if (!options)
            options = {};
        options.usageType = 'operations-api';
        // fastify can't clean up properly
        try {
            // now that server is fully loaded/ready, start listening on port provided in config settings or just use
            // zero to wait for sockets from the main thread
            Server_ts_1.server.http(server.server, options);
            if (!server.server.closeIdleConnections) {
                // before Node v18, closeIdleConnections is not available, and we have to setup a listener for fastify
                // to handle closing by setting up the dynamic port
                await server.listen({ port: 0, host: '::' });
            }
        }
        catch (err) {
            server.close();
            harper_logger_js_1.default.error(err);
            harper_logger_js_1.default.error(`Error configuring operations server`);
            throw err;
        }
    }
    catch (err) {
        console.error(`Failed to build server on ${process.pid}`, err);
        harper_logger_js_1.default.fatal(err);
        process.exit(1);
    }
}
/**
 * Makes sure global values are set and that clustering connections are set/ready before server starts.
 */
async function setUp() {
    harper_logger_js_1.default.trace('Configuring Harper process.');
    globalSchema_js_1.default.setSchemaDataToGlobal();
    return userSchema.setUsersWithRolesCache();
}
/**
 * This method configures and returns a Fastify server - for either HTTP or HTTPS - based on the provided config settings
 */
function buildServer(isHttps, resources) {
    harper_logger_js_1.default.debug(`Harper process starting to build ${isHttps ? 'HTTPS' : 'HTTP'} server.`);
    const serverOpts = getServerOptions(isHttps);
    const app = (0, fastify_1.default)(serverOpts);
    // Fastify does not set this property in the initial app construction
    app.server.headersTimeout = getHeaderTimeoutConfig();
    // Set a top-level error handler for the server - all errors caught/thrown within the API will bubble up to this
    // handler so that they can be handled in a coordinated way
    app.setErrorHandler(serverHandlers_js_1.serverErrorHandler);
    const corsOptions = getCORSOpts();
    if (corsOptions) {
        app.register(cors_1.default, corsOptions);
    }
    app.register(function (instance, options, done) {
        instance.setNotFoundHandler(function (request, reply) {
            if (reply.sent || reply.raw.headersSent || reply.raw.writableEnded)
                return;
            app.server.emit('unhandled', request.raw, reply.raw);
        });
        done();
    });
    app.register(requestTimePlugin_js_1.default);
    // This handles all get requests for the studio
    app.register(compress_1.default, {
        brotliOptions: {
            params: {
                [node_zlib_1.default.constants.BROTLI_PARAM_MODE]: node_zlib_1.default.constants.BROTLI_MODE_TEXT, // useful for APIs that primarily return text
                [node_zlib_1.default.constants.BROTLI_PARAM_QUALITY]: 2, // default is 4, max is 11, min is 0
            },
        },
    });
    (0, contentTypes_ts_1.registerContentHandlers)(app);
    // Add a simple health check
    app.get('/health', () => 'Harper is running.');
    // Add a top-level GET handler for browsers.
    app.register(static_1.default, { root: path_1.default.join(packageUtils_js_1.PACKAGE_ROOT, 'studio/web') });
    const studioOn = environmentManager_js_1.default.get(terms.HDB_SETTINGS_NAMES.LOCAL_STUDIO_ON);
    if (!common_utils_js_1.default.isEmpty(studioOn) && studioOn.toString().toLowerCase() === 'true') {
        app.get('/', (req, res) => res.sendFile('index.html'));
    }
    else {
        app.get('/', (req, res) => res.sendFile('running.html'));
    }
    // Describe the APIs.
    app.get('/api/openapi/rest', { preValidation: [serverHandlers_js_1.authAndEnsureUserOnRequest] }, restOpenAPIHandler(resources));
    // Add the top-level POST handler.
    app.post('/', {
        preValidation: [serverHandlers_js_1.reqBodyValidationHandler, serverHandlers_js_1.authHandler],
        config: { isOperation: true },
    }, handler);
    harper_logger_js_1.default.debug(`Harper process starting up ${isHttps ? 'HTTPS' : 'HTTP'} server listener.`);
    return app;
}
function restOpenAPIHandler(resources) {
    const httpPort = environmentManager_js_1.default.get(terms.CONFIG_PARAMS.HTTP_PORT);
    const httpSecurePort = environmentManager_js_1.default.get(terms.CONFIG_PARAMS.HTTP_SECUREPORT);
    return (req) => {
        if (req.hdb_user?.role?.permission?.super_user) {
            return (0, openApi_ts_1.generateJsonApi)(resources, calculateRestHttpURL(httpPort, httpSecurePort, req));
        }
        else {
            harper_logger_js_1.default.warn(`{"ip":"${req.socket.remoteAddress}", "error":"attempt to access /api/openapi/rest without being super_user"`);
            return new hdbError_js_1.ServerError(`Forbidden`, 403);
        }
    };
}
function calculateRestHttpURL(httpPort, httpSecurePort, req) {
    const httpURL = new URL(`${req.protocol}://${req.hostname}`);
    // note that the request is from fastify, which doesn't seem to have a correct hostname property (includes port), so the URL needs to be used for hostname
    if (httpURL.hostname.toLowerCase() === 'localhost' || httpURL.hostname.match(/^[\d.:]+$/)) {
        // Only use ports when running against localhost, or an ip address.
        if (httpSecurePort) {
            httpURL.port = httpSecurePort;
            httpURL.protocol = 'https:';
        }
        else if (httpPort) {
            httpURL.port = httpPort;
            httpURL.protocol = 'http:';
        }
    }
    else {
        // Otherwise, assume that port forwarding is happening, and possibly SSL termination.
        httpURL.port = '443';
        httpURL.protocol = 'https:';
    }
    return httpURL.toString();
}
function handler(req, reply) {
    // if the operation is a restart, we have to tell the client not to use keep alive on this connection
    // anymore; it needs to be closed because this thread is going to be terminated
    if (req.body?.operation?.startsWith('restart')) {
        reply.header('Connection', 'close');
    }
    //if no error is thrown below, the response 'data' returned from the handler will be returned with 200/OK code
    return (0, serverHandlers_js_1.handlePostRequest)(req, reply);
}
/**
 * Builds server options object to pass to Fastify when using server factory.
 */
function getServerOptions(isHttps) {
    const server_timeout = environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_TIMEOUT);
    const keep_alive_timeout = environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_KEEPALIVETIMEOUT);
    return {
        bodyLimit: REQ_MAX_BODY_SIZE,
        connectionTimeout: server_timeout,
        keepAliveTimeout: keep_alive_timeout,
        forceCloseConnections: true,
        return503OnClosing: false,
        http2: environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_HTTP2),
        https: isHttps /* && {
            allowHTTP1: true,
        },*/,
    };
}
/**
 * Builds CORS options object to pass to cors plugin when/if it needs to be registered with Fastify
 */
function getCORSOpts() {
    const propsCors = environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_CORS);
    const propsCorsAccesslist = environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_CORSACCESSLIST);
    let corsOptions;
    if (propsCors && (propsCors === true || propsCors.toUpperCase() === TRUE_COMPARE_VAL)) {
        corsOptions = {
            origin: true,
            allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
            credentials: false,
        };
        if (propsCorsAccesslist &&
            propsCorsAccesslist.length > 0 &&
            propsCorsAccesslist[0] !== null &&
            propsCorsAccesslist[0] !== '*') {
            corsOptions.origin = (origin, callback) => {
                return callback(null, propsCorsAccesslist.indexOf(origin) !== -1);
            };
        }
    }
    return corsOptions;
}
/**
 * Returns header timeout value from config file or, if not entered, the default value
 */
function getHeaderTimeoutConfig() {
    return environmentManager_js_1.default.get(CONFIG_PARAMS.OPERATIONSAPI_NETWORK_HEADERSTIMEOUT) ?? DEFAULT_HEADERS_TIMEOUT;
}
//# sourceMappingURL=operationsServer.js.map