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=="
8 <link rel=
"stylesheet" href=
"https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"
9 integrity=
"sha512-BBToHPBStgMiw0lD4AtkRIZmdndhB6aQbXpX7omcrXeG2PauGBl2lzq2xUZTxaLxYz5IDHlmneCZ1IJ+P3kYtQ=="
11 <link rel=
"stylesheet" href=
"https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"
12 integrity=
"sha512-RLEjtaFGdC4iQMJDbMzim/dOvAu+8Qp9sw7QE4wIMYcg2goVoivzwgSZq9CsIxp4xKAZPKh5J2f2lOko2Ze6FQ=="
15 <link rel="stylesheet" href="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.css"
16 integrity="sha512-wgiKVjb46JxgnGNL6xagIy2+vpqLQmmHH7fWD/BnPzouddSmbRTf6xatWIRbH2Rgr2F+tLtCZKbxnhm5Xz0BcA=="
28 .file-picker-enabled #map, #file-picker-container {
31 .file-picker-enabled #file-picker-container {
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; }
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>
53 <script src="https://unpkg.com/leaflet-measure@3.1.0/dist/leaflet-measure.js"
54 integrity="sha512-ovh6EqS7MUI3QjLWBM7CY8Gu8cSM5x6vQofUMwKGbHVDPSAS2lmNv6Wq5es5WCz1muyojQxcc8rA3CvVjD2Z+A=="
55 crossorigin=""></script>
60 function sortIpKey(v
) {
62 // Assume IPv4. Convert 192.0.2.34 -> 192.000.002.034 for alpha sort.
63 return v
.replace(/\b\d\b/g, '00$&').replace(/\b\d{2}\b/g, '0$&');
65 // Assume IPv6. We won't handle :: correctly. Hope for the best.
70 function escapeHtml(text
) {
72 return text
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g
, '>');
74 function sanitizeHtml(text
) {
75 // Handle legacy data containing <div class="geoip_property">...</div>
76 // (since Wireshark 2.0) or <br/> (before v1.99.0-rc1-1781-g7e63805708).
78 .replace(/<div[^>]*>/g, '')
79 .replace(/<\/div>|<br\/>/g, '\n')
80 .replace(/'/g, "'");
81 return escapeHtml(text
).replace(/\n/g, '<br>');
84 var RangeControl
= L
.Control
.extend({
86 // @option label: String = 'Speed:'
87 // The HTML text to be displayed next to the slider.
95 // @option onChange: Function = *
96 // A `Function` that is called on slider value changes.
97 // Called with two arguments, the new and previous range value.
99 onAdd: function(map
) {
100 var className
= 'range-control';
101 var container
= L
.DomUtil
.create('div', className
+ ' leaflet-bar');
102 L
.DomEvent
.disableClickPropagation(container
);
103 var label
= L
.DomUtil
.create('label', className
+ '-label', container
);
104 var labelText
= L
.DomUtil
.create('span', className
+ '-label', label
);
105 labelText
.title
= this.options
.title
;
106 labelText
.innerHTML
= this.options
.label
;
107 var input
= L
.DomUtil
.create('input', className
+ '-input', label
);
109 input
.type
= 'range';
110 input
.min
= this.options
.min
;
111 input
.max
= this.options
.max
;
112 this._lastValue
= input
.valueAsNumber
= this.options
.value
;
113 L
.DomEvent
.on(input
, 'change', this._onInputChange
, this);
116 _onInputChange: function(ev
) {
117 var value
= this._input
.valueAsNumber
;
118 if (value
!== this._lastValue
) {
119 if (this.options
.onChange
) {
120 this.options
.onChange(value
, this._lastValue
);
122 this._lastValue
= value
;
127 var rangeControl = function(options
) {
128 return new RangeControl(options
);
131 function loadGeoJSON(obj
) {
133 if (map
) map
.remove();
135 var tileServer
= 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
136 L
.tileLayer(tileServer
, {
140 attribution
: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
142 L
.control
.scale().addTo(map
);
144 // Measurement tool, useful for investigating accuracy-related issues.
145 if (L
.control
.measure
) {
147 primaryLengthUnit
: 'kilometers',
148 secondaryLengthUnit
: 'miles'
152 var geoJson
= L
.geoJSON(obj
, {
153 pointToLayer: function(feature
, latlng
) {
154 // MaxMind databases use km for accuracy, but they always use
155 // 50, 100, 200 or 1000. That is too course, so ignore it and use a
157 // See https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=14693#c12
158 return L
.circle(latlng
, {radius
: 1e3
});
160 onEachFeature: function(feature
, layer
) {
161 var props
= feature
.properties
;
162 var title
, lines
= [];
163 if (props
.title
&& props
.description
) {
164 title
= escapeHtml(props
.title
);
165 lines
.push(sanitizeHtml(props
.description
));
167 title
= escapeHtml(props
.ip
);
168 if (props
.autonomous_system_number
) {
169 var line
= 'AS: ' + props
.autonomous_system_number
;
170 line
+= ' (' + props
.autonomous_system_organization
+ ')';
171 lines
.push(escapeHtml(line
));
174 lines
.push(escapeHtml('City: ' + props
.city
));
177 lines
.push(escapeHtml('Country: ' + props
.country
));
179 if ('packets' in props
) {
180 lines
.push(escapeHtml('Packets: ' + props
.packets
));
182 if ('bytes' in props
) {
183 lines
.push(escapeHtml('Bytes: ' + props
.bytes
));
187 layer
.bindTooltip(title
, {
193 if (title
&& lines
.length
) {
194 layer
.bindPopup('<b>' + title
+ '</b><br>' + lines
.join('<br>'));
199 map
.on('zoomend', function() {
200 // Ensure that the circles are clearly visible even when zoomed out.
201 // Larger values will increase the size of the circle.
202 var visibleZoomLevel
= 9;
204 if (map
.getZoom() < visibleZoomLevel
) {
205 // Enlarge radius to ensure it is easy to select.
206 radius
*= map
.getZoomScale(visibleZoomLevel
, map
.getZoom());
208 geoJson
.eachLayer(function(layer
) {
209 layer
.setRadius(radius
);
213 // Cluster nearby/overlapping nodes by default.
214 var clusterGroup
= L
.markerClusterGroup({
215 zoomToBoundsOnClick
: false,
216 spiderfyOnMaxZoom
: false,
219 clusterGroup
.addTo(map
).addLayer(geoJson
);
220 map
.fitWorld().fitBounds(clusterGroup
.getBounds());
222 // Summarize nodes within the cluster.
223 clusterGroup
.on('clustermouseover', function(ev
) {
224 // More addresses will be stripped.
226 var cluster
= ev
.propagatedFrom
;
227 var addresses
= cluster
.getAllChildMarkers().map(function(marker
) {
228 return marker
.getTooltip().getContent();
230 addresses
.sort(function(a
, b
) {
233 return a
=== b
? 0 : (a
< b
? -1 : 1);
235 var deleted
= addresses
.splice(cutoff
).length
;
236 var title
= addresses
.join('<br>');
238 title
+= '<br>(and ' + deleted
+ ' more)';
240 cluster
.bindTooltip(title
, {
246 }).on('clustermouseout', function(ev
) {
247 ev
.propagatedFrom
.unbindTooltip();
248 }).on('clusterclick', function(ev
) {
249 ev
.propagatedFrom
.spiderfy();
252 // Provide an option to disable clustering
254 label
: 'Cluster radius:',
255 title
: 'Control merging of nearby nodes. Set to the minimum to disable merges.',
258 value
: clusterGroup
.options
.maxClusterRadius
,
259 onChange: function(value
, oldValue
) {
260 // Apply new radius: remove map, clear markers and finally add new.
261 clusterGroup
.options
.maxClusterRadius
= value
;
262 clusterGroup
.remove().clearLayers().addTo(map
);
263 // Value 0: clustering is disabled, the map is directly used.
264 geoJson
.remove().addTo(value
=== 0 ? map
: clusterGroup
);
269 function showError(msg
) {
270 document
.getElementById('error-message').textContent
= msg
;
271 document
.body
.classList
.add('file-picker-enabled');
274 function loadData(data
) {
276 var html_match
, what
, error
;
277 var reOldHtml
= /^ *var endpoints = (\{[\s\S]+? *\});$/m;
278 // Complicated regex to support html-minifier.
279 var reNewHtml
= /<script[^>]+id="?ipmap-data"?(?: [^>]*)?>\s*(\{[\S\s]+?\})\s*<\/script>/;
280 if ((html_match
= reNewHtml
.exec(data
))) {
281 // Match new ipmap.html file.
282 what
= 'new ipmap.html';
283 data
= html_match
[1];
284 } else if ((html_match
= reOldHtml
.exec(data
))) {
285 // Match old ipmap.html file
286 what
= 'old ipmap.html';
287 var text
= html_match
[1].replace(/'/g, '"');
288 text = text.replace(/ class="geoip_property
"/g, '');
289 data = text.replace(/\/\/ Start endpoint list.*/, '');
290 } else if (/^\s*\{[\s\S]+\}\s*$/.test(data)) {
291 // Assume GeoJSON (.json) file.
292 what = 'GeoJSON file';
294 what = 'unknown file';
295 error = 'Unrecognized file contents';
299 loadGeoJSON(JSON.parse(data));
305 var msg = 'Failed to load map data from ' + what + ': ' + error;
306 msg += '; data was: ' + data.substring(0, 120);
307 if (data.length > 100) msg += '... (' + data.length + ' bytes)';
313 function loadFromUrl(url) {
314 var xhr = new XMLHttpRequest();
315 xhr.open('GET', url, true);
316 xhr.onload = function() {
317 if (xhr.status !== 200) {
318 showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
321 loadData(xhr.responseText);
323 xhr.onerror = function() {
324 showError('Failed to retrieve ' + url + ': ' + xhr.status + ' ' + xhr.statusText);
329 addEventListener('load', function() {
330 // Note: FileReader and classList do not work with IE9 or older.
331 var fileSelector = document.getElementById('file-picker');
332 fileSelector.addEventListener('change', function() {
333 if (!fileSelector.files.length) {
336 document.body.classList.remove('file-picker-enabled');
337 var reader = new FileReader();
338 reader.onload = function() {
339 if (!loadData(reader.result)) {
340 document.body.classList.add('file-picker-enabled');
343 reader.onerror = function() {
344 showError('Failed to read file.');
346 reader.readAsText(fileSelector.files[0]);
349 // Force file picker when the "file
" URL is given.
350 var url = location.search.match(/[?&]url=([^&]*)/);
352 url = decodeURIComponent(url[1]);
361 var data = document.getElementById('ipmap-data');
363 loadData(data.textContent);
370 <div id="file
-picker
-container
">
371 <label>Select an ipmap.html or GeoJSON .json file as created by Wireshark.<br>
372 <input type="file
" id="file
-picker
" accept=".json
,.html
"></label>
373 <p id="error
-message
"></p>
377 Wireshark will append a script tag (id="ipmap
-data
" type="application
/json
")
378 below, containing a GeoJSON object. If missing, then a file picker will be
379 displayed which can be useful during development.