3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2021 Micah Lee <micah@micahflee.com>
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
41 SHARE = os.getenv("TBL_SHARE", sys.prefix + "/share") + "/torbrowser-launcher"
43 gettext.install("torbrowser-launcher")
45 # We're looking for output which:
47 # 1. The first portion must be `[GNUPG:] IMPORT_OK`
48 # 2. The second must be an integer between [0, 15], inclusive
49 # 3. The third must be an uppercased hex-encoded 160-bit fingerprint
50 gnupg_import_ok_pattern = re.compile(
51 b"(\[GNUPG\:\]) (IMPORT_OK) ([0-9]|[1]?[0-5]) ([A-F0-9]{40})"
56 def __init__(self, tbl_version):
57 self.tbl_version = tbl_version
60 self.default_mirror = "https://dist.torproject.org/"
61 self.discover_arch_lang()
63 for d in self.paths["dirs"]:
64 self.mkdir(self.paths["dirs"][d])
67 # some settings require a path rebuild, like force_en-US
69 self.mkdir(self.paths["download_dir"])
70 self.mkdir(self.paths["tbb"]["dir"])
73 # discover the architecture and language
74 def discover_arch_lang(self):
75 # figure out the architecture
76 self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
78 # figure out the language
79 available_languages = [
118 # a list of manually configured language fallback overriding
119 language_overrides = {
123 locale.setlocale(locale.LC_MESSAGES, "")
124 default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
125 if default_locale is None:
126 self.language = "en-US"
128 self.language = default_locale.replace("_", "-")
129 if self.language in language_overrides:
130 self.language = language_overrides[self.language]
131 if self.language not in available_languages:
132 self.language = self.language.split("-")[0]
133 if self.language not in available_languages:
134 for l in available_languages:
135 if l[0:2] == self.language:
137 # if language isn't available, default to english
138 if self.language not in available_languages:
139 self.language = "en-US"
141 # get value of environment variable, if it is not set return the default value
143 def get_env(var_name, default_value):
144 value = os.getenv(var_name)
146 value = default_value
149 # build all relevant paths
150 def build_paths(self, tbb_version=None):
151 homedir = os.getenv("HOME")
153 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
154 if not os.path.exists(homedir):
156 os.mkdir(homedir, 0o700)
159 "error", _("Error creating {0}").format(homedir), [], False
162 tbb_config = "{0}/torbrowser".format(
163 self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
165 tbb_cache = "{0}/torbrowser".format(
166 self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
168 tbb_local = "{0}/torbrowser".format(
169 self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
171 old_tbb_data = "{0}/.torbrowser".format(homedir)
173 if hasattr(self, "settings") and self.settings["force_en-US"]:
176 language = self.language
180 if self.architecture == "x86_64":
186 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
190 self.paths["tarball_url"] = (
191 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
193 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
194 self.paths["tarball_filename"] = tarball_filename
197 self.paths["sig_url"] = (
198 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
200 self.paths["sig_file"] = (
201 tbb_cache + "/download/" + tarball_filename + ".asc"
203 self.paths["sig_filename"] = tarball_filename + ".asc"
207 "config": tbb_config,
211 "old_data_dir": old_tbb_data,
212 "tbl_bin": sys.argv[0],
213 "icon_file": os.path.join(
214 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
216 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
218 "tor_browser_developers": os.path.join(
219 SHARE, "tor-browser-developers.asc"
221 "wkd_tmp": os.path.join(tbb_cache, "torbrowser.gpg")
224 os.path.join(SHARE, "mirrors.txt"),
225 tbb_config + "/mirrors.txt",
227 "download_dir": tbb_cache + "/download",
228 "gnupg_homedir": tbb_local + "/gnupg_homedir",
229 "settings_file": tbb_config + "/settings.json",
230 "settings_file_pickle": tbb_config + "/settings",
231 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
232 "version_check_file": tbb_cache + "/download/release.xml",
234 "changelog": tbb_local
239 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
240 "dir": tbb_local + "/tbb/" + self.architecture,
251 + "/start-tor-browser.desktop",
255 # Add the expected fingerprint for imported keys:
256 tor_browser_developers_fingerprint = "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
257 self.fingerprints = {
258 "tor_browser_developers": tor_browser_developers_fingerprint,
259 "wkd_tmp": tor_browser_developers_fingerprint,
266 if not os.path.exists(path):
267 os.makedirs(path, 0o700)
270 print(_("Cannot create directory {0}").format(path))
272 if not os.access(path, os.W_OK):
273 print(_("{0} is not writable").format(path))
277 # if gnupg_homedir isn't set up, set it up
278 def init_gnupg(self):
279 if not os.path.exists(self.paths["gnupg_homedir"]):
280 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
281 self.mkdir(self.paths["gnupg_homedir"])
285 # Use tor socks5 proxy, if enabled
286 if self.settings["download_over_tor"]:
287 socks5_address = "socks5h://{}".format(self.settings["tor_socks_address"])
288 return {"https": socks5_address, "http": socks5_address}
292 def refresh_keyring(self):
293 print("Downloading latest Tor Browser signing key...")
295 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
296 # Sometimes GPG throws errors, so comment this out and download it directly
297 # p = subprocess.Popen(
303 # self.paths["gnupg_homedir"],
304 # "--auto-key-locate",
307 # "torbrowser@torproject.org",
309 # stderr=subprocess.PIPE,
313 # Download the key from WKD directly
315 "https://torproject.org/.well-known/openpgpkey/hu/kounek7zrdx745qydx6p59t9mqjpuhdf?l=torbrowser",
316 proxies=self.proxies(),
318 if r.status_code != 200:
319 print(f"Error fetching key, status code = {r.status_code}")
321 with open(self.paths["signing_keys"]["wkd_tmp"], "wb") as f:
324 if self.import_key_and_check_status("wkd_tmp"):
325 print("Key imported successfully")
327 print("Key failed to import")
329 def import_key_and_check_status(self, key):
330 """Import a GnuPG key and check that the operation was successful.
331 :param str key: A string specifying the key's filepath from
334 :returns: ``True`` if the key is now within the keyring (or was
335 previously and hasn't changed). ``False`` otherwise.
337 with gpg.Context() as c:
339 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
342 impkey = self.paths["signing_keys"][key]
344 c.op_import(gpg.Data(file=impkey))
348 result = c.op_import_result()
349 if result and self.fingerprints[key] in result.imports[0].fpr:
355 def import_keys(self):
356 """Import all GnuPG keys.
358 :returns: ``True`` if all keys were successfully imported; ``False``
362 "tor_browser_developers",
364 all_imports_succeeded = True
367 imported = self.import_key_and_check_status(key)
371 "Could not import key with fingerprint: %s."
372 % self.fingerprints[key]
375 all_imports_succeeded = False
377 if not all_imports_succeeded:
378 print(_("Not all keys were imported successfully!"))
380 return all_imports_succeeded
383 def load_mirrors(self):
385 for srcfile in self.paths["mirrors_txt"]:
386 if not os.path.exists(srcfile):
388 for mirror in open(srcfile, "r").readlines():
389 if mirror.strip() not in self.mirrors:
390 self.mirrors.append(mirror.strip())
393 def load_settings(self):
395 "tbl_version": self.tbl_version,
397 "download_over_tor": False,
398 "tor_socks_address": "127.0.0.1:9050",
399 "mirror": self.default_mirror,
400 "force_en-US": False,
403 if os.path.isfile(self.paths["settings_file"]):
404 settings = json.load(open(self.paths["settings_file"]))
408 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
410 # make sure settings file is up-to-date
411 for setting in default_settings:
412 if setting not in settings:
413 settings[setting] = default_settings[setting]
416 # make sure tor_socks_address doesn't start with 'tcp:'
417 if settings["tor_socks_address"].startswith("tcp:"):
418 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
421 # make sure the version is current
422 if settings["tbl_version"] != self.tbl_version:
423 settings["tbl_version"] = self.tbl_version
426 self.settings = settings
430 # if settings file is still using old pickle format, convert to json
431 elif os.path.isfile(self.paths["settings_file_pickle"]):
432 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
434 os.remove(self.paths["settings_file_pickle"])
438 self.settings = default_settings
442 def save_settings(self):
443 json.dump(self.settings, open(self.paths["settings_file"], "w"))