#!/usr/bin/env python3
"""Log in from the shell.
Usage:
./_login.py -u YOUR_USERNAME_OR_EMAIL
PO_PASSWORD='secret' ./_login.py -u you@example.com --cookies po.txt
The password is read from the PO_PASSWORD environment variable or, if that is
unset, prompted for without echo. On success the session cookies are saved in
Netscape format (default: ~/._cookies.txt, mode 600), so you can
reuse them with curl:
curl -b ~/._cookies.txt https://.org/account
Only the Python standard library is needed.
"""
import argparse
import getpass
import http.cookiejar
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from html.parser import HTMLParser
UA = "Mozilla/5.0 (X11; Linux x86_64) -login-script/1.0"
class FormParser(HTMLParser):
"""Collects every <form> with its action, method and <input> fields."""
def __init__(self):
super().__init__()
self.forms = []
self._cur = None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "form":
self._cur = {"action": a.get("action", ""),
"method": a.get("method", "get").lower(),
"inputs": []}
self.forms.append(self._cur)
elif tag == "input" and self._cur is not None:
self._cur["inputs"].append(a)
def handle_endtag(self, tag):
if tag == "form":
self._cur = None
class ErrorParser(HTMLParser):
"""Pulls out error messages (class contains 'error')."""
def __init__(self):
super().__init__()
self.messages = []
self._depth = 0
self._buf = []
def handle_starttag(self, tag, attrs):
cls = dict(attrs).get("class") or ""
if self._depth:
self._depth += 1
elif "error" in cls:
self._depth = 1
self._buf = []
def handle_endtag(self, tag):
if self._depth:
self._depth -= 1
if self._depth == 0:
text = " ".join("".join(self._buf).split())
if text:
self.messages.append(text)
def handle_data(self, data):
if self._depth:
self._buf.append(data)
def find_login_form(html):
parser = FormParser()
parser.feed(html)
for form in parser.forms:
names = {i.get("name") for i in form["inputs"]}
if "emailhandle" in names and "password" in names:
return form
return None
def main():
ap = argparse.ArgumentParser(description="Log in ")
ap.add_argument("-u", "--user", required=True, help="username or email")
ap.add_argument("--base", default="https://.org",
help="site root (default: %(default)s; cookies differ "
"between the www and non-www hosts)")
ap.add_argument("--cookies",
default=os.path.expanduser("~/._cookies.txt"),
help="where to save session cookies (default: %(default)s)")
ap.add_argument("--no-remember", action="store_true",
help="don't tick 'remember me'")
args = ap.parse_args()
password = os.environ.get("PO_PASSWORD") or getpass.getpass("Password: ")
jar = http.cookiejar.MozillaCookieJar(args.cookies)
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
opener.addheaders = [("User-Agent", UA)]
login_url = args.base.rstrip("/") + "/login"
# 1. Fetch the login page: gets the session cookie and the hidden CSRF "code".
try:
with opener.open(login_url, timeout=30) as r:
page_url = r.geturl()
html = r.read().decode("utf-8", "replace")
except (urllib.error.URLError, OSError) as e:
sys.exit(f"Could not reach {login_url}: {e}")
form = find_login_form(html)
if form is None:
sys.exit("Could not find the login form (already logged in, or the "
"site layout changed).")
# 2. Build the POST body from the form's own fields, then fill in ours.
data = {}
for inp in form["inputs"]:
name = inp.get("name")
if not name or inp.get("type") in ("submit", "button", "image"):
continue
if inp.get("type") == "checkbox" and "checked" not in inp:
continue
data[name] = inp.get("value", "")
data["emailhandle"] = args.user
data["password"] = password
data["dologin"] = "1"
if not args.no_remember:
data["remember"] = "1"
action = urllib.parse.urljoin(page_url, form["action"] or login_url)
# 3. Submit.
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(action, data=body,
headers={"Referer": page_url})
try:
with opener.open(req, timeout=30) as r:
result = r.read().decode("utf-8", "replace")
final_url = r.geturl()
except (urllib.error.URLError, OSError) as e:
sys.exit(f"Login request failed: {e}")
# 4. Success = the page no longer shows a login form and offers a logout link.
still_login = find_login_form(result) is not None
has_logout = bool(re.search(r'href="[^"]*(?:qa=logout|/logout)', result))
if has_logout and not still_login:
jar.save(ignore_discard=True, ignore_expires=True)
os.chmod(args.cookies, 0o600)
print(f"Logged in as {args.user} ({final_url})")
print(f"Cookies saved to {args.cookies}")
return 0
errors = ErrorParser()
errors.feed(result)
print("Login failed.", file=sys.stderr)
for m in dict.fromkeys(errors.messages):
print(f" site says: {m}", file=sys.stderr)
if re.search(r"anti-spam|captcha", result, re.I):
print(" The site is asking for an anti-spam check, which this "
"script cannot solve; log in once in a browser.", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
No comments:
Post a Comment
no thanks
Note: Only a member of this blog may post a comment.