#!/usr/bin/env python3
# lxmf_html_browser.py - Web-based LXMF HTML Browser
import os
import sys
import json
import time
import threading
from datetime import datetime
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
# Check dependencies
try:
import RNS
import LXMF
except ImportError:
print("ERROR: Required packages not found!")
print("Install with: pip install rns lxmf flask flask-socketio")
sys.exit(1)
class HTMLServerAnnounceHandler:
"""Announce handler specifically for HTML servers (LXMF delivery destinations)"""
def __init__(self, browser):
self.aspect_filter = "lxmf.delivery" # Only catch LXMF delivery announces
self.browser = browser
def received_announce(self, destination_hash, announced_identity, app_data):
"""Called when an LXMF delivery destination announces"""
try:
peer_hash_str = RNS.prettyhexrep(destination_hash)
# Skip our own announces
if hasattr(self.browser, 'lxmf_destination'):
if destination_hash == self.browser.lxmf_destination.hash:
return
print(f"[ANNOUNCE] LXMF delivery destination: {peer_hash_str[:20]}...")
if app_data:
# Use LXMF's helper to extract display name
from LXMF import display_name_from_app_data
display_name = display_name_from_app_data(app_data)
if display_name:
print(f"[ANNOUNCE] Name: '{display_name}'")
# Check for HTML marker
if '[HTML]' in display_name or 'HTML' in display_name:
print(f"\n{'='*60}")
print(f"[ANNOUNCE] ✓✓✓ HTML SERVER FOUND ✓✓✓")
print(f"[ANNOUNCE] Name: {display_name}")
print(f"[ANNOUNCE] Hash: {peer_hash_str}")
print(f"{'='*60}\n")
self.browser._handle_discovery(peer_hash_str, display_name)
except Exception as e:
print(f"[!] HTML announce handler error: {e}")
import traceback
traceback.print_exc()
class LXMFHTMLBrowser:
def __init__(self, storage_path=None, identity_path=None):
# Terminal UI and paths setup
if storage_path is None:
storage_path = os.path.expanduser("~/.lxmf_html_browser")
self.storage_path = storage_path
self.cache_path = os.path.join(storage_path, "cache")
self.html_cache_path = os.path.join(storage_path, "html_cache")
self.bookmarks_file = os.path.join(storage_path, "bookmarks.json")
self.history_file = os.path.join(storage_path, "history.json")
self.discovered_file = os.path.join(storage_path, "discovered_servers.json")
self.identity_file = identity_path or os.path.join(storage_path, "identity")
# HTML field constants
self.FIELD_HTML_CONTENT = 10
self.FIELD_HTML_REQUEST = 11
# Runtime
self.bookmarks = []
self.history = []
self.discovered_servers = {}
self.known_peers = set()
self.pending_requests = {}
self.socketio = None
self.running = True
# Initialize storage and load data
self._init_storage()
self._load_data()
# Initialize Reticulum FIRST
self._init_reticulum()
# Register announce handler BEFORE LXMF is initialized
print("\n" + "="*60)
print("REGISTERING ANNOUNCE HANDLER (before LXMF)")
print("="*60 + "\n")
self._setup_announce_handler()
# NOW initialize LXMF (this will register its own handlers after ours)
self._init_lxmf()
# Don't need peer monitor
print("="*60)
print("Browser initialized - listening for announces")
print("="*60 + "\n")
def _init_storage(self):
for path in [self.storage_path, self.cache_path, self.html_cache_path]:
if not os.path.exists(path):
os.makedirs(path)
def _load_data(self):
if os.path.exists(self.bookmarks_file):
try:
with open(self.bookmarks_file, 'r') as f:
self.bookmarks = json.load(f)
except:
self.bookmarks = []
if os.path.exists(self.history_file):
try:
with open(self.history_file, 'r') as f:
self.history = json.load(f)
except:
self.history = []
if os.path.exists(self.discovered_file):
try:
with open(self.discovered_file, 'r') as f:
self.discovered_servers = json.load(f)
for server_hash in self.discovered_servers.keys():
self.known_peers.add(server_hash)
except:
self.discovered_servers = {}
def _save_data(self):
with open(self.bookmarks_file, 'w') as f:
json.dump(self.bookmarks, f, indent=2)
if len(self.history) > 100:
self.history = self.history[-100:]
with open(self.history_file, 'w') as f:
json.dump(self.history, f, indent=2)
with open(self.discovered_file, 'w') as f:
json.dump(self.discovered_servers, f, indent=2)
def _init_reticulum(self):
"""Initialize Reticulum"""
print("\n" + "="*60)
print("LXMF HTML Browser - Initializing Reticulum")
print("="*60 + "\n")
try:
self.reticulum = RNS.Reticulum()
print("✓ Reticulum initialized")
print(f" Version: {RNS.__version__ if hasattr(RNS, '__version__') else 'Unknown'}")
print(f" Transport available: {hasattr(RNS, 'Transport')}")
print(f" Can register handlers: {hasattr(RNS.Transport, 'register_announce_handler')}")
except Exception as e:
print(f"ERROR: Failed to initialize Reticulum: {e}")
sys.exit(1)
# Load or create identity
if os.path.exists(self.identity_file):
self.identity = RNS.Identity.from_file(self.identity_file)
print(f"✓ Loaded identity")
else:
self.identity = RNS.Identity()
self.identity.to_file(self.identity_file)
print(f"✓ Created new identity")
def _init_lxmf(self):
self.message_router = LXMF.LXMRouter(
identity=self.identity,
storagepath=self.storage_path
)
self.lxmf_destination = self.message_router.register_delivery_identity(
self.identity,
display_name="LXMF HTML Browser"
)
self.message_router.register_delivery_callback(self._handle_message)
self.lxmf_destination.announce()
self.client_hash = RNS.prettyhexrep(self.lxmf_destination.hash)
print(f"✓ LXMF initialized")
print(f" Client: {self.client_hash}\n")
def _setup_announce_handler(self):
"""Set up announce handler for HTML servers"""
print("Setting up HTML server announce handler...")
# Create handler instance
self.html_announce_handler = HTMLServerAnnounceHandler(self)
# Register with RNS Transport
RNS.Transport.register_announce_handler(self.html_announce_handler)
print("✓ HTML server announce handler registered")
print(f" Aspect filter: {self.html_announce_handler.aspect_filter}\n")
def _init_lxmf(self):
self.message_router = LXMF.LXMRouter(
identity=self.identity,
storagepath=self.storage_path
)
self.lxmf_destination = self.message_router.register_delivery_identity(
self.identity,
display_name="LXMF HTML Browser"
)
self.message_router.register_delivery_callback(self._handle_message)
self.lxmf_destination.announce()
self.client_hash = RNS.prettyhexrep(self.lxmf_destination.hash)
print(f"✓ LXMF initialized")
print(f" Client: {self.client_hash}\n")
# Process any discoveries that happened before LXMF was ready
if hasattr(self, '_pending_discoveries') and len(self._pending_discoveries) > 0:
print(f"[*] Processing {len(self._pending_discoveries)} pending discoveries...")
for discovery in self._pending_discoveries:
peer_hash_str, display_name = discovery
# Skip our own hash now that we know it
if peer_hash_str == self.client_hash:
continue
self._handle_discovery(peer_hash_str, display_name)
self._pending_discoveries = []
def _start_peer_monitor(self):
"""Just a simple status monitor"""
def monitor_loop():
print("✓ Status monitor started")
print("="*60 + "\n")
while self.running:
time.sleep(10)
threading.Thread(target=monitor_loop, daemon=True).start()
def _handle_discovery(self, peer_hash_str, display_name):
try:
server_name = display_name.replace('[HTML]', '').strip()
if not server_name:
server_name = "Unknown Server"
# Check if already discovered
if peer_hash_str in self.known_peers:
# Just update last seen
if peer_hash_str in self.discovered_servers:
self.discovered_servers[peer_hash_str]['last_seen'] = time.time()
self._save_data()
return
self.discovered_servers[peer_hash_str] = {
'name': server_name,
'pages': [],
'last_seen': time.time()
}
self.known_peers.add(peer_hash_str)
self._save_data()
print(f"\n{'='*60}")
print(f"[+] NEW HTML SERVER DISCOVERED!")
print(f" Name: {server_name}")
print(f" Hash: {peer_hash_str}")
print(f"{'='*60}\n")
if self.socketio:
with app.app_context():
self.socketio.emit('server_discovered', {
'hash': peer_hash_str,
'name': server_name,
'timestamp': time.time()
}, namespace='/')
print(f"[+] UI notification sent")
# REMOVED: Automatic page list request
# User can click on server to load pages when they want
except Exception as e:
print(f"[!] Discovery error: {e}")
import traceback
traceback.print_exc()
def _request_page_list(self, server_hash):
try:
if server_hash.startswith('<') and server_hash.endswith('>'):
server_hash = server_hash[1:-1]
dest_hash = bytes.fromhex(server_hash.replace(':', ''))
self.pending_requests[server_hash] = {'type': 'list', 'time': time.time()}
self.lxmf_destination.announce()
time.sleep(0.3)
dest_identity = RNS.Identity.recall(dest_hash)
if not dest_identity:
RNS.Transport.request_path(dest_hash)
time.sleep(2)
dest_identity = RNS.Identity.recall(dest_hash)
if dest_identity:
dest = RNS.Destination(
dest_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
"lxmf", "delivery"
)
lxmf_message = LXMF.LXMessage(
dest,
self.lxmf_destination,
"list"
)
self.message_router.handle_outbound(lxmf_message)
print(f"[>] Requested page list")
except Exception as e:
print(f"[!] Error requesting list: {e}")
def _parse_page_list(self, text):
pages = []
try:
lines = text.split('\n')
for line in lines:
line = line.strip()
if line.startswith('[') and ']' in line:
parts = line.split(']', 1)
if len(parts) > 1:
page_info = parts[1].strip()
if '(' in page_info:
page_name = page_info.split('(')[0].strip()
if page_name:
pages.append(page_name)
except:
pass
return pages
def _save_html_file(self, html_content, filename, server_hash):
"""Save HTML content with link interception script"""
# Inject JavaScript to intercept internal links
inject_script = f"""
"""
# Inject before or