ipmap.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <!doctype html>
  2. <meta charset="utf-8">
  3. <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0">
  4. <title>Wireshark: IP Location Map</title>
  5. <link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
  6. integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
  7. crossorigin="">
  8. <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"
  9. integrity="sha512-BBToHPBStgMiw0lD4AtkRIZmdndhB6aQbXpX7omcrXeG2PauGBl2lzq2xUZTxaLxYz5IDHlmneCZ1IJ+P3kYtQ=="
  10. crossorigin="">
  11. <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"
  12. integrity="sha512-RLEjtaFGdC4iQMJDbMzim/dOvAu+8Qp9sw7QE4wIMYcg2goVoivzwgSZq9CsIxp4xKAZPKh5J2f2lOko2Ze6FQ=="
  13. crossorigin="">
  14. <!--
  15. <link rel="stylesheet" href="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.css"
  16. integrity="sha512-wgiKVjb46JxgnGNL6xagIy2+vpqLQmmHH7fWD/BnPzouddSmbRTf6xatWIRbH2Rgr2F+tLtCZKbxnhm5Xz0BcA=="
  17. crossorigin="">
  18. -->
  19. <style>
  20. html, body {
  21. margin: 0;
  22. padding: 0;
  23. height: 100%;
  24. }
  25. #map {
  26. height: 100%;
  27. }
  28. .file-picker-enabled #map, #file-picker-container {
  29. display: none;
  30. }
  31. .file-picker-enabled #file-picker-container {
  32. display: block;
  33. margin: 2em;
  34. }
  35. .range-control {
  36. padding: 3px 5px;
  37. color: #333;
  38. background: #fff;
  39. opacity: .5;
  40. }
  41. .range-control:hover { opacity: 1; }
  42. .range-control-label { padding-right: 3px; }
  43. .range-control-input { padding: 0; width: 130px; }
  44. .range-control-input, .range-control-label { vertical-align: middle; }
  45. </style>
  46. <script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
  47. integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
  48. crossorigin=""></script>
  49. <script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"
  50. integrity="sha512-MQlyPV+ol2lp4KodaU/Xmrn+txc1TP15pOBF/2Sfre7MRsA/pB4Vy58bEqe9u7a7DczMLtU5wT8n7OblJepKbg=="
  51. crossorigin=""></script>
  52. <!--
  53. <script src="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.js"
  54. integrity="sha512-ovh6EqS7MUI3QjLWBM7CY8Gu8cSM5x6vQofUMwKGbHVDPSAS2lmNv6Wq5es5WCz1muyojQxcc8rA3CvVjD2Z+A=="
  55. crossorigin=""></script>
  56. -->
  57. <script>
  58. var map;
  59. function sortIpKey(v) {
  60. if (/\./.test(v)) {
  61. // Assume IPv4. Convert 192.0.2.34 -> 192.000.002.034 for alpha sort.
  62. return v.replace(/\b\d\b/g, '00$&').replace(/\b\d{2}\b/g, '0$&');
  63. } else {
  64. // Assume IPv6. We won't handle :: correctly. Hope for the best.
  65. return v;
  66. }
  67. }
  68. function escapeHtml(text) {
  69. if (!text) return '';
  70. return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  71. }
  72. function sanitizeHtml(text) {
  73. // Handle legacy data containing <div class="geoip_property">...</div>
  74. // (since Wireshark 2.0) or <br/> (before v1.99.0-rc1-1781-g7e63805708).
  75. text = text
  76. .replace(/<div[^>]*>/g, '')
  77. .replace(/<\/div>|<br\/>/g, '\n')
  78. .replace(/&#39;/g, "'");
  79. return escapeHtml(text).replace(/\n/g, '<br>');
  80. }
  81. var RangeControl = L.Control.extend({
  82. options: {
  83. // @option label: String = 'Speed:'
  84. // The HTML text to be displayed next to the slider.
  85. label: '',
  86. title: '',
  87. min: 0,
  88. max: 100,
  89. value: 0,
  90. // @option onChange: Function = *
  91. // A `Function` that is called on slider value changes.
  92. // Called with two arguments, the new and previous range value.
  93. },
  94. onAdd: function(map) {
  95. var className = 'range-control';
  96. var container = L.DomUtil.create('div', className + ' leaflet-bar');
  97. L.DomEvent.disableClickPropagation(container);
  98. var label = L.DomUtil.create('label', className + '-label', container);
  99. var labelText = L.DomUtil.create('span', className + '-label', label);
  100. labelText.title = this.options.title;
  101. labelText.innerHTML = this.options.label;
  102. var input = L.DomUtil.create('input', className + '-input', label);
  103. this._input = input;
  104. input.type = 'range';
  105. input.min = this.options.min;
  106. input.max = this.options.max;
  107. this._lastValue = input.valueAsNumber = this.options.value;
  108. L.DomEvent.on(input, 'change', this._onInputChange, this);
  109. return container;
  110. },
  111. _onInputChange: function(ev) {
  112. var value = this._input.valueAsNumber;
  113. if (value !== this._lastValue) {
  114. if (this.options.onChange) {
  115. this.options.onChange(value, this._lastValue);
  116. }
  117. this._lastValue = value;
  118. }
  119. }
  120. });
  121. var rangeControl = function(options) {
  122. return new RangeControl(options);
  123. };
  124. function loadGeoJSON(obj) {
  125. 'use strict';
  126. if (map) map.remove();
  127. map = L.map('map');
  128. var tileServer = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
  129. L.tileLayer(tileServer, {
  130. minZoom: 2,
  131. maxZoom: 16,
  132. subdomains: 'abcd',
  133. attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
  134. }).addTo(map);
  135. L.control.scale().addTo(map);
  136. // Measurement tool, useful for investigating accuracy-related issues.
  137. if (L.control.measure) {
  138. L.control.measure({
  139. primaryLengthUnit: 'kilometers',
  140. secondaryLengthUnit: 'miles'
  141. }).addTo(map);
  142. }
  143. var geoJson = L.geoJSON(obj, {
  144. pointToLayer: function(feature, latlng) {
  145. // MaxMind databases use km for accuracy, but they always use
  146. // 50, 100, 200 or 1000. That is too course, so ignore it and use a
  147. // fixed 1km radius.
  148. // See https://gitlab.com/wireshark/wireshark/-/issues/14693#note_400735005
  149. return L.circle(latlng, {radius: 1e3});
  150. },
  151. onEachFeature: function(feature, layer) {
  152. var props = feature.properties;
  153. var title, lines = [];
  154. if (props.title && props.description) {
  155. title = escapeHtml(props.title);
  156. lines.push(sanitizeHtml(props.description));
  157. } else {
  158. title = escapeHtml(props.ip);
  159. if (props.autonomous_system_number) {
  160. var line = 'AS: ' + props.autonomous_system_number;
  161. line += ' (' + props.autonomous_system_organization + ')';
  162. lines.push(escapeHtml(line));
  163. }
  164. if (props.city) {
  165. lines.push(escapeHtml('City: ' + props.city));
  166. }
  167. if (props.country) {
  168. lines.push(escapeHtml('Country: ' + props.country));
  169. }
  170. if ('packets' in props) {
  171. lines.push(escapeHtml('Packets: ' + props.packets));
  172. }
  173. if ('bytes' in props) {
  174. lines.push(escapeHtml('Bytes: ' + props.bytes));
  175. }
  176. }
  177. if (title) {
  178. layer.bindTooltip(title, {
  179. offset: [10, 0],
  180. direction: 'right',
  181. sticky: true
  182. });
  183. }
  184. if (title && lines.length) {
  185. layer.bindPopup('<b>' + title + '</b><br>' + lines.join('<br>'));
  186. }
  187. }
  188. });
  189. map.on('zoomend', function() {
  190. // Ensure that the circles are clearly visible even when zoomed out.
  191. // Larger values will increase the size of the circle.
  192. var visibleZoomLevel = 9;
  193. var radius = 1e3;
  194. if (map.getZoom() < visibleZoomLevel) {
  195. // Enlarge radius to ensure it is easy to select.
  196. radius *= map.getZoomScale(visibleZoomLevel, map.getZoom());
  197. }
  198. geoJson.eachLayer(function(layer) {
  199. layer.setRadius(radius);
  200. });
  201. });
  202. // Cluster nearby/overlapping nodes by default.
  203. var clusterGroup = L.markerClusterGroup({
  204. zoomToBoundsOnClick: false,
  205. spiderfyOnMaxZoom: false,
  206. maxClusterRadius: 10
  207. });
  208. clusterGroup.addTo(map).addLayer(geoJson);
  209. map.fitWorld().fitBounds(clusterGroup.getBounds());
  210. // Summarize nodes within the cluster.
  211. clusterGroup.on('clustermouseover', function(ev) {
  212. // More addresses will be stripped.
  213. var cutoff = 30;
  214. var cluster = ev.propagatedFrom;
  215. var addresses = cluster.getAllChildMarkers().map(function(marker) {
  216. return marker.getTooltip().getContent();
  217. });
  218. addresses.sort(function(a, b) {
  219. a = sortIpKey(a);
  220. b = sortIpKey(b);
  221. return a === b ? 0 : (a < b ? -1 : 1);
  222. });
  223. var deleted = addresses.splice(cutoff).length;
  224. var title = addresses.join('<br>');
  225. if (deleted) {
  226. title += '<br>(and ' + deleted + ' more)';
  227. }
  228. cluster.bindTooltip(title, {
  229. offset: [10, 0],
  230. direction: 'right',
  231. sticky: true,
  232. opacity: 0.8
  233. }).openTooltip();
  234. }).on('clustermouseout', function(ev) {
  235. ev.propagatedFrom.unbindTooltip();
  236. }).on('clusterclick', function(ev) {
  237. ev.propagatedFrom.spiderfy();
  238. });
  239. // Provide an option to disable clustering
  240. rangeControl({
  241. label: 'Cluster radius:',
  242. title: 'Control merging of nearby nodes. Set to the minimum to disable merges.',
  243. min: 0,
  244. max: 100,
  245. value: clusterGroup.options.maxClusterRadius,
  246. onChange: function(value, oldValue) {
  247. // Apply new radius: remove map, clear markers and finally add new.
  248. clusterGroup.options.maxClusterRadius = value;
  249. clusterGroup.remove().clearLayers().addTo(map);
  250. // Value 0: clustering is disabled, the map is directly used.
  251. geoJson.remove().addTo(value === 0 ? map : clusterGroup);
  252. }
  253. }).addTo(map);
  254. }
  255. function showError(msg) {
  256. document.getElementById('error-message').textContent = msg;
  257. document.body.classList.add('file-picker-enabled');
  258. }
  259. function loadData(data) {
  260. 'use strict';
  261. var html_match, what, error;
  262. var reOldHtml = /^ *var endpoints = (\{[\s\S]+? *\});$/m;
  263. // Complicated regex to support html-minifier.
  264. var reNewHtml = /<script[^>]+id="?ipmap-data"?(?: [^>]*)?>\s*(\{[\S\s]+?\})\s*<\/script>/;
  265. if ((html_match = reNewHtml.exec(data))) {
  266. // Match new ipmap.html file.
  267. what = 'new ipmap.html';
  268. data = html_match[1];
  269. } else if ((html_match = reOldHtml.exec(data))) {
  270. // Match old ipmap.html file
  271. what = 'old ipmap.html';
  272. var text = html_match[1].replace(/'/g, '"');
  273. text = text.replace(/ class="geoip_property"/g, '');
  274. data = text.replace(/\/\/ Start endpoint list.*/, '');
  275. } else if (/^\s*\{[\s\S]+\}\s*$/.test(data)) {
  276. // Assume GeoJSON (.json) file.
  277. what = 'GeoJSON file';
  278. } else {
  279. what = 'unknown file';
  280. error = 'Unrecognized file contents';
  281. }
  282. if (!error) {
  283. try {
  284. loadGeoJSON(JSON.parse(data));
  285. return true;
  286. } catch (e) {
  287. error = e;
  288. }
  289. }
  290. var msg = 'Failed to load map data from ' + what + ': ' + error;
  291. msg += '; data was: ' + data.substring(0, 120);
  292. if (data.length > 100) msg += '... (' + data.length + ' bytes)';
  293. showError(msg);
  294. }
  295. (function() {
  296. 'use strict';
  297. function loadFromUrl(url) {
  298. var xhr = new XMLHttpRequest();
  299. xhr.open('GET', url, true);
  300. xhr.onload = function() {
  301. if (xhr.status !== 200) {
  302. showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
  303. return;
  304. }
  305. loadData(xhr.responseText);
  306. };
  307. xhr.onerror = function() {
  308. showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
  309. };
  310. xhr.send(null);
  311. }
  312. addEventListener('load', function() {
  313. // Note: FileReader and classList do not work with IE9 or older.
  314. var fileSelector = document.getElementById('file-picker');
  315. fileSelector.addEventListener('change', function() {
  316. if (!fileSelector.files.length) {
  317. return;
  318. }
  319. document.body.classList.remove('file-picker-enabled');
  320. var reader = new FileReader();
  321. reader.onload = function() {
  322. if (!loadData(reader.result)) {
  323. document.body.classList.add('file-picker-enabled');
  324. }
  325. };
  326. reader.onerror = function() {
  327. showError('Failed to read file.');
  328. };
  329. reader.readAsText(fileSelector.files[0]);
  330. });
  331. // Force file picker when the "file" URL is given.
  332. var url = location.search.match(/[?&]url=([^&]*)/);
  333. if (url) {
  334. url = decodeURIComponent(url[1]);
  335. if (url) {
  336. loadFromUrl(url);
  337. } else {
  338. showError('');
  339. }
  340. return;
  341. }
  342. var data = document.getElementById('ipmap-data');
  343. if (data) {
  344. loadData(data.textContent);
  345. } else {
  346. showError('');
  347. }
  348. });
  349. }());
  350. </script>
  351. <div id="file-picker-container">
  352. <label>Select an ipmap.html or GeoJSON .json file as created by Wireshark.<br>
  353. <input type="file" id="file-picker" accept=".json,.html"></label>
  354. <p id="error-message"></p>
  355. </div>
  356. <div id="map"></div>
  357. <!--
  358. Wireshark will append a script tag (id="ipmap-data" type="application/json")
  359. below, containing a GeoJSON object. If missing, then a file picker will be
  360. displayed which can be useful during development.
  361. -->