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