firefox-rnd
1 #!/usr/bin/env python3 2 3 import argparse 4 import atexit 5 import itertools 6 import json 7 import os 8 import os.path 9 import shutil 10 import subprocess 11 import sys 12 from pathlib import Path 13 from random import randint 14 15 # Install requests for this tasks 16 try: 17 import requests 18 except ImportError: 19 print("Install requests with 'pip install --user requests'") 20 os.system("pip install --user requests") 21 import requests 22 23 PROFILES_FILENAME = "firefox-rnd.lst" 24 PROFILE_PREFIX = "firefox-rnd-" 25 26 parser = argparse.ArgumentParser(description="Create and use (or reuse) a random firefox profile") 27 28 parser.add_argument("-r", "--reuse", help="Reuse the last profile", 29 action="store_true") 30 parser.add_argument("-e", "--exe", help="The firefox executable to call", default="librewolf") 31 parser.add_argument("-f", "--folder", help="Folder that will contain the profiles", default="/tmp") 32 parser.add_argument("-n", "--name", help="Name of the profile to use") 33 parser.add_argument("-d", "--keep", help="Keep profile at the end of the process", 34 action="store_true") 35 36 args, rest = parser.parse_known_args() 37 38 profiles_dir = Path(args.folder) 39 profiles_file = profiles_dir / PROFILES_FILENAME 40 41 has_profiles_file = profiles_file.is_file() 42 profiles = [] 43 44 if args.reuse: 45 if not has_profiles_file: 46 print("No profiles have been created yet!", file=sys.stderr) 47 sys.exit(1) 48 profiles = profiles_file.read_text().splitlines() 49 profile_name = profiles[-1] 50 elif args.name: 51 profile_name = args.name 52 else: 53 profile_name = PROFILE_PREFIX + "".join(chr(randint(97, 122)) for i in range(7)) 54 55 print("Starting profile: %s" % profile_name) 56 profile_dir = profiles_dir / profile_name 57 58 # Copy default firefox profile 59 if not profile_dir.is_dir(): 60 # Make sure we can copy the default profile folder 61 shutil.rmtree(profile_dir, ignore_errors=True) 62 profile_dir.parent.mkdir(parents=True, exist_ok=True) 63 64 default_profile = Path(__file__).resolve().parent / "../configurations/firefox" 65 shutil.copytree(default_profile, profile_dir) 66 # Make dirs writeable in order to copy stuff into it 67 os.chmod(profile_dir, 0o755) 68 for root, dirs, _ in os.walk(profile_dir): 69 for d in dirs: 70 os.chmod(os.path.join(root, d), 0o755) 71 72 # Preload with extensions 73 # https://support.mozilla.org/en-US/kb/deploying-firefox-with-extensions 74 # Extensions are put into the calling user's home as /nix/store isn't writable 75 cache_home = Path(os.environ.get("XDG_CACHE_HOME", "~/.cache")).expanduser() 76 extensions_path = (cache_home / "firefox-rnd" / "extensions").resolve() 77 extensions_path.mkdir(parents=True, exist_ok=True) 78 # Extensions are configured by the user in their home dir - again /nix/store isn't writable 79 xdg_config_home = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser() 80 config_home = xdg_config_home / "firefox-rnd" 81 extensions_json_path = config_home / "extensions.json" 82 # Ensure extensions dir exists in profile dir 83 extensions_profile_dir = profile_dir / "extensions" 84 extensions_profile_dir.mkdir(parents=True, exist_ok=True) 85 86 if extensions_json_path.is_file(): 87 extensions = json.loads(extensions_json_path.read_text()) 88 for ext_name, ext_conf in extensions.items(): 89 ext_version = ext_conf.get("version", "latest") 90 # TODO: support targeting a specific version 91 if ext_version == "latest": 92 ext_id = ext_conf.get("id", ext_name) 93 # Find the latest version 94 # https://addons-server.readthedocs.io/en/latest/topics/api/v4_frozen/index.html 95 versions_response = requests.get( 96 f"https://addons.mozilla.org/api/v4/addons/addon/{ext_id}/versions/" 97 ) 98 status_code = versions_response.status_code 99 if status_code != 200: 100 print(f"Bad response for {ext_name} versions: {status_code}", 101 file=sys.stderr) 102 continue 103 ext_files = next( 104 iter(versions_response.json().get("results", [])), {} 105 ).get("files", []) 106 if not ext_files: 107 print(f"No files for {ext_name}", file=sys.stderr) 108 continue 109 110 file_url = ext_files[0].get("url") 111 if not file_url: 112 print(f"No file for {ext_name} to download", file=sys.stderr) 113 continue 114 115 # Download version if the files doesn't exist yet 116 _, __, filename = file_url.rpartition("/") 117 ext_file_dl = extensions_path / filename 118 if not ext_file_dl.exists(): 119 ext_file_response = requests.get(file_url) 120 if ext_file_response.status_code != 200: 121 print(f"Error downloading file for {ext_name}: {ext_file_response.content}", 122 file=sys.stderr) 123 continue 124 125 with open(ext_file_dl, mode="wb") as ext_file: 126 ext_file.write(ext_file_response.content) 127 print(f"downloaded latest version of {ext_name}") 128 129 shutil.copy(ext_file_dl, extensions_profile_dir / f"{ext_id}.xpi") 130 131 # Delete profile by default after firefox exits 132 if not args.keep: 133 @atexit.register 134 def delete(*args): 135 shutil.rmtree(profile_dir) 136 # Remove profile from profiles_file 137 if profiles_file.is_file(): 138 lines = profiles_file.read_text().splitlines() 139 profiles_file.write_text("\n".join( 140 itertools.dropwhile(lambda line: profile_name in line, lines) 141 ) 142 ) 143 144 # Add profile to list 145 if len(profiles) == 0: 146 if has_profiles_file: 147 profiles = profiles_file.read_text().splitlines() 148 profiles.append(profile_name) 149 profiles_file.write_text("\n".join(profiles)) 150 151 subprocess.call( 152 [ 153 args.exe, 154 "--no-remote", 155 "--profile", str(profile_dir), 156 ] + rest 157 )