UNPKG

geofeed-finder

Version:

A tool to find geofeed files in rpsl and parse them correctly according to draft-ietf-opsawg-finding-geofeeds

507 lines (504 loc) 25.6 kB
"use strict"; 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; } var remarkCheckCall = _this.params.strictGeofeedRemarks ? _this.testGeofeedRemarkStrict : _this.testGeofeedRemark; 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(remarkCheckCall); } 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 { if (_fs["default"].existsSync(cachedFile)) { _fs["default"].unlinkSync(cachedFile); } _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, "_basicFilterFunction", function (data) { var flat = data.map(function (i) { return i.data; }).flat().flat().flat().filter(function (i) { return !!i; }); var geofeedAttributes = flat.filter(function (i) { return i.key.toLowerCase() === "geofeed"; }); var remarks = flat.filter(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)); }); return [].concat(_toConsumableArray(geofeedAttributes), _toConsumableArray(remarks)).length > 0; }); _defineProperty(this, "_simplePrefixLookup", function (prefix) { return (0, _whoisWrapper.prefixLookup)({ query: prefix }).then(function (data) { if (_this._basicFilterFunction(data)) { return data.map(function (i) { return i.data; }).flat(); } else { return Promise.reject("No geofeed found"); } }); }); _defineProperty(this, "_transferCheck", function (prefix) { return (0, _whoisWrapper.explicitTransferCheck)({ query: prefix }).then(function (data) { if (_this._basicFilterFunction(data)) { return data.map(function (i) { return i.data; }).flat(); } else { return Promise.reject("No geofeed found"); } }); }); _defineProperty(this, "_bruteForceLessSpecific", function (prefix) { return (0, _whoisWrapper.lessSpecific)({ query: prefix }, _this._basicFilterFunction, 20).then(function (data) { if (_this._basicFilterFunction(data)) { return data.map(function (i) { return i.data; }).flat(); } else { return Promise.reject("No geofeed found"); } }); }); _defineProperty(this, "_sequentialAttempts", function (prefix) { return _this._simplePrefixLookup(prefix)["catch"](function () { return _this._transferCheck(prefix); })["catch"](function () { return []; }); // .catch(() => this._bruteForceLessSpecific(prefix)); }); _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"); } console.log("Testing prefix ".concat(prefix)); var index = {}; return _this._sequentialAttempts(prefix).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$value2; return ["remarks", "comment"].includes(i.key.toLowerCase()) && ((_i$value2 = i.value) === null || _i$value2 === void 0 ? void 0 : _i$value2.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$value3; return ["remarks", "comment"].includes(i.key.toLowerCase()) && ((_i$value3 = i.value) === null || _i$value3 === void 0 ? void 0 : _i$value3.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 && _this.params.exitOnError) { console.error("Error: the remark MUST be in the format: Geofeed https://url/file.csv. Uppercase G, no colon, no quotes, and one space."); console.error("Current remarks: ".concat(remarks)); process.exit(1); } 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 }); });