/ bin / firefox-rnd
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  )