"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Request = exports.Firewall = void 0;
/* eslint @typescript-eslint/no-explicit-any: 0 */
const wirefilter_1 = require("./wirefilter");
const node_fetch_1 = require("node-fetch");
const ip_address_1 = require("ip-address");
const path = __importStar(require("path"));
function wirefilterString(s) {
    const str = new wirefilter_1.wirefilter_externally_allocated_str_t();
    str.data = s;
    str.length = s.length;
    return str;
}
function wirefilterByteArray(s) {
    const str = new wirefilter_1.wirefilter_externally_allocated_byte_arr_t();
    str.data = Buffer.alloc(s.length, s);
    str.length = s.length;
    return str;
}
function checkAdded(added, msg) {
    if (!added) {
        throw new Error(msg);
    }
}
function addString(wirefilter, scheme, name) {
    checkAdded(wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter_1.WIREFILTER_TYPE_BYTES), `Failed to add ${name} to the scheme`);
}
// Map<String, Array<String>>
function addMapToStrArray(wirefilter, scheme, name) {
    checkAdded(wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter.wirefilter_create_map_type(wirefilter.wirefilter_create_array_type(wirefilter_1.WIREFILTER_TYPE_BYTES))), `Failed to add ${name} to the scheme`);
}
// Array<String>
function addStrArray(wirefilter, scheme, name) {
    checkAdded(wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter.wirefilter_create_array_type(wirefilter_1.WIREFILTER_TYPE_BYTES)), `Failed to add ${name} to the scheme`);
}
function addIPList(wirefilter, scheme, name) {
    checkAdded(wirefilter.wirefilter_add_type_list_to_scheme(scheme, wirefilter_1.WIREFILTER_TYPE_IP, wirefilter.wirefilter_create_never_list()), `Failed to add ${name} to the scheme`);
}
function addIPaddr(wirefilter, scheme, name) {
    wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter_1.WIREFILTER_TYPE_IP);
}
function addNumber(wirefilter, scheme, name) {
    wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter_1.WIREFILTER_TYPE_INT);
}
function addBoolen(wirefilter, scheme, name) {
    wirefilter.wirefilter_add_type_field_to_scheme(scheme, wirefilterString(name), wirefilter_1.WIREFILTER_TYPE_BOOL);
}
function parsePort(req) {
    if (req.port) {
        return req.port;
    }
    const url = new URL(req.url);
    if (url.port) {
        return parseInt(url.port, 10);
    }
    if (url.protocol == 'https:') {
        return 443;
    }
    return 80;
}
// the map isn't URL-decoded, so can't use searchParams since re-encoding can change
// the values.
function paramsToMap(params) {
    if (params.length == 0) {
        return new Map();
    }
    const map = new Map();
    if (params[0] == '?') {
        params = params.substring(1);
    }
    params.split('&')
        .map(v => v.split('='))
        .forEach(pair => {
        const [key, val] = pair;
        const arr = map.get(key);
        if (arr) {
            arr.push(val);
        }
        else {
            map.set(key, [val]);
        }
    });
    return map;
}
function paramNamesToArray(params) {
    if (params.length == 0) {
        return [];
    }
    return params.substring(1).split('&')
        .map(v => v.split('=')[0]);
}
function paramValuesToArray(params) {
    if (params.length == 0) {
        return [];
    }
    return params.substring(1).split('&')
        .map(v => v.split('=')[1]);
}
function headersToMap(headers) {
    const map = new Map();
    headers.forEach((val, key) => {
        const arr = map.get(key);
        if (arr) {
            arr.push(val);
        }
        else {
            map.set(key, [val]);
        }
    });
    return map;
}
/**
 * Firewall that defines the schema for the rules supported by the Cloudflare WAF language.
 *
 * see https://developers.cloudflare.com/firewall/cf-firewall-language/
 */
