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 = [
115 # a list of manually configured language fallback overriding
116 language_overrides = {
120 default_locale = locale.getlocale()[0]
121 if default_locale is None:
122 self.language = "en-US"
124 self.language = default_locale.replace("_", "-")
125 if self.language in language_overrides:
126 self.language = language_overrides[self.language]
127 if self.language not in available_languages:
128 self.language = self.language.split("-")[0]
129 if self.language not in available_languages:
130 for l in available_languages:
131 if l[0:2] == self.language:
133 # if language isn't available, default to english
134 if self.language not in available_languages:
135 self.language = "en-US"
137 # get value of environment variable, if it is not set return the default value
139 def get_env(var_name, default_value):
140 value = os.getenv(var_name)
142 value = default_value
145 # build all relevant paths
146 def build_paths(self, tbb_version=None):
147 homedir = os.getenv("HOME")
149 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
150 if not os.path.exists(homedir):
152 os.mkdir(homedir, 0o700)
155 "error", _("Error creating {0}").format(homedir), [], False
158 tbb_config = "{0}/torbrowser".format(
159 self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
161 tbb_cache = "{0}/torbrowser".format(
162 self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
164 tbb_local = "{0}/torbrowser".format(
165 self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
167 old_tbb_data = "{0}/.torbrowser".format(homedir)
171 if self.architecture == "x86_64":
176 if hasattr(self, "settings") and self.settings["force_en-US"]:
179 language = self.language
181 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
185 self.paths["tarball_url"] = (
186 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
188 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
189 self.paths["tarball_filename"] = tarball_filename
192 self.paths["sig_url"] = (
193 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
195 self.paths["sig_file"] = (
196 tbb_cache + "/download/" + tarball_filename + ".asc"
198 self.paths["sig_filename"] = tarball_filename + ".asc"
202 "config": tbb_config,
206 "old_data_dir": old_tbb_data,
207 "tbl_bin": sys.argv[0],
208 "icon_file": os.path.join(
209 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
211 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
213 "tor_browser_developers": os.path.join(
214 SHARE, "tor-browser-developers.asc"
218 os.path.join(SHARE, "mirrors.txt"),
219 tbb_config + "/mirrors.txt",
221 "download_dir": tbb_cache + "/download",
222 "gnupg_homedir": tbb_local + "/gnupg_homedir",
223 "settings_file": tbb_config + "/settings.json",
224 "settings_file_pickle": tbb_config + "/settings",
225 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
226 "version_check_file": tbb_cache + "/download/release.xml",
228 "changelog": tbb_local
233 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
234 "dir": tbb_local + "/tbb/" + self.architecture,
245 + "/start-tor-browser.desktop",
249 # Add the expected fingerprint for imported keys:
250 self.fingerprints = {
251 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
258 if not os.path.exists(path):
259 os.makedirs(path, 0o700)
262 print(_("Cannot create directory {0}").format(path))
264 if not os.access(path, os.W_OK):
265 print(_("{0} is not writable").format(path))
269 # if gnupg_homedir isn't set up, set it up
270 def init_gnupg(self):
271 if not os.path.exists(self.paths["gnupg_homedir"]):
272 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
273 self.mkdir(self.paths["gnupg_homedir"])
276 def refresh_keyring(self, fingerprint=None):
277 if fingerprint is not None:
278 print("Refreshing local keyring... Missing key: " + fingerprint)
280 print("Refreshing local keyring...")
282 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
283 p = subprocess.Popen(
289 self.paths["gnupg_homedir"],
293 "torbrowser@torproject.org",
295 stderr=subprocess.PIPE,
299 for output in p.stderr.readlines():
300 match = gnupg_import_ok_pattern.match(output)
301 if match and match.group(2) == "IMPORT_OK":
302 fingerprint = str(match.group(4))
303 if match.group(3) == "0":
304 print("Keyring refreshed successfully...")
305 print(" No key updates for key: " + fingerprint)
306 elif match.group(3) == "4":
307 print("Keyring refreshed successfully...")
308 print(" New signatures for key: " + fingerprint)
310 print("Keyring refreshed successfully...")
312 def import_key_and_check_status(self, key):
313 """Import a GnuPG key and check that the operation was successful.
314 :param str key: A string specifying the key's filepath from
317 :returns: ``True`` if the key is now within the keyring (or was
318 previously and hasn't changed). ``False`` otherwise.
320 with gpg.Context() as c:
322 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
325 impkey = self.paths["signing_keys"][key]
327 c.op_import(gpg.Data(file=impkey))
331 result = c.op_import_result()
332 if result and self.fingerprints[key] in result.imports[0].fpr:
338 def import_keys(self):
339 """Import all GnuPG keys.
341 :returns: ``True`` if all keys were successfully imported; ``False``
345 "tor_browser_developers",
347 all_imports_succeeded = True
350 imported = self.import_key_and_check_status(key)
354 "Could not import key with fingerprint: %s."
355 % self.fingerprints[key]
358 all_imports_succeeded = False
360 if not all_imports_succeeded:
361 print(_("Not all keys were imported successfully!"))
363 return all_imports_succeeded
366 def load_mirrors(self):
368 for srcfile in self.paths["mirrors_txt"]:
369 if not os.path.exists(srcfile):
371 for mirror in open(srcfile, "r").readlines():
372 if mirror.strip() not in self.mirrors:
373 self.mirrors.append(mirror.strip())
376 def load_settings(self):
378 "tbl_version": self.tbl_version,
380 "download_over_tor": False,
381 "tor_socks_address": "127.0.0.1:9050",
382 "mirror": self.default_mirror,
383 "force_en-US": False,
386 if os.path.isfile(self.paths["settings_file"]):
387 settings = json.load(open(self.paths["settings_file"]))
391 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
393 # make sure settings file is up-to-date
394 for setting in default_settings:
395 if setting not in settings:
396 settings[setting] = default_settings[setting]
399 # make sure tor_socks_address doesn't start with 'tcp:'
400 if settings["tor_socks_address"].startswith("tcp:"):
401 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
404 # make sure the version is current
405 if settings["tbl_version"] != self.tbl_version:
406 settings["tbl_version"] = self.tbl_version
409 self.settings = settings
413 # if settings file is still using old pickle format, convert to json
414 elif os.path.isfile(self.paths["settings_file_pickle"]):
415 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
417 os.remove(self.paths["settings_file_pickle"])
421 self.settings = default_settings
425 def save_settings(self):
426 json.dump(self.settings, open(self.paths["settings_file"], "w"))