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

488
app.py
View File

@@ -1,22 +1,25 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# COOKIE VAULT v2 — Netscape cookies.txt importer + one-click LOGIN launcher # COOKIE VAULT v5 — clean UI, session-1 chrome launch bridge, LLM fallback parse
# Host: Commando VM601, C:\cookievault\app.py, port 5066 import os, re, json, sqlite3, time, datetime, threading, subprocess, urllib.request
import os, re, json, sqlite3, time, datetime, threading, subprocess, tempfile
from flask import Flask, request, render_template_string, redirect, url_for, jsonify, Response from flask import Flask, request, render_template_string, redirect, url_for, jsonify, Response
APP_DIR = r"C:\cookievault" APP_DIR = r"C:\cookievault"
DB_PATH = os.path.join(APP_DIR, "cookies.db") DB_PATH = os.path.join(APP_DIR, "cookies.db")
WATCH_DIR = os.path.join(APP_DIR, "incoming") WATCH_DIR = os.path.join(APP_DIR, "incoming")
SESSION_DIR = os.path.join(APP_DIR, "sessions") SESSION_DIR = os.path.join(APP_DIR, "sessions")
WORK_DRIVE = r"D:\\" # the "Work" removable drive WORK_DRIVE = r"D:\\"
SCAN_ROOTS = [WATCH_DIR, WORK_DRIVE] 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(WATCH_DIR, exist_ok=True)
os.makedirs(SESSION_DIR, exist_ok=True) os.makedirs(SESSION_DIR, exist_ok=True)
app = Flask(__name__) app = Flask(__name__)
DOMAIN_RE = re.compile(r"^\.?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$", re.I)
def db(): def db():
c = sqlite3.connect(DB_PATH) c = sqlite3.connect(DB_PATH, timeout=30)
c.execute("""CREATE TABLE IF NOT EXISTS cookies( c.execute("""CREATE TABLE IF NOT EXISTS cookies(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT, flag TEXT, path TEXT, secure TEXT, domain TEXT, flag TEXT, path TEXT, secure TEXT,
@@ -27,10 +30,15 @@ def db():
return c return c
def utc(ts): 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" 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): def parse_netscape(text):
out = [] out = []
@@ -51,28 +59,60 @@ def parse_netscape(text):
out.append((domain, flag, path, secure, expiry, name, value)) out.append((domain, flag, path, secure, expiry, name, value))
return out return out
def _safe_expiry(val): def sniff_netscape(text):
try: """Deterministic: does the first data line look like 7-col cookie format?"""
iv = int(float(val)) seen = 0
return iv if iv > 0 else 0 for line in text.splitlines():
except (TypeError, ValueError): line = line.strip()
return 0 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) fname = os.path.basename(path)
with open(path, "r", encoding="utf-8", errors="replace") as f: with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read() 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) cookies = parse_netscape(text)
# guard: every cookie must have sane types (name+value non-empty, expiry numeric-ish)
clean = [] clean = []
for ck in cookies: for ck in cookies:
if not ck[5] or not ck[6]: if not ck[5] or not ck[6]:
continue continue
if re.match(r"^[0-9]+$", ck[4].strip() or "0") is None: if not re.match(r"^[0-9]+$", (ck[4] or "0").strip()):
# header junk line that survived parsing (e.g. split words) — skip continue
if not DOMAIN_RE.match(ck[0] or ""):
continue continue
clean.append(ck) clean.append(ck)
cookies = clean cookies = clean
if not cookies:
return 0, 0, None
c = db() c = db()
before = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0] before = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0]
for ck in cookies: for ck in cookies:
@@ -80,63 +120,89 @@ def import_file(path):
(*ck, fname, int(time.time()))) (*ck, fname, int(time.time())))
c.commit() c.commit()
after = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0] after = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0]
total = after
c.close() 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> # ---------------- UI ----------------
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} STYLE = """<style>
h1{margin:0;font-size:20px;color:#7aa2f7;letter-spacing:2px} *{box-sizing:border-box}
main{max-width:1100px;margin:24px auto;padding:0 16px} body{background:#0d0f14;color:#d7dae0;font-family:'Segoe UI',Consolas,monospace;margin:0}
.box{background:#141821;border:1px solid #232a36;border-radius:8px;padding:20px;margin-bottom:20px} 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}
input[type=text]{background:#0d0f14;border:1px solid #2c3547;color:#d7dae0;padding:8px 12px;border-radius:6px;width:60%} .brand{font-size:19px;color:#7aa2f7;letter-spacing:3px;font-weight:700}
button,.btn{background:#7aa2f7;color:#0d0f14;border:0;padding:8px 16px;border-radius:6px;font-weight:bold;cursor:pointer;font-family:inherit} .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.gray{background:#2c3547;color:#d7dae0}
.btn.green{background:#9ece6a} .btn.green{background:#9ece6a}
table{width:100%;border-collapse:collapse;font-size:13px} main{max-width:1000px;margin:20px auto;padding:0 16px}
th{color:#7aa2f7;text-align:left;padding:8px;border-bottom:1px solid #232a36} .flash{background:#141821;border:1px solid #9ece6a;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:14px}
td{padding:6px 8px;border-bottom:1px solid #1a2029;word-break:break-all} table{width:100%;border-collapse:collapse;font-size:14px}
td.val{color:#9ece6a;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} th{color:#7aa2f7;text-align:left;padding:10px 8px;border-bottom:2px solid #232a36;font-size:12px;text-transform:uppercase;letter-spacing:1px}
.pill{display:inline-block;background:#1f2735;color:#7aa2f7;border-radius:10px;padding:1px 10px;font-size:12px;margin-right:6px} 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} .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} #drop.hot{border-color:#7aa2f7;background:#1a2030}
.domtable td{font-size:14px} details summary{cursor:pointer;color:#7aa2f7;font-weight:600;padding:8px 0}
.domtable td b{color:#e0af68;font-size:15px} </style>"""
</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> HOME_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>COOKIE VAULT</title></head><body>
<main> <header>
<div class=box> <div class=brand>&#127850; COOKIE VAULT</div>
<h3 style=margin-top:0>Import Netscape cookies.txt</h3> <div class=stats><span>{{stats[0]}} cookies</span><span>{{stats[1]}} domains</span><span>{{stats[2]}} files</span></div>
<form id=f method=post action=/import enctype=multipart/form-data> </header>
<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> <nav>
<input id=m type=file name=files multiple accept=".txt,.json" hidden> <form method=get action=/>
<p class=muted>Or paste raw cookie text:</p> <input type=text name=q placeholder="search domains..." value="{{q}}" size=28>
<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> <button>SEARCH</button>
</div>
<p><button type=submit>IMPORT</button> <span class=muted>also watches C:\cookievault\incoming &mdash; dump files there and hit rescan</span></p>
</form> </form>
<form method=post action=/scan style=display:inline><button class="btn gray" type=submit>RESCAN incoming</button></form> <a class="btn gray" href="/browse">&#128193; BROWSE WORK DRIVE</a>
<a class=btn href="/browse">&#128193; PICK FOLDER ON WORK DRIVE</a> <a class="btn gray" href="/import_page">&#128229; IMPORT FILES</a>
<form method=post action=/scan_work style=display:inline><button type=submit>&#128269; SMART-SCAN ALL OF D:\</button></form> <a class="btn gray" href="/export{{'?q='+q if q}}">&#128190; EXPORT JSON</a>
<span class=muted>smart scan: any .txt containing Netscape cookies gets imported, whatever its name</span> <a class="btn gray" href="/export_netscape{{'?q='+q if q}}">&#128190; EXPORT TXT</a>
{% if msg %}<p class=ok>{{msg|safe}}</p>{% endif %} </nav>
</div> <main>
<div class=box> {% if msg %}<div class=flash>{{msg|safe}}</div>{% endif %}
<h3 style=margin-top:0>Domains &mdash; one-click login</h3> <table>
<form method=get action=/ style=margin-bottom:10px><input type=text name=q placeholder="filter domains..." value="{{q}}"> <tr><th>domain</th><th>cookies</th><th>fresh until</th><th></th></tr>
<button>FILTER</button> <a class="btn gray" href="/" style=text-decoration:none>ALL</a> {% for d in domains %}
<a class="btn gray" href="/export?{{('q='+q if q else '')|urlencode}}" style=text-decoration:none>EXPORT JSON</a> <tr>
<a class="btn gray" href="/export_netscape?{{('q='+q if q else '')|urlencode}}" style=text-decoration:none>EXPORT NETSCAPE</a></form> <td><b>{{d[0]}}</b></td>
<table class=domtable><tr><th>domain</th><th>cookies</th><th>fresh until</th><th>login</th></tr> <td>{{d[1]}}</td>
{% for d in domains %}<tr> <td class="{{'muted' if d[2] else 'ok'}}">{{utc(d[2]) if d[2] else 'session'}}</td>
<td><b>{{d[0]}}</b></td><td>{{d[1]}}</td> <td><a class="btn green" href="/open?domain={{d[0]|urlencode}}">LOGIN &rarr;</a></td>
<td class="{{'warn' if d[2]==0 else 'muted'}}">{{'has session cookies' if d[2]==0 else utc(d[2])}}</td> </tr>
<td><a class="btn green" href="/open?domain={{d[0]|urlencode}}" style=text-decoration:none>LOGIN &rarr;</a></td> {% endfor %}
</tr>{% endfor %}</table> </table>
<p class=muted>{{domains|length}} domains shown</p> <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>
</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> </main>
<script> <script>
const drop=document.getElementById('drop'), m=document.getElementById('m'); 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()); m.addEventListener('change',()=>document.getElementById('f').submit());
</script></body></html>""" </script></body></html>"""
BROWSE_PAGE = r"""<!doctype html><html><head><title>Pick a folder — COOKIE VAULT</title><style> BROWSE_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Pick folder — COOKIE VAULT</title></head><body>
body{background:#0d0f14;color:#d7dae0;font-family:Consolas,monospace;margin:0} <header><div class=brand>&#128193; WORK DRIVE &mdash; D:\{{' &raquo; ' + rel if rel != '.'}}</div>
header{background:#141821;padding:18px 24px;border-bottom:1px solid #232a36} <a class=btn href="/" style=margin-left:auto>&larr; back</a></header>
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>
<main> <main>
{% if parent is not none %}<div style=margin-bottom:10px><a class=btn href="/browse?path={{parent|urlencode}}">&#8592; UP</a></div>{% endif %} {% 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> <ul style="list-style:none;padding:0">
{% for e in entries %}<li><a href="/browse?path={{(rel ~ '/' ~ e) if rel != '.' else e|urlencode}}">&#128193; {{e}}/</a></li>{% endfor %} {% 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 %} {% if not entries %}<li class=muted style=padding:10px>no subfolders here</li>{% endif %}
</ul> </ul>
<div class=btnrow> <div style="margin-top:18px">
<form method=post action=/scan_folder><input type=hidden name=path value="{{rel}}"> <form method=post action=/scan_folder><input type=hidden name=path value="{{rel}}">
<button class=green>SMART-SCAN THIS FOLDER &#128269;</button></form> <button class=green>&#128269; SMART-SCAN THIS FOLDER</button></form>
<a class=btn href="/" style=margin-left:8px>Cancel</a>
</div> </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>""" </main></body></html>"""
PAGE_SIZE = 40
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])
def index(): def index():
q = request.args.get("q", "").strip() 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() c = db()
if q: if q:
like = f"%{q}%" 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 domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END)
WHERE domain LIKE ? GROUP BY domain ORDER BY domain LIMIT 500""", (like,)).fetchall() 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: else:
domains = c.execute("""SELECT domain, COUNT(*), MAX(CASE WHEN expiry GLOB '[0-9]*' THEN CAST(expiry AS INTEGER) ELSE 0 END) FROM cookies total = c.execute("SELECT COUNT(DISTINCT domain) FROM cookies").fetchone()[0]
GROUP BY domain ORDER BY domain LIMIT 500""").fetchall() 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], stats = (c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0],
c.execute("SELECT COUNT(DISTINCT domain) 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.execute("SELECT COUNT(DISTINCT source_file) FROM cookies").fetchone()[0])
c.close() 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"]) @app.route("/import", methods=["POST"])
def do_import(): def do_import():
msg, errs = [], [] msg, errs = [], []
files = request.files.getlist("files") for f in request.files.getlist("files"):
for f in files:
try: try:
fname = os.path.basename(f.filename or "").strip() fname = os.path.basename(f.filename or "").strip()
# sanitize: keep alnum, dash, underscore, dot
fname = re.sub(r"[^A-Za-z0-9._\- ]", "_", fname) fname = re.sub(r"[^A-Za-z0-9._\- ]", "_", fname)
if not fname or fname == ".": if not fname or fname == ".":
# no filename — generate one from content hash
data = f.read() data = f.read()
if not data.strip(): if not data.strip(): continue
continue fname = "unnamed_%d.txt" % (int(time.time()*1000) % 10**9)
fname = "unnamed_%d.txt" % int(time.time() * 1000 % 1000000000)
path = os.path.join(WATCH_DIR, fname) path = os.path.join(WATCH_DIR, fname)
with open(path, "wb") as fh: fh.write(data) with open(path, "wb") as fh: fh.write(data)
else: else:
path = os.path.join(WATCH_DIR, fname) path = os.path.join(WATCH_DIR, fname)
f.save(path) f.save(path)
n, new, total = import_file(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: except Exception as e:
errs.append(f"{getattr(f, 'filename', '?')}: {e}") errs.append(f"{getattr(f,'filename','?')}: {e}")
paste = request.form.get("paste", "").strip() paste = request.form.get("paste", "").strip()
if paste: if paste:
try: tmp = os.path.join(WATCH_DIR, "_pasted_%d.txt" % int(time.time()))
tmp = os.path.join(WATCH_DIR, "_pasted_%d.txt" % int(time.time())) with open(tmp, "w", encoding="utf-8") as fh: fh.write(paste)
with open(tmp, "w", encoding="utf-8") as fh: fh.write(paste) n, new, total = import_file(tmp)
n, new, total = import_file(tmp) msg.append(f"pasted: {new} new" if total is not None else "pasted: no cookie data detected")
msg.append(f"pasted: {n} parsed, {new} new") msg += ["<span class=warn>%s</span>" % e for e in errs]
except Exception as e: return redirect(url_for("index", msg=" &middot; ".join(msg) or "nothing imported"))
errs.append(f"paste: {e}")
if errs:
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)))
@app.route("/scan", methods=["POST"]) @app.route("/scan", methods=["POST"])
def scan(): 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"]) @app.route("/browse")
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"])
def browse(): def browse():
"""List subfolders of a path on the Work drive so the user can pick one."""
sub = request.args.get("path", "").strip() sub = request.args.get("path", "").strip()
base = os.path.abspath(WORK_DRIVE) base = os.path.abspath(WORK_DRIVE)
target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base 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): 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 = [] entries = []
try: for name in sorted(os.listdir(target)):
for name in sorted(os.listdir(target)): if os.path.isdir(os.path.join(target, name)) and name not in ("$RECYCLE.BIN", "System Volume Information"):
full = os.path.join(target, name) entries.append(name)
if os.path.isdir(full) 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) rel = os.path.relpath(target, base)
parent = os.path.dirname(rel) if rel != "." else None parent = os.path.dirname(rel) if rel != "." else None
return render_template_string(BROWSE_PAGE, entries=entries, rel=rel, parent=parent) 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) base = os.path.abspath(WORK_DRIVE)
target = os.path.abspath(os.path.join(base, sub.lstrip("/\\"))) if sub else base 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): 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) 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): def _smart_scan(root):
"""Smart scan: walk root, sniff every .txt for Netscape format regardless of filename."""
found = [] found = []
for rootpath, dirs, files in os.walk(root): 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")] dirs[:] = [d for d in dirs if d not in ("$RECYCLE.BIN", "System Volume Information", "node_modules")]
for fn in files: for fn in files:
if fn.lower().endswith(".txt"): if fn.lower().endswith((".txt", ".json", ".log")):
found.append(os.path.join(rootpath, fn)) found.append(os.path.join(rootpath, fn))
if not found: 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 = [], [] msg, errs = [], []
new_total, imported_files = 0, 0 new_total, imported_files, llm_used = 0, 0, 0
for p in found: for p in found:
try: try:
looks_like = False
with open(p, "r", encoding="utf-8", errors="replace") as f: with open(p, "r", encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f): text = f.read(65536)
line = line.strip() if not sniff_netscape(text):
if not line or line.startswith("#"): # deterministic miss -> try LLM only for small-ish files (cost control)
continue if os.path.getsize(p) > 200_000:
parts = line.split("\t") if "\t" in line else line.split() continue
if len(parts) >= 7 and parts[4].isdigit(): verdict = llm_classify(text)
looks_like = True if verdict is None:
if looks_like or i > 30: continue
break llm_used += 1
if not looks_like: if not verdict:
continue 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) n, new, total = import_file(p)
if total is None:
continue
imported_files += 1 imported_files += 1
new_total += new new_total += new
try: try: shown = os.path.relpath(p, root)
shown = os.path.relpath(p, root) except ValueError: shown = p
except ValueError:
shown = p
msg.append(f"{shown}: {new} new") msg.append(f"{shown}: {new} new")
except Exception as e: except Exception as e:
errs.append(f"{p}: {e}") errs.append(f"{p}: {e}")
if not imported_files and not errs: 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] 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): # ---------------- LOGIN (session-1 bridge) ----------------
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)
@app.route("/open") @app.route("/open")
def open_login(): def open_login():
domain = request.args.get("domain", "").strip() domain = request.args.get("domain", "").strip()
if not domain: if not domain:
return redirect(url_for("index")) return redirect(url_for("index"))
like = f"%{domain.strip('.')}%" stem = domain.strip(".")
like = stem + "%"
c = db() c = db()
rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies
WHERE domain LIKE ? OR domain LIKE ?""", WHERE domain LIKE ? OR domain LIKE ? ORDER BY (domain = ?) DESC LIMIT 4000""",
(domain.strip(".") + "%", "%" + domain.strip("."))).fetchall() (like, "%" + stem, domain)).fetchall()
c.close() c.close()
if not rows: if not rows:
return redirect(url_for("index", msg=f"no cookies stored for {domain}")) return redirect(url_for("index", msg=f"no cookies for {domain}"))
threading.Thread(target=_launch_login, args=(domain, rows), daemon=True).start() payload = {"domain": domain, "cookies": [
time.sleep(0.5) {"domain": r[0], "name": r[1], "value": r[2], "path": r[3],
return redirect(url_for("index", msg=f"launching Chrome for {domain} with {len(rows)} cookies...")) "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") @app.route("/export")
def export(): def export():
q = request.args.get("q", "").strip() q = request.args.get("q", "").strip()
c = db() c = db()
if q: if q:
like = f"%{q}%" rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies WHERE domain LIKE ? OR name LIKE ?", (f"%{q}%", f"%{q}%")).fetchall()
rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies WHERE domain LIKE ? OR name LIKE ?", (like, like)).fetchall()
else: else:
rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies").fetchall() rows = c.execute("SELECT domain,name,value,path,expiry,secure FROM cookies").fetchall()
c.close() c.close()
@@ -410,13 +445,12 @@ def export_ns():
q = request.args.get("q", "").strip() q = request.args.get("q", "").strip()
c = db() c = db()
if q: if q:
like = f"%{q}%" rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies WHERE domain LIKE ?", (f"%{q}%",)).fetchall()
rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies WHERE domain LIKE ? OR name LIKE ?", (like, like)).fetchall()
else: else:
rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies").fetchall() rows = c.execute("SELECT domain,flag,path,secure,expiry,name,value FROM cookies").fetchall()
c.close() c.close()
lines = ["# Netscape HTTP Cookie File", "# Exported by COOKIE VAULT"] 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", return Response("\n".join(lines) + "\n", mimetype="text/plain",
headers={"Content-Disposition": "attachment; filename=cookies.txt"}) 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