diff --git a/site_server.py b/site_server.py deleted file mode 100644 index a1064eb..0000000 --- a/site_server.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -import sys -import time -import mimetypes - -SITE_DIR = os.path.expanduser("~/apps/tinyweb-site") -DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb") -IDENTITY_FILE = "tinyweb-site_identity" - -APP_NAME = "tinyweb" -ASPECTS = ["server"] -RNS_REQUEST_PATH = "/tinyweb" - -import RNS - - -def load_or_create_identity(): - identity_path = os.path.join(DATA_DIR, IDENTITY_FILE) - if os.path.isfile(identity_path): - return RNS.Identity.from_file(identity_path) - identity = RNS.Identity() - os.makedirs(DATA_DIR, exist_ok=True) - identity.to_file(identity_path) - os.chmod(identity_path, 0o600) - return identity - - -def main(): - configdir = os.environ.get("RNS_CONFIG_DIR") - reticulum = RNS.Reticulum(configdir=configdir) - - identity = load_or_create_identity() - - destination = RNS.Destination( - identity, - RNS.Destination.IN, - RNS.Destination.SINGLE, - APP_NAME, - *ASPECTS, - ) - - destination.register_request_handler( - RNS_REQUEST_PATH, - response_generator=request_handler, - allow=RNS.Destination.ALLOW_ALL, - ) - - destination.announce() - dest_hash = destination.hash.hex() - print(f"tinyweb-site server running!") - print(f"Destination hash: <{dest_hash}>") - print(f"Add this hash to a TinyWeb instance as a mesh site to browse.") - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("\nShutting down...") - destination.unregister_request_handler() - - -def request_handler(path, data, request_id, link_id, remote_identity, requested_at): - if data is None: - data = {"method": "GET", "path": "/", "query": {}, "body": {}, "gateway_host": ""} - req_path = data.get("path", "/") - - if req_path in ("/", "/index.html") or not req_path.strip("/"): - fs_path = os.path.join(SITE_DIR, "index.html") - else: - fs_path = os.path.join(SITE_DIR, req_path.lstrip("/")) - - real_path = os.path.realpath(fs_path) - site_real = os.path.realpath(SITE_DIR) - - if not real_path.startswith(site_real + os.sep) and real_path != site_real: - body = f"

404 Not Found

{req_path}

" - return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}} - - if not os.path.isfile(real_path): - real_path = os.path.join(SITE_DIR, "index.html") - - if not os.path.isfile(real_path): - body = f"

404 Not Found

" - return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}} - - with open(real_path, "rb") as f: - content = f.read() - - content_type, _ = mimetypes.guess_type(real_path) - if not content_type: - content_type = "text/html; charset=utf-8" - elif content_type.startswith("text/"): - content_type += "; charset=utf-8" - - return { - "status": 200, - "content_type": content_type, - "body": content.decode("utf-8", errors="replace"), - "headers": {}, - } - - -if __name__ == "__main__": - main() diff --git a/src/tinyweb/handlers/__init__.py b/src/tinyweb/handlers/__init__.py index 14a37fd..cd52dd6 100644 --- a/src/tinyweb/handlers/__init__.py +++ b/src/tinyweb/handlers/__init__.py @@ -39,7 +39,7 @@ from .data import ( handle_reindex_form, handle_reindex_submit, _reindex_thread, ) from .rns import ( - handle_rns_delete_hash, handle_rns_browse, + handle_rns_list, handle_rns_add, handle_rns_delete, handle_rns_browse, ) forum_plugin = None @@ -97,6 +97,8 @@ def _dispatch_inner(data): if not dest_hash: return _error(404) return handle_rns_browse(path, dest_hash) + elif path == "/rns": + return handle_rns_list(query) elif path == "/reindex": return handle_reindex_form() elif path == "/api/sites": @@ -165,8 +167,10 @@ def _dispatch_inner(data): return handle_subscription_delete(sid) if sid is not None else _error(400) elif path == "/subscriptions/syncall": return handle_subscription_syncall() + elif path == "/rns/add": + return handle_rns_add(body) elif path == "/rns/delete": - return handle_rns_delete_hash(body) + return handle_rns_delete(body) return _error(404) diff --git a/src/tinyweb/handlers/pages.py b/src/tinyweb/handlers/pages.py index 9d7f96c..4eb2a12 100644 --- a/src/tinyweb/handlers/pages.py +++ b/src/tinyweb/handlers/pages.py @@ -29,11 +29,11 @@ def handle_add_form(msg="", action_type="index", prefill_url=""): ) url_value = f'value="{esc(prefill_url)}" ' if prefill_url else "" return _respond( - f"

add site

" - f"

Add a site to your index — URL or RNS destination hash

" + f"

add url

" + f"

Add a site to your index

" f'
' f'{_csrf_field()}' - f'

' + f'

' f'

' f'
' f'tag: private to exclude from sharing