class Firewall {
    constructor() {
        let libPath;
        let libName;
        switch (process.platform) {
            case 'darwin':
                libName = 'libwirefilter_ffi';
                if (process.arch == 'arm64') {
                    libName += '_aarch64';
                }
                libPath = path.join(__dirname, '..', 'lib', `${libName}.dylib`);
                break;
            case 'linux':
                libPath = path.join(__dirname, '..', 'lib', 'libwirefilter_ffi.so');
                break;
            default:
                throw new Error(`Unsupported platform: ${process.platform}`);
        }
        const wirefilter = wirefilter_1.initWirefilter(libPath);
        const scheme = wirefilter.wirefilter_create_scheme();
        // add transformation functions
        wirefilter.add_standard_functions(scheme);
        // Standard fields
        addString(wirefilter, scheme, 'http.cookie');
        addString(wirefilter, scheme, 'http.host');
        addString(wirefilter, scheme, 'http.referer');
        addString(wirefilter, scheme, 'http.request.full_uri');
        addString(wirefilter, scheme, 'http.request.method');
        addString(wirefilter, scheme, 'http.request.uri');
        addString(wirefilter, scheme, 'http.request.uri.path');
        addString(wirefilter, scheme, 'http.request.uri.query');
        addString(wirefilter, scheme, 'http.user_agent');
        addNumber(wirefilter, scheme, 'http.request.version');
        addString(wirefilter, scheme, 'http.x_forwarded_for');
        addIPaddr(wirefilter, scheme, 'ip.src');
        addNumber(wirefilter, scheme, 'ip.geoip.asnum');
        addString(wirefilter, scheme, 'ip.geoip.continent');
        addString(wirefilter, scheme, 'ip.geoip.country');
        addString(wirefilter, scheme, 'ip.geoip.subdivision_1_iso_code');
        addString(wirefilter, scheme, 'ip.geoip.subdivision_2_iso_code');
        addBoolen(wirefilter, scheme, 'ip.geoip.is_in_european_union');
        addBoolen(wirefilter, scheme, 'ssl');
        // Argument and value fields for URIs
        addMapToStrArray(wirefilter, scheme, 'http.request.uri.args');
        addStrArray(wirefilter, scheme, 'http.request.uri.args.names');
        addStrArray(wirefilter, scheme, 'http.request.uri.args.values');
        // Header fields
        addMapToStrArray(wirefilter, scheme, 'http.request.headers');
        addStrArray(wirefilter, scheme, 'http.request.headers.names');
        addStrArray(wirefilter, scheme, 'http.request.headers.values');
        addBoolen(wirefilter, scheme, 'http.request.headers.truncated');
        // Body fields
        addString(wirefilter, scheme, 'http.request.body.raw');
        addBoolen(wirefilter, scheme, 'http.request.body.truncated');
        addMapToStrArray(wirefilter, scheme, 'http.request.body.form');
        addStrArray(wirefilter, scheme, 'http.request.body.form.names');
        addStrArray(wirefilter, scheme, 'http.request.body.form.values');
        // Dynamic fields
        addString(wirefilter, scheme, 'cf.bot_management.ja3_hash');
        addNumber(wirefilter, scheme, 'cf.bot_management.score');
        addBoolen(wirefilter, scheme, 'cf.bot_management.verified_bot');
        addBoolen(wirefilter, scheme, 'cf.bot_management.corporate_proxy');
        addNumber(wirefilter, scheme, 'cf.threat_score');
        addNumber(wirefilter, scheme, 'cf.edge.server_port');
        addBoolen(wirefilter, scheme, 'cf.client.bot');
        addNumber(wirefilter, scheme, 'cf.client_trust_score');
        addIPList(wirefilter, scheme, 'any?');
        this.wirefilter = wirefilter;
        this.scheme = scheme;
    }
    /**
     * Creates a firewall rule from a WAF language expression.
     *
     * @param rule waf language expression
     */
    createRule(rule) {
        return new WirefilterFirewallRule(this.wirefilter, this.scheme, rule);
    }
}
exports.Firewall = Firewall;
/**
 * A request potentially augmented with the Cloudflare extension fields.
 */
