Cookie Vault v2: Netscape cookies.txt importer + one-click selenium login
This commit is contained in:
30
README.md
Normal file
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# COOKIE VAULT
|
||||||
|
|
||||||
|
Netscape `cookies.txt` bulk importer + one-click login launcher. Runs on Windows (Commando VM601), Flask + SQLite, port 5066.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Drag-drop / folder-scan / paste import of hundreds of cookies.txt files
|
||||||
|
- `#HttpOnly_` line handling, tab or space separated, junk-tolerant parser
|
||||||
|
- One-click LOGIN: launches Chrome (selenium) with that domain's cookies injected into a persistent per-domain profile
|
||||||
|
- Search by domain or cookie name
|
||||||
|
- Export as JSON (browser-extension format) or Netscape .txt
|
||||||
|
- Idempotent imports (UNIQUE per domain+path+name+source_file)
|
||||||
|
|
||||||
|
## Deploy (Windows)
|
||||||
|
```
|
||||||
|
pip install flask selenium
|
||||||
|
C:\cookievault\app.py # the app
|
||||||
|
C:\cookievault\incoming\ # dump cookies.txt here, hit RESCAN
|
||||||
|
C:\cookievault\sessions\ # chrome profiles (auto-created)
|
||||||
|
```
|
||||||
|
Autostart: HKLM `...\CurrentVersion\Run` key `CookieVault` -> `C:\cookievault\start_cookievault.bat`
|
||||||
|
|
||||||
|
## Use
|
||||||
|
Open http://<host>:5066 — drag files in, click IMPORT. Click LOGIN next to a domain to open a pre-authenticated Chrome on the host desktop.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
- `POST /import` (multipart files=, or form paste=)
|
||||||
|
- `POST /scan` — rescan incoming folder
|
||||||
|
- `GET /open?domain=X` — launch logged-in Chrome for X
|
||||||
|
- `GET /export?q=` — JSON export
|
||||||
|
- `GET /export_netscape?q=` — Netscape export
|
||||||
295
app.py
Normal file
295
app.py
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/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
|
||||||
|
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")
|
||||||
|
os.makedirs(WATCH_DIR, exist_ok=True)
|
||||||
|
os.makedirs(SESSION_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def db():
|
||||||
|
c = sqlite3.connect(DB_PATH)
|
||||||
|
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(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 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 import_file(path):
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
text = f.read()
|
||||||
|
cookies = parse_netscape(text)
|
||||||
|
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]
|
||||||
|
total = after
|
||||||
|
c.close()
|
||||||
|
return len(cookies), after - before, total
|
||||||
|
|
||||||
|
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}
|
||||||
|
.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}
|
||||||
|
.muted{color:#565f89}.ok{color:#9ece6a}.warn{color:#e0af68}
|
||||||
|
#drop{border:2px dashed #2c3547;border-radius:8px;padding:34px;text-align:center;transition:.2s}
|
||||||
|
#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>🍪 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) or <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 — dump files there and hit rescan</span></p>
|
||||||
|
</form>
|
||||||
|
<form method=post action=/scan style=display:inline><button class="btn gray" type=submit>RESCAN incoming folder</button></form>
|
||||||
|
{% if msg %}<p class=ok>{{msg|safe}}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class=box>
|
||||||
|
<h3 style=margin-top:0>Domains — 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 →</a></td>
|
||||||
|
</tr>{% endfor %}</table>
|
||||||
|
<p class=muted>{{domains|length}} domains shown</p>
|
||||||
|
</div>
|
||||||
|
</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>"""
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET"])
|
||||||
|
def index():
|
||||||
|
q = request.args.get("q", "").strip()
|
||||||
|
c = db()
|
||||||
|
if q:
|
||||||
|
like = f"%{q}%"
|
||||||
|
domains = c.execute("""SELECT domain, COUNT(*), MAX(CAST(expiry AS INTEGER)) FROM cookies
|
||||||
|
WHERE domain LIKE ? GROUP BY domain ORDER BY domain LIMIT 500""", (like,)).fetchall()
|
||||||
|
else:
|
||||||
|
domains = c.execute("""SELECT domain, COUNT(*), MAX(CAST(expiry AS INTEGER)) FROM cookies
|
||||||
|
GROUP BY domain ORDER BY domain LIMIT 500""").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)
|
||||||
|
|
||||||
|
@app.route("/import", methods=["POST"])
|
||||||
|
def do_import():
|
||||||
|
msg, errs = [], []
|
||||||
|
files = request.files.getlist("files")
|
||||||
|
for f in 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)
|
||||||
|
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")
|
||||||
|
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 += ["<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=" · ".join(msg)))
|
||||||
|
|
||||||
|
@app.route("/scan", methods=["POST"])
|
||||||
|
def scan():
|
||||||
|
msg, errs = [], []
|
||||||
|
for fn in os.listdir(WATCH_DIR):
|
||||||
|
if fn.lower().endswith((".txt", ".json")):
|
||||||
|
try:
|
||||||
|
n, new, total = import_file(os.path.join(WATCH_DIR, 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=" · ".join(msg) or "nothing new in incoming"))
|
||||||
|
|
||||||
|
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]}
|
||||||
|
dom = ck[0]
|
||||||
|
if not dom.startswith("."):
|
||||||
|
c["domain"] = dom
|
||||||
|
else:
|
||||||
|
c["domain"] = dom
|
||||||
|
c["path"] = ck[3] or "/"
|
||||||
|
try:
|
||||||
|
exp = int(ck[4])
|
||||||
|
if exp > 0:
|
||||||
|
c["expiry"] = exp
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
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")
|
||||||
|
def open_login():
|
||||||
|
domain = request.args.get("domain", "").strip()
|
||||||
|
if not domain:
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
like = f"%{domain.strip('.')}%"
|
||||||
|
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()
|
||||||
|
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..."))
|
||||||
|
|
||||||
|
@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()
|
||||||
|
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": int(r[4]) if r[4] and int(r[4]) > 0 else None,
|
||||||
|
"secure": 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:
|
||||||
|
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()
|
||||||
|
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)
|
||||||
3
start_cookievault.bat
Normal file
3
start_cookievault.bat
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d C:\cookievault
|
||||||
|
python app.py >> C:\cookievault\app.log 2>&1
|
||||||
Reference in New Issue
Block a user