v5.1: clean nav UI, junk-domain purge, smart LLM-assisted scan, session launcher with in-UI screenshot proof

This commit is contained in:
2026-09-25 18:53:43 -07:00
parent 03a4d3204b
commit 5b53fc351e
3 changed files with 377 additions and 227 deletions

468
app.py
View File

@@ -1,22 +1,25 @@
#!/usr/bin/env python3
# COOKIE VAULT v2 — Netscape cookies.txt importer + one-click LOGIN launcher
# Host: Commando VM601, C:\cookievault\app.py, port 5066
import os, re, json, sqlite3, time, datetime, threading, subprocess, tempfile
# COOKIE VAULT v5 — clean UI, session-1 chrome launch bridge, LLM fallback parse
import os, re, json, sqlite3, time, datetime, threading, subprocess, urllib.request
from flask import Flask, request, render_template_string, redirect, url_for, jsonify, Response
APP_DIR = r"C:\cookievault"
DB_PATH = os.path.join(APP_DIR, "cookies.db")
WATCH_DIR = os.path.join(APP_DIR, "incoming")
SESSION_DIR = os.path.join(APP_DIR, "sessions")
WORK_DRIVE = r"D:\\" # the "Work" removable drive
SCAN_ROOTS = [WATCH_DIR, WORK_DRIVE]
WORK_DRIVE = r"D:\\"
BRIDGE = "http://127.0.0.1:5067/launch"
OLLAMA_HOSTS = ["10.30.20.222", "10.30.20.29"]
LLM_MODEL = "ornith-1.5:9b-64k"
os.makedirs(WATCH_DIR, exist_ok=True)
os.makedirs(SESSION_DIR, exist_ok=True)
app = Flask(__name__)
DOMAIN_RE = re.compile(r"^\.?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$", re.I)
def db():
c = sqlite3.connect(DB_PATH)
c = sqlite3.connect(DB_PATH, timeout=30)
c.execute("""CREATE TABLE IF NOT EXISTS cookies(
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT, flag TEXT, path TEXT, secure TEXT,
@@ -27,10 +30,15 @@ def db():
return c
def utc(ts):
try: return datetime.datetime.utcfromtimestamp(int(ts)).strftime("%Y-%m-%d")
try: return datetime.datetime.utcfromtimestamp(int(float(ts))).strftime("%Y-%m-%d")
except Exception: return "session"
NS_RE = re.compile(r"^(#\S+)?\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$")
def _safe_expiry(val):
try:
iv = int(float(val))
return iv if iv > 0 else 0
except (TypeError, ValueError):
return 0
def parse_netscape(text):
out = []
@@ -51,28 +59,60 @@ def parse_netscape(text):
out.append((domain, flag, path, secure, expiry, name, value))
return out
def _safe_expiry(val):
try:
iv = int(float(val))
return iv if iv > 0 else 0
except (TypeError, ValueError):
return 0
def sniff_netscape(text):
"""Deterministic: does the first data line look like 7-col cookie format?"""
seen = 0
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t") if "\t" in line else line.split()
if len(parts) >= 7 and parts[4].isdigit():
return True
seen += 1
if seen > 5:
return False
return False
def import_file(path):
def llm_classify(text):
"""Fallback: ask local ornith whether the file holds cookie data. Returns True/False/None(fail)."""
prompt = ("You are a file classifier. Does this text contain browser cookie data in Netscape "
"format (7 tab-separated fields: domain, TRUE/FALSE, path, TRUE/FALSE, numeric expiry, name, value)? "
"Answer exactly one word: YES or NO.\n\n" + text[:2000])
for host in OLLAMA_HOSTS:
try:
req = urllib.request.Request(f"http://{host}:11434/api/generate",
data=json.dumps({"model": LLM_MODEL, "prompt": prompt, "stream": False, "think": False}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=45) as r:
ans = json.load(r)["response"].strip().upper()
if "YES" in ans[:6]: return True
if "NO" in ans[:6]: return False
if ans and "?" not in ans[:6]: return False
except Exception:
continue
return None
def import_file(path, use_llm_fallback=True):
fname = os.path.basename(path)
with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
if not sniff_netscape(text):
if not (use_llm_fallback and llm_classify(text)):
return 0, 0, None # not a cookie file
cookies = parse_netscape(text)
# guard: every cookie must have sane types (name+value non-empty, expiry numeric-ish)
clean = []
for ck in cookies:
if not ck[5] or not ck[6]:
continue
if re.match(r"^[0-9]+$", ck[4].strip() or "0") is None:
# header junk line that survived parsing (e.g. split words) — skip
if not re.match(r"^[0-9]+$", (ck[4] or "0").strip()):
continue
if not DOMAIN_RE.match(ck[0] or ""):
continue
clean.append(ck)
cookies = clean
if not cookies:
return 0, 0, None
c = db()
before = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0]
for ck in cookies:
@@ -80,63 +120,89 @@ def import_file(path):
(*ck, fname, int(time.time())))
c.commit()
after = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0]
total = after
c.close()
return len(cookies), after - before, total
return len(cookies), after - before, after
PAGE = r"""<!doctype html><html><head><title>COOKIE VAULT</title><style>
body{background:#0d0f14;color:#d7dae0;font-family:Consolas,monospace;margin:0}
header{background:#141821;padding:18px 24px;border-bottom:1px solid #232a36;display:flex;justify-content:space-between;align-items:center}
h1{margin:0;font-size:20px;color:#7aa2f7;letter-spacing:2px}
main{max-width:1100px;margin:24px auto;padding:0 16px}
.box{background:#141821;border:1px solid #232a36;border-radius:8px;padding:20px;margin-bottom:20px}
input[type=text]{background:#0d0f14;border:1px solid #2c3547;color:#d7dae0;padding:8px 12px;border-radius:6px;width:60%}
button,.btn{background:#7aa2f7;color:#0d0f14;border:0;padding:8px 16px;border-radius:6px;font-weight:bold;cursor:pointer;font-family:inherit}
# ---------------- UI ----------------
STYLE = """<style>
*{box-sizing:border-box}
body{background:#0d0f14;color:#d7dae0;font-family:'Segoe UI',Consolas,monospace;margin:0}
header{background:#141821;padding:14px 24px;border-bottom:1px solid #232a36;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
.brand{font-size:19px;color:#7aa2f7;letter-spacing:3px;font-weight:700}
.stats span{display:inline-block;background:#1f2735;color:#7aa2f7;border-radius:12px;padding:3px 12px;font-size:12px;margin-left:6px}
nav{background:#10131a;border-bottom:1px solid #232a36;padding:10px 24px;display:flex;gap:10px;flex-wrap:wrap;align-items:center}
nav form{display:flex;gap:8px;align-items:center;margin:0}
input[type=text]{background:#0d0f14;border:1px solid #2c3547;color:#d7dae0;padding:8px 14px;border-radius:6px;font-size:14px}
input[type=text]:focus{outline:none;border-color:#7aa2f7}
button,.btn{background:#7aa2f7;color:#0d0f14;border:0;padding:8px 16px;border-radius:6px;font-weight:700;cursor:pointer;font-family:inherit;font-size:13px;text-decoration:none;display:inline-block}
button:hover,.btn:hover{filter:brightness(1.1)}
.btn.gray{background:#2c3547;color:#d7dae0}
.btn.green{background:#9ece6a}
table{width:100%;border-collapse:collapse;font-size:13px}
th{color:#7aa2f7;text-align:left;padding:8px;border-bottom:1px solid #232a36}
td{padding:6px 8px;border-bottom:1px solid #1a2029;word-break:break-all}
td.val{color:#9ece6a;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pill{display:inline-block;background:#1f2735;color:#7aa2f7;border-radius:10px;padding:1px 10px;font-size:12px;margin-right:6px}
main{max-width:1000px;margin:20px auto;padding:0 16px}
.flash{background:#141821;border:1px solid #9ece6a;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:14px}
table{width:100%;border-collapse:collapse;font-size:14px}
th{color:#7aa2f7;text-align:left;padding:10px 8px;border-bottom:2px solid #232a36;font-size:12px;text-transform:uppercase;letter-spacing:1px}
td{padding:9px 8px;border-bottom:1px solid #1a2029}
td b{color:#e0af68;font-size:15px}
.muted{color:#565f89}.ok{color:#9ece6a}.warn{color:#e0af68}
#drop{border:2px dashed #2c3547;border-radius:8px;padding:34px;text-align:center;transition:.2s}
.pager{margin:14px 0;text-align:center}
.pager a,.pager span{display:inline-block;padding:6px 12px;margin:0 3px;border-radius:6px;background:#1f2735;color:#7aa2f7;text-decoration:none}
.pager .cur{background:#7aa2f7;color:#0d0f14;font-weight:700}
#drop{border:2px dashed #2c3547;border-radius:8px;padding:28px;text-align:center;transition:.2s;margin-top:10px}
#drop.hot{border-color:#7aa2f7;background:#1a2030}
.domtable td{font-size:14px}
.domtable td b{color:#e0af68;font-size:15px}
</style></head><body>
<header><h1>&#127850; COOKIE VAULT</h1><div><span class=pill>{{stats[0]}} cookies</span><span class=pill>{{stats[1]}} domains</span><span class=pill>{{stats[2]}} files</span></div></header>
<main>
<div class=box>
<h3 style=margin-top:0>Import Netscape cookies.txt</h3>
<form id=f method=post action=/import enctype=multipart/form-data>
<div id=drop>Drop <b>cookies.txt</b> files here (as many as you want) &nbsp;or&nbsp; <button type=button onclick="document.getElementById('m').click()">choose files</button>
<input id=m type=file name=files multiple accept=".txt,.json" hidden>
<p class=muted>Or paste raw cookie text:</p>
<textarea name=paste rows=4 style="width:95%;background:#0d0f14;color:#9ece6a;border:1px solid #2c3547;border-radius:6px;font-family:inherit" placeholder="# Netscape HTTP Cookie File ..."></textarea>
</div>
<p><button type=submit>IMPORT</button> <span class=muted>also watches C:\cookievault\incoming &mdash; dump files there and hit rescan</span></p>
details summary{cursor:pointer;color:#7aa2f7;font-weight:600;padding:8px 0}
</style>"""
HOME_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>COOKIE VAULT</title></head><body>
<header>
<div class=brand>&#127850; COOKIE VAULT</div>
<div class=stats><span>{{stats[0]}} cookies</span><span>{{stats[1]}} domains</span><span>{{stats[2]}} files</span></div>
</header>
<nav>
<form method=get action=/>
<input type=text name=q placeholder="search domains..." value="{{q}}" size=28>
<button>SEARCH</button>
</form>
<form method=post action=/scan style=display:inline><button class="btn gray" type=submit>RESCAN incoming</button></form>
<a class=btn href="/browse">&#128193; PICK FOLDER ON WORK DRIVE</a>
<form method=post action=/scan_work style=display:inline><button type=submit>&#128269; SMART-SCAN ALL OF D:\</button></form>
<span class=muted>smart scan: any .txt containing Netscape cookies gets imported, whatever its name</span>
{% if msg %}<p class=ok>{{msg|safe}}</p>{% endif %}
<a class="btn gray" href="/browse">&#128193; BROWSE WORK DRIVE</a>
<a class="btn gray" href="/import_page">&#128229; IMPORT FILES</a>
<a class="btn gray" href="/export{{'?q='+q if q}}">&#128190; EXPORT JSON</a>
<a class="btn gray" href="/export_netscape{{'?q='+q if q}}">&#128190; EXPORT TXT</a>
</nav>
<main>
{% if msg %}<div class=flash>{{msg|safe}}</div>{% endif %}
<table>
<tr><th>domain</th><th>cookies</th><th>fresh until</th><th></th></tr>
{% for d in domains %}
<tr>
<td><b>{{d[0]}}</b></td>
<td>{{d[1]}}</td>
<td class="{{'muted' if d[2] else 'ok'}}">{{utc(d[2]) if d[2] else 'session'}}</td>
<td><a class="btn green" href="/open?domain={{d[0]|urlencode}}">LOGIN &rarr;</a></td>
</tr>
{% endfor %}
</table>
<div class=pager>
{% if page>1 %}<a href="/?page={{page-1}}{{'&q='+q if q}}">&laquo; prev</a>{% endif %}
<span class=cur>{{page}}</span>
{% if has_more %}<a href="/?page={{page+1}}{{'&q='+q if q}}">next &raquo;</a>{% endif %}
</div>
<div class=box>
<h3 style=margin-top:0>Domains &mdash; one-click login</h3>
<form method=get action=/ style=margin-bottom:10px><input type=text name=q placeholder="filter domains..." value="{{q}}">
<button>FILTER</button> <a class="btn gray" href="/" style=text-decoration:none>ALL</a>
<a class="btn gray" href="/export?{{('q='+q if q else '')|urlencode}}" style=text-decoration:none>EXPORT JSON</a>
<a class="btn gray" href="/export_netscape?{{('q='+q if q else '')|urlencode}}" style=text-decoration:none>EXPORT NETSCAPE</a></form>
<table class=domtable><tr><th>domain</th><th>cookies</th><th>fresh until</th><th>login</th></tr>
{% for d in domains %}<tr>
<td><b>{{d[0]}}</b></td><td>{{d[1]}}</td>
<td class="{{'warn' if d[2]==0 else 'muted'}}">{{'has session cookies' if d[2]==0 else utc(d[2])}}</td>
<td><a class="btn green" href="/open?domain={{d[0]|urlencode}}" style=text-decoration:none>LOGIN &rarr;</a></td>
</tr>{% endfor %}</table>
<p class=muted>{{domains|length}} domains shown</p>
</main></body></html>"""
IMPORT_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Import — COOKIE VAULT</title></head><body>
<header><div class=brand>&#127850; COOKIE VAULT</div><div class=stats><a class=btn href="/" style=margin-left:auto>&larr; back</a></div></header>
<main>
<div class=flash>Drop cookie files below, <a href="/browse" style=color:#7aa2f7>browse the Work drive</a>, or paste raw text.</div>
<form id=f method=post action=/import enctype=multipart/form-data>
<div id=drop>Drag <b>cookie files</b> here &mdash; any filename, any format &mdash; or
<button type=button onclick="document.getElementById('m').click()">choose files</button>
<input id=m type=file name=files multiple hidden>
<p style="margin-top:14px"><textarea name=paste rows=5 style="width:95%;background:#0d0f14;color:#9ece6a;border:1px solid #2c3547;border-radius:6px;font-family:inherit" placeholder="...or paste raw cookie text here"></textarea></p>
</div>
<p><button class=green>IMPORT</button>
<button type=submit formaction=/scan formmethod=post class=gray>RESCAN incoming</button>
<button type=submit formaction=/scan_work formmethod=post>SMART-SCAN ALL OF D:\</button></p>
</form>
</main>
<script>
const drop=document.getElementById('drop'), m=document.getElementById('m');
@@ -146,119 +212,106 @@ drop.addEventListener('drop',ev=>{m.files=ev.dataTransfer.files;document.getElem
m.addEventListener('change',()=>document.getElementById('f').submit());
</script></body></html>"""
BROWSE_PAGE = r"""<!doctype html><html><head><title>Pick a folder — COOKIE VAULT</title><style>
body{background:#0d0f14;color:#d7dae0;font-family:Consolas,monospace;margin:0}
header{background:#141821;padding:18px 24px;border-bottom:1px solid #232a36}
h1{margin:0 0 4px;font-size:20px;color:#7aa2f7;letter-spacing:2px}
.crumb{color:#565f89;font-size:13px}
.crumb a{color:#7aa2f7;text-decoration:none}
main{max-width:800px;margin:24px auto;padding:0 16px}
ul{list-style:none;padding:0}
li{border-bottom:1px solid #1a2029}
li a{display:block;padding:10px 8px;color:#e0af68;text-decoration:none;font-size:15px}
li a:hover{background:#141821}
.btnrow{margin-top:18px}
button,.btn{background:#7aa2f7;color:#0d0f14;border:0;padding:9px 18px;border-radius:6px;font-weight:bold;cursor:pointer;font-family:inherit;font-size:14px;text-decoration:none;display:inline-block}
.btn.green{background:#9ece6a}
.muted{color:#565f89;font-size:13px}
</style></head><body>
<header><h1>&#128193; Pick a folder on WORK (D:\)</h1>
<div class=crumb>D:\ {% if rel != '.' %}&raquo; {{rel}}{% endif %}</div></header>
BROWSE_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Pick folder — COOKIE VAULT</title></head><body>
<header><div class=brand>&#128193; WORK DRIVE &mdash; D:\{{' &raquo; ' + rel if rel != '.'}}</div>
<a class=btn href="/" style=margin-left:auto>&larr; back</a></header>
<main>
{% if parent is not none %}<div style=margin-bottom:10px><a class=btn href="/browse?path={{parent|urlencode}}">&#8592; UP</a></div>{% endif %}
<ul>
{% for e in entries %}<li><a href="/browse?path={{(rel ~ '/' ~ e) if rel != '.' else e|urlencode}}">&#128193; {{e}}/</a></li>{% endfor %}
{% if parent is not none %}<div style=margin-bottom:10px><a class="btn gray" href="/browse?path={{parent|urlencode}}">&#8592; UP</a></div>{% endif %}
<ul style="list-style:none;padding:0">
{% for e in entries %}<li style="border-bottom:1px solid #1a2029">
<a href="/browse?path={{((rel + '/' + e) if rel != '.' else e)|urlencode}}" style="display:block;padding:10px 8px;color:#e0af68;text-decoration:none;font-size:15px">&#128193; {{e}}/</a></li>{% endfor %}
{% if not entries %}<li class=muted style=padding:10px>no subfolders here</li>{% endif %}
</ul>
<div class=btnrow>
<div style="margin-top:18px">
<form method=post action=/scan_folder><input type=hidden name=path value="{{rel}}">
<button class=green>SMART-SCAN THIS FOLDER &#128269;</button></form>
<a class=btn href="/" style=margin-left:8px>Cancel</a>
<button class=green>&#128269; SMART-SCAN THIS FOLDER</button></form>
</div>
<p class=muted>Scans this folder + all subfolders. Any .txt that contains Netscape cookies (any filename) gets imported.</p>
<p class=muted>Scans this folder + subfolders. Any file with cookie data gets imported &mdash; any filename, any format (LLM-assisted detection).</p>
</main></body></html>"""
PAGE_SIZE = 40
@app.route("/", methods=["GET"])
def index():
q = request.args.get("q", "").strip()
try: page = max(1, int(request.args.get("page", 1)))
except ValueError: page = 1
like = f"%{q}%" if q else "%"
c = db()
if q:
like = f"%{q}%"
domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) FROM cookies
WHERE domain LIKE ? GROUP BY domain ORDER BY domain LIMIT 500""", (like,)).fetchall()
total = c.execute("SELECT COUNT(DISTINCT domain) FROM cookies WHERE domain LIKE ?", (like,)).fetchone()[0]
domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END)
FROM cookies WHERE domain LIKE ? GROUP BY domain
HAVING COUNT(*) >= 2 AND domain GLOB '*.*.*' OR (domain LIKE ? AND COUNT(*) >= 1)
ORDER BY COUNT(*) DESC, domain LIMIT ? OFFSET ?""",
(like, like, PAGE_SIZE, (page-1)*PAGE_SIZE)).fetchall()
else:
domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) FROM cookies
GROUP BY domain ORDER BY domain LIMIT 500""").fetchall()
total = c.execute("SELECT COUNT(DISTINCT domain) FROM cookies").fetchone()[0]
domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END)
FROM cookies GROUP BY domain
HAVING COUNT(*) >= 3 AND domain GLOB '*.*.*'
ORDER BY COUNT(*) DESC, domain LIMIT ? OFFSET ?""",
(PAGE_SIZE, (page-1)*PAGE_SIZE)).fetchall()
stats = (c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0],
c.execute("SELECT COUNT(DISTINCT domain) FROM cookies").fetchone()[0],
c.execute("SELECT COUNT(DISTINCT source_file) FROM cookies").fetchone()[0])
c.close()
return render_template_string(PAGE, domains=domains, stats=stats, q=q, msg=request.args.get("msg"), utc=utc)
return render_template_string(HOME_PAGE, domains=domains, stats=stats, q=q, page=page,
has_more=(page*PAGE_SIZE) < total, msg=request.args.get("msg"), utc=utc)
@app.route("/import_page")
def import_page():
return render_template_string(IMPORT_PAGE, msg=request.args.get("msg"))
@app.route("/import", methods=["POST"])
def do_import():
msg, errs = [], []
files = request.files.getlist("files")
for f in files:
for f in request.files.getlist("files"):
try:
fname = os.path.basename(f.filename or "").strip()
# sanitize: keep alnum, dash, underscore, dot
fname = re.sub(r"[^A-Za-z0-9._\- ]", "_", fname)
if not fname or fname == ".":
# no filename — generate one from content hash
data = f.read()
if not data.strip():
continue
fname = "unnamed_%d.txt" % int(time.time() * 1000 % 1000000000)
if not data.strip(): continue
fname = "unnamed_%d.txt" % (int(time.time()*1000) % 10**9)
path = os.path.join(WATCH_DIR, fname)
with open(path, "wb") as fh: fh.write(data)
else:
path = os.path.join(WATCH_DIR, fname)
f.save(path)
n, new, total = import_file(path)
msg.append(f"{fname}: {n} parsed, {new} new")
msg.append(f"{fname}: {new} new" if total is not None else f"{fname}: no cookie data found")
except Exception as e:
errs.append(f"{getattr(f,'filename','?')}: {e}")
paste = request.form.get("paste", "").strip()
if paste:
try:
tmp = os.path.join(WATCH_DIR, "_pasted_%d.txt" % int(time.time()))
with open(tmp, "w", encoding="utf-8") as fh: fh.write(paste)
n, new, total = import_file(tmp)
msg.append(f"pasted: {n} parsed, {new} new")
except Exception as e:
errs.append(f"paste: {e}")
if errs:
msg.append(f"pasted: {new} new" if total is not None else "pasted: no cookie data detected")
msg += ["<span class=warn>%s</span>" % e for e in errs]
if not msg:
msg = ["nothing imported — pick a cookies.txt file or paste cookie text first"]
return redirect(url_for("index", msg=" &middot; ".join(msg)))
return redirect(url_for("index", msg=" &middot; ".join(msg) or "nothing imported"))
@app.route("/scan", methods=["POST"])
def scan():
return _scan_dirs(SCAN_ROOTS)
msg = []
for fn in os.listdir(WATCH_DIR):
if fn.lower().endswith((".txt", ".json")) and not fn.startswith("_pasted"):
n, new, total = import_file(os.path.join(WATCH_DIR, fn))
msg.append(f"{fn}: {new} new")
return redirect(url_for("index", msg=" &middot; ".join(msg) or "nothing new"))
@app.route("/scan_work", methods=["POST"])
def scan_work():
"""Deep-scan the Work drive: every .txt file that LOOKS like Netscape cookie format."""
return _smart_scan(WORK_DRIVE)
@app.route("/browse", methods=["GET"])
@app.route("/browse")
def browse():
"""List subfolders of a path on the Work drive so the user can pick one."""
sub = request.args.get("path", "").strip()
base = os.path.abspath(WORK_DRIVE)
target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base
if not target.startswith(base) or not os.path.isdir(target):
return redirect(url_for("index", msg="<span class=warn>invalid folder</span>"))
return redirect(url_for("index", msg="invalid folder"))
entries = []
try:
for name in sorted(os.listdir(target)):
full = os.path.join(target, name)
if os.path.isdir(full) and name not in ("$RECYCLE.BIN", "System Volume Information"):
if os.path.isdir(os.path.join(target, name)) and name not in ("$RECYCLE.BIN", "System Volume Information"):
entries.append(name)
except Exception as e:
return redirect(url_for("index", msg=f"<span class=warn>{e}</span>"))
rel = os.path.relpath(target, base)
parent = os.path.dirname(rel) if rel != "." else None
return render_template_string(BROWSE_PAGE, entries=entries, rel=rel, parent=parent)
@@ -269,133 +322,115 @@ def scan_folder():
base = os.path.abspath(WORK_DRIVE)
target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base
if not target.startswith(base) or not os.path.isdir(target):
return redirect(url_for("index", msg="<span class=warn>invalid folder</span>"))
return redirect(url_for("index", msg="invalid folder"))
return _smart_scan(target)
@app.route("/scan_work", methods=["POST"])
def scan_work():
if not os.path.isdir(WORK_DRIVE):
return redirect(url_for("index", msg="Work drive (D:\\) not plugged in"))
return _smart_scan(WORK_DRIVE)
def _smart_scan(root):
"""Smart scan: walk root, sniff every .txt for Netscape format regardless of filename."""
found = []
for rootpath, dirs, files in os.walk(root):
dirs[:] = [d for d in dirs if d not in ("$RECYCLE.BIN", "System Volume Information", "node_modules")]
for fn in files:
if fn.lower().endswith(".txt"):
if fn.lower().endswith((".txt", ".json", ".log")):
found.append(os.path.join(rootpath, fn))
if not found:
return redirect(url_for("index", msg="no .txt files found in that folder"))
return redirect(url_for("index", msg="no text files found in that folder"))
msg, errs = [], []
new_total, imported_files = 0, 0
new_total, imported_files, llm_used = 0, 0, 0
for p in found:
try:
looks_like = False
with open(p, "r", encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f):
line = line.strip()
if not line or line.startswith("#"):
text = f.read(65536)
if not sniff_netscape(text):
# deterministic miss -> try LLM only for small-ish files (cost control)
if os.path.getsize(p) > 200_000:
continue
parts = line.split("\t") if "\t" in line else line.split()
if len(parts) >= 7 and parts[4].isdigit():
looks_like = True
if looks_like or i > 30:
break
if not looks_like:
verdict = llm_classify(text)
if verdict is None:
continue
llm_used += 1
if not verdict:
continue
with open(p, "r", encoding="utf-8", errors="replace") as f:
pass # full read happens in import_file
n, new, total = import_file(p)
if total is None:
continue
imported_files += 1
new_total += new
try:
shown = os.path.relpath(p, root)
except ValueError:
shown = p
try: shown = os.path.relpath(p, root)
except ValueError: shown = p
msg.append(f"{shown}: {new} new")
except Exception as e:
errs.append(f"{p}: {e}")
if not imported_files and not errs:
return redirect(url_for("index", msg=f"scanned {len(found)} .txt files — none were Netscape cookie format"))
return redirect(url_for("index", msg=f"scanned {len(found)} files — no cookie data found (LLM checked {llm_used} ambiguous ones)"))
msg += ["<span class=warn>%s</span>" % e for e in errs]
return redirect(url_for("index", msg=f"<b>Scan of {root}:</b> {imported_files} cookie files of {len(found)} txt, {new_total} new cookies &middot; " + " &middot; ".join(msg[:25])))
return redirect(url_for("index", msg=f"<b>{imported_files} cookie files</b> ({new_total} new cookies, LLM-assisted on {llm_used}) &middot; " + " &middot; ".join(msg[:20])))
def _scan_dirs(dirs):
msg, errs = [], []
for d in dirs:
if not os.path.isdir(d):
continue
for fn in os.listdir(d):
if fn.lower().endswith((".txt", ".json")) and not fn.startswith("_pasted"):
try:
n, new, total = import_file(os.path.join(d, fn))
msg.append(f"{fn}: {new} new")
except Exception as e:
errs.append(f"{fn}: {e}")
msg += ["<span class=warn>%s</span>" % e for e in errs]
return redirect(url_for("index", msg=" &middot; ".join(msg) or "nothing new found"))
def _launch_login(domain, cookies):
"""Runs in background thread: selenium Chrome with cookies injected."""
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
profile = os.path.join(SESSION_DIR, re.sub(r'[^a-zA-Z0-9]', '_', domain))
os.makedirs(profile, exist_ok=True)
opts.add_argument(r"--user-data-dir=" + profile)
opts.add_argument("--no-first-run")
opts.add_argument("--no-default-browser-check")
opts.add_argument("--start-maximized")
driver = webdriver.Chrome(options=opts) # selenium-manager auto-fetches driver
try:
# land on the site's origin first so cookies can be set for it
host = domain.lstrip(".")
driver.get(f"https://{host}/favicon.ico")
except Exception:
try: driver.get(f"https://{host}/")
except Exception: pass
time.sleep(1)
for ck in cookies:
c = {"name": ck[1], "value": ck[2]}
c["domain"] = ck[0]
c["path"] = ck[3] or "/"
exp = _safe_expiry(ck[4])
if exp > 0:
c["expiry"] = exp
c["secure"] = str(ck[5]).upper() == "TRUE"
try:
driver.add_cookie(c)
except Exception:
try:
c2 = dict(c); c2.pop("expiry", None)
driver.add_cookie(c2)
except Exception:
pass
try:
driver.get(f"https://{host}/")
except Exception:
pass
# keep the process ref alive — selenium closes browser if driver is GC'd
globals().setdefault("_drivers", []).append(driver)
# ---------------- LOGIN (session-1 bridge) ----------------
@app.route("/open")
def open_login():
domain = request.args.get("domain", "").strip()
if not domain:
return redirect(url_for("index"))
like = f"%{domain.strip('.')}%"
stem = domain.strip(".")
like = stem + "%"
c = db()
rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies
WHERE domain LIKE ? OR domain LIKE ?""",
(domain.strip(".") + "%", "%" + domain.strip("."))).fetchall()
WHERE domain LIKE ? OR domain LIKE ? ORDER BY (domain = ?) DESC LIMIT 4000""",
(like, "%" + stem, domain)).fetchall()
c.close()
if not rows:
return redirect(url_for("index", msg=f"no cookies stored for {domain}"))
threading.Thread(target=_launch_login, args=(domain, rows), daemon=True).start()
time.sleep(0.5)
return redirect(url_for("index", msg=f"launching Chrome for {domain} with {len(rows)} cookies..."))
return redirect(url_for("index", msg=f"no cookies for {domain}"))
payload = {"domain": domain, "cookies": [
{"domain": r[0], "name": r[1], "value": r[2], "path": r[3],
"expiry": _safe_expiry(r[4]), "secure": str(r[5]).upper() == "TRUE"} for r in rows]}
try:
req = urllib.request.Request(BRIDGE, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=15) as r:
res = json.load(r)
if res.get("ok"):
return render_template_string(LAUNCH_PAGE, domain=domain, n=len(rows),
token=res.get("token", ""), url=f"https://{stem}/")
return redirect(url_for("index", msg=f"bridge error: {res.get('error')}"))
except Exception as e:
return redirect(url_for("index", msg=f"launcher bridge not reachable ({e})"))
LAUNCH_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Launching…</title>
<meta http-equiv="refresh" content="12"></head><body>
<header><div class=brand>&#127919; logging in to {{domain}}</div></header>
<main>
<div class=flash>Launching Chrome with <b>{{n}} cookies</b>… this page auto-refreshes; the screenshot appears below when the session is ready.</div>
{% if token %}<img src="/shot/{{token}}" style="max-width:100%;border:1px solid #232a36;border-radius:8px" onerror="this.style.display='none'">{% endif %}
<div style="margin-top:16px"><a class=btn href="/">&larr; back to vault</a></div>
</main></body></html>"""
@app.route("/shot/<token>")
def shot(token):
token = re.sub(r"[^a-z0-9_]", "", token)
try:
with urllib.request.urlopen(f"http://127.0.0.1:5067/shot/{token}", timeout=10) as r:
img = r.read()
return Response(img, mimetype="image/png")
except Exception:
return Response(b"", mimetype="image/png", status=503)
# ---------------- EXPORT ----------------
@app.route("/export")
def export():
q = request.args.get("q", "").strip()
c = db()
if q:
like = f"%{q}%"
rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies WHERE domain LIKE ? OR name LIKE ?", (like, like)).fetchall()
rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies WHERE domain LIKE ? OR name LIKE ?", (f"%{q}%", f"%{q}%")).fetchall()
else:
rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies").fetchall()
c.close()
@@ -410,13 +445,12 @@ def export_ns():
q = request.args.get("q", "").strip()
c = db()
if q:
like = f"%{q}%"
rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies WHERE domain LIKE ? OR name LIKE ?", (like, like)).fetchall()
rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies WHERE domain LIKE ?", (f"%{q}%",)).fetchall()
else:
rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies").fetchall()
c.close()
lines = ["# Netscape HTTP Cookie File", "# Exported by COOKIE VAULT"]
lines += ["\t".join(str(x) for x in r) for r in rows if _safe_expiry(r[4]) >= 0]
lines += ["\t".join(str(x) for x in r) for r in rows]
return Response("\n".join(lines) + "\n", mimetype="text/plain",
headers={"Content-Disposition": "attachment; filename=cookies.txt"})

113
launcher_bridge.py Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# COOKIE VAULT v5 — session-1 chrome launcher bridge
# Runs AS drjones (interactive Session 1) via scheduled task.
# Listens 127.0.0.1:5067, receives {cookies, domain} JSON, launches visible Chrome+selenium.
import json, os, re, time, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
SESSION_DIR = r"C:\cookievault\sessions"
class Handler(BaseHTTPRequestHandler):
def log_message(self, *a): pass
def do_GET(self):
if self.path.startswith("/shot/"):
tok = self.path[len("/shot/"):]
img = SCREENSHOTS.get(tok)
if not img:
self.send_response(404); self.end_headers(); return
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", str(len(img)))
self.end_headers()
self.wfile.write(img)
return
self.send_response(404); self.end_headers()
def do_POST(self):
if urlparse(self.path).path != "/launch":
self.send_response(404); self.end_headers(); return
try:
ln = int(self.headers.get("Content-Length", 0))
data = json.loads(self.rfile.read(ln))
domain = data.get("domain", "")
cookies = data.get("cookies", [])
if not domain or not cookies:
self._json(400, {"ok": False, "error": "domain and cookies required"}); return
tok = "%s_%d" % (re.sub(r'[^a-z0-9]', '', domain.lower())[:20], int(time.time()))
threading.Thread(target=launch, args=(domain, cookies, tok), daemon=True).start()
self._json(200, {"ok": True, "launched": len(cookies), "token": tok})
except Exception as e:
self._json(500, {"ok": False, "error": str(e)})
def _json(self, code, obj):
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def launch(domain, cookies, tok):
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
profile = os.path.join(SESSION_DIR, re.sub(r'[^a-zA-Z0-9]', '_', domain))
os.makedirs(profile, exist_ok=True)
opts.add_argument(r"--user-data-dir=" + profile)
opts.add_argument("--no-first-run")
opts.add_argument("--no-default-browser-check")
opts.add_argument("--start-maximized")
opts.add_argument("--window-size=1440,900")
opts.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=opts)
try:
SCREENSHOTS[tok] = _png(driver, "starting")
host = domain.lstrip(".")
try:
driver.get(f"https://{host}/favicon.ico")
except Exception:
try: driver.get(f"https://{host}/")
except Exception: pass
time.sleep(1.5)
added = 0
for ck in cookies:
try:
c = {"name": ck.get("name"), "value": ck.get("value", ""),
"domain": ck.get("domain", ""), "path": ck.get("path", "/") or "/",
"secure": bool(ck.get("secure"))}
if not c["name"]:
continue
exp = ck.get("expiry") or 0
try: exp = int(float(exp))
except Exception: exp = 0
if exp > 0: c["expiry"] = exp
try:
driver.add_cookie(c); added += 1
except Exception:
c2 = dict(c); c2.pop("expiry", None)
try: driver.add_cookie(c2); added += 1
except Exception: pass
except Exception:
pass
try:
driver.get(f"https://{host}/")
time.sleep(4)
SCREENSHOTS[tok] = _png(driver, "done")
except Exception:
SCREENSHOTS[tok] = _png(driver, "cookies-set")
with open(os.path.join(SESSION_DIR, "_launch.log"), "a", encoding="utf-8") as f:
f.write(f"{time.ctime()} {domain}: added {added}/{len(cookies)} cookies\n")
finally:
try: driver.quit()
except Exception: pass
def _png(driver, stage):
try:
return driver.get_screenshot_as_png()
except Exception:
return b""
SCREENSHOTS = {}
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 5067), Handler).serve_forever()

3
start_bridge.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
cd /d C:\cookievault
python launcher_bridge.py >> C:\cookievault\bridge.log 2>&1