geofeed-finder
Version:
A tool to find geofeed files in rpsl and parse them correctly according to draft-ietf-opsawg-finding-geofeeds
456 lines (453 loc) • 23.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _batchPromises = _interopRequireDefault(require("batch-promises"));
var _redaxios = _interopRequireDefault(require("redaxios"));
var _bulkWhoisParser = _interopRequireDefault(require("bulk-whois-parser"));
var _longestPrefixMatch = _interopRequireDefault(require("longest-prefix-match"));
var _csvParser = _interopRequireDefault(require("./csvParser"));
var _md = _interopRequireDefault(require("md5"));
var _fs = _interopRequireDefault(require("fs"));
var _moment = _interopRequireDefault(require("moment"));
var _ipSub = _interopRequireDefault(require("ip-sub"));
var _whoisWrapper = require("whois-wrapper");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
require("events").EventEmitter.defaultMaxListeners = 200;
var Finder = exports["default"] = /*#__PURE__*/_createClass(function Finder(params) {
var _this = this;
_classCallCheck(this, Finder);
_defineProperty(this, "filterFunction", function (inetnum) {
var _inetnum$remarks;
if (inetnum.geofeed && _this.matchGeofeedFile(inetnum.geofeed).length) {
return true;
}
if ((inetnum === null || inetnum === void 0 || (_inetnum$remarks = inetnum.remarks) === null || _inetnum$remarks === void 0 ? void 0 : _inetnum$remarks.length) > 0) {
return inetnum.remarks.some(_this.testGeofeedRemark);
}
return false;
});
_defineProperty(this, "getBlocks", function () {
var selector = _this.params.af.map(function (i) {
return i === 4 ? "inetnum" : "inet6num";
});
return _this.whois.getObjects(selector, _this.filterFunction, ["inetnum", "inet6num", "remarks", "geofeed", "last-updated"]).then(function (blocks) {
return blocks.flat().filter(function (i) {
return !!i.inetnum || !!i.inet6num;
});
})["catch"](_this.logger.log);
});
_defineProperty(this, "_getFileName", function (file) {
return _this.cacheDir + (0, _md["default"])(file);
});
_defineProperty(this, "_setGeofeedCacheHeaders", function (response, cachedFile) {
var _this$cacheHeadersInd;
var setAge = 3600 * 24 * (_this.params.geofeedCacheDays || 7); // default 1 week (see draft)
if (response.headers["cache-control"]) {
var _maxAge$split$pop, _maxAge$split;
var maxAge = response.headers["cache-control"].split(",").filter(function (h) {
return h.includes("max-age");
}).map(function (h) {
return h.trim();
}).pop();
var age = (_maxAge$split$pop = maxAge === null || maxAge === void 0 || (_maxAge$split = maxAge.split("=")) === null || _maxAge$split === void 0 ? void 0 : _maxAge$split.pop()) !== null && _maxAge$split$pop !== void 0 ? _maxAge$split$pop : 0;
age = isNaN(age) ? 0 : age;
setAge = Math.min(Math.max(parseInt(age), 3600), 3600 * 24 * 7); // Min 1 hour, max 1 week of cache (to avoid random max-age settings)
}
_this.cacheHeadersIndex[cachedFile] = (_this$cacheHeadersInd = _this.cacheHeadersIndex[cachedFile]) !== null && _this$cacheHeadersInd !== void 0 ? _this$cacheHeadersInd : (0, _moment["default"])(_this.startTime).add(setAge, "seconds");
});
_defineProperty(this, "_isCachedGeofeedValid", function (cachedFile) {
if (_this.params.test) {
return false;
} else {
return _fs["default"].existsSync(cachedFile) && _this.cacheHeadersIndex[cachedFile] && (0, _moment["default"])(_this.cacheHeadersIndex[cachedFile]).isSameOrAfter(_this.startTime);
}
});
_defineProperty(this, "_importCacheHeaderIndex", function () {
var tmp;
if (_fs["default"].existsSync(_this.cacheHeadersIndexFileName)) {
tmp = JSON.parse(_fs["default"].readFileSync(_this.cacheHeadersIndexFileName, "utf-8"));
for (var key in tmp) {
tmp[key] = (0, _moment["default"])(tmp[key]);
}
}
_this.cacheHeadersIndex = tmp || {};
});
_defineProperty(this, "_persistCacheIndex", function () {
_fs["default"].writeFileSync(_this.cacheHeadersIndexFileName, JSON.stringify(_this.cacheHeadersIndex));
});
_defineProperty(this, "logEntry", function (file, cache) {
console.log("".concat(file, " ").concat(cache ? "[cache]" : "[download]"));
});
_defineProperty(this, "_getGeofeedFile", function (file) {
var abortTimeout = parseInt(_this.params.downloadTimeout) * 1000;
return new Promise(function (resolve, reject) {
var timeout = setTimeout(function () {
_this.logger.log("Error: ".concat(file, " timeout"));
resolve(null);
}, abortTimeout);
var resolveAndClear = function resolveAndClear(data) {
resolve(data);
clearTimeout(timeout);
};
var cachedFile = _this._getFileName(file);
if (_this._isCachedGeofeedValid(cachedFile)) {
_this.logEntry(file, true);
resolveAndClear();
} else {
_this.logEntry(file, false);
(0, _redaxios["default"])({
url: file,
method: "GET",
timeout: abortTimeout
}).then(function (response) {
var data = response.data;
if (/<a|<div|<span|<style|<link/gi.test(data)) {
var message = "Error: ".concat(file, " is not CSV but HTML, stop with this nonsense!");
_this.logger.log(message);
console.log(message);
resolveAndClear(null);
} else {
_fs["default"].writeFileSync(cachedFile, data);
_this._setGeofeedCacheHeaders(response, cachedFile);
resolveAndClear();
}
})["catch"](function (error) {
var _error$message;
_this.logger.log("Error: ".concat(file, " ").concat((_error$message = error === null || error === void 0 ? void 0 : error.message) !== null && _error$message !== void 0 ? _error$message : "Unknown error"));
resolveAndClear();
});
}
}).then(function () {}) // Avoid empty logs
["catch"](function () {}); // Avoid empty logs
});
_defineProperty(this, "getGeofeedsFiles", function (blocks) {
var out = [];
var uniqueBlocks = _toConsumableArray(new Set(blocks.map(function (i) {
return i.geofeed;
})));
var half = Math.floor(uniqueBlocks.length / 2);
// pre load all files
return Promise.all([(0, _batchPromises["default"])(10, uniqueBlocks.slice(0, half), function (file) {
return _this._getGeofeedFile(file);
}), (0, _batchPromises["default"])(10, uniqueBlocks.slice(half), function (file) {
return _this._getGeofeedFile(file);
})]).then(function () {
console.log("All files downloaded. Processing files.");
var _iterator = _createForOfIteratorHelper(blocks),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var block = _step.value;
var cachedFile = _this._getFileName(block.geofeed);
try {
var _data = _fs["default"].readFileSync(cachedFile, "utf8");
if (_data && _data.length) {
out.push(_this.validateGeofeeds(_this.csvParser.parse(block.inetnum, _data)));
}
} catch (error) {
// Nothing - these are files that are not CSV
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var data = out.flat();
var _iterator2 = _createForOfIteratorHelper(data),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var g = _step2.value;
if (!_this.params.includeZip) {
g.zip = null;
}
g.af = _ipSub["default"].getAddressFamily(g.prefix);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
return data;
});
});
_defineProperty(this, "validateGeofeeds", function (geofeeds) {
return geofeeds.filter(function (geofeed) {
return !!(geofeed !== null && geofeed !== void 0 && geofeed.inetnum) && !!(geofeed !== null && geofeed !== void 0 && geofeed.prefix);
}).filter(function (geofeed) {
var errors = geofeed.validate();
if (_this.params.keepInvalidSubdivisions || _this.params.removeInvalidSubdivisions) {
var noSubErrors = errors.filter(function (i) {
return !i.includes("Not valid Subdivision Code") && !i.includes("The Subdivision is not inside the Country");
});
if (_this.params.removeInvalidSubdivisions && noSubErrors.length !== errors.length) {
geofeed.region = null; // If there is an error in the region and removeInvalidSubdivisions=true, remove the region
}
errors = noSubErrors; // Ignore subdivision errors.
}
if (errors.length > 0) {
var message = "".concat(geofeed, " ").concat(errors.join(", "));
if (_this.params.test) {
console.log(message);
}
_this.logger.log(message);
}
return _this.params.keepNonIso || errors.length === 0;
});
});
_defineProperty(this, "getMostUpdatedInetnums", function (inetnums) {
var index = {};
var _iterator3 = _createForOfIteratorHelper(inetnums),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var inetnum = _step3.value;
index[inetnum.inetnum] = !index[inetnum.inetnum] || index[inetnum.inetnum].lastUpdate < inetnum.lastUpdate ? inetnum : index[inetnum.inetnum];
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return Object.values(index);
});
_defineProperty(this, "setGeofeedPriority", function () {
var geofeeds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
console.log("Validating prefix ownership");
return [].concat(_toConsumableArray(_this.params.af.includes(4) ? _this._setGeofeedPriority(geofeeds.filter(function (i) {
return i.af === 4;
})) : []), _toConsumableArray(_this.params.af.includes(6) ? _this._setGeofeedPriority(geofeeds.filter(function (i) {
return i.af === 6;
})) : [])).flat();
});
_defineProperty(this, "_setGeofeedPriority", function () {
var geofeeds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var longestPrefixMatch = new _longestPrefixMatch["default"]();
var tmp = {};
for (var _i = 0, _arr = _toConsumableArray(new Set(geofeeds.map(function (i) {
return i.inetnum;
}))); _i < _arr.length; _i++) {
var inetnum = _arr[_i];
longestPrefixMatch.addPrefix(inetnum, inetnum);
}
var _iterator4 = _createForOfIteratorHelper(geofeeds),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var geofeed = _step4.value;
var _inetnum = longestPrefixMatch.getMatch(geofeed.prefix, false);
if (_inetnum && _inetnum.length === 1 && geofeed.inetnum === _inetnum[0]) {
var _geofeed$prefix, _tmp$_geofeed$prefix;
(_tmp$_geofeed$prefix = tmp[_geofeed$prefix = geofeed.prefix]) !== null && _tmp$_geofeed$prefix !== void 0 ? _tmp$_geofeed$prefix : tmp[_geofeed$prefix] = geofeed;
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
return Object.values(tmp);
});
_defineProperty(this, "testGeofeedRemark", function (remark) {
return /^Geofeed:?\s+https?:\/\/\S+/gi.test(remark);
});
_defineProperty(this, "testGeofeedRemarkStrict", function (remark) {
return /^Geofeed https?:\/\/\S+/gi.test(remark);
});
_defineProperty(this, "matchGeofeedFile", function (remark) {
return remark.match(/\bhttps?:\/\/\S+/gi) || [];
});
_defineProperty(this, "translateObject", function (object) {
var _object$remarks, _object$geofeed;
var inetnum = object.inetnum || object.inet6num;
var remarks = (_object$remarks = object.remarks) !== null && _object$remarks !== void 0 ? _object$remarks : [];
var geofeedField = object !== null && object !== void 0 && (_object$geofeed = object.geofeed) !== null && _object$geofeed !== void 0 && _object$geofeed.length ? _this.matchGeofeedFile(object.geofeed).pop() : null;
var inetnums = [inetnum];
if (!inetnum.includes("/")) {
var ips = inetnum.split("-").map(function (ip) {
return ip.trim();
});
inetnums = _ipSub["default"].ipRangeToCidr(ips[0], ips[1]);
}
var lastUpdate = (0, _moment["default"])(object["last-updated"]);
var remark = remarks.find(function (i) {
return i.toLowerCase().startsWith("geofeed");
});
var geofeed = null;
if (geofeedField) {
geofeed = geofeedField;
} else if (remark) {
geofeed = _this.matchGeofeedFile(remark).pop();
}
return inetnums.map(function (inetnum) {
return {
inetnum: inetnum,
geofeed: geofeed,
lastUpdate: lastUpdate
};
});
});
_defineProperty(this, "getGeofeedInetnumPairs", function () {
try {
if (_this.params.test) {
var prefix = _ipSub["default"].toPrefix(_this.params.test.toString().trim());
if (!_ipSub["default"].isValidPrefix(prefix) && !_ipSub["default"].isValidIP(prefix)) {
throw new Error("The input must be an IP or a prefix");
}
var index = {};
return (0, _whoisWrapper.lessSpecific)({
flag: "h",
query: prefix
}, function (data) {
var flat = data.map(function (i) {
return i.data;
}).flat().flat().flat();
var geofeedAttributes = flat.filter(function (i) {
return i.key.toLowerCase() === "geofeed";
});
var remarks = flat.filter(function (i) {
return ["remarks", "comment"].includes(i.key.toLowerCase());
});
return [].concat(_toConsumableArray(geofeedAttributes), _toConsumableArray(remarks)).length > 0;
}, 12).then(function (data) {
return data.map(function (i) {
return i.data;
}).flat();
}).then(function (answers) {
var items = answers.filter(function (i) {
return i.find(function (i) {
return ["inetnum", "inet6num", "netrange"].includes(i.key.toLowerCase());
}) && (i.find(function (i) {
return i.key === "geofeed";
}) || i.find(function (i) {
var _i$value;
return ["remarks", "comment"].includes(i.key.toLowerCase()) && ((_i$value = i.value) === null || _i$value === void 0 ? void 0 : _i$value.some(_this.testGeofeedRemark));
}));
});
var rangeToPrefix = function rangeToPrefix(inetnum) {
return inetnum !== null && inetnum !== void 0 && inetnum.includes("-") ? _ipSub["default"].ipRangeToCidr.apply(_ipSub["default"], _toConsumableArray(inetnum === null || inetnum === void 0 ? void 0 : inetnum.split("-").map(function (n) {
return n.trim();
}))) : [inetnum];
};
var _iterator5 = _createForOfIteratorHelper(items),
_step5;
try {
var _loop = function _loop() {
var _item$find, _item$find2, _item$find3, _this$matchGeofeedFil;
var item = _step5.value;
var inetnums = rangeToPrefix((_item$find = item.find(function (i) {
return ["inetnum", "inet6num", "netrange"].includes(i.key.toLowerCase());
})) === null || _item$find === void 0 ? void 0 : _item$find.value);
var geofeedAttributes = (_item$find2 = item.find(function (i) {
return i.key === "geofeed";
})) === null || _item$find2 === void 0 ? void 0 : _item$find2.value;
var remarks = (_item$find3 = item.find(function (i) {
var _i$value2;
return ["remarks", "comment"].includes(i.key.toLowerCase()) && ((_i$value2 = i.value) === null || _i$value2 === void 0 ? void 0 : _i$value2.some(_this.testGeofeedRemark));
})) === null || _item$find3 === void 0 ? void 0 : _item$find3.value.find(_this.testGeofeedRemark);
var geofeed = (_this$matchGeofeedFil = _this.matchGeofeedFile(geofeedAttributes !== null && geofeedAttributes !== void 0 ? geofeedAttributes : remarks)) === null || _this$matchGeofeedFil === void 0 ? void 0 : _this$matchGeofeedFil[0];
if (geofeed) {
var strict = !remarks || _this.testGeofeedRemarkStrict(remarks);
if (!strict) {
console.log("Error: the remark MUST be in the format: Geofeed https://url/file.csv. Uppercase G, no colon, no quotes, and one space. Current remarks: ".concat(strict));
}
inetnums.forEach(function (inetnum) {
index["".concat(inetnum, "-").concat(geofeed)] = {
inetnum: inetnum,
geofeedAttribute: !!geofeedAttributes,
isRemark: !!remarks,
geofeed: geofeed,
strict: strict,
whois: item,
lastUpdate: (0, _moment["default"])() // It doesn't matter in this case
};
});
}
};
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
_loop();
}
} catch (err) {
_iterator5.e(err);
} finally {
_iterator5.f();
}
return Object.values(index);
});
} else {
return _this.getBlocks().then(function () {
var objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return objects.map(_this.translateObject).flat();
}).then(_this.getMostUpdatedInetnums);
}
} catch (error) {
return Promise.reject(error);
}
});
_defineProperty(this, "getGeofeeds", function () {
return _this.getGeofeedInetnumPairs().then(_this.getGeofeedsFiles).then(function (data) {
_this._persistCacheIndex();
return _this.params.test ? data : _this.setGeofeedPriority(data);
});
});
var defaults = {
cacheDir: ".cache/",
whoisCacheDays: 3,
geofeedCacheDays: 7,
af: [4, 6],
includeZip: false,
silent: false,
keepNonIso: false,
keepInvalidSubdivisions: false,
removeInvalidSubdivisions: false,
include: ["ripe", "afrinic", "apnic", "arin", "lacnic"],
output: "result.csv",
test: null,
downloadTimeout: 14,
daysWhoisSuballocationsCache: 7,
// Cannot be less than this
skipSuballocations: false,
compileSuballocationLocally: false
};
this.params = _objectSpread(_objectSpread({}, defaults), params !== null && params !== void 0 ? params : {});
this.logger = this.params.logger;
this.cacheDir = this.params.cacheDir.split("/").filter(function (i) {
return !!i;
}).join("/") + "/";
this.csvParser = new _csvParser["default"]();
this.startTime = (0, _moment["default"])();
this.cacheHeadersIndexFileName = this.cacheDir + "cache-index.json";
this._importCacheHeaderIndex();
this.connectors = defaults.include.filter(function (key) {
return _this.params.include.includes(key);
});
this.whois = new _bulkWhoisParser["default"]({
cacheDir: this.cacheDir,
repos: this.connectors,
daysWhoisSuballocationsCache: this.params.daysWhoisSuballocationsCache,
skipSuballocations: this.params.skipSuballocations,
defaultCacheDays: this.params.whoisCacheDays,
compileSuballocationLocally: this.params.compileSuballocationLocally,
userAgent: "geofeed-finder",
deleteCorruptedCacheFile: true
});
});