mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 03:02:51 +08:00
394 lines
9.7 KiB
HTML
394 lines
9.7 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<title>SecBox Map</title>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
|
<style>
|
|
html {
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
margin: 0;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
#toolbar {
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.6em;
|
|
padding: 0.5em 0.8em;
|
|
box-sizing: border-box;
|
|
font-family: sans-serif;
|
|
font-size: 14px;
|
|
color: CanvasText;
|
|
background: Canvas;
|
|
border-bottom: 1px solid ButtonBorder;
|
|
}
|
|
|
|
#setSelect {
|
|
min-width: 16em;
|
|
max-width: 70vw;
|
|
padding: 0.25em;
|
|
}
|
|
|
|
#map {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
.title {
|
|
display: block;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.mono {
|
|
font-size: small;
|
|
font-family: monospace;
|
|
white-space: nowrap;
|
|
margin: 0.3em !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="toolbar">
|
|
<label for="setSelect">Set:</label>
|
|
<select id="setSelect"></select>
|
|
</div>
|
|
|
|
<div id="map"></div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
/* maximum display length of the free form GeoIP fields */
|
|
const MAX_IP_LEN = 38;
|
|
const MAX_AS_LEN = 38;
|
|
const MAX_CITY_LEN = 33;
|
|
|
|
/* zoom level applied when the view is centered on a single point */
|
|
const POINT_ZOOM = 6;
|
|
|
|
/* mapData key holding the geo data of the local uplink(s) */
|
|
const HOME_KEY = 'homeIP';
|
|
|
|
const HOME_STYLE = { color: '#378242', opacity: 1.0, fillColor: '#378242', fillOpacity: 0.5, radius: 6 };
|
|
const BLOCKED_STYLE = { color: '#C22121', opacity: 1.0, fillColor: '#C22121', fillOpacity: 0.5, radius: 3 };
|
|
|
|
/* initialize map, canvas rendering keeps the per Set marker volume cheap */
|
|
const map = L.map('map', {
|
|
zoom: 3,
|
|
minZoom: 2,
|
|
maxZoom: 18,
|
|
center: [50, 10],
|
|
worldCopyJump: true,
|
|
preferCanvas: true
|
|
});
|
|
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
}).addTo(map);
|
|
|
|
/* feature groups, so redrawing is a single clearLayers() and getBounds() is available */
|
|
const homeLayer = L.featureGroup().addTo(map);
|
|
const blockedLayer = L.featureGroup().addTo(map);
|
|
|
|
const setSelect = document.getElementById('setSelect');
|
|
|
|
/* accept a coordinate only if it is a finite number in range */
|
|
function toLatLng(geo) {
|
|
const lat = Number(geo.lat);
|
|
const lon = Number(geo.lon);
|
|
|
|
if (geo.lat === null || geo.lat === undefined || geo.lat === '' ||
|
|
geo.lon === null || geo.lon === undefined || geo.lon === '')
|
|
return null;
|
|
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lon))
|
|
return null;
|
|
|
|
if (lat < -90 || lat > 90 || lon < -180 || lon > 180)
|
|
return null;
|
|
|
|
return [lat, lon];
|
|
}
|
|
|
|
/* GeoIP lookups regularly return empty city/as fields, keep the entry anyway */
|
|
function fieldText(value, maxLength) {
|
|
const text = String(value === null || value === undefined ? '' : value).trim();
|
|
|
|
if (!text)
|
|
return 'n/a';
|
|
|
|
return text.length > maxLength ? text.slice(0, maxLength - 1) + '\u2026' : text;
|
|
}
|
|
|
|
/*
|
|
* Popups are built as DOM nodes, not as HTML strings, so the third party
|
|
* GeoIP fields cannot be interpreted as markup. Passing a function to
|
|
* bindPopup() defers this until the popup is actually opened.
|
|
*/
|
|
function buildPopup(title, entry) {
|
|
const content = document.createElement('div');
|
|
const heading = document.createElement('span');
|
|
const body = document.createElement('p');
|
|
const geo = entry.geo;
|
|
const latlng = entry.latlng;
|
|
const more = entry.count > 1 ? ' (+' + (entry.count - 1) + ' more)' : '';
|
|
const lines = [
|
|
'City: ' + fieldText(geo.city, MAX_CITY_LEN) + ' (' + fieldText(geo.countryCode, 8) + ')',
|
|
'Latitude : ' + latlng[0],
|
|
'Longitude: ' + latlng[1],
|
|
'IP: ' + fieldText(geo.query, MAX_IP_LEN) + more,
|
|
'AS: ' + fieldText(geo.as, MAX_AS_LEN)
|
|
];
|
|
|
|
heading.className = 'title';
|
|
heading.textContent = title;
|
|
body.className = 'mono';
|
|
|
|
lines.forEach(function (line, index) {
|
|
if (index)
|
|
body.appendChild(document.createElement('br'));
|
|
|
|
body.appendChild(document.createTextNode(line));
|
|
});
|
|
|
|
content.appendChild(heading);
|
|
content.appendChild(body);
|
|
|
|
return content;
|
|
}
|
|
|
|
function addMarker(layer, title, entry, style) {
|
|
L.circleMarker(entry.latlng, style)
|
|
.bindPopup(function () {
|
|
return buildPopup(title, entry);
|
|
})
|
|
.addTo(layer);
|
|
}
|
|
|
|
/*
|
|
* GeoIP resolves to city level, so several IPs of one Set regularly share a
|
|
* coordinate. Stacked markers are indistinguishable and only the topmost one
|
|
* can be opened, so collapse them into one marker and count the IPs behind it.
|
|
* Collapsing happens per Set, otherwise the Set that reaches a coordinate first
|
|
* would swallow the points of every later Set and make the counts depend on the
|
|
* order of ban_map.jsn.
|
|
*/
|
|
function addEntry(list, index, geo, latlng) {
|
|
const key = latlng[0] + ',' + latlng[1];
|
|
const known = index.get(key);
|
|
|
|
if (known) {
|
|
known.count++;
|
|
return;
|
|
}
|
|
|
|
const entry = { geo: geo, latlng: latlng, count: 1 };
|
|
|
|
index.set(key, entry);
|
|
list.push(entry);
|
|
}
|
|
|
|
/*
|
|
* mapData is an array of single key objects, the key being the SecBox Set name:
|
|
*
|
|
* [ {}, { "homeIP": {...} }, { "country_ir.v4": {...} }, ... ]
|
|
*/
|
|
function collectEntries(parsedData) {
|
|
const result = { home: [], sets: {} };
|
|
const homeIndex = new Map();
|
|
const setIndex = new Map();
|
|
|
|
parsedData.forEach(function (item) {
|
|
if (!item || typeof item !== 'object')
|
|
return;
|
|
|
|
Object.keys(item).forEach(function (setName) {
|
|
const geo = item[setName];
|
|
|
|
if (!geo || typeof geo !== 'object')
|
|
return;
|
|
|
|
const latlng = toLatLng(geo);
|
|
|
|
if (!latlng)
|
|
return;
|
|
|
|
if (setName === HOME_KEY) {
|
|
addEntry(result.home, homeIndex, geo, latlng);
|
|
return;
|
|
}
|
|
|
|
if (!result.sets[setName]) {
|
|
result.sets[setName] = [];
|
|
setIndex.set(setName, new Map());
|
|
}
|
|
|
|
addEntry(result.sets[setName], setIndex.get(setName), geo, latlng);
|
|
});
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function countIPs(entries) {
|
|
let total = 0;
|
|
|
|
entries.forEach(function (entry) {
|
|
total += entry.count;
|
|
});
|
|
|
|
return total;
|
|
}
|
|
|
|
function setNames(sets) {
|
|
return Object.keys(sets).sort();
|
|
}
|
|
|
|
/*
|
|
* An empty selection renders all Sets, otherwise the selected one only.
|
|
* Every mapped location of a rendered Set is drawn, no cap is applied, so
|
|
* the result does not depend on how f_report() truncates set_elements.
|
|
*/
|
|
function renderBlocked(sets, selected) {
|
|
const names = selected ? [selected] : setNames(sets);
|
|
|
|
blockedLayer.clearLayers();
|
|
|
|
names.forEach(function (setName) {
|
|
const entries = sets[setName] || [];
|
|
|
|
entries.forEach(function (entry) {
|
|
addMarker(blockedLayer, 'blocked IP by ' + setName, entry, BLOCKED_STYLE);
|
|
});
|
|
});
|
|
|
|
/* the canvas renderer draws in insertion order, keep home on top */
|
|
homeLayer.eachLayer(function (marker) {
|
|
marker.bringToFront();
|
|
});
|
|
}
|
|
|
|
function fitLayer(layer) {
|
|
const layers = layer.getLayers();
|
|
|
|
if (!layers.length)
|
|
return false;
|
|
|
|
if (layers.length === 1)
|
|
map.setView(layers[0].getLatLng(), POINT_ZOOM);
|
|
else
|
|
map.fitBounds(layer.getBounds(), { maxZoom: POINT_ZOOM, padding: [30, 30] });
|
|
|
|
return true;
|
|
}
|
|
|
|
/* center on the local uplink(s), fall back to the blocked IPs */
|
|
function resetView() {
|
|
if (!fitLayer(homeLayer))
|
|
fitLayer(blockedLayer);
|
|
}
|
|
|
|
function addOption(value, label) {
|
|
const option = document.createElement('option');
|
|
|
|
option.value = value;
|
|
option.textContent = label;
|
|
setSelect.appendChild(option);
|
|
}
|
|
|
|
function setNotice(label) {
|
|
setSelect.textContent = '';
|
|
addOption('', label);
|
|
setSelect.disabled = true;
|
|
}
|
|
|
|
function plural(count, noun) {
|
|
return count + ' ' + noun + (count === 1 ? '' : 's');
|
|
}
|
|
|
|
function optionLabel(name, entries) {
|
|
return name + ' (' + plural(countIPs(entries), 'IP') +
|
|
' at ' + plural(entries.length, 'location') + ')';
|
|
}
|
|
|
|
function fillSelect(sets) {
|
|
const names = setNames(sets);
|
|
let all = [];
|
|
|
|
names.forEach(function (setName) {
|
|
all = all.concat(sets[setName]);
|
|
});
|
|
|
|
if (!names.length) {
|
|
setNotice('No mappable Set entries');
|
|
return;
|
|
}
|
|
|
|
setSelect.textContent = '';
|
|
setSelect.disabled = false;
|
|
addOption('', optionLabel('All Sets', all));
|
|
|
|
names.forEach(function (setName) {
|
|
addOption(setName, optionLabel(setName, sets[setName]));
|
|
});
|
|
}
|
|
|
|
/* load the data handed over by the SecBox set report page */
|
|
const mapData = sessionStorage.getItem('mapData');
|
|
let parsedData = null;
|
|
|
|
if (mapData) {
|
|
try {
|
|
parsedData = JSON.parse(mapData);
|
|
} catch (error) {
|
|
console.error('SecBox: unable to parse mapData,', error);
|
|
}
|
|
}
|
|
|
|
if (!mapData) {
|
|
setNotice('No SecBox map data available');
|
|
} else if (!Array.isArray(parsedData)) {
|
|
setNotice('Unable to load SecBox map data');
|
|
} else {
|
|
const entries = collectEntries(parsedData);
|
|
|
|
entries.home.forEach(function (entry) {
|
|
addMarker(homeLayer, 'local IP', entry, HOME_STYLE);
|
|
});
|
|
|
|
fillSelect(entries.sets);
|
|
renderBlocked(entries.sets, '');
|
|
resetView();
|
|
|
|
setSelect.addEventListener('change', function () {
|
|
renderBlocked(entries.sets, setSelect.value);
|
|
|
|
if (!setSelect.value || !fitLayer(blockedLayer))
|
|
resetView();
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |