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 # build all relevant paths
119 def build_paths(self, tbb_version=None):
120 homedir = os.getenv("HOME")
122 homedir = "/tmp/.torbrowser-" + os.getenv("USER")
123 if not os.path.exists(homedir):
125 os.mkdir(homedir, 0o700)
128 "error", _("Error creating {0}").format(homedir), [], False
130 if not os.access(homedir, os.W_OK):
131 self.set_gui("error", _("{0} is not writable").format(homedir), [], False)
133 tbb_config = "{0}/.config/torbrowser".format(homedir)
134 tbb_cache = "{0}/.cache/torbrowser".format(homedir)
135 tbb_local = "{0}/.local/share/torbrowser".format(homedir)
136 old_tbb_data = "{0}/.torbrowser".format(homedir)
140 if self.architecture == "x86_64":
145 if hasattr(self, "settings") and self.settings["force_en-US"]:
148 language = self.language
150 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
154 self.paths["tarball_url"] = (
155 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
157 self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
158 self.paths["tarball_filename"] = tarball_filename
161 self.paths["sig_url"] = (
162 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
164 self.paths["sig_file"] = (
165 tbb_cache + "/download/" + tarball_filename + ".asc"
167 self.paths["sig_filename"] = tarball_filename + ".asc"
170 "dirs": {"config": tbb_config, "cache": tbb_cache, "local": tbb_local,},
171 "old_data_dir": old_tbb_data,
172 "tbl_bin": sys.argv[0],
173 "icon_file": os.path.join(
174 os.path.dirname(SHARE), "pixmaps/torbrowser.png"
176 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
178 "tor_browser_developers": os.path.join(
179 SHARE, "tor-browser-developers.asc"
183 os.path.join(SHARE, "mirrors.txt"),
184 tbb_config + "/mirrors.txt",
186 "download_dir": tbb_cache + "/download",
187 "gnupg_homedir": tbb_local + "/gnupg_homedir",
188 "settings_file": tbb_config + "/settings.json",
189 "settings_file_pickle": tbb_config + "/settings",
190 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
191 "version_check_file": tbb_cache + "/download/release.xml",
193 "changelog": tbb_local
198 + "/Browser/TorBrowser/Docs/ChangeLog.txt",
199 "dir": tbb_local + "/tbb/" + self.architecture,
210 + "/start-tor-browser.desktop",
214 # Add the expected fingerprint for imported keys:
215 self.fingerprints = {
216 "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
223 if not os.path.exists(path):
224 os.makedirs(path, 0o700)
227 print(_("Cannot create directory {0}").format(path))
229 if not os.access(path, os.W_OK):
230 print(_("{0} is not writable").format(path))
234 # if gnupg_homedir isn't set up, set it up
235 def init_gnupg(self):
236 if not os.path.exists(self.paths["gnupg_homedir"]):
237 print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
238 self.mkdir(self.paths["gnupg_homedir"])
241 def refresh_keyring(self, fingerprint=None):
242 if fingerprint is not None:
243 print("Refreshing local keyring... Missing key: " + fingerprint)
245 print("Refreshing local keyring...")
247 # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
248 p = subprocess.Popen(
254 self.paths["gnupg_homedir"],
258 "torbrowser@torproject.org",
260 stderr=subprocess.PIPE,
264 for output in p.stderr.readlines():
265 match = gnupg_import_ok_pattern.match(output)
266 if match and match.group(2) == "IMPORT_OK":
267 fingerprint = str(match.group(4))
268 if match.group(3) == "0":
269 print("Keyring refreshed successfully...")
270 print(" No key updates for key: " + fingerprint)
271 elif match.group(3) == "4":
272 print("Keyring refreshed successfully...")
273 print(" New signatures for key: " + fingerprint)
275 print("Keyring refreshed successfully...")
277 def import_key_and_check_status(self, key):
278 """Import a GnuPG key and check that the operation was successful.
279 :param str key: A string specifying the key's filepath from
282 :returns: ``True`` if the key is now within the keyring (or was
283 previously and hasn't changed). ``False`` otherwise.
285 with gpg.Context() as c:
287 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
290 impkey = self.paths["signing_keys"][key]
292 c.op_import(gpg.Data(file=impkey))
296 result = c.op_import_result()
297 if result and self.fingerprints[key] in result.imports[0].fpr:
303 def import_keys(self):
304 """Import all GnuPG keys.
306 :returns: ``True`` if all keys were successfully imported; ``False``
310 "tor_browser_developers",
312 all_imports_succeeded = True
315 imported = self.import_key_and_check_status(key)
319 "Could not import key with fingerprint: %s."
320 % self.fingerprints[key]
323 all_imports_succeeded = False
325 if not all_imports_succeeded:
326 print(_("Not all keys were imported successfully!"))
328 return all_imports_succeeded
331 def load_mirrors(self):
333 for srcfile in self.paths["mirrors_txt"]:
334 if not os.path.exists(srcfile):
336 for mirror in open(srcfile, "r").readlines():
337 if mirror.strip() not in self.mirrors:
338 self.mirrors.append(mirror.strip())
341 def load_settings(self):
343 "tbl_version": self.tbl_version,
345 "download_over_tor": False,
346 "tor_socks_address": "127.0.0.1:9050",
347 "mirror": self.default_mirror,
348 "force_en-US": False,
351 if os.path.isfile(self.paths["settings_file"]):
352 settings = json.load(open(self.paths["settings_file"]))
356 settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
358 # make sure settings file is up-to-date
359 for setting in default_settings:
360 if setting not in settings:
361 settings[setting] = default_settings[setting]
364 # make sure tor_socks_address doesn't start with 'tcp:'
365 if settings["tor_socks_address"].startswith("tcp:"):
366 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
369 # make sure the version is current
370 if settings["tbl_version"] != self.tbl_version:
371 settings["tbl_version"] = self.tbl_version
374 self.settings = settings
378 # if settings file is still using old pickle format, convert to json
379 elif os.path.isfile(self.paths["settings_file_pickle"]):
380 self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
382 os.remove(self.paths["settings_file_pickle"])
386 self.settings = default_settings
390 def save_settings(self):
391 json.dump(self.settings, open(self.paths["settings_file"], "w"))