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 self.mkdir(self.paths["download_dir"])
68 self.mkdir(self.paths["tbb"]["dir"])
71 # discover the architecture and language
72 def discover_arch_lang(self):
73 # figure out the architecture
74 self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
76 # figure out the language
77 available_languages = [
116 # a list of manually configured language fallback overriding
117 language_overrides = {
121 locale.setlocale(locale.LC_MESSAGES, "")
122 default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
123 if default_locale is None:
124 self.language = "en-US"
126 self.language = default_locale.replace("_", "-")
127 if self.language in language_overrides:
128 self.language = language_overrides[self.language]
129 if self.language not in available_languages:
130 self.language = self.language.split("-")[0]
131 if self.language not in available_languages:
132 for l in available_languages:
133 if l[0:2] == self.language:
135 # if language isn't available, default to english
136 if self.language not in available_languages:
137 self.language = "en-US"
139 # get value of environment variable, if it is not set return the default value
141 def get_env(var_name, default_value):
142 value = os.getenv(var_name)
144 value = default_value
147 # build all relevant paths
148 def build_paths(self, tbb_version=None):
149 homedir = os.getenv("HOME")
151 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
152 if not os.path.exists(homedir):
154 os.mkdir(homedir, 0o700)
157 "error", _("Error creating {0}").format(homedir), [], False
160 tbb_config = "{0}/torbrowser".format(
161 self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
163 tbb_cache = "{0}/torbrowser".format(
164 self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
166 tbb_local = "{0}/torbrowser".format(
167 self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
169 old_tbb_data = "{0}/.torbrowser".format(homedir)
173 if self.architecture == "x86_64":
178 if hasattr(self, "settings") and self.settings["force_en-US"]:
181 language = self.language
183 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
187 self.paths["tarball_url"] = (
188 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
190 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
191 self.paths["tarball_filename"] = tarball_filename
194 self.paths["sig_url"] = (
195 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
197 self.paths["sig_file"] = (
198 tbb_cache + "/download/" + tarball_filename + ".asc"
200 self.paths["sig_filename"] = tarball_filename + ".asc"
204 "config": tbb_config,
208 "old_data_dir": old_tbb_data,
209 "tbl_bin": sys.argv[0],
210 "icon_file": os.path.join(
211 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
213 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
215 "tor_browser_developers": os.path.join(
216 SHARE, "tor-browser-developers.asc"
218 "wkd_tmp": os.path.join(tbb_cache, "torbrowser.gpg")
221 os.path.join(SHARE, "mirrors.txt"),
222 tbb_config + "/mirrors.txt",
224 "download_dir": tbb_cache + "/download",
225 "gnupg_homedir": tbb_local + "/gnupg_homedir",
226 "settings_file": tbb_config + "/settings.json",
227 "settings_file_pickle": tbb_config + "/settings",
228 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
229 "version_check_file": tbb_cache + "/download/release.xml",
231 "changelog": tbb_local
236 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
237 "dir": tbb_local + "/tbb/" + self.architecture,
248 + "/start-tor-browser.desktop",
252 # Add the expected fingerprint for imported keys:
253 tor_browser_developers_fingerprint = "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
254 self.fingerprints = {
255 "tor_browser_developers": tor_browser_developers_fingerprint,
256 "wkd_tmp": tor_browser_developers_fingerprint,
263 if not os.path.exists(path):
264 os.makedirs(path, 0o700)
267 print(_("Cannot create directory {0}").format(path))
269 if not os.access(path, os.W_OK):
270 print(_("{0} is not writable").format(path))
274 # if gnupg_homedir isn't set up, set it up
275 def init_gnupg(self):
276 if not os.path.exists(self.paths["gnupg_homedir"]):
277 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
278 self.mkdir(self.paths["gnupg_homedir"])
282 # Use tor socks5 proxy, if enabled
283 if self.settings["download_over_tor"]:
284 socks5_address = "socks5h://{}".format(self.settings["tor_socks_address"])
285 return {"https": socks5_address, "http": socks5_address}
289 def refresh_keyring(self):
290 print("Downloading latest Tor Browser signing key...")
292 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
293 # Sometimes GPG throws errors, so comment this out and download it directly
294 # p = subprocess.Popen(
300 # self.paths["gnupg_homedir"],
301 # "--auto-key-locate",
304 # "torbrowser@torproject.org",
306 # stderr=subprocess.PIPE,
310 # Download the key from WKD directly
312 "https://torproject.org/.well-known/openpgpkey/hu/kounek7zrdx745qydx6p59t9mqjpuhdf?l=torbrowser",
313 proxies=self.proxies(),
315 if r.status_code != 200:
316 print(f"Error fetching key, status code = {r.status_code}")
318 with open(self.paths["signing_keys"]["wkd_tmp"], "wb") as f:
321 if self.import_key_and_check_status("wkd_tmp"):
322 print("Key imported successfully")
324 print("Key failed to import")
326 def import_key_and_check_status(self, key):
327 """Import a GnuPG key and check that the operation was successful.
328 :param str key: A string specifying the key's filepath from
331 :returns: ``True`` if the key is now within the keyring (or was
332 previously and hasn't changed). ``False`` otherwise.
334 with gpg.Context() as c:
336 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
339 impkey = self.paths["signing_keys"][key]
341 c.op_import(gpg.Data(file=impkey))
345 result = c.op_import_result()
346 if result and self.fingerprints[key] in result.imports[0].fpr:
352 def import_keys(self):
353 """Import all GnuPG keys.
355 :returns: ``True`` if all keys were successfully imported; ``False``
359 "tor_browser_developers",
361 all_imports_succeeded = True
364 imported = self.import_key_and_check_status(key)
368 "Could not import key with fingerprint: %s."
369 % self.fingerprints[key]
372 all_imports_succeeded = False
374 if not all_imports_succeeded:
375 print(_("Not all keys were imported successfully!"))
377 return all_imports_succeeded
380 def load_mirrors(self):
382 for srcfile in self.paths["mirrors_txt"]:
383 if not os.path.exists(srcfile):
385 for mirror in open(srcfile, "r").readlines():
386 if mirror.strip() not in self.mirrors:
387 self.mirrors.append(mirror.strip())
390 def load_settings(self):
392 "tbl_version": self.tbl_version,
394 "download_over_tor": False,
395 "tor_socks_address": "127.0.0.1:9050",
396 "mirror": self.default_mirror,
397 "force_en-US": False,
400 if os.path.isfile(self.paths["settings_file"]):
401 settings = json.load(open(self.paths["settings_file"]))
405 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
407 # make sure settings file is up-to-date
408 for setting in default_settings:
409 if setting not in settings:
410 settings[setting] = default_settings[setting]
413 # make sure tor_socks_address doesn't start with 'tcp:'
414 if settings["tor_socks_address"].startswith("tcp:"):
415 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
418 # make sure the version is current
419 if settings["tbl_version"] != self.tbl_version:
420 settings["tbl_version"] = self.tbl_version
423 self.settings = settings
427 # if settings file is still using old pickle format, convert to json
428 elif os.path.isfile(self.paths["settings_file_pickle"]):
429 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
431 os.remove(self.paths["settings_file_pickle"])
435 self.settings = default_settings
439 def save_settings(self):
440 json.dump(self.settings, open(self.paths["settings_file"], "w"))