Add rbw integration for automated Google login
- Look up credentials from rbw password manager - Automate email/password entry in Google login flow - Add --copy flag to copy cookie JSON to clipboard - Add --entry flag to specify rbw entry name - Add --manual flag to skip auto-login - Update flake.nix with rbw, wl-clipboard, xclip deps
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
pkgs.python3Packages.selenium
|
||||
pkgs.chromedriver
|
||||
pkgs.chromium
|
||||
pkgs.rbw
|
||||
pkgs.wl-clipboard
|
||||
pkgs.xclip
|
||||
];
|
||||
in {
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
|
||||
@@ -1,69 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
# Configure Chrome options to run in incognito mode.
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--incognito")
|
||||
# Uncomment the following option if you want to auto-open developer tools for each tab.
|
||||
# options.add_argument("--auto-open-devtools-for-tabs")
|
||||
|
||||
# Initialize the Chrome driver.
|
||||
# If ChromeDriver is not in your PATH, specify its location: Service('/path/to/chromedriver')
|
||||
service = Service() # Assumes chromedriver is in your PATH
|
||||
def check_rbw_unlocked():
|
||||
"""Check if rbw vault is unlocked."""
|
||||
result = subprocess.run(["rbw", "unlocked"], capture_output=True)
|
||||
return result.returncode == 0
|
||||
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
|
||||
print("Opening https://chat.google.com ...")
|
||||
driver.get("https://chat.google.com")
|
||||
def get_rbw_credentials(entry: str):
|
||||
"""Get username and password from rbw."""
|
||||
# Get password
|
||||
result = subprocess.run(
|
||||
["rbw", "get", entry],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None, None
|
||||
|
||||
print("\nA new incognito window has been opened.")
|
||||
print("Please log in normally. To inspect cookies manually:")
|
||||
print(" 1. Press F12 to open developer tools.")
|
||||
print(" 2. Navigate to the Application (Chrome) or Storage (Firefox) tab.")
|
||||
print(" 3. Expand Cookies and select https://chat.google.com.")
|
||||
print(" 4. Verify that the COMPASS, SSID, SID, OSID, and HSID cookies are present.")
|
||||
print("\nIMPORTANT: Once you are logged in and have confirmed the cookies (or just logged in),")
|
||||
print("press Enter here. (Remember: to keep the validity of these cookies, close the browser soon!)")
|
||||
input("Press Enter to extract cookies...")
|
||||
password = result.stdout.strip()
|
||||
|
||||
# Navigate to some invalid URL on chat.google.com. This way we won't be redirected back to a
|
||||
# mail.google.com page and this allows us to extract the correct cookies.
|
||||
driver.get("https://chat.google.com/u/0/mole/world")
|
||||
# Get username
|
||||
result = subprocess.run(
|
||||
["rbw", "get", "--field", "username", entry],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
username = result.stdout.strip() if result.returncode == 0 else None
|
||||
|
||||
# Get all cookies
|
||||
all_cookies = driver.get_cookies()
|
||||
return username, password
|
||||
|
||||
# Define the cookie names we want (case-insensitive)
|
||||
target_names = {"COMPASS", "SSID", "SID", "OSID", "HSID"}
|
||||
extracted = {}
|
||||
|
||||
# For COMPASS cookie, if multiple are present, prefer the one with path == "/"
|
||||
compass_cookie = None
|
||||
def copy_to_clipboard(text: str):
|
||||
"""Copy text to clipboard using wl-copy or xclip."""
|
||||
# Try wl-copy first (Wayland)
|
||||
try:
|
||||
subprocess.run(
|
||||
["wl-copy"],
|
||||
input=text.encode(),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
for cookie in all_cookies:
|
||||
name_upper = cookie["name"].upper()
|
||||
if name_upper not in target_names:
|
||||
continue
|
||||
if name_upper == "COMPASS":
|
||||
if cookie.get("path", "") == "/":
|
||||
compass_cookie = cookie
|
||||
elif compass_cookie is None:
|
||||
compass_cookie = cookie
|
||||
else:
|
||||
extracted[name_upper] = cookie["value"]
|
||||
# Fall back to xclip (X11)
|
||||
try:
|
||||
subprocess.run(
|
||||
["xclip", "-selection", "clipboard"],
|
||||
input=text.encode(),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
if compass_cookie:
|
||||
extracted["COMPASS"] = compass_cookie["value"]
|
||||
return False
|
||||
|
||||
# Form JSON object of the extracted cookies.
|
||||
json_data = json.dumps(extracted, indent=2)
|
||||
print("\nExtracted Cookie JSON:")
|
||||
print(json_data)
|
||||
|
||||
print("\nClosing the browser window to avoid invalidating the cookies (Google uses refresh tokens).")
|
||||
def automate_google_login(driver, username: str, password: str):
|
||||
"""Attempt to automate Google login. Returns True if successful."""
|
||||
wait = WebDriverWait(driver, 10)
|
||||
|
||||
# Close the browser window.
|
||||
driver.quit()
|
||||
try:
|
||||
# Wait for and fill email field
|
||||
email_field = wait.until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, 'input[type="email"]'))
|
||||
)
|
||||
email_field.send_keys(username)
|
||||
email_field.send_keys(Keys.RETURN)
|
||||
|
||||
# Wait for password field
|
||||
time.sleep(2) # Google has animation between screens
|
||||
password_field = wait.until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, 'input[type="password"]'))
|
||||
)
|
||||
password_field.send_keys(password)
|
||||
password_field.send_keys(Keys.RETURN)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Auto-login failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def extract_cookies(driver):
|
||||
"""Extract the required cookies from the browser."""
|
||||
# Navigate to a chat.google.com path to ensure we get the right cookies
|
||||
driver.get("https://chat.google.com/u/0/mole/world")
|
||||
|
||||
all_cookies = driver.get_cookies()
|
||||
target_names = {"COMPASS", "SSID", "SID", "OSID", "HSID"}
|
||||
extracted = {}
|
||||
compass_cookie = None
|
||||
|
||||
for cookie in all_cookies:
|
||||
name_upper = cookie["name"].upper()
|
||||
if name_upper not in target_names:
|
||||
continue
|
||||
if name_upper == "COMPASS":
|
||||
if cookie.get("path", "") == "/":
|
||||
compass_cookie = cookie
|
||||
elif compass_cookie is None:
|
||||
compass_cookie = cookie
|
||||
else:
|
||||
extracted[name_upper] = cookie["value"]
|
||||
|
||||
if compass_cookie:
|
||||
extracted["COMPASS"] = compass_cookie["value"]
|
||||
|
||||
return extracted
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract Google Chat cookies for Mautrix bridge authentication"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--entry",
|
||||
default="google.com",
|
||||
help="rbw entry name for Google credentials (default: google.com)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--copy",
|
||||
action="store_true",
|
||||
help="Copy the cookie JSON to clipboard",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manual",
|
||||
action="store_true",
|
||||
help="Skip auto-login, do manual login only",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
username = None
|
||||
password = None
|
||||
|
||||
# Get credentials from rbw if not manual mode
|
||||
if not args.manual:
|
||||
if not check_rbw_unlocked():
|
||||
print("rbw vault is locked. Please run: rbw unlock")
|
||||
sys.exit(1)
|
||||
|
||||
username, password = get_rbw_credentials(args.entry)
|
||||
if not username or not password:
|
||||
print(f"Could not get credentials from rbw entry '{args.entry}'")
|
||||
print("Falling back to manual login...")
|
||||
|
||||
# Configure Chrome options
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--incognito")
|
||||
|
||||
# Initialize the Chrome driver
|
||||
service = Service()
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
|
||||
try:
|
||||
print("Opening https://chat.google.com ...")
|
||||
driver.get("https://chat.google.com")
|
||||
|
||||
if username and password and not args.manual:
|
||||
print("Attempting automatic login...")
|
||||
if automate_google_login(driver, username, password):
|
||||
print("Credentials entered. Complete any 2FA if prompted.")
|
||||
else:
|
||||
print("Auto-login failed, please log in manually.")
|
||||
|
||||
print("\nOnce logged in, press Enter to extract cookies...")
|
||||
input()
|
||||
|
||||
cookies = extract_cookies(driver)
|
||||
json_data = json.dumps(cookies, indent=2)
|
||||
|
||||
print("\nExtracted Cookie JSON:")
|
||||
print(json_data)
|
||||
|
||||
if args.copy:
|
||||
if copy_to_clipboard(json_data):
|
||||
print("\nCopied to clipboard!")
|
||||
else:
|
||||
print("\nFailed to copy to clipboard (wl-copy/xclip not found)")
|
||||
|
||||
finally:
|
||||
print("\nClosing browser...")
|
||||
driver.quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user