class Request extends node_fetch_1.Request {
    constructor(input, init) {
        super(input, init);
        this.cf = (init || {}).cf || {};
    }
}
exports.Request = Request;
class WirefilterFirewallRule {
    constructor(wirefilter, //
    scheme, //
    rule) {
        this.wirefilter = wirefilter;
        this.scheme = scheme;
        this.rule = rule;
        const parsingResult = wirefilter.wirefilter_parse_filter(scheme, wirefilterString(rule));
        if (parsingResult.ok.success != 1) {
            throw Error(`Can't parse the rule: ${parsingResult.err.msg.data}`);
        }
        const ast = parsingResult.ok.ast;
        const compilingResult = wirefilter.wirefilter_compile_filter(ast);
        if (compilingResult.ok.success != 1) {
            throw Error(`Can't compile the rule: ${compilingResult.err.msg.data}`);
        }
        this.filter = compilingResult.ok.filter;
    }
    match(req) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
        const wirefilter = this.wirefilter;
        const exec_ctx = wirefilter.wirefilter_create_execution_context(this.scheme);
        const url = new URL(req.url);
        // Standard fields
        this.addStringToCtx(exec_ctx, 'http.cookie', (_a = req.headers.get('Cookie')) !== null && _a !== void 0 ? _a : '');
        this.addStringToCtx(exec_ctx, 'http.host', url.hostname);
        this.addStringToCtx(exec_ctx, 'http.referer', (_b = req.headers.get('Referer')) !== null && _b !== void 0 ? _b : '');
        this.addStringToCtx(exec_ctx, 'http.request.full_uri', req.url);
        this.addStringToCtx(exec_ctx, 'http.request.method', req.method);
        this.addStringToCtx(exec_ctx, 'http.request.uri', `${url.pathname}${url.search}`);
        this.addStringToCtx(exec_ctx, 'http.request.uri.path', url.pathname);
        this.addStringToCtx(exec_ctx, 'http.request.uri.query', url.searchParams.toString());
        this.addStringToCtx(exec_ctx, 'http.user_agent', (_c = req.headers.get('User-Agent')) !== null && _c !== void 0 ? _c : '');
        this.addNumberToCtx(exec_ctx, 'http.request.version', (_d = req.cf['http.request.version']) !== null && _d !== void 0 ? _d : 1);
        this.addStringToCtx(exec_ctx, 'http.x_forwarded_for', (_e = req.headers.get('X-Forwarded-For')) !== null && _e !== void 0 ? _e : '');
        this.addIpAddrToCtx(exec_ctx, 'ip.src', (_f = req.cf['ip.src']) !== null && _f !== void 0 ? _f : '0.0.0.0');
        this.addNumberToCtx(exec_ctx, 'ip.geoip.asnum', (_g = req.cf['ip.geoip.asnum']) !== null && _g !== void 0 ? _g : 0);
        this.addStringToCtx(exec_ctx, 'ip.geoip.continent', (_h = req.cf['ip.geoip.continent']) !== null && _h !== void 0 ? _h : '');
        this.addStringToCtx(exec_ctx, 'ip.geoip.country', (_j = req.cf['ip.geoip.country']) !== null && _j !== void 0 ? _j : '');
        this.addStringToCtx(exec_ctx, 'ip.geoip.subdivision_1_iso_code', (_k = req.cf['ip.geoip.subdivision_1_iso_code']) !== null && _k !== void 0 ? _k : '');
        this.addStringToCtx(exec_ctx, 'ip.geoip.subdivision_2_iso_code', (_l = req.cf['ip.geoip.subdivision_2_iso_code']) !== null && _l !== void 0 ? _l : '');
        this.addBoolenToCtx(exec_ctx, 'ip.geoip.is_in_european_union', (_m = req.cf['ip.geoip.is_in_european_union']) !== null && _m !== void 0 ? _m : false);
        this.addBoolenToCtx(exec_ctx, 'ssl', url.protocol === 'https:');
        // Argument and value fields for URIs
        this.addMapStrToCtx(exec_ctx, 'http.request.uri.args', paramsToMap(url.search));
        this.addStrArrayCtx(exec_ctx, 'http.request.uri.args.names', paramNamesToArray(url.search));
        this.addStrArrayCtx(exec_ctx, 'http.request.uri.args.values', paramValuesToArray(url.search));
        // Header fields
        this.addMapStrToCtx(exec_ctx, 'http.request.headers', headersToMap(req.headers));
        this.addStrArrayCtx(exec_ctx, 'http.request.headers.names', [...req.headers.keys()]);
        this.addStrArrayCtx(exec_ctx, 'http.request.headers.values', [].concat(...req.headers.values()));
        this.addBoolenToCtx(exec_ctx, 'http.request.headers.truncated', (_o = req.cf['http.request.headers.truncated']) !== null && _o !== void 0 ? _o : false);
        // Body fields
        this.addStringToCtx(exec_ctx, 'http.request.body.raw', String((_p = req.body) !== null && _p !== void 0 ? _p : ''));
        this.addBoolenToCtx(exec_ctx, 'http.request.body.truncated', (_q = req.cf['http.request.body.truncated']) !== null && _q !== void 0 ? _q : false);
        let bodyParams;
        if (((_r = req.headers.get('Content-Type')) !== null && _r !== void 0 ? _r : '').startsWith('application/x-www-form-urlencoded')) {
            bodyParams = paramsToMap(String((_s = req.body) !== null && _s !== void 0 ? _s : ''));
        }
        else {
            bodyParams = new Map();
        }
        this.addMapStrToCtx(exec_ctx, 'http.request.body.form', bodyParams);
        this.addStrArrayCtx(exec_ctx, 'http.request.body.form.names', [...bodyParams.keys()]);
        this.addStrArrayCtx(exec_ctx, 'http.request.body.form.values', [].concat(...bodyParams.values()));
        // Dynamic fields
        this.addStringToCtx(exec_ctx, 'cf.bot_management.ja3_hash', (_t = req.cf['cf.bot_management.ja3_hash']) !== null && _t !== void 0 ? _t : '');
        this.addNumberToCtx(exec_ctx, 'cf.bot_management.score', (_u = req.cf['cf.bot_management.score']) !== null && _u !== void 0 ? _u : 100);
        this.addBoolenToCtx(exec_ctx, 'cf.bot_management.verified_bot', (_v = req.cf['cf.bot_management.verified_bot']) !== null && _v !== void 0 ? _v : false);
        this.addBoolenToCtx(exec_ctx, 'cf.bot_management.corporate_proxy', (_w = req.cf['cf.bot_management.corporate_proxy']) !== null && _w !== void 0 ? _w : false);
        this.addNumberToCtx(exec_ctx, 'cf.client_trust_score', (_x = req.cf['cf.client_trust_score']) !== null && _x !== void 0 ? _x : 100);
        this.addNumberToCtx(exec_ctx, 'cf.threat_score', (_y = req.cf['cf.threat_score']) !== null && _y !== void 0 ? _y : 100);
        this.addNumberToCtx(exec_ctx, 'cf.edge.server_port', parsePort(req));
        this.addBoolenToCtx(exec_ctx, 'cf.client.bot', (_z = req.cf['cf.client.bot']) !== null && _z !== void 0 ? _z : false);
        // IP Lists
        checkAdded(this.wirefilter.set_nevermatch_iplist(exec_ctx), 'can\'t add nevermatch list');
        try {
            const matchResult = wirefilter.wirefilter_match(this.filter, exec_ctx);
            if (matchResult.ok.success != 1) {
                throw Error(`Filter can't be matched: ${matchResult.err.msg.data}`);
            }
            return matchResult.ok.value;
        }
        finally {
            wirefilter.wirefilter_free_execution_context(exec_ctx);
        }
    }
    addNumberToCtx(execCtx, name, value) {
        checkAdded(this.wirefilter.wirefilter_add_int_value_to_execution_context(execCtx, wirefilterString(name), value), `Failed to add ${name}=${value} to the context`);
    }
    addStringToCtx(execCtx, name, value) {
        checkAdded(this.wirefilter.wirefilter_add_bytes_value_to_execution_context(execCtx, wirefilterString(name), wirefilterByteArray(value)), `Failed to add ${name}=${value} to the context`);
    }
    addBoolenToCtx(execCtx, name, value) {
        checkAdded(this.wirefilter.wirefilter_add_bool_value_to_execution_context(execCtx, wirefilterString(name), value), `Failed to add ${name}=${value} to the context`);
    }
    addIpAddrToCtx(execCtx, name, value) {
        if (value.indexOf('.') != -1) {
            const ipv4 = new ip_address_1.Address4(value).toArray();
            checkAdded(this.wirefilter.wirefilter_add_ipv4_value_to_execution_context(execCtx, wirefilterString(name), ipv4), `Failed to add ${name}=${value} to the context`);
        }
        else if (value.indexOf(':') != -1) {
            const ipv6 = new ip_address_1.Address6(value).toUnsignedByteArray();
            checkAdded(this.wirefilter.wirefilter_add_ipv6_value_to_execution_context(execCtx, wirefilterString(name), ipv6), `Failed to add ${name}=${value} to the context`);
        }
        else {
            throw new Error(`Can't parse IP address: ${value}`);
        }
    }
    addMapStrToCtx(execCtx, name, value) {
        const arrayType = this.wirefilter.wirefilter_create_array_type(wirefilter_1.WIREFILTER_TYPE_BYTES);
        const mapValue = this.wirefilter.wirefilter_create_map(arrayType);
        value.forEach((mapVal, mapKey) => {
            const arr = this.wirefilter.wirefilter_create_array(wirefilter_1.WIREFILTER_TYPE_BYTES);
            mapVal.forEach((val, ind) => {
                checkAdded(this.wirefilter.wirefilter_add_bytes_value_to_array(arr, ind, wirefilterByteArray(val)), `Failed to add a ${val} to array`);
            });
            checkAdded(this.wirefilter.wirefilter_add_array_value_to_map(mapValue, wirefilterString(mapKey), arr), `Failed to add ${mapKey} value to map`);
        });
        checkAdded(this.wirefilter.wirefilter_add_map_value_to_execution_context(execCtx, wirefilterString(name), mapValue), `Failed to add ${name}=${JSON.stringify(value)} to the context`);
    }
    addStrArrayCtx(execCtx, name, value) {
        const arr = this.wirefilter.wirefilter_create_array(wirefilter_1.WIREFILTER_TYPE_BYTES);
        value.forEach((val, ind) => {
            checkAdded(this.wirefilter.wirefilter_add_bytes_value_to_array(arr, ind, wirefilterByteArray(val)), `Failed to add a ${val} to array`);
        });
        checkAdded(this.wirefilter.wirefilter_add_array_value_to_execution_context(execCtx, wirefilterString(name), arr), `Failed to add ${name}=${JSON.stringify(value)} to the context`);
    }
}
//# sourceMappingURL=firewall.js.map