]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Revert "Use correct sys prefix"
[torbrowser-launcher.git] / torbrowser_launcher / common.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2021 Micah Lee <micah@micahflee.com>
6
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
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
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.
27 """
28
29 import os
30 import sys
31 import platform
32 import subprocess
33 import locale
34 import pickle
35 import json
36 import re
37 import gettext
38 import gpg
39
40 SHARE = os.getenv("TBL_SHARE", sys.prefix + "/share") + "/torbrowser-launcher"
41
42 gettext.install("torbrowser-launcher")
43
44 # We're looking for output which:
45 #
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})"
51 )
52
53
54 class Common(object):
55     def __init__(self, tbl_version):
56         self.tbl_version = tbl_version
57
58         # initialize the app
59         self.default_mirror = "https://dist.torproject.org/"
60         self.discover_arch_lang()
61         self.build_paths()
62         for d in self.paths["dirs"]:
63             self.mkdir(self.paths["dirs"][d])
64         self.load_mirrors()
65         self.load_settings()
66         self.mkdir(self.paths["download_dir"])
67         self.mkdir(self.paths["tbb"]["dir"])
68         self.init_gnupg()
69
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"
74
75         # figure out the language
76         available_languages = [
77             "ar",
78             "ca",
79             "cs",
80             "da",
81             "de",
82             "el",
83             "en-US",
84             "es-AR",
85             "es-ES",
86             "fa",
87             "fr",
88             "ga-IE",
89             "he",
90             "hu",
91             "id",
92             "is",
93             "it",
94             "ja",
95             "ka",
96             "ko",
97             "lt",
98             "mk",
99             "ms",
100             "my",
101             "nb-NO",
102             "nl",
103             "pl",
104             "pt-BR",
105             "ro",
106             "ru",
107             "sv-SE",
108             "th",
109             "tr",
110             "vi",
111             "zh-CN",
112             "zh-TW",
113         ]
114
115         # a list of manually configured language fallback overriding
116         language_overrides = {
117             "zh-HK": "zh-TW",
118         }
119
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"
124         else:
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:
133                             self.language = l
134             # if language isn't available, default to english
135             if self.language not in available_languages:
136                 self.language = "en-US"
137
138     # get value of environment variable, if it is not set return the default value
139     @staticmethod
140     def get_env(var_name, default_value):
141         value = os.getenv(var_name)
142         if not value:
143             value = default_value
144         return value
145
146     # build all relevant paths
147     def build_paths(self, tbb_version=None):
148         homedir = os.getenv("HOME")
149         if not homedir:
150             homedir = "/tmp/.torbrowser-" + os.getenv("USER")
151             if not os.path.exists(homedir):
152                 try:
153                     os.mkdir(homedir, 0o700)
154                 except:
155                     self.set_gui(
156                         "error", _("Error creating {0}").format(homedir), [], False
157                     )
158
159         tbb_config = "{0}/torbrowser".format(
160             self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
161         )
162         tbb_cache = "{0}/torbrowser".format(
163             self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
164         )
165         tbb_local = "{0}/torbrowser".format(
166             self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
167         )
168         old_tbb_data = "{0}/.torbrowser".format(homedir)
169
170         if tbb_version:
171             # tarball filename
172             if self.architecture == "x86_64":
173                 arch = "linux64"
174             else:
175                 arch = "linux32"
176
177             if hasattr(self, "settings") and self.settings["force_en-US"]:
178                 language = "en-US"
179             else:
180                 language = self.language
181             tarball_filename = (
182                 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
183             )
184
185             # tarball
186             self.paths["tarball_url"] = (
187                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
188             )
189             self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
190             self.paths["tarball_filename"] = tarball_filename
191
192             # sig
193             self.paths["sig_url"] = (
194                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
195             )
196             self.paths["sig_file"] = (
197                 tbb_cache + "/download/" + tarball_filename + ".asc"
198             )
199             self.paths["sig_filename"] = tarball_filename + ".asc"
200         else:
201             self.paths = {
202                 "dirs": {
203                     "config": tbb_config,
204                     "cache": tbb_cache,
205                     "local": tbb_local,
206                 },
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"
211                 ),
212                 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
213                 "signing_keys": {
214                     "tor_browser_developers": os.path.join(
215                         SHARE, "tor-browser-developers.asc"
216                     )
217                 },
218                 "mirrors_txt": [
219                     os.path.join(SHARE, "mirrors.txt"),
220                     tbb_config + "/mirrors.txt",
221                 ],
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",
228                 "tbb": {
229                     "changelog": tbb_local
230                     + "/tbb/"
231                     + self.architecture
232                     + "/tor-browser_"
233                     + self.language
234                     + "/Browser/TorBrowser/Docs/ChangeLog.txt",
235                     "dir": tbb_local + "/tbb/" + self.architecture,
236                     "dir_tbb": tbb_local
237                     + "/tbb/"
238                     + self.architecture
239                     + "/tor-browser_"
240                     + self.language,
241                     "start": tbb_local
242                     + "/tbb/"
243                     + self.architecture
244                     + "/tor-browser_"
245                     + self.language
246                     + "/start-tor-browser.desktop",
247                 },
248             }
249
250         # Add the expected fingerprint for imported keys:
251         self.fingerprints = {
252             "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
253         }
254
255     # create a directory
256     @staticmethod
257     def mkdir(path):
258         try:
259             if not os.path.exists(path):
260                 os.makedirs(path, 0o700)
261                 return True
262         except:
263             print(_("Cannot create directory {0}").format(path))
264             return False
265         if not os.access(path, os.W_OK):
266             print(_("{0} is not writable").format(path))
267             return False
268         return True
269
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"])
275         self.import_keys()
276
277     def refresh_keyring(self, fingerprint=None):
278         if fingerprint is not None:
279             print("Refreshing local keyring... Missing key: " + fingerprint)
280         else:
281             print("Refreshing local keyring...")
282
283         # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
284         p = subprocess.Popen(
285             [
286                 "gpg",
287                 "--status-fd",
288                 "2",
289                 "--homedir",
290                 self.paths["gnupg_homedir"],
291                 "--auto-key-locate",
292                 "nodefault,wkd",
293                 "--locate-keys",
294                 "torbrowser@torproject.org",
295             ],
296             stderr=subprocess.PIPE,
297         )
298         p.wait()
299
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)
310                 else:
311                     print("Keyring refreshed successfully...")
312
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
316             ``Common.paths``
317         :rtype: bool
318         :returns: ``True`` if the key is now within the keyring (or was
319             previously and hasn't changed). ``False`` otherwise.
320         """
321         with gpg.Context() as c:
322             c.set_engine_info(
323                 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
324             )
325
326             impkey = self.paths["signing_keys"][key]
327             try:
328                 c.op_import(gpg.Data(file=impkey))
329             except:
330                 return False
331             else:
332                 result = c.op_import_result()
333                 if result and self.fingerprints[key] in result.imports[0].fpr:
334                     return True
335                 else:
336                     return False
337
338     # import gpg keys
339     def import_keys(self):
340         """Import all GnuPG keys.
341         :rtype: bool
342         :returns: ``True`` if all keys were successfully imported; ``False``
343             otherwise.
344         """
345         keys = [
346             "tor_browser_developers",
347         ]
348         all_imports_succeeded = True
349
350         for key in keys:
351             imported = self.import_key_and_check_status(key)
352             if not imported:
353                 print(
354                     _(
355                         "Could not import key with fingerprint: %s."
356                         % self.fingerprints[key]
357                     )
358                 )
359                 all_imports_succeeded = False
360
361         if not all_imports_succeeded:
362             print(_("Not all keys were imported successfully!"))
363
364         return all_imports_succeeded
365
366     # load mirrors
367     def load_mirrors(self):
368         self.mirrors = []
369         for srcfile in self.paths["mirrors_txt"]:
370             if not os.path.exists(srcfile):
371                 continue
372             for mirror in open(srcfile, "r").readlines():
373                 if mirror.strip() not in self.mirrors:
374                     self.mirrors.append(mirror.strip())
375
376     # load settings
377     def load_settings(self):
378         default_settings = {
379             "tbl_version": self.tbl_version,
380             "installed": False,
381             "download_over_tor": False,
382             "tor_socks_address": "127.0.0.1:9050",
383             "mirror": self.default_mirror,
384             "force_en-US": False,
385         }
386
387         if os.path.isfile(self.paths["settings_file"]):
388             settings = json.load(open(self.paths["settings_file"]))
389             resave = False
390
391             # detect installed
392             settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
393
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]
398                     resave = True
399
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:]
403                 resave = True
404
405             # make sure the version is current
406             if settings["tbl_version"] != self.tbl_version:
407                 settings["tbl_version"] = self.tbl_version
408                 resave = True
409
410             self.settings = settings
411             if resave:
412                 self.save_settings()
413
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"]))
417             self.save_settings()
418             os.remove(self.paths["settings_file_pickle"])
419             self.load_settings()
420
421         else:
422             self.settings = default_settings
423             self.save_settings()
424
425     # save settings
426     def save_settings(self):
427         json.dump(self.settings, open(self.paths["settings_file"], "w"))
428         return True