forked from osmocom/wireshark
381 lines
14 KiB
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
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(/'/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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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://bugs.wireshark.org/bugzilla/show_bug.cgi?id=14693#c12
|
|
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.
|
|
-->
|