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.
40 SHARE = os.getenv("TBL_SHARE", sys.prefix + "/share") + "/torbrowser-launcher"
42 gettext.install("torbrowser-launcher")
44 # We're looking for output which:
46 # 1. The first portion must be `[GNUPG:] IMPORT_OK`
47 # 2. The second must be an integer between [0, 15], inclusive
48 # 3. The third must be an uppercased hex-encoded 160-bit fingerprint
49 gnupg_import_ok_pattern = re.compile(
50 b"(\[GNUPG\:\]) (IMPORT_OK) ([0-9]|[1]?[0-5]) ([A-F0-9]{40})"
55 def __init__(self, tbl_version):
56 self.tbl_version = tbl_version
59 self.default_mirror = "https://dist.torproject.org/"
60 self.discover_arch_lang()
62 for d in self.paths["dirs"]:
63 self.mkdir(self.paths["dirs"][d])
66 # some settings require a path rebuild, like force_en-US
68 self.mkdir(self.paths["download_dir"])
69 self.mkdir(self.paths["tbb"]["dir"])
72 # discover the architecture and language
73 def discover_arch_lang(self):
74 # figure out the architecture
75 self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
77 # figure out the language
78 available_languages = [
117 # a list of manually configured language fallback overriding
118 language_overrides = {
122 locale.setlocale(locale.LC_MESSAGES, "")
123 default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
124 if default_locale is None:
125 self.language = "en-US"
127 self.language = default_locale.replace("_", "-")
128 if self.language in language_overrides:
129 self.language = language_overrides[self.language]
130 if self.language not in available_languages:
131 self.language = self.language.split("-")[0]
132 if self.language not in available_languages:
133 for l in available_languages:
134 if l[0:2] == self.language:
136 # if language isn't available, default to english
137 if self.language not in available_languages:
138 self.language = "en-US"
140 # get value of environment variable, if it is not set return the default value
142 def get_env(var_name, default_value):
143 value = os.getenv(var_name)
145 value = default_value
148 # build all relevant paths
149 def build_paths(self, tbb_version=None):
150 homedir = os.getenv("HOME")
152 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
153 if not os.path.exists(homedir):
155 os.mkdir(homedir, 0o700)
158 "error", _("Error creating {0}").format(homedir), [], False
161 tbb_config = "{0}/torbrowser".format(
162 self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
164 tbb_cache = "{0}/torbrowser".format(
165 self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
167 tbb_local = "{0}/torbrowser".format(
168 self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
170 old_tbb_data = "{0}/.torbrowser".format(homedir)
172 if hasattr(self, "settings") and self.settings["force_en-US"]:
175 language = self.language
179 if self.architecture == "x86_64":
185 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
189 self.paths["tarball_url"] = (
190 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
192 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
193 self.paths["tarball_filename"] = tarball_filename
196 self.paths["sig_url"] = (
197 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
199 self.paths["sig_file"] = (
200 tbb_cache + "/download/" + tarball_filename + ".asc"
202 self.paths["sig_filename"] = tarball_filename + ".asc"
206 "config": tbb_config,
210 "old_data_dir": old_tbb_data,
211 "tbl_bin": sys.argv[0],
212 "icon_file": os.path.join(
213 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
215 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
217 "tor_browser_developers": os.path.join(
218 SHARE, "tor-browser-developers.asc"
222 os.path.join(SHARE, "mirrors.txt"),
223 tbb_config + "/mirrors.txt",
225 "download_dir": tbb_cache + "/download",
226 "gnupg_homedir": tbb_local + "/gnupg_homedir",
227 "settings_file": tbb_config + "/settings.json",
228 "settings_file_pickle": tbb_config + "/settings",
229 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
230 "version_check_file": tbb_cache + "/download/release.xml",
232 "changelog": tbb_local
237 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
238 "dir": tbb_local + "/tbb/" + self.architecture,
249 + "/start-tor-browser.desktop",
253 # Add the expected fingerprint for imported keys:
254 self.fingerprints = {
255 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
262 if not os.path.exists(path):
263 os.makedirs(path, 0o700)
266 print(_("Cannot create directory {0}").format(path))
268 if not os.access(path, os.W_OK):
269 print(_("{0} is not writable").format(path))
273 # if gnupg_homedir isn't set up, set it up
274 def init_gnupg(self):
275 if not os.path.exists(self.paths["gnupg_homedir"]):
276 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
277 self.mkdir(self.paths["gnupg_homedir"])
280 def refresh_keyring(self, fingerprint=None):
281 if fingerprint is not None:
282 print("Refreshing local keyring... Missing key: " + fingerprint)
284 print("Refreshing local keyring...")
286 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
287 p = subprocess.Popen(
293 self.paths["gnupg_homedir"],
297 "torbrowser@torproject.org",
299 stderr=subprocess.PIPE,
303 for output in p.stderr.readlines():
304 match = gnupg_import_ok_pattern.match(output)
305 if match and match.group(2) == "IMPORT_OK":
306 fingerprint = str(match.group(4))
307 if match.group(3) == "0":
308 print("Keyring refreshed successfully...")
309 print(" No key updates for key: " + fingerprint)
310 elif match.group(3) == "4":
311 print("Keyring refreshed successfully...")
312 print(" New signatures for key: " + fingerprint)
314 print("Keyring refreshed successfully...")
316 def import_key_and_check_status(self, key):
317 """Import a GnuPG key and check that the operation was successful.
318 :param str key: A string specifying the key's filepath from
321 :returns: ``True`` if the key is now within the keyring (or was
322 previously and hasn't changed). ``False`` otherwise.
324 with gpg.Context() as c:
326 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
329 impkey = self.paths["signing_keys"][key]
331 c.op_import(gpg.Data(file=impkey))
335 result = c.op_import_result()
336 if result and self.fingerprints[key] in result.imports[0].fpr:
342 def import_keys(self):
343 """Import all GnuPG keys.
345 :returns: ``True`` if all keys were successfully imported; ``False``
349 "tor_browser_developers",
351 all_imports_succeeded = True
354 imported = self.import_key_and_check_status(key)
358 "Could not import key with fingerprint: %s."
359 % self.fingerprints[key]
362 all_imports_succeeded = False
364 if not all_imports_succeeded:
365 print(_("Not all keys were imported successfully!"))
367 return all_imports_succeeded
370 def load_mirrors(self):
372 for srcfile in self.paths["mirrors_txt"]:
373 if not os.path.exists(srcfile):
375 for mirror in open(srcfile, "r").readlines():
376 if mirror.strip() not in self.mirrors:
377 self.mirrors.append(mirror.strip())
380 def load_settings(self):
382 "tbl_version": self.tbl_version,
384 "download_over_tor": False,
385 "tor_socks_address": "127.0.0.1:9050",
386 "mirror": self.default_mirror,
387 "force_en-US": False,
390 if os.path.isfile(self.paths["settings_file"]):
391 settings = json.load(open(self.paths["settings_file"]))
395 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
397 # make sure settings file is up-to-date
398 for setting in default_settings:
399 if setting not in settings:
400 settings[setting] = default_settings[setting]
403 # make sure tor_socks_address doesn't start with 'tcp:'
404 if settings["tor_socks_address"].startswith("tcp:"):
405 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
408 # make sure the version is current
409 if settings["tbl_version"] != self.tbl_version:
410 settings["tbl_version"] = self.tbl_version
413 self.settings = settings
417 # if settings file is still using old pickle format, convert to json
418 elif os.path.isfile(self.paths["settings_file_pickle"]):
419 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
421 os.remove(self.paths["settings_file_pickle"])
425 self.settings = default_settings
429 def save_settings(self):
430 json.dump(self.settings, open(self.paths["settings_file"], "w"))