' @@ -45,37 +45,27 @@ def handle_add_form(msg="", action_type="index", prefill_url=""): def handle_add_submit(body): - raw = body.get("url", [""])[0].strip().replace("<", "").replace(">", "") + input_type = body.get("input_type", ["url"])[0] + url = body.get("url", [""])[0].strip() + reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "") note = body.get("note", [""])[0].strip() tags = body.get("tags", [""])[0].strip() - if not raw: - return handle_add_form("URL or RNS hash is required.") - - is_rns = ( - len(raw) == 32 - and all(c in "0123456789abcdefABCDEF" for c in raw) - ) - if raw.startswith("rns:") or raw.startswith("RNS:"): - raw = raw[4:] - is_rns = ( - len(raw) == 32 - and all(c in "0123456789abcdefABCDEF" for c in raw) - ) - - if is_rns: - from .rns import handle_rns_add_hash - errs = handle_rns_add_hash(raw) - if errs: - return handle_add_form(f"Hash saved but indexing failed: {'; '.join(errs)}") - return _redirect("/") - - url = clean_url(raw) - if not url.startswith(("http://", "https://")): - return handle_add_form("Enter a URL (http:// or https://) or a 32-char RNS destination hash.") + if input_type == "url": + if not url: + return handle_add_form("URL is required.") + url = clean_url(url) + if not url.startswith(("http://", "https://")): + return handle_add_form("URL must start with http:// or https://") + else: + if not reticulum_dest: + return handle_add_form("Reticulum destination hash is required.") + if len(reticulum_dest) != 32 or not all(c in "0123456789abcdefABCDEF" for c in reticulum_dest): + return handle_add_form("Invalid reticulum destination hash. Must be 32 hex characters.") + url = f"reticulum:{reticulum_dest}" try: - title = index_url(url, note) + title = index_url(url, note, reticulum_dest if reticulum_dest else "") if tags: db = get_db() try: @@ -85,9 +75,12 @@ def handle_add_submit(body): db.commit() finally: return_db(db) + return handle_add_form(f'Indexed: {esc(url)}') + except ValueError as e: return handle_add_form(f"Error: {esc(str(e))}") + except Exception as e: error_msg = str(e).lower() if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")): @@ -175,17 +168,10 @@ def handle_pages(query=None): if tags: tag_links = " ".join(f'[{esc(t)}]' for t in tags) tags_html = f' {tag_links}' - url = r["url"] - if url.startswith("rns:"): - display_url = url - link_url = f"/rns/{esc(url[4:])}/" - else: - display_url = url - link_url = url items += ( f'
  • {note_html}{tags_html} ' - f'({esc(display_url)}) ' + f'({esc(r["url"])}) ' f'edit ' f'remove
  • ' ) diff --git a/src/tinyweb/handlers/rns.py b/src/tinyweb/handlers/rns.py index 8941f99..af699d4 100644 --- a/src/tinyweb/handlers/rns.py +++ b/src/tinyweb/handlers/rns.py @@ -1,8 +1,10 @@ import json -import traceback +from urllib.parse import urlparse from tinyweb.db import get_db, return_db from tinyweb.rns_client import fetch_remote_page from tinyweb.templates import esc +from ._helpers import _respond, _redirect, _error +from .customize import _set_flash def _get_mesh_sites(): @@ -13,68 +15,12 @@ def _get_mesh_sites(): return_db(db) -def handle_rns_add_hash(dest_hash, name=""): - db = get_db() - errors = [] - try: - db.execute( - "INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)", - (dest_hash, name or ""), - ) - - try: - resp = fetch_remote_page(dest_hash, "/") - if resp.get("status") == 200: - body = resp.get("body", "") - title = name or dest_hash[:16] - import re - m = re.search(r"]*>(.*?)", body, re.IGNORECASE | re.DOTALL) - if m: - title = m.group(1).strip() - desc = "" - m = re.search(r']+>", " ", body) - text = re.sub(r"\s+", " ", text).strip() - desc = text[:200].strip() - url = f"rns:{dest_hash}" - import datetime - now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - db.execute( - "INSERT OR REPLACE INTO pages (url, title, body, last_modified, summary) VALUES (?, ?, ?, ?, ?)", - (url, title, body, now, desc), - ) - else: - errors.append(f"Remote returned status {resp.get('status')}") - except Exception as e: - errors.append(str(e)) - traceback.print_exc() - - db.commit() - finally: - return_db(db) - - return errors if errors else None - - -def handle_rns_delete_hash(body): - dest_hash = body.get("hash", [""])[0].strip() if isinstance(body, dict) else body - db = get_db() - try: - db.execute("DELETE FROM mesh_sites WHERE hash = ?", (dest_hash,)) - db.commit() - finally: - return_db(db) - from tinyweb.handlers.pages import _redirect - return _redirect("/") - - def _rewrite_links(html, dest_hash): + """Rewrite all relative links in HTML to route through /rns//...""" out = [] i = 0 while i < len(html): + # Check for href="... href_start = html.find('href="', i) src_start = html.find('src="', i) action_start = html.find('action="', i) @@ -95,6 +41,7 @@ def _rewrite_links(html, dest_hash): pos, attr, prefix = candidates[0] out.append(html[i:pos + len(prefix)]) + # Find end of attribute value value_start = pos + len(prefix) value_end = html.find('"', value_start) if value_end < 0: @@ -103,8 +50,10 @@ def _rewrite_links(html, dest_hash): value = html[value_start:value_end] if value.startswith("/"): + # Relative path — rewrite out.append(f"/rns/{dest_hash}{value}") else: + # External URL or empty — leave as-is out.append(value) out.append('"') @@ -113,7 +62,76 @@ def _rewrite_links(html, dest_hash): return "".join(out) +def handle_rns_list(query=None): + sites = _get_mesh_sites() + rows_html = "" + for s in sites: + rows_html += ( + f'' + f'{esc(s["name"] or s["hash"][:16])}' + f'{esc(s["hash"])}' + f'{esc(s["added_at"])}' + f'' + f'' + f'' + f'' + f'' + f'' + f'' + ) + + html = ( + f'

    mesh sites

    ' + f'{rows_html}
    sitehashadded
    ' + f'
    ' + f'
    ' + f'' + f'' + f'' + f'
    ' + f'
    back' + ) + return _respond(html) + + +def handle_rns_add(body): + dest_hash = body.get("hash", [""])[0].strip() + name = body.get("name", [""])[0].strip() + if not dest_hash: + return handle_rns_list() + try: + bytes.fromhex(dest_hash) + except ValueError: + _set_flash("Invalid hash — must be hex.") + return _redirect("/rns") + + db = get_db() + try: + db.execute( + "INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)", + (dest_hash, name or ""), + ) + db.commit() + finally: + return_db(db) + return _redirect("/rns") + + +def handle_rns_delete(body): + dest_hash = body.get("hash", [""])[0].strip() + if not dest_hash: + return _redirect("/rns") + db = get_db() + try: + db.execute("DELETE FROM mesh_sites WHERE hash = ?", (dest_hash,)) + db.commit() + finally: + return_db(db) + return _redirect("/rns") + + def handle_rns_browse(path, dest_hash): + # Extract the sub-path after /rns/ prefix = f"/rns/{dest_hash}" sub_path = path[len(prefix):] if path.startswith(prefix) else "/" if not sub_path: @@ -122,30 +140,16 @@ def handle_rns_browse(path, dest_hash): try: resp = fetch_remote_page(dest_hash, sub_path) except ConnectionError as e: - return { - "status": 200, - "content_type": "text/html; charset=utf-8", - "body": f"

    could not connect

    {esc(str(e))}

    ", - "headers": {}, - } + return _respond(f"

    could not connect

    {esc(str(e))}

    ") except PermissionError: - return { - "status": 200, - "content_type": "text/html; charset=utf-8", - "body": "

    forbidden

    the remote instance blocked this request.

    ", - "headers": {}, - } + return _respond("

    forbidden

    the remote instance blocked this request.

    ") if resp.get("status") != 200: - return { - "status": 200, - "content_type": "text/html; charset=utf-8", - "body": f"

    error

    remote returned status {resp['status']}

    ", - "headers": {}, - } + return _respond(f"

    error

    remote returned status {resp['status']}

    ") body = resp.get("body", "") + # If the remote returned JSON (e.g. /api/sites), wrap for display if resp.get("content_type", "").startswith("application/json"): try: data = json.loads(body) diff --git a/src/tinyweb/handlers/search.py b/src/tinyweb/handlers/search.py index 10b87fd..951b4d4 100644 --- a/src/tinyweb/handlers/search.py +++ b/src/tinyweb/handlers/search.py @@ -83,17 +83,10 @@ def handle_search(query): tag_links = " ".join(f'[{esc(t)}]' for t in tags) tags_html = f'
    {tag_links}
    ' snip_html = f'
    {esc(r["summary"])}' if r["summary"] else "" - url = r["url"] - if url.startswith("rns:"): - display_url = url - link_url = f"/rns/{esc(url[4:])}/" - else: - display_url = url - link_url = url result_html += ( f'
    ' - f'{esc(r["title"])}
    ' - f'{esc(display_url)}' + f'{esc(r["title"])}
    ' + f'{esc(r["url"])}' f'{snip_html}' f'{note_html}{tags_html}' f'
    ' @@ -169,7 +162,6 @@ def handle_search(query): sub_count = "" if q and remote_rows: sub_count = f" + {len(remote_rows)} from subscriptions" - welcome_html = "" if count == 0 and not q: welcome_html = ( diff --git a/src/tinyweb/templates.py b/src/tinyweb/templates.py index 8208211..8cc3535 100644 --- a/src/tinyweb/templates.py +++ b/src/tinyweb/templates.py @@ -13,7 +13,7 @@ def _nav_html(): return ( f'

    {name}' ' | search | browse' - ' | tags | subscriptions' + ' | tags | subscriptions | mesh' f'{forum_link}' ' | customize | about

    \n' "
    \n"