mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-06-11 01:34:52 +02:00
made changes
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""Drop node_public_key table
|
||||
|
||||
Revision ID: e8f2c4b6d9a1
|
||||
Revises: c6b1d8f2a9e3
|
||||
Create Date: 2026-05-25 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e8f2c4b6d9a1"
|
||||
down_revision: str | None = "c6b1d8f2a9e3"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_index("idx_node_public_key_public_key", table_name="node_public_key")
|
||||
op.drop_index("idx_node_public_key_node_id", table_name="node_public_key")
|
||||
op.drop_table("node_public_key")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.create_table(
|
||||
"node_public_key",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column("node_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("public_key", sa.String(), nullable=False),
|
||||
sa.Column("first_seen_us", sa.BigInteger(), nullable=True),
|
||||
sa.Column("last_seen_us", sa.BigInteger(), nullable=True),
|
||||
)
|
||||
op.create_index("idx_node_public_key_node_id", "node_public_key", ["node_id"], unique=False)
|
||||
op.create_index(
|
||||
"idx_node_public_key_public_key",
|
||||
"node_public_key",
|
||||
["public_key"],
|
||||
unique=False,
|
||||
)
|
||||
@@ -1 +1 @@
|
||||
cb1f89372a70b0d4b4f8caf05aec28de8d4a13e0
|
||||
59cb394dcfc4432cb216358ca26e861c7d13f462
|
||||
|
||||
@@ -243,7 +243,6 @@
|
||||
"share_contact_qr": "Share Contact QR",
|
||||
"copy_url": "Copy URL",
|
||||
"copied": "Copied!",
|
||||
"potential_impersonation": "Potential Impersonation Detected",
|
||||
"scan_qr_to_add": "Scan this QR code to add this node as a contact on another device."
|
||||
},
|
||||
"packet": {
|
||||
|
||||
@@ -229,7 +229,6 @@
|
||||
"share_contact_qr": "Compartir contacto QR",
|
||||
"copy_url": "Copiar URL",
|
||||
"copied": "¡Copiado!",
|
||||
"potential_impersonation": "Posible suplantación detectada",
|
||||
"scan_qr_to_add": "Escanea este código QR para agregar este nodo como contacto en otro dispositivo."
|
||||
},
|
||||
|
||||
|
||||
@@ -224,7 +224,6 @@
|
||||
"share_contact_qr": "Поделиться QR контакта",
|
||||
"copy_url": "Копировать URL",
|
||||
"copied": "Скопировано!",
|
||||
"potential_impersonation": "Обнаружено возможное самозванство",
|
||||
"scan_qr_to_add": "Отсканируйте этот QR-код, чтобы добавить эту ноду в качестве контакта на другом устройстве."
|
||||
},
|
||||
"packet": {
|
||||
|
||||
@@ -108,21 +108,6 @@ class Traceroute(Base):
|
||||
)
|
||||
|
||||
|
||||
class NodePublicKey(Base):
|
||||
__tablename__ = "node_public_key"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
node_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
public_key: Mapped[str] = mapped_column(nullable=False)
|
||||
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
last_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_node_public_key_node_id", "node_id"),
|
||||
Index("idx_node_public_key_public_key", "public_key"),
|
||||
)
|
||||
|
||||
|
||||
class DailySnapshot(Base):
|
||||
__tablename__ = "daily_snapshot"
|
||||
|
||||
|
||||
+1
-22
@@ -12,7 +12,7 @@ from meshtastic.protobuf.config_pb2 import Config
|
||||
from meshtastic.protobuf.mesh_pb2 import HardwareModel
|
||||
from meshtastic.protobuf.portnums_pb2 import PortNum
|
||||
from meshview import decode_payload, mqtt_database
|
||||
from meshview.models import DailySnapshot, Node, NodePublicKey, Packet, PacketSeen, Traceroute
|
||||
from meshview.models import DailySnapshot, Node, Packet, PacketSeen, Traceroute
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -292,27 +292,6 @@ async def process_envelope(topic, env):
|
||||
)
|
||||
session.add(node)
|
||||
|
||||
if user.public_key:
|
||||
public_key_hex = user.public_key.hex()
|
||||
existing_key = (
|
||||
await session.execute(
|
||||
select(NodePublicKey).where(
|
||||
NodePublicKey.node_id == node_id,
|
||||
NodePublicKey.public_key == public_key_hex,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing_key:
|
||||
existing_key.last_seen_us = now_us
|
||||
else:
|
||||
new_key = NodePublicKey(
|
||||
node_id=node_id,
|
||||
public_key=public_key_hex,
|
||||
first_seen_us=now_us,
|
||||
last_seen_us=now_us,
|
||||
)
|
||||
session.add(new_key)
|
||||
except Exception as e:
|
||||
print(f"Error processing NODEINFO_APP: {e}")
|
||||
|
||||
|
||||
@@ -296,32 +296,6 @@
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
/* --- Impersonation Warning --- */
|
||||
.impersonation-warning {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.impersonation-warning .warning-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.impersonation-warning .warning-content {
|
||||
flex: 1;
|
||||
}
|
||||
.impersonation-warning .warning-title {
|
||||
color: #f87171;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.impersonation-warning .warning-text {
|
||||
font-size: 0.85rem;
|
||||
color: #ccc;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -351,15 +325,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Impersonation Warning -->
|
||||
<div id="impersonationWarning" class="impersonation-warning" style="display:none;">
|
||||
<span class="warning-icon">⚠️</span>
|
||||
<div class="warning-content">
|
||||
<div class="warning-title" data-translate-lang="potential_impersonation">Potential Impersonation Detected</div>
|
||||
<div class="warning-text" id="impersonationText"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node Info -->
|
||||
<div id="node-info" class="node-info">
|
||||
<div class="node-info-col">
|
||||
@@ -1637,8 +1602,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
requestAnimationFrame(async () => {
|
||||
await loadNodeInfo();
|
||||
|
||||
// Load QR code URL and impersonation check
|
||||
await loadNodeQrAndImpersonation();
|
||||
await loadNodeQr();
|
||||
|
||||
// ✅ MAP MUST EXIST FIRST
|
||||
if (!map) initMap();
|
||||
@@ -1774,16 +1738,11 @@ async function loadNodeStats(nodeId) {
|
||||
|
||||
let currentMeshtasticUrl = "";
|
||||
|
||||
async function loadNodeQrAndImpersonation() {
|
||||
async function loadNodeQr() {
|
||||
const actionsDiv = document.getElementById("nodeActions");
|
||||
const warningDiv = document.getElementById("impersonationWarning");
|
||||
|
||||
try {
|
||||
const [qrRes, impRes] = await Promise.all([
|
||||
fetch(`/api/node/${fromNodeId}/qr`),
|
||||
fetch(`/api/node/${fromNodeId}/impersonation-check`)
|
||||
]);
|
||||
|
||||
const qrRes = await fetch(`/api/node/${fromNodeId}/qr`);
|
||||
const qrData = await qrRes.json();
|
||||
if (qrRes.ok && qrData.meshtastic_url) {
|
||||
currentMeshtasticUrl = qrData.meshtastic_url;
|
||||
@@ -1791,19 +1750,9 @@ async function loadNodeQrAndImpersonation() {
|
||||
} else {
|
||||
actionsDiv.style.display = "none";
|
||||
}
|
||||
|
||||
const impData = await impRes.json();
|
||||
if (impRes.ok && impData.potential_impersonation) {
|
||||
warningDiv.style.display = "flex";
|
||||
document.getElementById("impersonationText").textContent =
|
||||
impData.warning || `This node has sent ${impData.unique_public_key_count} different public keys. This could indicate impersonation.`;
|
||||
} else {
|
||||
warningDiv.style.display = "none";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load QR/impersonation data:", err);
|
||||
console.error("Failed to load QR data:", err);
|
||||
actionsDiv.style.display = "none";
|
||||
warningDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-39
@@ -13,7 +13,7 @@ from meshtastic.protobuf.portnums_pb2 import PortNum
|
||||
from meshview import database, decode_payload, store
|
||||
from meshview.__version__ import __version__, _git_revision_short, get_version_info
|
||||
from meshview.config import CONFIG
|
||||
from meshview.models import DailySnapshot, Node, NodePublicKey
|
||||
from meshview.models import DailySnapshot, Node
|
||||
from meshview.models import Packet as PacketModel
|
||||
from meshview.models import PacketSeen as PacketSeenModel
|
||||
from meshview.radio.coverage import (
|
||||
@@ -1097,44 +1097,6 @@ async def api_node_qr(request):
|
||||
return web.json_response({"error": f"Failed to generate URL: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@routes.get("/api/node/{node_id}/impersonation-check")
|
||||
async def api_node_impersonation_check(request):
|
||||
"""
|
||||
Check if a node has multiple different public keys, which could indicate impersonation.
|
||||
"""
|
||||
try:
|
||||
node_id_str = request.match_info["node_id"]
|
||||
node_id = int(node_id_str, 0)
|
||||
except (KeyError, ValueError):
|
||||
return web.json_response({"error": "Invalid node_id"}, status=400)
|
||||
|
||||
try:
|
||||
async with database.async_session() as session:
|
||||
result = await session.execute(
|
||||
select(NodePublicKey.public_key).where(NodePublicKey.node_id == node_id).distinct()
|
||||
)
|
||||
public_keys = result.scalars().all()
|
||||
|
||||
unique_key_count = len(public_keys)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"node_id": node_id,
|
||||
"unique_public_key_count": unique_key_count,
|
||||
"potential_impersonation": unique_key_count > 1,
|
||||
"public_keys": public_keys
|
||||
if unique_key_count <= 3
|
||||
else public_keys[:3] + ["..."],
|
||||
"warning": "Multiple different public keys detected. This node may be getting impersonated."
|
||||
if unique_key_count > 1
|
||||
else None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking impersonation for node {node_id}: {e}")
|
||||
return web.json_response({"error": "Failed to check impersonation"}, status=500)
|
||||
|
||||
|
||||
@routes.get("/api/coverage/{node_id}")
|
||||
async def api_coverage(request):
|
||||
try:
|
||||
|
||||
-10
@@ -213,16 +213,6 @@ async def daily_cleanup_at(
|
||||
)
|
||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from Traceroute")
|
||||
|
||||
# -------------------------
|
||||
# NodePublicKey
|
||||
# -------------------------
|
||||
result = await session.execute(
|
||||
delete(models.NodePublicKey).where(
|
||||
models.NodePublicKey.last_seen_us < cutoff_us
|
||||
)
|
||||
)
|
||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from NodePublicKey")
|
||||
|
||||
# -------------------------
|
||||
# Node
|
||||
# -------------------------
|
||||
|
||||
Reference in New Issue
Block a user