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 self.mkdir(self.paths["download_dir"])
67 self.mkdir(self.paths["tbb"]["dir"])
70 # discover the architecture and language
71 def discover_arch_lang(self):
72 # figure out the architecture
73 self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
75 # figure out the language
76 available_languages = [
114 default_locale = locale.getlocale()[0]
115 if default_locale is None:
116 self.language = "en-US"
118 self.language = default_locale.replace("_", "-")
119 if self.language not in available_languages:
120 self.language = self.language.split("-")[0]
121 if self.language not in available_languages:
122 for l in available_languages:
123 if l[0:2] == self.language:
125 # if language isn't available, default to english
126 if self.language not in available_languages:
127 self.language = "en-US"
129 # get value of environment variable, if it is not set return the default value
131 def get_env(var_name, default_value):
132 value = os.getenv(var_name)
134 value = default_value
137 # build all relevant paths
138 def build_paths(self, tbb_version=None):
139 homedir = os.getenv("HOME")
141 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
142 if not os.path.exists(homedir):
144 os.mkdir(homedir, 0o700)
147 "error", _("Error creating {0}").format(homedir), [], False
150 tbb_config = '{0}/torbrowser'.format(self.get_env('XDG_CONFIG_HOME', '{0}/.config'.format(homedir)))
151 tbb_cache = '{0}/torbrowser'.format(self.get_env('XDG_CACHE_HOME', '{0}/.cache'.format(homedir)))
152 tbb_local = '{0}/torbrowser'.format(self.get_env('XDG_DATA_HOME', '{0}/.local/share'.format(homedir)))
153 old_tbb_data = '{0}/.torbrowser'.format(homedir)
157 if self.architecture == "x86_64":
162 if hasattr(self, "settings") and self.settings["force_en-US"]:
165 language = self.language
167 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
171 self.paths["tarball_url"] = (
172 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
174 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
175 self.paths["tarball_filename"] = tarball_filename
178 self.paths["sig_url"] = (
179 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
181 self.paths["sig_file"] = (
182 tbb_cache + "/download/" + tarball_filename + ".asc"
184 self.paths["sig_filename"] = tarball_filename + ".asc"
187 "dirs": {"config": tbb_config, "cache": tbb_cache, "local": tbb_local,},
188 "old_data_dir": old_tbb_data,
189 "tbl_bin": sys.argv[0],
190 "icon_file": os.path.join(
191 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
193 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
195 "tor_browser_developers": os.path.join(
196 SHARE, "tor-browser-developers.asc"
200 os.path.join(SHARE, "mirrors.txt"),
201 tbb_config + "/mirrors.txt",
203 "download_dir": tbb_cache + "/download",
204 "gnupg_homedir": tbb_local + "/gnupg_homedir",
205 "settings_file": tbb_config + "/settings.json",
206 "settings_file_pickle": tbb_config + "/settings",
207 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
208 "version_check_file": tbb_cache + "/download/release.xml",
210 "changelog": tbb_local
215 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
216 "dir": tbb_local + "/tbb/" + self.architecture,
227 + "/start-tor-browser.desktop",
231 # Add the expected fingerprint for imported keys:
232 self.fingerprints = {
233 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
240 if not os.path.exists(path):
241 os.makedirs(path, 0o700)
244 print(_("Cannot create directory {0}").format(path))
246 if not os.access(path, os.W_OK):
247 print(_("{0} is not writable").format(path))
251 # if gnupg_homedir isn't set up, set it up
252 def init_gnupg(self):
253 if not os.path.exists(self.paths["gnupg_homedir"]):
254 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
255 self.mkdir(self.paths["gnupg_homedir"])
258 def refresh_keyring(self, fingerprint=None):
259 if fingerprint is not None:
260 print("Refreshing local keyring... Missing key: " + fingerprint)
262 print("Refreshing local keyring...")
264 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
265 p = subprocess.Popen(
271 self.paths["gnupg_homedir"],
275 "torbrowser@torproject.org",
277 stderr=subprocess.PIPE,
281 for output in p.stderr.readlines():
282 match = gnupg_import_ok_pattern.match(output)
283 if match and match.group(2) == "IMPORT_OK":
284 fingerprint = str(match.group(4))
285 if match.group(3) == "0":
286 print("Keyring refreshed successfully...")
287 print(" No key updates for key: " + fingerprint)
288 elif match.group(3) == "4":
289 print("Keyring refreshed successfully...")
290 print(" New signatures for key: " + fingerprint)
292 print("Keyring refreshed successfully...")
294 def import_key_and_check_status(self, key):
295 """Import a GnuPG key and check that the operation was successful.
296 :param str key: A string specifying the key's filepath from
299 :returns: ``True`` if the key is now within the keyring (or was
300 previously and hasn't changed). ``False`` otherwise.
302 with gpg.Context() as c:
304 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
307 impkey = self.paths["signing_keys"][key]
309 c.op_import(gpg.Data(file=impkey))
313 result = c.op_import_result()
314 if result and self.fingerprints[key] in result.imports[0].fpr:
320 def import_keys(self):
321 """Import all GnuPG keys.
323 :returns: ``True`` if all keys were successfully imported; ``False``
327 "tor_browser_developers",
329 all_imports_succeeded = True
332 imported = self.import_key_and_check_status(key)
336 "Could not import key with fingerprint: %s."
337 % self.fingerprints[key]
340 all_imports_succeeded = False
342 if not all_imports_succeeded:
343 print(_("Not all keys were imported successfully!"))
345 return all_imports_succeeded
348 def load_mirrors(self):
350 for srcfile in self.paths["mirrors_txt"]:
351 if not os.path.exists(srcfile):
353 for mirror in open(srcfile, "r").readlines():
354 if mirror.strip() not in self.mirrors:
355 self.mirrors.append(mirror.strip())
358 def load_settings(self):
360 "tbl_version": self.tbl_version,
362 "download_over_tor": False,
363 "tor_socks_address": "127.0.0.1:9050",
364 "mirror": self.default_mirror,
365 "force_en-US": False,
368 if os.path.isfile(self.paths["settings_file"]):
369 settings = json.load(open(self.paths["settings_file"]))
373 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
375 # make sure settings file is up-to-date
376 for setting in default_settings:
377 if setting not in settings:
378 settings[setting] = default_settings[setting]
381 # make sure tor_socks_address doesn't start with 'tcp:'
382 if settings["tor_socks_address"].startswith("tcp:"):
383 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
386 # make sure the version is current
387 if settings["tbl_version"] != self.tbl_version:
388 settings["tbl_version"] = self.tbl_version
391 self.settings = settings
395 # if settings file is still using old pickle format, convert to json
396 elif os.path.isfile(self.paths["settings_file_pickle"]):
397 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
399 os.remove(self.paths["settings_file_pickle"])
403 self.settings = default_settings
407 def save_settings(self):
408 json.dump(self.settings, open(self.paths["settings_file"], "w"))