]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Rebuild paths in case force_en-US was set
[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         # some settings require a path rebuild, like force_en-US
67         self.build_paths()
68         self.mkdir(self.paths["download_dir"])
69         self.mkdir(self.paths["tbb"]["dir"])
70         self.init_gnupg()
71
72     # discover the architecture and language
73     def discover_arch_lang(self):
74         # figure out the architecture
75         self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
76
77         # figure out the language
78         available_languages = [
79             "ar",
80             "ca",
81             "cs",
82             "da",
83             "de",
84             "el",
85             "en-US",
86             "es-AR",
87             "es-ES",
88             "fa",
89             "fr",
90             "ga-IE",
91             "he",
92             "hu",
93             "id",
94             "is",
95             "it",
96             "ja",
97             "ka",
98             "ko",
99             "lt",
100             "mk",
101             "ms",
102             "my",
103             "nb-NO",
104             "nl",
105             "pl",
106             "pt-BR",
107             "ro",
108             "ru",
109             "sv-SE",
110             "th",
111             "tr",
112             "vi",
113             "zh-CN",
114             "zh-TW",
115         ]
116
117         # a list of manually configured language fallback overriding
118         language_overrides = {
119             "zh-HK": "zh-TW",
120         }
121
122         locale.setlocale(locale.LC_MESSAGES, "")
123         default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
124         if default_locale is None:
125             self.language = "en-US"
126         else:
127             self.language = default_locale.replace("_", "-")
128             if self.language in language_overrides:
129                 self.language = language_overrides[self.language]
130             if self.language not in available_languages:
131                 self.language = self.language.split("-")[0]
132                 if self.language not in available_languages:
133                     for l in available_languages:
134                         if l[0:2] == self.language:
135                             self.language = l
136             # if language isn't available, default to english
137             if self.language not in available_languages:
138                 self.language = "en-US"
139
140     # get value of environment variable, if it is not set return the default value
141     @staticmethod
142     def get_env(var_name, default_value):
143         value = os.getenv(var_name)
144         if not value:
145             value = default_value
146         return value
147
148     # build all relevant paths
149     def build_paths(self, tbb_version=None):
150         homedir = os.getenv("HOME")
151         if not homedir:
152             homedir = "/tmp/.torbrowser-" + os.getenv("USER")
153             if not os.path.exists(homedir):
154                 try:
155                     os.mkdir(homedir, 0o700)
156                 except:
157                     self.set_gui(
158                         "error", _("Error creating {0}").format(homedir), [], False
159                     )
160
161         tbb_config = "{0}/torbrowser".format(
162             self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
163         )
164         tbb_cache = "{0}/torbrowser".format(
165             self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
166         )
167         tbb_local = "{0}/torbrowser".format(
168             self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
169         )
170         old_tbb_data = "{0}/.torbrowser".format(homedir)
171
172         if hasattr(self, "settings") and self.settings["force_en-US"]:
173             language = "en-US"
174         else:
175             language = self.language
176
177         if tbb_version:
178             # tarball filename
179             if self.architecture == "x86_64":
180                 arch = "linux64"
181             else:
182                 arch = "linux32"
183
184             tarball_filename = (
185                 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
186             )
187
188             # tarball
189             self.paths["tarball_url"] = (
190                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
191             )
192             self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
193             self.paths["tarball_filename"] = tarball_filename
194
195             # sig
196             self.paths["sig_url"] = (
197                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
198             )
199             self.paths["sig_file"] = (
200                 tbb_cache + "/download/" + tarball_filename + ".asc"
201             )
202             self.paths["sig_filename"] = tarball_filename + ".asc"
203         else:
204             self.paths = {
205                 "dirs": {
206                     "config": tbb_config,
207                     "cache": tbb_cache,
208                     "local": tbb_local,
209                 },
210                 "old_data_dir": old_tbb_data,
211                 "tbl_bin": sys.argv[0],
212                 "icon_file": os.path.join(
213                     os.path.dirname(SHARE), "pixmaps/torbrowser.png"
214                 ),
215                 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
216                 "signing_keys": {
217                     "tor_browser_developers": os.path.join(
218                         SHARE, "tor-browser-developers.asc"
219                     )
220                 },
221                 "mirrors_txt": [
222                     os.path.join(SHARE, "mirrors.txt"),
223                     tbb_config + "/mirrors.txt",
224                 ],
225                 "download_dir": tbb_cache + "/download",
226                 "gnupg_homedir": tbb_local + "/gnupg_homedir",
227                 "settings_file": tbb_config + "/settings.json",
228                 "settings_file_pickle": tbb_config + "/settings",
229                 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
230                 "version_check_file": tbb_cache + "/download/release.xml",
231                 "tbb": {
232                     "changelog": tbb_local
233                     + "/tbb/"
234                     + self.architecture
235                     + "/tor-browser_"
236                     + language
237                     + "/Browser/TorBrowser/Docs/ChangeLog.txt",
238                     "dir": tbb_local + "/tbb/" + self.architecture,
239                     "dir_tbb": tbb_local
240                     + "/tbb/"
241                     + self.architecture
242                     + "/tor-browser_"
243                     + language,
244                     "start": tbb_local
245                     + "/tbb/"
246                     + self.architecture
247                     + "/tor-browser_"
248                     + language
249                     + "/start-tor-browser.desktop",
250                 },
251             }
252
253         # Add the expected fingerprint for imported keys:
254         self.fingerprints = {
255             "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
256         }
257
258     # create a directory
259     @staticmethod
260     def mkdir(path):
261         try:
262             if not os.path.exists(path):
263                 os.makedirs(path, 0o700)
264                 return True
265         except:
266             print(_("Cannot create directory {0}").format(path))
267             return False
268         if not os.access(path, os.W_OK):
269             print(_("{0} is not writable").format(path))
270             return False
271         return True
272
273     # if gnupg_homedir isn't set up, set it up
274     def init_gnupg(self):
275         if not os.path.exists(self.paths["gnupg_homedir"]):
276             print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
277             self.mkdir(self.paths["gnupg_homedir"])
278         self.import_keys()
279
280     def refresh_keyring(self, fingerprint=None):
281         if fingerprint is not None:
282             print("Refreshing local keyring... Missing key: " + fingerprint)
283         else:
284             print("Refreshing local keyring...")
285
286         # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
287         p = subprocess.Popen(
288             [
289                 "gpg",
290                 "--status-fd",
291                 "2",
292                 "--homedir",
293                 self.paths["gnupg_homedir"],
294                 "--auto-key-locate",
295                 "nodefault,wkd",
296                 "--locate-keys",
297                 "torbrowser@torproject.org",
298             ],
299             stderr=subprocess.PIPE,
300         )
301         p.wait()
302
303         for output in p.stderr.readlines():
304             match = gnupg_import_ok_pattern.match(output)
305             if match and match.group(2) == "IMPORT_OK":
306                 fingerprint = str(match.group(4))
307                 if match.group(3) == "0":
308                     print("Keyring refreshed successfully...")
309                     print("  No key updates for key: " + fingerprint)
310                 elif match.group(3) == "4":
311                     print("Keyring refreshed successfully...")
312                     print("  New signatures for key: " + fingerprint)
313                 else:
314                     print("Keyring refreshed successfully...")
315
316     def import_key_and_check_status(self, key):
317         """Import a GnuPG key and check that the operation was successful.
318         :param str key: A string specifying the key's filepath from
319             ``Common.paths``
320         :rtype: bool
321         :returns: ``True`` if the key is now within the keyring (or was
322             previously and hasn't changed). ``False`` otherwise.
323         """
324         with gpg.Context() as c:
325             c.set_engine_info(
326                 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
327             )
328
329             impkey = self.paths["signing_keys"][key]
330             try:
331                 c.op_import(gpg.Data(file=impkey))
332             except:
333                 return False
334             else:
335                 result = c.op_import_result()
336                 if result and self.fingerprints[key] in result.imports[0].fpr:
337                     return True
338                 else:
339                     return False
340
341     # import gpg keys
342     def import_keys(self):
343         """Import all GnuPG keys.
344         :rtype: bool
345         :returns: ``True`` if all keys were successfully imported; ``False``
346             otherwise.
347         """
348         keys = [
349             "tor_browser_developers",
350         ]
351         all_imports_succeeded = True
352
353         for key in keys:
354             imported = self.import_key_and_check_status(key)
355             if not imported:
356                 print(
357                     _(
358                         "Could not import key with fingerprint: %s."
359                         % self.fingerprints[key]
360                     )
361                 )
362                 all_imports_succeeded = False
363
364         if not all_imports_succeeded:
365             print(_("Not all keys were imported successfully!"))
366
367         return all_imports_succeeded
368
369     # load mirrors
370     def load_mirrors(self):
371         self.mirrors = []
372         for srcfile in self.paths["mirrors_txt"]:
373             if not os.path.exists(srcfile):
374                 continue
375             for mirror in open(srcfile, "r").readlines():
376                 if mirror.strip() not in self.mirrors:
377                     self.mirrors.append(mirror.strip())
378
379     # load settings
380     def load_settings(self):
381         default_settings = {
382             "tbl_version": self.tbl_version,
383             "installed": False,
384             "download_over_tor": False,
385             "tor_socks_address": "127.0.0.1:9050",
386             "mirror": self.default_mirror,
387             "force_en-US": False,
388         }
389
390         if os.path.isfile(self.paths["settings_file"]):
391             settings = json.load(open(self.paths["settings_file"]))
392             resave = False
393
394             # detect installed
395             settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
396
397             # make sure settings file is up-to-date
398             for setting in default_settings:
399                 if setting not in settings:
400                     settings[setting] = default_settings[setting]
401                     resave = True
402
403             # make sure tor_socks_address doesn't start with 'tcp:'
404             if settings["tor_socks_address"].startswith("tcp:"):
405                 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
406                 resave = True
407
408             # make sure the version is current
409             if settings["tbl_version"] != self.tbl_version:
410                 settings["tbl_version"] = self.tbl_version
411                 resave = True
412
413             self.settings = settings
414             if resave:
415                 self.save_settings()
416
417         # if settings file is still using old pickle format, convert to json
418         elif os.path.isfile(self.paths["settings_file_pickle"]):
419             self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
420             self.save_settings()
421             os.remove(self.paths["settings_file_pickle"])
422             self.load_settings()
423
424         else:
425             self.settings = default_settings
426             self.save_settings()
427
428     # save settings
429     def save_settings(self):
430         json.dump(self.settings, open(self.paths["settings_file"], "w"))
431         return True