wireshark/ipmap.html

381 lines
14 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0">
<title>Wireshark: IP Location Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
crossorigin="">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"
integrity="sha512-BBToHPBStgMiw0lD4AtkRIZmdndhB6aQbXpX7omcrXeG2PauGBl2lzq2xUZTxaLxYz5IDHlmneCZ1IJ+P3kYtQ=="
crossorigin="">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"
integrity="sha512-RLEjtaFGdC4iQMJDbMzim/dOvAu+8Qp9sw7QE4wIMYcg2goVoivzwgSZq9CsIxp4xKAZPKh5J2f2lOko2Ze6FQ=="
crossorigin="">
<!--
<link rel="stylesheet" href="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.css"
integrity="sha512-wgiKVjb46JxgnGNL6xagIy2+vpqLQmmHH7fWD/BnPzouddSmbRTf6xatWIRbH2Rgr2F+tLtCZKbxnhm5Xz0BcA=="
crossorigin="">
-->
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#map {
height: 100%;
}
.file-picker-enabled #map, #file-picker-container {
display: none;
}
.file-picker-enabled #file-picker-container {
display: block;
margin: 2em;
}
.range-control {
padding: 3px 5px;
color: #333;
background: #fff;
opacity: .5;
}
.range-control:hover { opacity: 1; }
.range-control-label { padding-right: 3px; }
.range-control-input { padding: 0; width: 130px; }
.range-control-input, .range-control-label { vertical-align: middle; }
</style>
<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"
integrity="sha512-MQlyPV+ol2lp4KodaU/Xmrn+txc1TP15pOBF/2Sfre7MRsA/pB4Vy58bEqe9u7a7DczMLtU5wT8n7OblJepKbg=="
crossorigin=""></script>
<!--
<script src="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.js"
integrity="sha512-ovh6EqS7MUI3QjLWBM7CY8Gu8cSM5x6vQofUMwKGbHVDPSAS2lmNv6Wq5es5WCz1muyojQxcc8rA3CvVjD2Z+A=="
crossorigin=""></script>
-->
<script>
var map;
function sortIpKey(v) {
if (/\./.test(v)) {
// Assume IPv4. Convert 192.0.2.34 -> 192.000.002.034 for alpha sort.
return v.replace(/\b\d\b/g, '00$&').replace(/\b\d{2}\b/g, '0$&');
} else {
// Assume IPv6. We won't handle :: correctly. Hope for the best.
return v;
}
}
function escapeHtml(text) {
if (!text) return '';
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function sanitizeHtml(text) {
// Handle legacy data containing <div class="geoip_property">...</div>
// (since Wireshark 2.0) or <br/> (before v1.99.0-rc1-1781-g7e63805708).
text = text
.replace(/<div[^>]*>/g, '')
.replace(/<\/div>|<br\/>/g, '\n')
.replace(/&#39;/g, "'");
return escapeHtml(text).replace(/\n/g, '<br>');
}
var RangeControl = L.Control.extend({
options: {
// @option label: String = 'Speed:'
// The HTML text to be displayed next to the slider.
label: '',
title: '',
min: 0,
max: 100,
value: 0,
// @option onChange: Function = *
// A `Function` that is called on slider value changes.
// Called with two arguments, the new and previous range value.
},
onAdd: function(map) {
var className = 'range-control';
var container = L.DomUtil.create('div', className + ' leaflet-bar');
L.DomEvent.disableClickPropagation(container);
var label = L.DomUtil.create('label', className + '-label', container);
var labelText = L.DomUtil.create('span', className + '-label', label);
labelText.title = this.options.title;
labelText.innerHTML = this.options.label;
var input = L.DomUtil.create('input', className + '-input', label);
this._input = input;
input.type = 'range';
input.min = this.options.min;
input.max = this.options.max;
this._lastValue = input.valueAsNumber = this.options.value;
L.DomEvent.on(input, 'change', this._onInputChange, this);
return container;
},
_onInputChange: function(ev) {
var value = this._input.valueAsNumber;
if (value !== this._lastValue) {
if (this.options.onChange) {
this.options.onChange(value, this._lastValue);
}
this._lastValue = value;
}
}
});
var rangeControl = function(options) {
return new RangeControl(options);
};
function loadGeoJSON(obj) {
'use strict';
if (map) map.remove();
map = L.map('map');
var tileServer = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
L.tileLayer(tileServer, {
minZoom: 2,
maxZoom: 16,
subdomains: 'abcd',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
}).addTo(map);
L.control.scale().addTo(map);
// Measurement tool, useful for investigating accuracy-related issues.
if (L.control.measure) {
L.control.measure({
primaryLengthUnit: 'kilometers',
secondaryLengthUnit: 'miles'
}).addTo(map);
}
var geoJson = L.geoJSON(obj, {
pointToLayer: function(feature, latlng) {
// MaxMind databases use km for accuracy, but they always use
// 50, 100, 200 or 1000. That is too course, so ignore it and use a
// fixed 1km radius.
// See https://gitlab.com/wireshark/wireshark/-/issues/14693#note_400735005
return L.circle(latlng, {radius: 1e3});
},
onEachFeature: function(feature, layer) {
var props = feature.properties;
var title, lines = [];
if (props.title && props.description) {
title = escapeHtml(props.title);
lines.push(sanitizeHtml(props.description));
} else {
title = escapeHtml(props.ip);
if (props.autonomous_system_number) {
var line = 'AS: ' + props.autonomous_system_number;
line += ' (' + props.autonomous_system_organization + ')';
lines.push(escapeHtml(line));
}
if (props.city) {
lines.push(escapeHtml('City: ' + props.city));
}
if (props.country) {
lines.push(escapeHtml('Country: ' + props.country));
}
if ('packets' in props) {
lines.push(escapeHtml('Packets: ' + props.packets));
}
if ('bytes' in props) {
lines.push(escapeHtml('Bytes: ' + props.bytes));
}
}
if (title) {
layer.bindTooltip(title, {
offset: [10, 0],
direction: 'right',
sticky: true
});
}
if (title && lines.length) {
layer.bindPopup('<b>' + title + '</b><br>' + lines.join('<br>'));
}
}
});
map.on('zoomend', function() {
// Ensure that the circles are clearly visible even when zoomed out.
// Larger values will increase the size of the circle.
var visibleZoomLevel = 9;
var radius = 1e3;
if (map.getZoom() < visibleZoomLevel) {
// Enlarge radius to ensure it is easy to select.
radius *= map.getZoomScale(visibleZoomLevel, map.getZoom());
}
geoJson.eachLayer(function(layer) {
layer.setRadius(radius);
});
});
// Cluster nearby/overlapping nodes by default.
var clusterGroup = L.markerClusterGroup({
zoomToBoundsOnClick: false,
spiderfyOnMaxZoom: false,
maxClusterRadius: 10
});
clusterGroup.addTo(map).addLayer(geoJson);
map.fitWorld().fitBounds(clusterGroup.getBounds());
// Summarize nodes within the cluster.
clusterGroup.on('clustermouseover', function(ev) {
// More addresses will be stripped.
var cutoff = 30;
var cluster = ev.propagatedFrom;
var addresses = cluster.getAllChildMarkers().map(function(marker) {
return marker.getTooltip().getContent();
});
addresses.sort(function(a, b) {
a = sortIpKey(a);
b = sortIpKey(b);
return a === b ? 0 : (a < b ? -1 : 1);
});
var deleted = addresses.splice(cutoff).length;
var title = addresses.join('<br>');
if (deleted) {
title += '<br>(and ' + deleted + ' more)';
}
cluster.bindTooltip(title, {
offset: [10, 0],
direction: 'right',
sticky: true,
opacity: 0.8
}).openTooltip();
}).on('clustermouseout', function(ev) {
ev.propagatedFrom.unbindTooltip();
}).on('clusterclick', function(ev) {
ev.propagatedFrom.spiderfy();
});
// Provide an option to disable clustering
rangeControl({
label: 'Cluster radius:',
title: 'Control merging of nearby nodes. Set to the minimum to disable merges.',
min: 0,
max: 100,
value: clusterGroup.options.maxClusterRadius,
onChange: function(value, oldValue) {
// Apply new radius: remove map, clear markers and finally add new.
clusterGroup.options.maxClusterRadius = value;
clusterGroup.remove().clearLayers().addTo(map);
// Value 0: clustering is disabled, the map is directly used.
geoJson.remove().addTo(value === 0 ? map : clusterGroup);
}
}).addTo(map);
}
function showError(msg) {
document.getElementById('error-message').textContent = msg;
document.body.classList.add('file-picker-enabled');
}
function loadData(data) {
'use strict';
var html_match, what, error;
var reOldHtml = /^ *var endpoints = (\{[\s\S]+? *\});$/m;
// Complicated regex to support html-minifier.
var reNewHtml = /<script[^>]+id="?ipmap-data"?(?: [^>]*)?>\s*(\{[\S\s]+?\})\s*<\/script>/;
if ((html_match = reNewHtml.exec(data))) {
// Match new ipmap.html file.
what = 'new ipmap.html';
data = html_match[1];
} else if ((html_match = reOldHtml.exec(data))) {
// Match old ipmap.html file
what = 'old ipmap.html';
var text = html_match[1].replace(/'/g, '"');
text = text.replace(/ class="geoip_property"/g, '');
data = text.replace(/\/\/ Start endpoint list.*/, '');
} else if (/^\s*\{[\s\S]+\}\s*$/.test(data)) {
// Assume GeoJSON (.json) file.
what = 'GeoJSON file';
} else {
what = 'unknown file';
error = 'Unrecognized file contents';
}
if (!error) {
try {
loadGeoJSON(JSON.parse(data));
return true;
} catch (e) {
error = e;
}
}
var msg = 'Failed to load map data from ' + what + ': ' + error;
msg += '; data was: ' + data.substring(0, 120);
if (data.length > 100) msg += '... (' + data.length + ' bytes)';
showError(msg);
}
(function() {
'use strict';
function loadFromUrl(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (xhr.status !== 200) {
showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
return;
}
loadData(xhr.responseText);
};
xhr.onerror = function() {
showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
};
xhr.send(null);
}
addEventListener('load', function() {
// Note: FileReader and classList do not work with IE9 or older.
var fileSelector = document.getElementById('file-picker');
fileSelector.addEventListener('change', function() {
if (!fileSelector.files.length) {
return;
}
document.body.classList.remove('file-picker-enabled');
var reader = new FileReader();
reader.onload = function() {
if (!loadData(reader.result)) {
document.body.classList.add('file-picker-enabled');
}
};
reader.onerror = function() {
showError('Failed to read file.');
};
reader.readAsText(fileSelector.files[0]);
});
// Force file picker when the "file" URL is given.
var url = location.search.match(/[?&]url=([^&]*)/);
if (url) {
url = decodeURIComponent(url[1]);
if (url) {
loadFromUrl(url);
} else {
showError('');
}
return;
}
var data = document.getElementById('ipmap-data');
if (data) {
loadData(data.textContent);
} else {
showError('');
}
});
}());
</script>
<div id="file-picker-container">
<label>Select an ipmap.html or GeoJSON .json file as created by Wireshark.<br>
<input type="file" id="file-picker" accept=".json,.html"></label>
<p id="error-message"></p>
</div>
<div id="map"></div>
<!--
Wireshark will append a script tag (id="ipmap-data" type="application/json")
below, containing a GeoJSON object. If missing, then a file picker will be
displayed which can be useful during development.
-->