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", "/app" + "/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 locale.setlocale(locale.LC_MESSAGES, "")
121 default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
122 if default_locale is None:
123 self.language = "en-US"
125 self.language = default_locale.replace("_", "-")
126 if self.language in language_overrides:
127 self.language = language_overrides[self.language]
128 if self.language not in available_languages:
129 self.language = self.language.split("-")[0]
130 if self.language not in available_languages:
131 for l in available_languages:
132 if l[0:2] == self.language:
134 # if language isn't available, default to english
135 if self.language not in available_languages:
136 self.language = "en-US"
138 # get value of environment variable, if it is not set return the default value
140 def get_env(var_name, default_value):
141 value = os.getenv(var_name)
143 value = default_value
146 # build all relevant paths
147 def build_paths(self, tbb_version=None):
148 homedir = os.getenv("HOME")
150 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
151 if not os.path.exists(homedir):
153 os.mkdir(homedir, 0o700)
156 "error", _("Error creating {0}").format(homedir), [], False
159 tbb_config = "{0}/torbrowser".format(
160 self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
162 tbb_cache = "{0}/torbrowser".format(
163 self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
165 tbb_local = "{0}/torbrowser".format(
166 self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
168 old_tbb_data = "{0}/.torbrowser".format(homedir)
172 if self.architecture == "x86_64":
177 if hasattr(self, "settings") and self.settings["force_en-US"]:
180 language = self.language
182 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
186 self.paths["tarball_url"] = (
187 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
189 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
190 self.paths["tarball_filename"] = tarball_filename
193 self.paths["sig_url"] = (
194 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
196 self.paths["sig_file"] = (
197 tbb_cache + "/download/" + tarball_filename + ".asc"
199 self.paths["sig_filename"] = tarball_filename + ".asc"
203 "config": tbb_config,
207 "old_data_dir": old_tbb_data,
208 "tbl_bin": sys.argv[0],
209 "icon_file": os.path.join(
210 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
212 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
214 "tor_browser_developers": os.path.join(
215 SHARE, "tor-browser-developers.asc"
219 os.path.join(SHARE, "mirrors.txt"),
220 tbb_config + "/mirrors.txt",
222 "download_dir": tbb_cache + "/download",
223 "gnupg_homedir": tbb_local + "/gnupg_homedir",
224 "settings_file": tbb_config + "/settings.json",
225 "settings_file_pickle": tbb_config + "/settings",
226 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
227 "version_check_file": tbb_cache + "/download/release.xml",
229 "changelog": tbb_local
234 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
235 "dir": tbb_local + "/tbb/" + self.architecture,
246 + "/start-tor-browser.desktop",
250 # Add the expected fingerprint for imported keys:
251 self.fingerprints = {
252 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
259 if not os.path.exists(path):
260 os.makedirs(path, 0o700)
263 print(_("Cannot create directory {0}").format(path))
265 if not os.access(path, os.W_OK):
266 print(_("{0} is not writable").format(path))
270 # if gnupg_homedir isn't set up, set it up
271 def init_gnupg(self):
272 if not os.path.exists(self.paths["gnupg_homedir"]):
273 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
274 self.mkdir(self.paths["gnupg_homedir"])
277 def refresh_keyring(self, fingerprint=None):
278 if fingerprint is not None:
279 print("Refreshing local keyring... Missing key: " + fingerprint)
281 print("Refreshing local keyring...")
283 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
284 p = subprocess.Popen(
290 self.paths["gnupg_homedir"],
294 "torbrowser@torproject.org",
296 stderr=subprocess.PIPE,
300 for output in p.stderr.readlines():
301 match = gnupg_import_ok_pattern.match(output)
302 if match and match.group(2) == "IMPORT_OK":
303 fingerprint = str(match.group(4))
304 if match.group(3) == "0":
305 print("Keyring refreshed successfully...")
306 print(" No key updates for key: " + fingerprint)
307 elif match.group(3) == "4":
308 print("Keyring refreshed successfully...")
309 print(" New signatures for key: " + fingerprint)
311 print("Keyring refreshed successfully...")
313 def import_key_and_check_status(self, key):
314 """Import a GnuPG key and check that the operation was successful.
315 :param str key: A string specifying the key's filepath from
318 :returns: ``True`` if the key is now within the keyring (or was
319 previously and hasn't changed). ``False`` otherwise.
321 with gpg.Context() as c:
323 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
326 impkey = self.paths["signing_keys"][key]
328 c.op_import(gpg.Data(file=impkey))
332 result = c.op_import_result()
333 if result and self.fingerprints[key] in result.imports[0].fpr:
339 def import_keys(self):
340 """Import all GnuPG keys.
342 :returns: ``True`` if all keys were successfully imported; ``False``
346 "tor_browser_developers",
348 all_imports_succeeded = True
351 imported = self.import_key_and_check_status(key)
355 "Could not import key with fingerprint: %s."
356 % self.fingerprints[key]
359 all_imports_succeeded = False
361 if not all_imports_succeeded:
362 print(_("Not all keys were imported successfully!"))
364 return all_imports_succeeded
367 def load_mirrors(self):
369 for srcfile in self.paths["mirrors_txt"]:
370 if not os.path.exists(srcfile):
372 for mirror in open(srcfile, "r").readlines():
373 if mirror.strip() not in self.mirrors:
374 self.mirrors.append(mirror.strip())
377 def load_settings(self):
379 "tbl_version": self.tbl_version,
381 "download_over_tor": False,
382 "tor_socks_address": "127.0.0.1:9050",
383 "mirror": self.default_mirror,
384 "force_en-US": False,
387 if os.path.isfile(self.paths["settings_file"]):
388 settings = json.load(open(self.paths["settings_file"]))
392 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
394 # make sure settings file is up-to-date
395 for setting in default_settings:
396 if setting not in settings:
397 settings[setting] = default_settings[setting]
400 # make sure tor_socks_address doesn't start with 'tcp:'
401 if settings["tor_socks_address"].startswith("tcp:"):
402 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
405 # make sure the version is current
406 if settings["tbl_version"] != self.tbl_version:
407 settings["tbl_version"] = self.tbl_version
410 self.settings = settings
414 # if settings file is still using old pickle format, convert to json
415 elif os.path.isfile(self.paths["settings_file_pickle"]):
416 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
418 os.remove(self.paths["settings_file_pickle"])
422 self.settings = default_settings
426 def save_settings(self):
427 json.dump(self.settings, open(self.paths["settings_file"], "w"))