#!/usr/bin/env ucode let fs = require("fs"); let ip = require("core.ip"); let domain_config = require("config.domain"); let common = require("core.common"); let as_string = common.as_string; let read_json_file = common.read_json_file; let write_json_file = common.write_json_file; let object_or_empty = common.object_or_empty; let array_or_empty = common.array_or_empty; function json_decode_text(text) { try { return json(as_string(text)); } catch (e) { return null; } } function write_text_file(path, text) { let result = fs.writefile(path, text); if (result == null) return false; if (type(result) == "boolean" && !result) return false; return true; } function array_from_value(value) { if (value == null) return []; if (type(value) == "array") return value; return [ value ]; } function trim_string(value) { let text = as_string(value); let start_match = match(text, /^[ \t\r\n]*/); let start = start_match ? length(start_match[0]) : 0; let end = length(text); while (end > start && match(substr(text, end - 1, 1), /[ \t\r\n]/)) end--; return substr(text, start, end - start); } function sort_values(values) { sort(values, function(first, second) { first = sprintf("%J", first); second = sprintf("%J", second); return first == second ? 0 : (first < second ? -1 : 1); }); return values; } function unique_values(values) { values = sort_values(values); let result = []; let previous = null; let has_previous = false; for (let value in values) { let encoded = sprintf("%J", value); if (!has_previous || encoded != previous) { push(result, value); previous = encoded; has_previous = true; } } return result; } function create_source(path) { if (!write_json_file(path, { version: 3, rules: [] })) exit(1); } function ruleset_tag(section, name, kind) { kind = as_string(kind); return kind == "" ? as_string(section) + "-" + as_string(name) + "-ruleset" : as_string(section) + "-" + as_string(name) + "-" + kind + "-ruleset"; } function patch_source_values(path, key, values) { let ruleset = object_or_empty(read_json_file(path)); values = array_or_empty(values); if (type(ruleset.rules) != "array") ruleset.rules = []; let found = false; for (let rule in ruleset.rules) { if (type(rule) == "object" && rule[key] != null) { let merged = []; for (let item in array_or_empty(rule[key])) push(merged, item); for (let item in values) push(merged, item); rule[key] = unique_values(merged); found = true; break; } } if (!found) { let rule = {}; rule[key] = values; push(ruleset.rules, rule); } if (!write_json_file(path, ruleset)) exit(1); } function patch_source(path, key, value_text) { patch_source_values(path, key, json_decode_text(value_text)); } function normalize_plain_ruleset_value(value, kind) { if (kind == "domains") return domain_config.suffix_to_ascii(value); if (kind == "subnets") return ip.nft_ip_or_cidr(value) ? value : null; return null; } function is_plain_ruleset_value(value, kind) { if (kind == "domains") return domain_config.valid_suffix(value); if (kind == "subnets") return ip.nft_ip_or_cidr(value); return false; } function write_optional_lines(path, lines) { if (path == null || path == "") return; if (!write_text_file(path, length(lines) > 0 ? join("\n", lines) + "\n" : "")) exit(1); } function import_plain_list(plain_path, ruleset_path, key, kind, chunk_size_text, invalid_path, counts_path) { let data = fs.readfile(plain_path); if (data == null) exit(1); let chunk_size = int(chunk_size_text || 5000); if (chunk_size < 1) chunk_size = 5000; let chunk = []; let invalid = []; let counts = []; for (let line in split(as_string(data), "\n")) { line = trim_string(line); if (line == "") continue; let normalized = normalize_plain_ruleset_value(line, kind); if (normalized == null) { push(invalid, line); continue; } push(chunk, normalized); if (length(chunk) == chunk_size) { push(counts, "" + length(chunk)); patch_source_values(ruleset_path, key, chunk); chunk = []; } } if (length(chunk) > 0) { push(counts, "" + length(chunk)); patch_source_values(ruleset_path, key, chunk); } write_optional_lines(invalid_path, invalid); write_optional_lines(counts_path, counts); } function extract_ip_cidr(json_path, output_path) { let ruleset = object_or_empty(read_json_file(json_path)); let lines = []; for (let rule in array_or_empty(ruleset.rules)) { if (type(rule) != "object") continue; for (let ip in array_or_empty(rule.ip_cidr)) push(lines, as_string(ip)); } if (!write_text_file(output_path, length(lines) > 0 ? join("\n", lines) + "\n" : "")) exit(1); } function parse_port_number(value) { let text = trim_string(value); if (text == "" || match(text, /[^0-9]/)) return null; let number = int(text); if (number < 1 || number > 65535) return null; return number; } function parse_port_interval(value, allow_open) { let text = trim_string(value); if (text == "") return null; let separator = index(text, ":") >= 0 ? ":" : (index(text, "-") >= 0 ? "-" : ""); if (separator == "") { let port = parse_port_number(text); return port == null ? null : { start: port, end: port }; } let parts = split(text, separator); if (length(parts) != 2) return null; let start = parts[0] == "" && allow_open ? 1 : parse_port_number(parts[0]); let end = parts[1] == "" && allow_open ? 65535 : parse_port_number(parts[1]); if (start == null || end == null || start > end) return null; return { start, end }; } function normalize_port_intervals(intervals) { let sorted = []; for (let interval in array_or_empty(intervals)) { if (type(interval) == "object" && interval.start != null && interval.end != null) push(sorted, { start: int(interval.start), end: int(interval.end) }); } sort(sorted, function(first, second) { if (first.start == second.start) return first.end - second.end; return first.start - second.start; }); let result = []; for (let interval in sorted) { if (length(result) == 0) { push(result, interval); continue; } let last = result[length(result) - 1]; if (interval.start <= last.end + 1) { if (interval.end > last.end) last.end = interval.end; } else { push(result, interval); } } return result; } function direct_port_filter(rule) { if (type(rule) != "object") return null; let intervals = []; for (let port in array_from_value(rule.port)) { let interval = parse_port_interval(port, false); if (interval != null) push(intervals, interval); } for (let port_range in array_from_value(rule.port_range)) { let interval = parse_port_interval(port_range, true); if (interval != null) push(intervals, interval); } return length(intervals) == 0 ? null : normalize_port_intervals(intervals); } function filter_is_empty(filter) { return type(filter) == "array" && length(filter) == 0; } function intersect_port_filters(first, second) { if (first == null) return second; if (second == null) return first; if (filter_is_empty(first) || filter_is_empty(second)) return []; let result = []; for (let left in first) { for (let right in second) { let start = left.start > right.start ? left.start : right.start; let end = left.end < right.end ? left.end : right.end; if (start <= end) push(result, { start, end }); } } return normalize_port_intervals(result); } function union_port_filters(first, second) { if (first == null || second == null) return null; let result = []; for (let item in first) push(result, item); for (let item in second) push(result, item); return normalize_port_intervals(result); } function extract_port_filter(rule) { if (type(rule) != "object") return null; let own_filter = direct_port_filter(rule); if (rule.type != "logical" || type(rule.rules) != "array") return own_filter; let mode = as_string(rule.mode); if (mode == "or") { let result = null; let initialized = false; for (let child in rule.rules) { let child_filter = extract_port_filter(child); if (!initialized) { result = child_filter; initialized = true; } else { result = union_port_filters(result, child_filter); } if (result == null) break; } return intersect_port_filters(own_filter, result); } let result = own_filter; for (let child in rule.rules) { result = intersect_port_filters(result, extract_port_filter(child)); if (filter_is_empty(result)) return result; } return result; } function nft_port_value(interval) { return interval.start == interval.end ? "" + interval.start : interval.start + "-" + interval.end; } function add_line(lines, value) { if (value != "") lines[value] = true; } function add_ip_cidr_values_to_nft_outputs(rule, port_filter, unscoped_lines, scoped_lines) { for (let ip in array_from_value(rule.ip_cidr)) { ip = trim_string(ip); if (ip == "") continue; if (port_filter == null) { add_line(unscoped_lines, ip); continue; } if (filter_is_empty(port_filter)) continue; for (let interval in port_filter) add_line(scoped_lines, ip + " . " + nft_port_value(interval)); } } function collect_ip_cidr_nft_outputs(rule, inherited_filter, unscoped_lines, scoped_lines) { if (type(rule) != "object") return; let own_filter = direct_port_filter(rule); let filter = intersect_port_filters(inherited_filter, own_filter); if (filter_is_empty(filter)) return; if (rule.type == "logical" && type(rule.rules) == "array") { if (as_string(rule.mode) == "and") { for (let child in rule.rules) { filter = intersect_port_filters(filter, extract_port_filter(child)); if (filter_is_empty(filter)) return; } } add_ip_cidr_values_to_nft_outputs(rule, filter, unscoped_lines, scoped_lines); for (let child in rule.rules) collect_ip_cidr_nft_outputs(child, filter, unscoped_lines, scoped_lines); return; } add_ip_cidr_values_to_nft_outputs(rule, filter, unscoped_lines, scoped_lines); } function write_lines(path, lines_object) { let lines = keys(lines_object); sort(lines, function(first, second) { return first == second ? 0 : (first < second ? -1 : 1); }); if (!write_text_file(path, length(lines) > 0 ? join("\n", lines) + "\n" : "")) exit(1); } function extract_ip_cidr_nft_elements(json_path, unscoped_output_path, scoped_output_path, ports_text, port_ranges_text) { let ruleset = object_or_empty(read_json_file(json_path)); let unscoped_lines = {}; let scoped_lines = {}; let outer_filter = direct_port_filter({ port: array_or_empty(json_decode_text(ports_text)), port_range: array_or_empty(json_decode_text(port_ranges_text)) }); for (let rule in array_or_empty(ruleset.rules)) collect_ip_cidr_nft_outputs(rule, outer_filter, unscoped_lines, scoped_lines); write_lines(unscoped_output_path, unscoped_lines); write_lines(scoped_output_path, scoped_lines); } function value_has_domain_matchers(value) { if (type(value) == "array") { for (let item in value) { if (value_has_domain_matchers(item)) return true; } return false; } if (type(value) != "object") return false; for (let key, item in value) { if (key == "domain" || key == "domain_suffix" || key == "domain_keyword" || key == "domain_regex") { if (type(item) == "array" && length(item) > 0) return true; if (type(item) == "string" && item != "") return true; } if (value_has_domain_matchers(item)) return true; } return false; } function has_domain_matchers(path) { return value_has_domain_matchers(read_json_file(path)); } function has_rules(path) { let ruleset = object_or_empty(read_json_file(path)); return length(array_or_empty(ruleset.rules)) > 0; } function module_exports() { return { create_source, patch_source, patch_source_values, import_plain_list, extract_ip_cidr, extract_ip_cidr_nft_elements, has_domain_matchers, has_rules, ruleset_tag, read_json_file, write_json_file }; } if (sourcepath(1) != null && sourcepath(1) != "") return module_exports(); let mode = ARGV[0] || ""; if (mode == "create-source") create_source(ARGV[1]); else if (mode == "tag") print(ruleset_tag(ARGV[1], ARGV[2], ARGV[3]), "\n"); else if (mode == "patch-source") patch_source(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "import-plain-list") import_plain_list(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7]); else if (mode == "extract-ip-cidr") extract_ip_cidr(ARGV[1], ARGV[2]); else if (mode == "extract-ip-cidr-nft") extract_ip_cidr_nft_elements(ARGV[1], ARGV[2], ARGV[3], ARGV[4] || "[]", ARGV[5] || "[]"); else if (mode == "has-domain-matchers") exit(has_domain_matchers(ARGV[1]) ? 0 : 1); else if (mode == "has-rules") exit(has_rules(ARGV[1]) ? 0 : 1); else { warn("Usage: routing/rulesets.uc ...\n"); exit(1); }