no sqli pls

This commit is contained in:
ajvpot
2025-07-03 00:00:00 +00:00
parent c3379b0bee
commit 015f910bdc
5 changed files with 85 additions and 31 deletions

17
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "meshexplorer",
"version": "0.1.0",
"dependencies": {
"@clickhouse/client": "^1.11.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"@types/leaflet": "^1.9.19",
@@ -62,6 +63,22 @@
"node": ">=6.0.0"
}
},
"node_modules/@clickhouse/client": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.2.tgz",
"integrity": "sha512-ZE7Q1qxsDNXCkGPf1zqmhpZpwAKxKT+1s4Z432J1Mb2Gm26Y4tG/sJoug81AfAJTt6s7taO2vzNBAKfSR3SStg==",
"dependencies": {
"@clickhouse/client-common": "1.11.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@clickhouse/client-common": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.11.2.tgz",
"integrity": "sha512-H4ECHqaipzMgiZKqpb1Z4N3Ofq+lVTCn8I59XsSynqrsfR4jWZD3PipXVvIzMpDmTMvrlJWrOwAdm0DMNiMQbA=="
},
"node_modules/@emnapi/core": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",

View File

@@ -3,12 +3,13 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@clickhouse/client": "^1.11.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"@types/leaflet": "^1.9.19",

View File

