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