summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/nodes.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/nodes.py58
1 files changed, 49 insertions, 9 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
index 321e43c..0770148 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.auth import issue_access_token
from meshbay_hub.api.deps import get_current_user
from meshbay_hub.api.middleware import limiter
+from meshbay_hub.api.netutil import client_ip
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import GroupMember, IPLog, Node, User
@@ -59,7 +60,7 @@ async def node_auth(
sig = base64.b64decode(body.signature)
pk.verify(sig, message)
except (InvalidSignature, Exception):
- db.add(IPLog(event="node_auth_fail", ip_address=_ip(request), detail=body.username))
+ db.add(IPLog(event="node_auth_fail", ip_address=client_ip(request), detail=body.username))
await db.commit()
raise HTTPException(status_code=401, detail="Invalid signature")
@@ -68,9 +69,9 @@ async def node_auth(
group_ids = [gid for (gid,) in memberships.all()]
access_token = issue_access_token(
- user.id, user.pk_node_ed25519, ttl=3600, groups=group_ids, scope="node")
+ user.id, ttl=3600, groups=group_ids, scope="node")
- db.add(IPLog(user_id=user.id, event="node_auth", ip_address=_ip(request)))
+ db.add(IPLog(user_id=user.id, event="node_auth", ip_address=client_ip(request)))
await db.commit()
return {
@@ -83,6 +84,8 @@ async def node_auth(
class NodeAnnounceRequest(BaseModel):
pk_node: str
endpoint_hint: str | None = None
+ timestamp: int | None = None # unix seconds
+ signature: str | None = None # base64 Ed25519 over the announce message
@router.post("/announce", status_code=201)
@@ -92,6 +95,48 @@ async def announce_node(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
+ """
+ Register a node record.
+
+ Finding M8: this accepted any pk_node with no proof the announcer held the
+ matching private key, so a user could announce a record carrying someone
+ else's node key — useful for muddying node identity, and records accumulated
+ without limit. The announcer must now sign a domain-separated message binding
+ the key to their account, the same pattern already used by /v1/nodes/auth.
+ """
+ if body.timestamp is None or not body.signature:
+ raise HTTPException(
+ status_code=400,
+ detail="announce requires timestamp and signature (proof of possession)")
+
+ now = int(time.time())
+ if abs(now - body.timestamp) > NODE_AUTH_TIMESTAMP_WINDOW:
+ raise HTTPException(status_code=401, detail="Timestamp too old or too far ahead")
+
+ message = (f"meshbay:node_announce:{current_user.id}:"
+ f"{body.pk_node}:{body.timestamp}").encode()
+ try:
+ pk = Ed25519PublicKey.from_public_bytes(base64.b64decode(body.pk_node))
+ pk.verify(base64.b64decode(body.signature), message)
+ except Exception:
+ db.add(IPLog(user_id=current_user.id, event="node_announce_fail",
+ ip_address=client_ip(request), detail=body.pk_node[:16]))
+ await db.commit()
+ raise HTTPException(status_code=401, detail="Invalid node key proof of possession")
+
+ # One active record per key per account — announcing again updates in place
+ # instead of accumulating rows.
+ existing = await db.execute(
+ select(Node).where(Node.user_id == current_user.id,
+ Node.pk_node == body.pk_node))
+ node = existing.scalar_one_or_none()
+ if node is not None:
+ node.endpoint_hint = body.endpoint_hint
+ db.add(IPLog(user_id=current_user.id, event="node_announce",
+ ip_address=client_ip(request), detail=body.endpoint_hint))
+ await db.commit()
+ return {"node_id": node.id}
+
node = Node(
user_id=current_user.id,
pk_node=body.pk_node,
@@ -101,7 +146,7 @@ async def announce_node(
db.add(IPLog(
user_id=current_user.id,
event="node_announce",
- ip_address=_ip(request),
+ ip_address=client_ip(request),
detail=body.endpoint_hint,
))
await db.commit()
@@ -128,8 +173,3 @@ async def get_node(
}
-def _ip(request: Request) -> str:
- fwd = request.headers.get("X-Forwarded-For")
- if fwd:
- return fwd.split(",")[0].strip()
- return request.client.host if request.client else "unknown"