@@ -16,7 +16,7 @@ const DEFAULT = {
};
type NodePosition = {
from_node_id: string;
node_id: string;
latitude: number;
longitude: number;
altitude?: number;
@@ -56,10 +56,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
});
const marker = L.marker([node.latitude, node.longitude], { icon });
(marker as any).options.nodeData = node;
let popupHtml = `<div><div><b>ID:</b> ${node.from_node_id}</div><div><b>Lat:</b> ${node.latitude}</div><div><b>Lng:</b> ${node.longitude}</div>`;
if (node.altitude !== undefined) popupHtml += `<div><b>Alt:</b> ${node.altitude}</div>`;
if (node.last_seen) popupHtml += `<div><b>Last seen:</b> ${node.last_seen}</div>`;
if (node.type) popupHtml += `<div><b>Type:</b> ${node.type}</div>`;
let popupHtml = `<div>`;
popupHtml += `<div><b>ID:</b> ${node.node_id}</div>`;
popupHtml += `<div><b>Short Name:</b> ${node.short_name ?? "-"}</div>`;
popupHtml += `<div><b>Type:</b> ${node.type ?? "-"}</div>`;
popupHtml += `<div><b>Lat:</b> ${node.latitude}</div>`;
popupHtml += `<div><b>Lng:</b> ${node.longitude}</div>`;
popupHtml += `<div><b>Alt:</b> ${node.altitude !== undefined ? node.altitude : "-"}</div>`;
popupHtml += `<div><b>Last seen:</b> ${node.last_seen ?? "-"}</div>`;
popupHtml += `</div>`;
marker.bindPopup(popupHtml);
marker.addTo(map);
@@ -155,10 +159,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
});
const marker = L.marker([node.latitude, node.longitude], { icon });
(marker as any).options.nodeData = node;
let popupHtml = `<div><div><b>ID:</b> ${node.from_node_id}</div><div><b>Lat:</b> ${node.latitude}</div><div><b>Lng:</b> ${node.longitude}</div>`;
if (node.altitude !== undefined) popupHtml += `<div><b>Alt:</b> ${node.altitude}</div>`;
if (node.last_seen) popupHtml += `<div><b>Last seen:</b> ${node.last_seen}</div>`;
if (node.type) popupHtml += `<div><b>Type:</b> ${node.type}</div>`;
let popupHtml = `<div>`;
popupHtml += `<div><b>ID:</b> ${node.node_id}</div>`;
popupHtml += `<div><b>Short Name:</b> ${node.short_name ?? "-"}</div>`;
popupHtml += `<div><b>Type:</b> ${node.type ?? "-"}</div>`;
popupHtml += `<div><b>Lat:</b> ${node.latitude}</div>`;
popupHtml += `<div><b>Lng:</b> ${node.longitude}</div>`;
popupHtml += `<div><b>Alt:</b> ${node.altitude !== undefined ? node.altitude : "-"}</div>`;
popupHtml += `<div><b>Last seen:</b> ${node.last_seen ?? "-"}</div>`;
popupHtml += `</div>`;
marker.bindPopup(popupHtml);
markers.addLayer(marker);
@@ -177,6 +185,7 @@ export default function MapView() {
const [nodePositions, setNodePositions] = useState<NodePosition[]>([]);
const [bounds, setBounds] = useState<[[number, number], [number, number]] | null>(null);
const [loading, setLoading] = useState(false);
const [lastResultCount, setLastResultCount] = useState<number>(0);
const fetchController = useRef<AbortController | null>(null);
const lastRequestedBounds = useRef<[[number, number], [number, number]] | null>(null);
const { config } = useConfig ? useConfig() : { config: undefined };
@@ -231,7 +240,10 @@ export default function MapView() {
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data)) setNodePositions(data);
if (Array.isArray(data)) {
setNodePositions(data);
setLastResultCount(data.length);
}
if (fetchController.current === controller) setLoading(false);
})
.catch((err) => {
@@ -255,7 +267,7 @@ export default function MapView() {
useMapEvents({
moveend: (e) => {
const b = e.target.getBounds();
const buffer = 0.2; // 20% buffer
const buffer = 0.05; // 5% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
const newBounds: [[number, number], [number, number]] = [
@@ -268,13 +280,18 @@ export default function MapView() {
b.getNorthEast().lng + lngDiff * buffer,
],
];
if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
// Only always refetch if we have too many nodes depending on clustering setting.
if (
(lastResultCount > (config?.clustering ? 5000: 1000)) ||
!lastRequestedBounds.current ||
!isBoundsInside(newBounds, lastRequestedBounds.current)
) {
setBounds(newBounds);
}
},
zoomend: (e) => {
const b = e.target.getBounds();
const buffer = 0.2; // 20% buffer
const buffer = 0.05; // 5% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
const newBounds: [[number, number], [number, number]] = [
@@ -287,7 +304,12 @@ export default function MapView() {
b.getNorthEast().lng + lngDiff * buffer,
],
];
if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
// Only always refetch if clustering is disabled and lastResultCount > 1000
if (
(config?.clustering === false && lastResultCount > 1000) ||
!lastRequestedBounds.current ||
!isBoundsInside(newBounds, lastRequestedBounds.current)
) {
setBounds(newBounds);
}
},

View File

@@ -6,19 +6,34 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
"latitude IS NOT NULL",
"longitude IS NOT NULL"
];
if (minLat) where.push(`latitude >= ${minLat}`);
if (maxLat) where.push(`latitude <= ${maxLat}`);
if (minLng) where.push(`longitude >= ${minLng}`);
if (maxLng) where.push(`longitude <= ${maxLng}`);
const params: Record<string, any> = {};
if (minLat !== null && minLat !== undefined && minLat !== "") {
where.push(`latitude >= {minLat:Float64}`);
params.minLat = Number(minLat);
}
if (maxLat !== null && maxLat !== undefined && maxLat !== "") {
where.push(`latitude <= {maxLat:Float64}`);
params.maxLat = Number(maxLat);
}
if (minLng !== null && minLng !== undefined && minLng !== "") {
where.push(`longitude >= {minLng:Float64}`);
params.minLng = Number(minLng);
}
if (maxLng !== null && maxLng !== undefined && maxLng !== "") {
where.push(`longitude <= {maxLng:Float64}`);
params.maxLng = Number(maxLng);
}
if (nodeTypes && nodeTypes.length > 0) {
const types = nodeTypes.map(t => `'${t}'`).join(",");
where.push(`type IN (${types})`);
where.push(`type IN {nodeTypes:Array(String)}`);
params.nodeTypes = nodeTypes;
}
if (lastSeen !== null && lastSeen !== undefined && lastSeen !== "") {
where.push(`last_seen >= now() - INTERVAL ${lastSeen} SECOND`);
where.push(`last_seen >= now() - INTERVAL {lastSeen:UInt32} SECOND`);
params.lastSeen = Number(lastSeen);
}
const query = `SELECT node_id, name, short_name, latitude, longitude, last_seen, type FROM unified_latest_nodeinfo WHERE ${where.join(" AND ")}`;
const rows = await clickhouse.query(query).toPromise();
const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' });
const rows = await resultSet.json();
return rows as Array<{
node_id: string;
name?: string | null;
@@ -28,4 +43,4 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
last_seen: string;
type: string;
}>;
}
}

View File

@@ -1,14 +1,13 @@
import { ClickHouse } from 'clickhouse';
import { createClient } from '@clickhouse/client';
const host = process.env.CLICKHOUSE_HOST || 'localhost';
const port = process.env.CLICKHOUSE_PORT || '8123';
const user = process.env.CLICKHOUSE_USER || 'default';
const password = process.env.CLICKHOUSE_PASSWORD || 'password';
export const clickhouse = new ClickHouse({
url: `http://${host}`,
port: Number(port),
basicAuth: { username: user, password },
isUseGzip: false,
format: 'json',
export const clickhouse = createClient({
host: `http://${host}:${port}`,
username: user,
password: password,
// You can add more options as needed, e.g. database, compression, etc.
});