3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2017 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 = [
103 default_locale = locale.getlocale()[0]
104 if default_locale is None:
105 self.language = "en-US"
107 self.language = default_locale.replace("_", "-")
108 if self.language not in available_languages:
109 self.language = self.language.split("-")[0]
110 if self.language not in available_languages:
111 for l in available_languages:
112 if l[0:2] == self.language:
114 # if language isn't available, default to english
115 if self.language not in available_languages:
116 self.language = "en-US"
118 # get value of environment variable, if it is not set return the default value
120 def get_env(var_name, default_value):
121 value = os.getenv(var_name)
123 value = default_value
126 # build all relevant paths
127 def build_paths(self, tbb_version=None):
128 homedir = os.getenv("HOME")
130 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
131 if not os.path.exists(homedir):
133 os.mkdir(homedir, 0o700)
136 "error", _("Error creating {0}").format(homedir), [], False
139 tbb_config = '{0}/torbrowser'.format(self.get_env('XDG_CONFIG_HOME', '{0}/.config'.format(homedir)))
140 tbb_cache = '{0}/torbrowser'.format(self.get_env('XDG_CACHE_HOME', '{0}/.cache'.format(homedir)))
141 tbb_local = '{0}/torbrowser'.format(self.get_env('XDG_DATA_HOME', '{0}/.local/share'.format(homedir)))
142 old_tbb_data = '{0}/.torbrowser'.format(homedir)
146 if self.architecture == "x86_64":
151 if hasattr(self, "settings") and self.settings["force_en-US"]:
154 language = self.language
156 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
160 self.paths["tarball_url"] = (
161 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
163 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
164 self.paths["tarball_filename"] = tarball_filename
167 self.paths["sig_url"] = (
168 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
170 self.paths["sig_file"] = (
171 tbb_cache + "/download/" + tarball_filename + ".asc"
173 self.paths["sig_filename"] = tarball_filename + ".asc"
176 "dirs": {"config": tbb_config, "cache": tbb_cache, "local": tbb_local,},
177 "old_data_dir": old_tbb_data,
178 "tbl_bin": sys.argv[0],
179 "icon_file": os.path.join(
180 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
182 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
184 "tor_browser_developers": os.path.join(
185 SHARE, "tor-browser-developers.asc"
189 os.path.join(SHARE, "mirrors.txt"),
190 tbb_config + "/mirrors.txt",
192 "download_dir": tbb_cache + "/download",
193 "gnupg_homedir": tbb_local + "/gnupg_homedir",
194 "settings_file": tbb_config + "/settings.json",
195 "settings_file_pickle": tbb_config + "/settings",
196 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
197 "version_check_file": tbb_cache + "/download/release.xml",
199 "changelog": tbb_local
204 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
205 "dir": tbb_local + "/tbb/" + self.architecture,
216 + "/start-tor-browser.desktop",
220 # Add the expected fingerprint for imported keys:
221 self.fingerprints = {
222 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
229 if not os.path.exists(path):
230 os.makedirs(path, 0o700)
233 print(_("Cannot create directory {0}").format(path))
235 if not os.access(path, os.W_OK):
236 print(_("{0} is not writable").format(path))
240 # if gnupg_homedir isn't set up, set it up
241 def init_gnupg(self):
242 if not os.path.exists(self.paths["gnupg_homedir"]):
243 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
244 self.mkdir(self.paths["gnupg_homedir"])
247 def refresh_keyring(self, fingerprint=None):
248 if fingerprint is not None:
249 print("Refreshing local keyring... Missing key: " + fingerprint)
251 print("Refreshing local keyring...")
253 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
254 p = subprocess.Popen(
260 self.paths["gnupg_homedir"],
264 "torbrowser@torproject.org",
266 stderr=subprocess.PIPE,
270 for output in p.stderr.readlines():
271 match = gnupg_import_ok_pattern.match(output)
272 if match and match.group(2) == "IMPORT_OK":
273 fingerprint = str(match.group(4))
274 if match.group(3) == "0":
275 print("Keyring refreshed successfully...")
276 print(" No key updates for key: " + fingerprint)
277 elif match.group(3) == "4":
278 print("Keyring refreshed successfully...")
279 print(" New signatures for key: " + fingerprint)
281 print("Keyring refreshed successfully...")
283 def import_key_and_check_status(self, key):
284 """Import a GnuPG key and check that the operation was successful.
285 :param str key: A string specifying the key's filepath from
288 :returns: ``True`` if the key is now within the keyring (or was
289 previously and hasn't changed). ``False`` otherwise.
291 with gpg.Context() as c:
293 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
296 impkey = self.paths["signing_keys"][key]
298 c.op_import(gpg.Data(file=impkey))
302 result = c.op_import_result()
303 if result and self.fingerprints[key] in result.imports[0].fpr:
309 def import_keys(self):
310 """Import all GnuPG keys.
312 :returns: ``True`` if all keys were successfully imported; ``False``
316 "tor_browser_developers",
318 all_imports_succeeded = True
321 imported = self.import_key_and_check_status(key)
325 "Could not import key with fingerprint: %s."
326 % self.fingerprints[key]
329 all_imports_succeeded = False
331 if not all_imports_succeeded:
332 print(_("Not all keys were imported successfully!"))
334 return all_imports_succeeded
337 def load_mirrors(self):
339 for srcfile in self.paths["mirrors_txt"]:
340 if not os.path.exists(srcfile):
342 for mirror in open(srcfile, "r").readlines():
343 if mirror.strip() not in self.mirrors:
344 self.mirrors.append(mirror.strip())
347 def load_settings(self):
349 "tbl_version": self.tbl_version,
351 "download_over_tor": False,
352 "tor_socks_address": "127.0.0.1:9050",
353 "mirror": self.default_mirror,
354 "force_en-US": False,
357 if os.path.isfile(self.paths["settings_file"]):
358 settings = json.load(open(self.paths["settings_file"]))
362 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
364 # make sure settings file is up-to-date
365 for setting in default_settings:
366 if setting not in settings:
367 settings[setting] = default_settings[setting]
370 # make sure tor_socks_address doesn't start with 'tcp:'
371 if settings["tor_socks_address"].startswith("tcp:"):
372 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
375 # make sure the version is current
376 if settings["tbl_version"] != self.tbl_version:
377 settings["tbl_version"] = self.tbl_version
380 self.settings = settings
384 # if settings file is still using old pickle format, convert to json
385 elif os.path.isfile(self.paths["settings_file_pickle"]):
386 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
388 os.remove(self.paths["settings_file_pickle"])
392 self.settings = default_settings
396 def save_settings(self):
397 json.dump(self.settings, open(self.paths["settings_file"], "w"))