feat(api): add firmware download endpoint with GitHub URL validation and enhance initiation status response with target name
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import atexit
|
||||
import base64
|
||||
import configparser
|
||||
@@ -1807,6 +1808,16 @@ class ReticulumMeshChat:
|
||||
ctx = context or self.current_context
|
||||
if not ctx:
|
||||
return
|
||||
|
||||
target_name = None
|
||||
if target_hash:
|
||||
try:
|
||||
contact = ctx.database.contacts.get_contact_by_hash(target_hash)
|
||||
if contact:
|
||||
target_name = contact.name
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
AsyncUtils.run_async(
|
||||
self.websocket_broadcast(
|
||||
json.dumps(
|
||||
@@ -1814,6 +1825,7 @@ class ReticulumMeshChat:
|
||||
"type": "telephone_initiation_status",
|
||||
"status": status,
|
||||
"target_hash": target_hash,
|
||||
"target_name": target_name,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -2245,6 +2257,40 @@ class ReticulumMeshChat:
|
||||
},
|
||||
)
|
||||
|
||||
@routes.get("/api/v1/tools/rnode/download_firmware")
|
||||
async def tools_rnode_download_firmware(request):
|
||||
url = request.query.get("url")
|
||||
if not url:
|
||||
return web.json_response({"error": "URL is required"}, status=400)
|
||||
|
||||
# Restrict to GitHub for safety
|
||||
if not url.startswith("https://github.com/") and not url.startswith(
|
||||
"https://objects.githubusercontent.com/"
|
||||
):
|
||||
return web.json_response({"error": "Invalid download URL"}, status=403)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, allow_redirects=True) as response:
|
||||
if response.status != 200:
|
||||
return web.json_response(
|
||||
{"error": f"Failed to download: {response.status}"},
|
||||
status=response.status,
|
||||
)
|
||||
|
||||
data = await response.read()
|
||||
filename = url.split("/")[-1]
|
||||
|
||||
return web.Response(
|
||||
body=data,
|
||||
content_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
# fetch reticulum interfaces
|
||||
@routes.get("/api/v1/reticulum/interfaces")
|
||||
async def reticulum_interfaces(request):
|
||||
@@ -3825,6 +3871,18 @@ class ReticulumMeshChat:
|
||||
"is_contact": contact is not None,
|
||||
}
|
||||
|
||||
initiation_target_hash = self.telephone_manager.initiation_target_hash
|
||||
initiation_target_name = None
|
||||
if initiation_target_hash:
|
||||
try:
|
||||
contact = self.database.contacts.get_contact_by_hash(
|
||||
initiation_target_hash
|
||||
)
|
||||
if contact:
|
||||
initiation_target_name = contact.name
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"enabled": True,
|
||||
@@ -3839,7 +3897,8 @@ class ReticulumMeshChat:
|
||||
"latest_id": self.database.voicemails.get_latest_voicemail_id(),
|
||||
},
|
||||
"initiation_status": self.telephone_manager.initiation_status,
|
||||
"initiation_target_hash": self.telephone_manager.initiation_target_hash,
|
||||
"initiation_target_hash": initiation_target_hash,
|
||||
"initiation_target_name": initiation_target_name,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -5541,9 +5600,26 @@ class ReticulumMeshChat:
|
||||
max_hops = request.query.get("max_hops")
|
||||
if max_hops:
|
||||
max_hops = int(max_hops)
|
||||
|
||||
search = request.query.get("search")
|
||||
interface = request.query.get("interface")
|
||||
hops = request.query.get("hops")
|
||||
if hops:
|
||||
hops = int(hops)
|
||||
|
||||
page = int(request.query.get("page", 1))
|
||||
limit = int(request.query.get("limit", 50))
|
||||
|
||||
try:
|
||||
table = self.rnpath_handler.get_path_table(max_hops=max_hops)
|
||||
return web.json_response({"table": table})
|
||||
result = self.rnpath_handler.get_path_table(
|
||||
max_hops=max_hops,
|
||||
search=search,
|
||||
interface=interface,
|
||||
hops=hops,
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
return web.json_response({"message": str(e)}, status=500)
|
||||
|
||||
@@ -7014,6 +7090,8 @@ class ReticulumMeshChat:
|
||||
@web.middleware
|
||||
async def mime_type_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
if response is None:
|
||||
return None
|
||||
path = request.path
|
||||
if path.endswith(".js") or path.endswith(".mjs"):
|
||||
response.headers["Content-Type"] = (
|
||||
@@ -7033,6 +7111,8 @@ class ReticulumMeshChat:
|
||||
@web.middleware
|
||||
async def security_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
if response is None:
|
||||
return None
|
||||
# Add security headers to all responses
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
|
||||
@@ -7055,7 +7135,7 @@ class ReticulumMeshChat:
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org; "
|
||||
"font-src 'self' data:; "
|
||||
"connect-src 'self' ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org; "
|
||||
"connect-src 'self' ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://api.github.com https://objects.githubusercontent.com https://github.com; "
|
||||
"media-src 'self' blob:; "
|
||||
"worker-src 'self' blob:; "
|
||||
"frame-src 'self'; "
|
||||
@@ -7698,8 +7778,16 @@ class ReticulumMeshChat:
|
||||
elif _type == "nomadnet.page.archives.get":
|
||||
destination_hash = data["destination_hash"]
|
||||
page_path = data["page_path"]
|
||||
|
||||
# Try relative path first
|
||||
archives = self.get_archived_page_versions(destination_hash, page_path)
|
||||
|
||||
# If nothing found and path doesn't look like it's already absolute,
|
||||
# try searching with the destination hash prefix (support for old buggy archives)
|
||||
if not archives and not page_path.startswith(destination_hash):
|
||||
buggy_path = f"{destination_hash}:{page_path}"
|
||||
archives = self.get_archived_page_versions(destination_hash, buggy_path)
|
||||
|
||||
AsyncUtils.run_async(
|
||||
client.send_str(
|
||||
json.dumps(
|
||||
@@ -7711,6 +7799,8 @@ class ReticulumMeshChat:
|
||||
{
|
||||
"id": archive.id,
|
||||
"hash": archive.hash,
|
||||
"destination_hash": archive.destination_hash,
|
||||
"page_path": archive.page_path,
|
||||
"created_at": archive.created_at.isoformat()
|
||||
if hasattr(archive.created_at, "isoformat")
|
||||
else str(archive.created_at),
|
||||
|
||||
Reference in New Issue
Block a user