diff --git a/README.md b/README.md index 67a1a91..81d6258 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The goal is to keep it simple! For feature-rich solutions please check out [hedg - optional caching with valkey/redis - pad creation with HTTP post requests with curl (see *Usage*) - `{pad_id}/raw` HTTP endpoint +- optional password protection (HTTP endpoint accessible with GET /{pad_id}/raw?pw=mysecurepasswordhunter2) **Ideas**: [Check out the open feature requests](https://git.uphillsecurity.com/cf7/aukpad/issues?q=&type=all&sort=&state=open&labels=12&milestone=0&project=0&assignee=0&poster=0&archived=false) diff --git a/app.py b/app.py index 74e27bf..ddae9b4 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ # aukpad.py from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, PlainTextResponse, FileResponse -import json, secrets, string, time, os, threading, asyncio +import json, secrets, string, time, os, threading, asyncio, hashlib from collections import defaultdict from typing import Optional @@ -19,7 +19,7 @@ DESCRIPTION = os.getenv("DESCRIPTION", "powered by aukpad.com") # Valkey/Redis client (initialized later if enabled) redis_client = None -# In-memory rooms: {doc_id: {"text": str, "ver": int, "peers": set[WebSocket], "last_access": float}} +# In-memory rooms: {doc_id: {"text": str, "ver": int, "peers": set[WebSocket], "last_access": float, "pw_hash": bytes|None, "pw_salt": bytes|None}} rooms: dict[str, dict] = {} # Rate limiting: {ip: [timestamp, timestamp, ...]} @@ -52,16 +52,28 @@ def get_room_data_from_cache(doc_id: str) -> Optional[dict]: try: data = redis_client.get(f"room:{doc_id}") if data: - return json.loads(data) + cached = json.loads(data) + # Convert hex strings back to bytes + if cached.get("pw_hash"): + cached["pw_hash"] = bytes.fromhex(cached["pw_hash"]) + if cached.get("pw_salt"): + cached["pw_salt"] = bytes.fromhex(cached["pw_salt"]) + return cached except Exception as e: print(f"Cache read error for {doc_id}: {e}") return None -def save_room_data_to_cache(doc_id: str, text: str, ver: int): +def save_room_data_to_cache(doc_id: str, room: dict): if redis_client: try: - data = {"text": text, "ver": ver, "last_access": time.time()} - redis_client.setex(f"room:{doc_id}", RETENTION_HOURS * 3600, json.dumps(data)) # TTL in seconds + data = { + "text": room["text"], + "ver": room["ver"], + "last_access": room.get("last_access", time.time()), + "pw_hash": room["pw_hash"].hex() if room.get("pw_hash") else None, + "pw_salt": room["pw_salt"].hex() if room.get("pw_salt") else None, + } + redis_client.setex(f"room:{doc_id}", RETENTION_HOURS * 3600, json.dumps(data)) except Exception as e: print(f"Cache write error for {doc_id}: {e}") @@ -125,33 +137,65 @@ HTML = """ :root { --line-h: 1.4; } * { box-sizing: border-box; } html, body { height: 100%; margin: 0; padding: 0; background-color: #fbfbfb; } - body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji"; + body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji"; max-width: 1000px; margin: 0 auto; padding: 1rem; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } header { display:flex; justify-content:space-between; align-items:center; margin-bottom: .5rem; flex-shrink: 0; } - a,button { padding:.35rem .6rem; text-decoration:none; border:1px solid #ddd; border-radius:4px; background:#fff; } + a,button { padding:.35rem .6rem; text-decoration:none; border:1px solid #ddd; border-radius:4px; background:#fff; cursor:pointer; } #newpad { background:#000; color:#fff; border:1px solid #000; font-weight:bold; } #status { font-size:.9rem; opacity:.7; margin-left:.5rem; } #status::before { content: "●"; margin-right: .3rem; color: #ef4444; } #status.connected::before { color: #22c55e; } - #wrap { display:grid; grid-template-columns: max-content 1fr; border:1px solid #ddd; border-radius:4px; overflow:hidden; + #wrap { display:grid; grid-template-columns: max-content 1fr; border:1px solid #ddd; border-radius:4px; overflow:hidden; flex: 1; } #gutter, #t { font: 14px/var(--line-h) ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } - #gutter { padding:.5rem .75rem; text-align:right; color:#9ca3af; background:#f8fafc; border-right:1px solid #eee; + #gutter { padding:.5rem .75rem; text-align:right; color:#9ca3af; background:#f8fafc; border-right:1px solid #eee; user-select:none; min-width: 3ch; white-space: pre; height: 100%; overflow: hidden; } - #t { padding:.5rem .75rem; width:100%; height: 100%; resize: none; border:0; outline:0; + #t { padding:.5rem .75rem; width:100%; height: 100%; resize: none; border:0; outline:0; overflow:auto; white-space: pre; } #newpad { margin-left:.5rem; } #info { margin-left:.5rem; color: black; font-size: 0.8rem; } - pre {margin: 0; } + pre { margin: 0; } + /* Password protection */ + #lock-btn.locked { background:#fef3c7; border-color:#f59e0b; } + #pw-panel { display:none; position:absolute; top:2.5rem; right:0; background:#fff; border:1px solid #ddd; + border-radius:6px; padding:.75rem; box-shadow:0 4px 12px rgba(0,0,0,.1); z-index:10; min-width:230px; } + #pw-panel.open { display:block; } + #pw-panel label { font-size:.8rem; font-weight:bold; display:block; margin-bottom:.4rem; } + #pw-panel input { width:100%; padding:.35rem .5rem; border:1px solid #ddd; border-radius:4px; + margin-bottom:.5rem; font-size:.9rem; font-family:inherit; } + .pw-btns { display:flex; gap:.4rem; } + .pw-btns button { flex:1; font-size:.8rem; padding:.3rem .4rem; } + #pw-msg { font-size:.8rem; margin-top:.4rem; color:#6b7280; min-height:1.2em; } + #pw-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:100; + align-items:center; justify-content:center; } + #pw-overlay.open { display:flex; } + #pw-box { background:#fff; border-radius:8px; padding:1.5rem; width:300px; } + #pw-box h2 { margin:0 0 .75rem; font-size:1rem; } + #auth-input { width:100%; padding:.45rem .6rem; border:1px solid #ddd; border-radius:4px; + font-size:1rem; margin-bottom:.5rem; font-family:inherit; display:block; } + #auth-error { color:#ef4444; font-size:.85rem; margin-bottom:.5rem; display:none; } + #auth-submit { width:100%; padding:.45rem; background:#000; color:#fff; border:none; + border-radius:4px; font-size:.95rem; cursor:pointer; }
disconnected
-
+
+ New pad Info +
+ + +
+ + + +
+
+
@@ -159,6 +203,15 @@ HTML = """
+
+
+

This pad is password protected

+ +
+ +
+