"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("lodash");
const WasmProgram_1 = require("./WasmProgram");
// The following headers are prohibited from being read by the user.
// Connascence of meaning with EdgeControl schema. See edgecontrol-scl-schema.json
// in https://gitlab.edgecastcdn.net/edgecast/customer-config-management/edgecontrol-scl repo.
// Connascence of meaning with Sailfish mod_wasm common.cc
const READ_PROHIBITED_HEADER_NAMES_REGEX = [/^x-ec-.*/i, /^x-edg-.*/i];
const READ_ALLOWED_HEADER_NAMES_REGEX = [/^x-edg-experiments-info$/i];
// The following headers are prohibited from being set by the user.
// Connascence of meaning with EdgeControl schema. See edgecontrol-scl-schema.json
// in https://gitlab.edgecastcdn.net/edgecast/customer-config-management/edgecontrol-scl repo.
// Connascence of meaning with Sailfish mod_wasm common.cc
const WRITE_PROHIBITED_HEADER_NAMES_REGEX = [
    /^$/i,
    /^Accept-Ranges$/i,
    /^Age$/i,
    /^Connection$/i,
    /^Content-Encoding$/i,
    /^Content-Length$/i,
    /^Content-Range$/i,
    /^Date$/i,
    /^Server$/i,
    /^Trailer$/i,
    /^Transfer-Encoding$/i,
    /^Upgrade$/i,
    /^Vary$/i,
    /^Via$/i,
    /^Warning$/i,
];
// Connascence of meaning with Sailfish mod_wasm common.cc
// Checks if the header name contains any of the following characters: \n, \r, or \0, column (:), or space ( )
const WEAK_INVALID_HEADER_NAME_REGEX = /([\n\r: ]|\x00)/; // eslint-disable-line no-control-regex
// Connascence of meaning with Sailfish mod_wasm common.cc
// Checks if the header value contains any of the following characters: \n, \r, or \0
const WEAK_INVALID_HEADER_VALUE_REGEX = /([\n\r]|\x00)/; // eslint-disable-line no-control-regex
// Connascence of value with Sailfish mod_wasm.
const MAX_HEADER_NAME_LEN = 8 * 1024;
// Connascence of value with Sailfish mod_wasm.
const MAX_HEADER_VALUE_LEN = 8 * 1024;
// EdgeFunctionHeaders is attached to Requests and Responses to encapsulate their header behavior
class EdgeFunctionHeaders {
    constructor(headers) {
        // Raw headers are passed in as an array of alternating header names and values.
        if (Array.isArray(headers)) {
            this._headers = {};
            for (let i = 0; i < headers.length; ++i) {
                const item = headers[i];
                if (Array.isArray(item)) {
                    // When the item is an array itself, we assume that an array of [key, value] pairs
                    // have been passed and we use the items accordingly.
                    const name = item[0];
                    const value = item[1];
                    this.append(name, value);
                }
                else {
                    // When the item is not an array, we assume that the names and values are alternating
                    // in the array, just like Node's rawHeaders.
                    const name = item;
                    // Look ahead to get the value.
                    const value = headers[++i];
                    this.append(name, value);
                }
            }
        }
        else {
            // This case covers three scenarios for the headers parameter:
            // 1) headers parameter is an EdgeFunctionHeaders instance, but in the worker thread we lose the
            //    class information, so we just have to check for the existence of headers._headers. This
            //    is why one option for the headers parameter is "| any"
            // 2) headers parameters is a dictionary. { [name: string]: string | string[] }
            // 3) headers parameters is undefined
            this._headers = (headers === null || headers === void 0 ? void 0 : headers._headers) || headers || {};
        }
    }
    // Find the original key using case insensitive find
    _getCaseInsensitiveKey(name) {
        const asLowercase = name.toLowerCase();
        const key = Object.keys(this._headers).find(k => k.toLowerCase() === asLowercase);
        // If the key is not found, return the original name so we never return undefined
        return key || name;
    }
    // Ensure the value is an array or string
    _validateValue(value) {
        if (Array.isArray(value)) {
            return value;
        }
        else if (value === undefined || value === null) {
            throw Error(`set() header value cannot be ${value}`);
        }
        return value.toString();
    }
    // If there is no existing value, set it to newValue.
    // If there is an existing string value, convert to it array and push newValue
    // If there is an existing array value, push newValue to it
    append(name, newValue) {
        let value = this.get(name);
        let validatedNewValue = this._validateValue(newValue);
        if (Array.isArray(value)) {
            // Existing value is an Array, and newValue is an Array
            if (Array.isArray(validatedNewValue)) {
                value.push(...validatedNewValue);
            }
            else {
                value.push(validatedNewValue);
            }
        }
        else if (typeof value === 'string') {
            if (Array.isArray(validatedNewValue)) {
                // Existing value is a string, and newValue is an Array
                validatedNewValue.unshift(value);
                value = validatedNewValue;
            }
            else {
                value = [value, validatedNewValue];
            }
        }
        else {
            value = validatedNewValue;
        }
        this.set(name, value);
    }
    check(name) {
        return this.get(name) !== undefined;
    }
    get(name) {
        return this._headers[this._getCaseInsensitiveKey(name)];
    }
    // `headers` is used to interface with the edge functions and
    // we only allow the edge function to see the headers that
    // it is allowed to read.
    headers() {
        return (0, lodash_1.pick)(this._headers, this.allowed_keys());
    }
    // `key` is used to interface with the edge functions and
    // we only allow the edge function to see the headers that
    // it is allowed to read.
    key(index) {
        return this.allowed_keys()[index];
    }
    // Return all the header names that are allowed to be read.
    allowed_keys() {
        return Object.keys(this._headers).filter(key => EdgeFunctionHeaders.is_allowed_read_header_name(key));
    }
    remove(name) {
        if (!this.check(name)) {
            return false;
        }
        delete this._headers[this._getCaseInsensitiveKey(name)];
        return true;
    }
    set(name, value) {
        // Always store header values as an array to keep things normalized.
        if (Array.isArray(value)) {
            this._headers[this._getCaseInsensitiveKey(name)] = this._validateValue(value);
        }
        else {
            this._headers[this._getCaseInsensitiveKey(name)] = this._validateValue([value]);
        }
    }
    get_total_bytes() {
        let total_bytes = 0;
        Object.entries(this._headers).forEach(([key, value]) => {
            // Add 4 bytes for ": \r\n"
            if (Array.isArray(value)) {
                value.forEach(v => {
                    total_bytes += key.length + 4 + v.length;
                });
            }
            else {
                total_bytes += key.length + 4 + value.length;
            }
        });
        return total_bytes;
    }
    // Connascence of algorithm with Sailfish mod_wasm common.cc.
    static is_header_name_in_list(name, prohibitedHeaderNames) {
        return prohibitedHeaderNames.find(regex => name.match(regex)) !== undefined;
    }
    // Connascence of algorithm with Sailfish mod_wasm common.cc.
    static is_allowed_read_header_name(name) {
        if (EdgeFunctionHeaders.is_header_name_in_list(name, READ_ALLOWED_HEADER_NAMES_REGEX)) {
            return true;
        }
        return !EdgeFunctionHeaders.is_header_name_in_list(name, READ_PROHIBITED_HEADER_NAMES_REGEX);
    }
    // Connascence of algorithm with Sailfish mod_wasm common.cc.
    static is_allowed_write_header_name(name) {
        return (EdgeFunctionHeaders.is_allowed_read_header_name(name) &&
            !EdgeFunctionHeaders.is_header_name_in_list(name, WRITE_PROHIBITED_HEADER_NAMES_REGEX));
    }
    // Connascence of algorithm with Sailfish mod_wasm common.cc.
    static weak_is_valid_header_name(name) {
        return typeof name === 'string' && !name.match(WEAK_INVALID_HEADER_NAME_REGEX);
    }
    // Connascence of algorithm with Sailfish mod_wasm common.cc.
    static weak_is_valid_header_value(value) {
        return typeof value === 'string' && !value.match(WEAK_INVALID_HEADER_VALUE_REGEX);
    }
    // Connascence of algorithm with Sailfish mod_wasm hostcall.cc.
    static validate_header_name(name) {
        if (typeof name !== 'string' || name.length === 0) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, 'Header name is too short');
        }
        if (name.length > MAX_HEADER_NAME_LEN) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, `Header name is too long (length ${name.length} > ${MAX_HEADER_NAME_LEN})`);
        }
        if (!EdgeFunctionHeaders.weak_is_valid_header_name(name)) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, `Header name is invalid: ${name}`);
        }
        return WasmProgram_1.WasmError.ok();
    }
    // Connascence of algorithm with Sailfish mod_wasm hostcall.cc.
    static validate_header_value(value) {
        if (typeof value !== 'string' || value.length === 0) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, 'Header name is too short');
        }
        if (value.length > MAX_HEADER_VALUE_LEN) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, `Header value is too long (length ${value.length} > ${MAX_HEADER_VALUE_LEN})`);
        }
        if (!EdgeFunctionHeaders.weak_is_valid_header_value(value)) {
            return new WasmProgram_1.WasmError(WasmProgram_1.wasm_status.invalid_argument, `Header value is invalid: ${value}`);
        }
        return WasmProgram_1.WasmError.ok();
    }
}
exports.default = EdgeFunctionHeaders;
