aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--docbook/wsug_src/WSUG_chapter_statistics.adoc3
-rw-r--r--epan/maxmind_db.c9
-rw-r--r--epan/maxmind_db.h10
-rw-r--r--ipmap.html380
-rw-r--r--mmdbresolve.c4
-rw-r--r--packaging/nsis/uninstall.nsi1
-rw-r--r--packaging/nsis/wireshark.nsi1
-rw-r--r--packaging/wix/ComponentGroups.wxi3
-rw-r--r--ui/qt/endpoint_dialog.cpp162
-rw-r--r--ui/qt/endpoint_dialog.h21
-rw-r--r--ui/traffic_table_ui.c176
-rw-r--r--ui/traffic_table_ui.h38
13 files changed, 785 insertions, 24 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6bfd0dcb47..600406c66d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1643,6 +1643,7 @@ set(INSTALL_FILES
colorfilters
dfilters
enterprises.tsv
+ ipmap.html
manuf
pdml2html.xsl
services
diff --git a/docbook/wsug_src/WSUG_chapter_statistics.adoc b/docbook/wsug_src/WSUG_chapter_statistics.adoc
index 3a66fe51aa..65da6d6d3b 100644
--- a/docbook/wsug_src/WSUG_chapter_statistics.adoc
+++ b/docbook/wsug_src/WSUG_chapter_statistics.adoc
@@ -276,7 +276,8 @@ example we have MaxMind DB configured which gives us extra geographic
columns. See <<ChMaxMindDbPaths>> for more information.
The btn:[Copy] button will copy the list values to the clipboard in CSV
-(Comma Separated Values) or YAML format.
+(Comma Separated Values) or YAML format. The btn:[Map] button will show the
+endpoints mapped in your web browser.
btn:[Endpoint Types] lets you choose which traffic type tabs are shown. See
<<ChStatEndpoints>> above for a list of endpoint types. The enabled
diff --git a/epan/maxmind_db.c b/epan/maxmind_db.c
index 9acf8e047e..a0086b3569 100644
--- a/epan/maxmind_db.c
+++ b/epan/maxmind_db.c
@@ -119,6 +119,7 @@ static void mmdb_resolve_stop(void);
#define RES_ASN_NUMBER "autonomous_system_number"
#define RES_LOCATION_LATITUDE "location.latitude"
#define RES_LOCATION_LONGITUDE "location.longitude"
+#define RES_LOCATION_ACCURACY "location.accuracy_radius"
#define RES_END "# End "
// Interned strings and v6 addresses, similar to GLib's string chunks.
@@ -145,7 +146,7 @@ static const void *chunkify_v6_addr(const ws_in6_addr *addr) {
}
static void init_lookup(mmdb_lookup_t *lookup) {
- mmdb_lookup_t empty_lookup = { FALSE, NULL, NULL, NULL, 0, NULL, DBL_MAX, DBL_MAX };
+ mmdb_lookup_t empty_lookup = { FALSE, NULL, NULL, NULL, 0, NULL, DBL_MAX, DBL_MAX, 0 };
*lookup = empty_lookup;
}
@@ -311,6 +312,12 @@ read_mmdbr_stdout_worker(gpointer data _U_) {
} else if (val_start && g_str_has_prefix(line, RES_LOCATION_LONGITUDE)) {
response->mmdb_val.found = TRUE;
response->mmdb_val.longitude = g_ascii_strtod(val_start, NULL);
+ } else if (val_start && g_str_has_prefix(line, RES_LOCATION_ACCURACY)) {
+ if (ws_strtou16(val_start, NULL, &response->mmdb_val.accuracy)) {
+ response->mmdb_val.found = TRUE;
+ } else {
+ MMDB_DEBUG("Invalid accuracy radius: %s", val_start);
+ }
} else if (g_str_has_prefix(line, RES_END)) {
if (response->mmdb_val.found && cur_addr[0]) {
if (country_iso->len) {
diff --git a/epan/maxmind_db.h b/epan/maxmind_db.h
index 251bf558e8..c7404a12a8 100644
--- a/epan/maxmind_db.h
+++ b/epan/maxmind_db.h
@@ -31,6 +31,7 @@ typedef struct _mmdb_lookup_t {
const char *as_org;
double latitude;
double longitude;
+ guint16 accuracy; /** Accuracy radius in kilometers. */
} mmdb_lookup_t;
/**
@@ -75,6 +76,15 @@ WS_DLL_PUBLIC gchar *maxmind_db_get_paths(void);
*/
WS_DLL_LOCAL gboolean maxmind_db_lookup_process(void);
+/**
+ * Checks whether the lookup result was successful and has valid coordinates.
+ */
+static inline gboolean maxmind_db_has_coords(const mmdb_lookup_t *result)
+{
+ return result && result->found &&
+ result->longitude != DBL_MAX && result->latitude != DBL_MAX;
+}
+
#ifdef __cplusplus
}
#endif /* __cplusplus */
diff --git a/ipmap.html b/ipmap.html
new file mode 100644
index 0000000000..d31140c839
--- /dev/null
+++ b/ipmap.html
@@ -0,0 +1,380 @@
+<!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://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.
+-->
diff --git a/mmdbresolve.c b/mmdbresolve.c
index 3b0018976c..6dc6f97759 100644
--- a/mmdbresolve.c
+++ b/mmdbresolve.c
@@ -39,6 +39,7 @@ static const char *asn_o_key[] = {"autonomous_system_organization", NULL};
static const char *asn_key[] = {"autonomous_system_number", NULL};
static const char *l_lat_key[] = {"location", "latitude", NULL};
static const char *l_lon_key[] = {"location", "longitude", NULL};
+static const char *l_accuracy_key[] = {"location", "accuracy_radius", NULL};
static const char *empty_key[] = {NULL};
static const char **lookup_keys[] = {
@@ -49,6 +50,7 @@ static const char **lookup_keys[] = {
asn_key,
l_lat_key,
l_lon_key,
+ l_accuracy_key,
empty_key
};
@@ -181,7 +183,7 @@ main(int argc, char *argv[])
}
/*
- * Editor modelines - http://www.wireshark.org/tools/modelines.html
+ * Editor modelines - https://www.wireshark.org/tools/modelines.html
*
* Local variables:
* c-basic-offset: 4
diff --git a/packaging/nsis/uninstall.nsi b/packaging/nsis/uninstall.nsi
index b0dd5d7643..3ce5173587 100644
--- a/packaging/nsis/uninstall.nsi
+++ b/packaging/nsis/uninstall.nsi
@@ -233,6 +233,7 @@ Delete "$INSTDIR\pdml2html.xsl"
Delete "$INSTDIR\pcrepattern.3.txt"
Delete "$INSTDIR\user-guide.chm"
Delete "$INSTDIR\example_snmp_users_file"
+Delete "$INSTDIR\ipmap.html"
Delete "$INSTDIR\radius\*.*"
Delete "$INSTDIR\dtds\*.*"
diff --git a/packaging/nsis/wireshark.nsi b/packaging/nsis/wireshark.nsi
index 88182cfc7e..754e2bccc6 100644
--- a/packaging/nsis/wireshark.nsi
+++ b/packaging/nsis/wireshark.nsi
@@ -503,6 +503,7 @@ File "${STAGING_DIR}\wireshark-filter.html"
File "${STAGING_DIR}\dumpcap.exe"
File "${STAGING_DIR}\dumpcap.html"
File "${STAGING_DIR}\extcap.html"
+File "${STAGING_DIR}\ipmap.html"
; C-runtime redistributable
; vcredist_x64.exe or vc_redist_x86.exe - copy and execute the redistributable installer
diff --git a/packaging/wix/ComponentGroups.wxi b/packaging/wix/ComponentGroups.wxi
index 03f4caf851..0532306c01 100644
--- a/packaging/wix/ComponentGroups.wxi
+++ b/packaging/wix/ComponentGroups.wxi
@@ -75,6 +75,9 @@
<Component Id="cmpExtcap_html" Guid="*">
<File Id="filExtcap_html" KeyPath="yes" Source="$(var.Staging.Dir)\extcap.html" />
</Component>
+ <Component Id="cmpIpmap_html" Guid="*">
+ <File Id="filIpmap_html" KeyPath="yes" Source="$(var.Staging.Dir)\ipmap.html" />
+ </Component>
</DirectoryRef>
</Fragment>
<Fragment>
diff --git a/ui/qt/endpoint_dialog.cpp b/ui/qt/endpoint_dialog.cpp
index 1fd38ef169..f606825420 100644
--- a/ui/qt/endpoint_dialog.cpp
+++ b/ui/qt/endpoint_dialog.cpp
@@ -16,10 +16,15 @@
#include "ui/recent.h"
#include "ui/traffic_table_ui.h"
+#include "wsutil/file_util.h"
#include "wsutil/pint.h"
#include "wsutil/str_util.h"
+#include "wsutil/tempfile.h"
+#include <wsutil/utf8_entities.h>
#include <ui/qt/utils/qt_ui_utils.h>
+#include <ui/qt/utils/variant_pointer.h>
+#include <ui/qt/widgets/wireshark_file_dialog.h>
#include "wireshark_application.h"
#include <QCheckBox>
@@ -33,6 +38,19 @@ static const QString table_name_ = QObject::tr("Endpoint");
EndpointDialog::EndpointDialog(QWidget &parent, CaptureFile &cf, int cli_proto_id, const char *filter) :
TrafficTableDialog(parent, cf, filter, table_name_)
{
+#ifdef HAVE_MAXMINDDB
+ map_bt_ = buttonBox()->addButton(tr("Map"), QDialogButtonBox::ActionRole);
+ map_bt_->setToolTip(tr("Draw IPv4 or IPv6 endpoints on a map."));
+ connect(trafficTableTabWidget(), &QTabWidget::currentChanged, this, &EndpointDialog::tabChanged);
+
+ QMenu *map_menu_ = new QMenu(map_bt_);
+ QAction *action;
+ action = map_menu_->addAction(tr("Open in browser"));
+ connect(action, &QAction::triggered, this, &EndpointDialog::openMap);
+ action = map_menu_->addAction(tr("Save As" UTF8_HORIZONTAL_ELLIPSIS));
+ connect(action, &QAction::triggered, this, &EndpointDialog::saveMap);
+ map_bt_->setMenu(map_menu_);
+#endif
addProgressFrame(&parent);
QList<int> endp_protos;
@@ -125,6 +143,10 @@ bool EndpointDialog::addTrafficTable(register_ct_t *table)
this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
connect(nameResolutionCheckBox(), SIGNAL(toggled(bool)),
endp_tree, SLOT(setNameResolutionEnabled(bool)));
+#ifdef HAVE_MAXMINDDB
+ connect(endp_tree, &EndpointTreeWidget::geoIPStatusChanged,
+ this, &EndpointDialog::tabChanged);
+#endif
// XXX Move to ConversationTreeWidget ctor?
QByteArray filter_utf8;
@@ -145,6 +167,105 @@ bool EndpointDialog::addTrafficTable(register_ct_t *table)
return true;
}
+#ifdef HAVE_MAXMINDDB
+void EndpointDialog::tabChanged()
+{
+ EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->currentWidget());
+ map_bt_->setEnabled(cur_tree && cur_tree->hasGeoIPData());
+}
+
+QUrl EndpointDialog::createMap(bool json_only)
+{
+ EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->currentWidget());
+ if (!cur_tree) {
+ return QUrl();
+ }
+
+ // Construct list of hosts with a valid MMDB entry.
+ QTreeWidgetItemIterator it(cur_tree);
+ GPtrArray *hosts_arr = g_ptr_array_new();
+ while (*it) {
+ const mmdb_lookup_t *geo = VariantPointer<const mmdb_lookup_t>::asPtr((*it)->data(0, Qt::UserRole + 1));
+ if (maxmind_db_has_coords(geo)) {
+ hostlist_talker_t *host = VariantPointer<hostlist_talker_t>::asPtr((*it)->data(0, Qt::UserRole));
+ g_ptr_array_add(hosts_arr, (gpointer)host);
+ }
+ ++it;
+ }
+ if (hosts_arr->len == 0) {
+ QMessageBox::warning(this, tr("Map file error"), tr("No endpoints available to map"));
+ g_ptr_array_free(hosts_arr, TRUE);
+ return QUrl();
+ }
+ g_ptr_array_add(hosts_arr, NULL);
+ hostlist_talker_t **hosts = (hostlist_talker_t **)g_ptr_array_free(hosts_arr, FALSE);
+
+ char *map_path = NULL;
+ int fd = create_tempfile(&map_path, "ipmap", ".html");
+ FILE *fp = NULL;
+ if (fd != -1) {
+ fp = ws_fdopen(fd, "wb");
+ if (!fp) {
+ ws_close(fd);
+ fd = -1;
+ }
+ }
+ if (fd == -1) {
+ QMessageBox::warning(this, tr("Map file error"), tr("Unable to create temporary file"));
+ g_free(hosts);
+ return QUrl();
+ }
+ QString map_path_str(map_path);
+
+ gchar *err_str;
+ if (!write_endpoint_geoip_map(fp, json_only, hosts, &err_str)) {
+ QMessageBox::warning(this, tr("Map file error"), err_str);
+ g_free(err_str);
+ g_free(hosts);
+ fclose(fp);
+ ws_unlink(qPrintable(map_path_str));
+ return QUrl();
+ }
+ g_free(hosts);
+ if (fclose(fp) == EOF) {
+ QMessageBox::warning(this, tr("Map file error"), g_strerror(errno));
+ ws_unlink(qPrintable(map_path_str));
+ return QUrl();
+ }
+
+ return QUrl::fromLocalFile(map_path_str);
+}
+
+void EndpointDialog::openMap()
+{
+ QUrl map_file = createMap(false);
+ if (!map_file.isEmpty()) {
+ QDesktopServices::openUrl(map_file);
+ }
+}
+
+void EndpointDialog::saveMap()
+{
+ QString destination_file =
+ WiresharkFileDialog::getSaveFileName(this, tr("Save Endpoints Map"),
+ "ipmap.html",
+ "HTML files (*.html);;GeoJSON files (*.json)");
+ if (destination_file.isEmpty()) {
+ return;
+ }
+ QUrl map_file = createMap(destination_file.endsWith(".json"));
+ if (!map_file.isEmpty()) {
+ QString source_file = map_file.toLocalFile();
+ QFile::remove(destination_file);
+ if (!QFile::rename(source_file, destination_file)) {
+ QMessageBox::warning(this, tr("Map file error"),
+ tr("Failed to save map file %1.").arg(destination_file));
+ QFile::remove(source_file);
+ }
+ }
+}
+#endif
+
void EndpointDialog::on_buttonBox_helpRequested()
{
wsApp->helpTopicAction(HELP_STATS_ENDPOINTS_DIALOG);
@@ -200,20 +321,32 @@ public:
return QVariant(data_none_);
}
}
+ if (role == Qt::UserRole) {
+ hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
+ return VariantPointer<hostlist_talker_t>::asQVariant(endp_item);
+ }
+ if (role == Qt::UserRole + 1) {
+ return VariantPointer<const mmdb_lookup_t>::asQVariant(mmdbLookup());
+ }
return QTreeWidgetItem::data(column, role);
}
- // Column text raw representation.
- // Return a string, qulonglong, double, or invalid QVariant representing the raw column data.
- QVariant colData(int col, bool resolve_names) const {
+ const mmdb_lookup_t *mmdbLookup() const {
hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
-
const mmdb_lookup_t *mmdb_lookup = NULL;
if (endp_item->myaddress.type == AT_IPv4) {
mmdb_lookup = maxmind_db_lookup_ipv4((ws_in4_addr *) endp_item->myaddress.data);
} else if (endp_item->myaddress.type == AT_IPv6) {
mmdb_lookup = maxmind_db_lookup_ipv6((ws_in6_addr *) endp_item->myaddress.data);
}
+ return mmdb_lookup && mmdb_lookup->found ? mmdb_lookup : NULL;
+ }
+
+ // Column text raw representation.
+ // Return a string, qulonglong, double, or invalid QVariant representing the raw column data.
+ QVariant colData(int col, bool resolve_names) const {
+ hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
+ const mmdb_lookup_t *mmdb_lookup = mmdbLookup();
switch (col) {
case ENDP_COLUMN_ADDR:
@@ -245,22 +378,22 @@ public:
case ENDP_COLUMN_BYTES_BA:
return quint64(endp_item->rx_bytes);
case ENDP_COLUMN_GEO_COUNTRY:
- if (mmdb_lookup && mmdb_lookup->found && mmdb_lookup->country) {
+ if (mmdb_lookup && mmdb_lookup->country) {
return QVariant(mmdb_lookup->country);
}
return QVariant();
case ENDP_COLUMN_GEO_CITY:
- if (mmdb_lookup && mmdb_lookup->found && mmdb_lookup->city) {
+ if (mmdb_lookup && mmdb_lookup->city) {
return QVariant(mmdb_lookup->city);
}
return QVariant();
case ENDP_COLUMN_GEO_AS_NUM:
- if (mmdb_lookup && mmdb_lookup->found && mmdb_lookup->as_number) {
+ if (mmdb_lookup && mmdb_lookup->as_number) {
return QVariant(mmdb_lookup->as_number);
}
return QVariant();
case ENDP_COLUMN_GEO_AS_ORG:
- if (mmdb_lookup && mmdb_lookup->found && mmdb_lookup->as_org) {
+ if (mmdb_lookup && mmdb_lookup->as_org) {
return QVariant(mmdb_lookup->as_org);
}
return QVariant();
@@ -330,6 +463,9 @@ private:
EndpointTreeWidget::EndpointTreeWidget(QWidget *parent, register_ct_t *table) :
TrafficTableTreeWidget(parent, table),
+#ifdef HAVE_MAXMINDDB
+ has_geoip_data_(false),
+#endif
table_address_type_(AT_NONE)
{
setColumnCount(ENDP_NUM_COLUMNS);
@@ -468,6 +604,16 @@ void EndpointTreeWidget::updateItems()
etwi->setTextAlignment(col, Qt::AlignRight);
}
}
+
+#ifdef HAVE_MAXMINDDB
+ // Assume that an asynchronous MMDB lookup has completed before (for
+ // example, in the dissection tree). If so, then we do not have to check
+ // all previous items for availability of any MMDB result.
+ if (!has_geoip_data_ && maxmind_db_has_coords(etwi->mmdbLookup())) {
+ has_geoip_data_ = true;
+ emit geoIPStatusChanged();
+ }
+#endif
}
addTopLevelItems(new_items);
setSortingEnabled(true);
diff --git a/ui/qt/endpoint_dialog.h b/ui/qt/endpoint_dialog.h
index 0a2786baf5..a30aee277a 100644
--- a/ui/qt/endpoint_dialog.h
+++ b/ui/qt/endpoint_dialog.h
@@ -19,12 +19,24 @@ public:
explicit EndpointTreeWidget(QWidget *parent, register_ct_t* table);
~EndpointTreeWidget();
+#ifdef HAVE_MAXMINDDB
+ bool hasGeoIPData() const { return has_geoip_data_; }
+#endif
+
static void tapReset(void *conv_hash_ptr);
static void tapDraw(void *conv_hash_ptr);
+#ifdef HAVE_MAXMINDDB
+signals:
+ void geoIPStatusChanged();
+#endif
+
private:
void updateItems();
+#ifdef HAVE_MAXMINDDB
+ bool has_geoip_data_;
+#endif
address_type table_address_type_;
private slots:
@@ -51,10 +63,19 @@ public slots:
void captureFileClosing();
private:
+#ifdef HAVE_MAXMINDDB
+ QPushButton *map_bt_;
+ QUrl createMap(bool json_only);
+#endif
bool addTrafficTable(register_ct_t* table);
private slots:
+#ifdef HAVE_MAXMINDDB
+ void tabChanged();
+ void openMap();
+ void saveMap();
+#endif
void on_buttonBox_helpRequested();
};
diff --git a/ui/traffic_table_ui.c b/ui/traffic_table_ui.c
index f24db5deee..03bd24d8c8 100644
--- a/ui/traffic_table_ui.c
+++ b/ui/traffic_table_ui.c
@@ -1,6 +1,5 @@
/* traffic_table_ui.c
- * Copied from gtk/conversations_table.c 2003 Ronnie Sahlberg
- * Helper routines common to all conversations taps.
+ * Helper routines common to conversation/endpoint tables.
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
@@ -15,6 +14,14 @@
#include "traffic_table_ui.h"
#include <wsutil/utf8_entities.h>
+#ifdef HAVE_MAXMINDDB
+#include <errno.h>
+
+#include "wsutil/filesystem.h"
+#include "wsutil/file_util.h"
+#include "wsutil/json_dumper.h"
+#endif
+
const char *conv_column_titles[CONV_NUM_COLUMNS] = {
"Address A",
"Port A",
@@ -53,15 +60,174 @@ const char *endp_column_titles[ENDP_NUM_GEO_COLUMNS] = {
const char *endp_conn_title = "Connection";
+#ifdef HAVE_MAXMINDDB
+gboolean
+write_endpoint_geoip_map(FILE *fp, gboolean json_only, hostlist_talker_t *const *hosts, gchar **err_str)
+{
+ if (!json_only) {
+ char *base_html_path = get_datafile_path("ipmap.html");
+ FILE *base_html_fp = ws_fopen(base_html_path, "rb");
+ if (!base_html_fp) {
+ *err_str = g_strdup_printf("Could not open base file %s for reading: %s",
+ base_html_path, g_strerror(errno));
+ g_free(base_html_path);
+ return FALSE;
+ }
+ g_free(base_html_path);
+
+ /* Copy ipmap.html to map file. */
+ size_t n;
+ char buf[4096];
+ while ((n = fread(buf, 1, sizeof(buf), base_html_fp)) != 0) {
+ if (fwrite(buf, 1, n, fp) != n) {
+ *err_str = g_strdup_printf("Failed to write to map file: %s", g_strerror(errno));
+ fclose(base_html_fp);
+ return FALSE;
+ }
+ }
+ if (ferror(base_html_fp)) {
+ *err_str = g_strdup_printf("Failed to read base file: %s", g_strerror(errno));
+ fclose(base_html_fp);
+ return FALSE;
+ }
+ fclose(base_html_fp);
+
+ fputs("<script id=\"ipmap-data\" type=\"application/json\">\n", fp);
+ }
+
+ /*
+ * Writes a feature for each resolved address, the output will look like:
+ * {
+ * "type": "FeatureCollection",
+ * "features": [
+ * {
+ * "type": "Feature",
+ * "geometry": {
+ * "type": "Point",
+ * "coordinates": [ -97.821999, 37.750999 ]
+ * },
+ * "properties": {
+ * "ip": "8.8.4.4",
+ * "autonomous_system_number": 15169,
+ * "autonomous_system_organization": "Google LLC",
+ * "city": "(omitted, but key is shown for documentation reasons)",
+ * "country": "United States",
+ * "radius": 1000,
+ * "packets": 1,
+ * "bytes": 1543
+ * }
+ * }
+ * ]
+ * }
+ */
+ json_dumper dumper = {
+ .output_file = fp,
+ .flags = JSON_DUMPER_FLAGS_PRETTY_PRINT
+ };
+ json_dumper_begin_object(&dumper);
+ json_dumper_set_member_name(&dumper, "type");
+ json_dumper_value_string(&dumper, "FeatureCollection");
+ json_dumper_set_member_name(&dumper, "features");
+ json_dumper_begin_array(&dumper);
+
+ /* Append map data. */
+ size_t count = 0;
+ const hostlist_talker_t *host;
+ for (hostlist_talker_t *const *iter = hosts; (host = *iter) != NULL; ++iter) {
+ char addr[WS_INET6_ADDRSTRLEN];
+ const mmdb_lookup_t *result = NULL;
+ if (host->myaddress.type == AT_IPv4) {
+ ws_in4_addr *ip4 = (ws_in4_addr *)host->myaddress.data;
+ result = maxmind_db_lookup_ipv4(ip4);
+ ws_inet_ntop4(ip4, addr, sizeof(addr));
+ } else if (host->myaddress.type == AT_IPv6) {
+ ws_in6_addr *ip6 = (ws_in6_addr *)host->myaddress.data;
+ result = maxmind_db_lookup_ipv6(ip6);
+ ws_inet_ntop6(ip6, addr, sizeof(addr));
+ }
+ if (!maxmind_db_has_coords(result)) {
+ // result could be NULL if the caller did not trigger a lookup
+ // before. result->found could be FALSE if no MMDB entry exists.
+ continue;
+ }
+
+ ++count;
+ json_dumper_begin_object(&dumper);
+
+ json_dumper_set_member_name(&dumper, "type");
+ json_dumper_value_string(&dumper, "Feature");
+
+ json_dumper_set_member_name(&dumper, "geometry");
+ {
+ json_dumper_begin_object(&dumper);
+ json_dumper_set_member_name(&dumper, "type");
+ json_dumper_value_string(&dumper, "Point");
+ json_dumper_set_member_name(&dumper, "coordinates");
+ json_dumper_begin_array(&dumper);
+ json_dumper_value_double(&dumper, result->longitude);
+ json_dumper_value_double(&dumper, result->latitude);
+ json_dumper_end_array(&dumper); // end coordinates
+ }
+ json_dumper_end_object(&dumper); // end geometry
+
+ json_dumper_set_member_name(&dumper, "properties");
+ json_dumper_begin_object(&dumper);
+ {
+ json_dumper_set_member_name(&dumper, "ip");
+ json_dumper_value_string(&dumper, addr);
+ if (result->as_number && result->as_org) {
+ json_dumper_set_member_name(&dumper, "autonomous_system_number");
+ json_dumper_value_anyf(&dumper, "%u", result->as_number);
+ json_dumper_set_member_name(&dumper, "autonomous_system_organization");
+ json_dumper_value_string(&dumper, result->as_org);
+ }
+ if (result->city) {
+ json_dumper_set_member_name(&dumper, "city");
+ json_dumper_value_string(&dumper, result->city);
+ }
+ if (result->country) {
+ json_dumper_set_member_name(&dumper, "country");
+ json_dumper_value_string(&dumper, result->country);
+ }
+ if (result->accuracy) {
+ json_dumper_set_member_name(&dumper, "radius");
+ json_dumper_value_anyf(&dumper, "%u", result->accuracy);
+ }
+ json_dumper_set_member_name(&dumper, "packets");
+ json_dumper_value_anyf(&dumper, "%" G_GUINT64_FORMAT, host->rx_frames + host->tx_frames);
+ json_dumper_set_member_name(&dumper, "bytes");
+ json_dumper_value_anyf(&dumper, "%" G_GUINT64_FORMAT, host->rx_bytes + host->tx_bytes);
+ }
+ json_dumper_end_object(&dumper); // end properties
+
+ json_dumper_end_object(&dumper);
+ }
+
+ json_dumper_end_array(&dumper); // end features
+ json_dumper_end_object(&dumper);
+ json_dumper_finish(&dumper);
+ if (!json_only) {
+ fputs("</script>\n", fp);
+ }
+
+ if (count == 0) {
+ *err_str = g_strdup("No endpoints available to map");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+#endif
+
/*
- * Editor modelines
+ * Editor modelines - https://www.wireshark.org/tools/modelines.html
*
- * Local Variables:
+ * Local variables:
* c-basic-offset: 4
* tab-width: 8
* indent-tabs-mode: nil
* End:
*
- * ex: set shiftwidth=4 tabstop=8 expandtab:
+ * vi: set shiftwidth=4 tabstop=8 expandtab:
* :indentSize=4:tabSize=8:noTabs=true:
*/
diff --git a/ui/traffic_table_ui.h b/ui/traffic_table_ui.h
index f1f858fc12..6d50d9eeeb 100644
--- a/ui/traffic_table_ui.h
+++ b/ui/traffic_table_ui.h
@@ -1,6 +1,5 @@
/* traffic_table_ui.h
- * Copied from gtk/conversations_table.h 2003 Ronnie Sahlberg
- * Helper routines common to all conversations taps.
+ * Helper routines common to conversation/endpoint tables.
*
* Wireshark - Network traffic analyzer
* By Gerald Combs <gerald@wireshark.org>
@@ -9,8 +8,15 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
-#ifndef __CONVERSATION_UI_H__
-#define __CONVERSATION_UI_H__
+#ifndef __TRAFFIC_TABLE_UI_H__
+#define __TRAFFIC_TABLE_UI_H__
+
+#ifdef HAVE_MAXMINDDB
+#include <stdio.h>
+
+#include "epan/maxmind_db.h"
+#include <epan/conversation_table.h>
+#endif
#ifdef __cplusplus
extern "C" {
@@ -66,21 +72,37 @@ extern const char *endp_column_titles[ENDP_NUM_GEO_COLUMNS];
extern const char *endp_conn_title;
+#ifdef HAVE_MAXMINDDB
+/**
+ * Writes an HTML file containing a map showing the geographical locations
+ * of IPv4 and IPv6 addresses.
+ *
+ * @param [in] fp File handle for writing the HTML file.
+ * @param [in] json_only Write GeoJSON data only.
+ * @param [in] hosts A NULL-terminated array of 'hostlist_talker_t'. A MMDB
+ * lookup should have been completed before for these addresses.
+ * @param [in,out] err_str Set to error string on failure. Error string must
+ * be g_freed. May be NULL.
+ * @return Whether the map file was successfully written with non-empty data.
+ */
+gboolean write_endpoint_geoip_map(FILE *fp, gboolean json_only, hostlist_talker_t *const *hosts, gchar **err_str);
+#endif
+
#ifdef __cplusplus
}
#endif /* __cplusplus */
-#endif /* __CONVERSATION_UI_H__ */
+#endif /* __TRAFFIC_TABLE_UI_H__ */
/*
- * Editor modelines
+ * Editor modelines - https://www.wireshark.org/tools/modelines.html
*
- * Local Variables:
+ * Local variables:
* c-basic-offset: 4
* tab-width: 8
* indent-tabs-mode: nil
* End:
*
- * ex: set shiftwidth=4 tabstop=8 expandtab:
+ * vi: set shiftwidth=4 tabstop=8 expandtab:
* :indentSize=4:tabSize=8:noTabs=true:
*/