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 argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
import atexit
|
import atexit
|
||||||
import base64
|
import base64
|
||||||
import configparser
|
import configparser
|
||||||
@@ -1807,6 +1808,16 @@ class ReticulumMeshChat:
|
|||||||
ctx = context or self.current_context
|
ctx = context or self.current_context
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return
|
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(
|
AsyncUtils.run_async(
|
||||||
self.websocket_broadcast(
|
self.websocket_broadcast(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -1814,6 +1825,7 @@ class ReticulumMeshChat:
|
|||||||
"type": "telephone_initiation_status",
|
"type": "telephone_initiation_status",
|
||||||
"status": status,
|
"status": status,
|
||||||
"target_hash": target_hash,
|
"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
|
# fetch reticulum interfaces
|
||||||
@routes.get("/api/v1/reticulum/interfaces")
|
@routes.get("/api/v1/reticulum/interfaces")
|
||||||
async def reticulum_interfaces(request):
|
async def reticulum_interfaces(request):
|
||||||
@@ -3825,6 +3871,18 @@ class ReticulumMeshChat:
|
|||||||
"is_contact": contact is not None,
|
"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(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -3839,7 +3897,8 @@ class ReticulumMeshChat:
|
|||||||
"latest_id": self.database.voicemails.get_latest_voicemail_id(),
|
"latest_id": self.database.voicemails.get_latest_voicemail_id(),
|
||||||
},
|
},
|
||||||
"initiation_status": self.telephone_manager.initiation_status,
|
"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")
|
max_hops = request.query.get("max_hops")
|
||||||
if max_hops:
|
if max_hops:
|
||||||
max_hops = int(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:
|
try:
|
||||||
table = self.rnpath_handler.get_path_table(max_hops=max_hops)
|
result = self.rnpath_handler.get_path_table(
|
||||||
return web.json_response({"table": table})
|
max_hops=max_hops,
|
||||||
|
search=search,
|
||||||
|
interface=interface,
|
||||||
|
hops=hops,
|
||||||
|
page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
return web.json_response(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"message": str(e)}, status=500)
|
return web.json_response({"message": str(e)}, status=500)
|
||||||
|
|
||||||
@@ -7014,6 +7090,8 @@ class ReticulumMeshChat:
|
|||||||
@web.middleware
|
@web.middleware
|
||||||
async def mime_type_middleware(request, handler):
|
async def mime_type_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
path = request.path
|
path = request.path
|
||||||
if path.endswith(".js") or path.endswith(".mjs"):
|
if path.endswith(".js") or path.endswith(".mjs"):
|
||||||
response.headers["Content-Type"] = (
|
response.headers["Content-Type"] = (
|
||||||
@@ -7033,6 +7111,8 @@ class ReticulumMeshChat:
|
|||||||
@web.middleware
|
@web.middleware
|
||||||
async def security_middleware(request, handler):
|
async def security_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
# Add security headers to all responses
|
# Add security headers to all responses
|
||||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
|
|
||||||
@@ -7055,7 +7135,7 @@ class ReticulumMeshChat:
|
|||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org; "
|
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org; "
|
||||||
"font-src 'self' data:; "
|
"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:; "
|
"media-src 'self' blob:; "
|
||||||
"worker-src 'self' blob:; "
|
"worker-src 'self' blob:; "
|
||||||
"frame-src 'self'; "
|
"frame-src 'self'; "
|
||||||
@@ -7698,8 +7778,16 @@ class ReticulumMeshChat:
|
|||||||
elif _type == "nomadnet.page.archives.get":
|
elif _type == "nomadnet.page.archives.get":
|
||||||
destination_hash = data["destination_hash"]
|
destination_hash = data["destination_hash"]
|
||||||
page_path = data["page_path"]
|
page_path = data["page_path"]
|
||||||
|
|
||||||
|
# Try relative path first
|
||||||
archives = self.get_archived_page_versions(destination_hash, page_path)
|
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(
|
AsyncUtils.run_async(
|
||||||
client.send_str(
|
client.send_str(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -7711,6 +7799,8 @@ class ReticulumMeshChat:
|
|||||||
{
|
{
|
||||||
"id": archive.id,
|
"id": archive.id,
|
||||||
"hash": archive.hash,
|
"hash": archive.hash,
|
||||||
|
"destination_hash": archive.destination_hash,
|
||||||
|
"page_path": archive.page_path,
|
||||||
"created_at": archive.created_at.isoformat()
|
"created_at": archive.created_at.isoformat()
|
||||||
if hasattr(archive.created_at, "isoformat")
|
if hasattr(archive.created_at, "isoformat")
|
||||||
else str(archive.created_at),
|
else str(archive.created_at),
|
||||||
|
|||||||
Reference in New Issue
Block a user