mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-10-30 18:51:57 +01:00
made changes
This commit is contained in:
1 parent
2ecaaae2a9
commit
db04f4dc9f
10 files changed
+50
-145
No files matched your search
@@ -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",
|
"share_contact_qr": "Share Contact QR",
|
||||||
"copy_url": "Copy URL",
|
"copy_url": "Copy URL",
|
||||||
"copied": "Copied!",
|
"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."
|
"scan_qr_to_add": "Scan this QR code to add this node as a contact on another device."
|
||||||
},
|
},
|
||||||
"packet": {
|
"packet": {
|
||||||
|
|||||||
@@ -229,7 +229,6 @@
|
|||||||
"share_contact_qr": "Compartir contacto QR",
|
"share_contact_qr": "Compartir contacto QR",
|
||||||
"copy_url": "Copiar URL",
|
"copy_url": "Copiar URL",
|
||||||
"copied": "¡Copiado!",
|
"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."
|
"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 контакта",
|
"share_contact_qr": "Поделиться QR контакта",
|
||||||
"copy_url": "Копировать URL",
|
"copy_url": "Копировать URL",
|
||||||
"copied": "Скопировано!",
|
"copied": "Скопировано!",
|
||||||
"potential_impersonation": "Обнаружено возможное самозванство",
|
|
||||||
"scan_qr_to_add": "Отсканируйте этот QR-код, чтобы добавить эту ноду в качестве контакта на другом устройстве."
|
"scan_qr_to_add": "Отсканируйте этот QR-код, чтобы добавить эту ноду в качестве контакта на другом устройстве."
|
||||||
},
|
},
|
||||||
"packet": {
|
"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):
|
class DailySnapshot(Base):
|
||||||
__tablename__ = "daily_snapshot"
|
__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.mesh_pb2 import HardwareModel
|
||||||
from meshtastic.protobuf.portnums_pb2 import PortNum
|
from meshtastic.protobuf.portnums_pb2 import PortNum
|
||||||
from meshview import decode_payload, mqtt_database
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -292,27 +292,6 @@ async def process_envelope(topic, env):
|
|||||||
)
|
)
|
||||||
session.add(node)
|
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:
|
except Exception as e:
|
||||||
print(f"Error processing NODEINFO_APP: {e}")
|
print(f"Error processing NODEINFO_APP: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -296,32 +296,6 @@
|
|||||||
color:#fff;
|
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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@@ -351,15 +325,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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 -->
|
<!-- Node Info -->
|
||||||
<div id="node-info" class="node-info">
|
<div id="node-info" class="node-info">
|
||||||
<div class="node-info-col">
|
<div class="node-info-col">
|
||||||
@@ -1637,8 +1602,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
requestAnimationFrame(async () => {
|
requestAnimationFrame(async () => {
|
||||||
await loadNodeInfo();
|
await loadNodeInfo();
|
||||||
|
|
||||||
// Load QR code URL and impersonation check
|
await loadNodeQr();
|
||||||
await loadNodeQrAndImpersonation();
|
|
||||||
|
|
||||||
// ✅ MAP MUST EXIST FIRST
|
// ✅ MAP MUST EXIST FIRST
|
||||||
if (!map) initMap();
|
if (!map) initMap();
|
||||||
@@ -1774,16 +1738,11 @@ async function loadNodeStats(nodeId) {
|
|||||||
|
|
||||||
let currentMeshtasticUrl = "";
|
let currentMeshtasticUrl = "";
|
||||||
|
|
||||||
async function loadNodeQrAndImpersonation() {
|
async function loadNodeQr() {
|
||||||
const actionsDiv = document.getElementById("nodeActions");
|
const actionsDiv = document.getElementById("nodeActions");
|
||||||
const warningDiv = document.getElementById("impersonationWarning");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [qrRes, impRes] = await Promise.all([
|
const qrRes = await fetch(`/api/node/${fromNodeId}/qr`);
|
||||||
fetch(`/api/node/${fromNodeId}/qr`),
|
|
||||||
fetch(`/api/node/${fromNodeId}/impersonation-check`)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const qrData = await qrRes.json();
|
const qrData = await qrRes.json();
|
||||||
if (qrRes.ok && qrData.meshtastic_url) {
|
if (qrRes.ok && qrData.meshtastic_url) {
|
||||||
currentMeshtasticUrl = qrData.meshtastic_url;
|
currentMeshtasticUrl = qrData.meshtastic_url;
|
||||||
@@ -1791,19 +1750,9 @@ async function loadNodeQrAndImpersonation() {
|
|||||||
} else {
|
} else {
|
||||||
actionsDiv.style.display = "none";
|
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) {
|
} catch (err) {
|
||||||
console.error("Failed to load QR/impersonation data:", err);
|
console.error("Failed to load QR data:", err);
|
||||||
actionsDiv.style.display = "none";
|
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 import database, decode_payload, store
|
||||||
from meshview.__version__ import __version__, _git_revision_short, get_version_info
|
from meshview.__version__ import __version__, _git_revision_short, get_version_info
|
||||||
from meshview.config import CONFIG
|
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 Packet as PacketModel
|
||||||
from meshview.models import PacketSeen as PacketSeenModel
|
from meshview.models import PacketSeen as PacketSeenModel
|
||||||
from meshview.radio.coverage import (
|
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)
|
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}")
|
@routes.get("/api/coverage/{node_id}")
|
||||||
async def api_coverage(request):
|
async def api_coverage(request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
-10
@@ -213,16 +213,6 @@ async def daily_cleanup_at(
|
|||||||
)
|
)
|
||||||
cleanup_logger.info(f"Deleted {result.rowcount} rows from Traceroute")
|
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
|
# Node
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user