]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Reformat all python code using black
[torbrowser-launcher.git] / torbrowser_launcher / common.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2017 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             "da",
80             "de",
81             "en-US",
82             "es-ES",
83             "fa",
84             "fr",
85             "ga-IE",
86             "he",
87             "id",
88             "is",
89             "it",
90             "ja",
91             "ko",
92             "nb-NO",
93             "nl",
94             "pl",
95             "pt-BR",
96             "ru",
97             "sv-SE",
98             "tr",
99             "vi",
100             "zh-CN",
101             "zh-TW",
102         ]
103         default_locale = locale.getlocale()[0]
104         if default_locale is None:
105             self.language = "en-US"
106         else:
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:
113                             self.language = l
114             # if language isn't available, default to english
115             if self.language not in available_languages:
116                 self.language = "en-US"
117
118     # build all relevant paths
119     def build_paths(self, tbb_version=None):
120         homedir = os.getenv("HOME")
121         if not homedir:
122             homedir = "/tmp/.torbrowser-" + os.getenv("USER")
123             if not os.path.exists(homedir):
124                 try:
125                     os.mkdir(homedir, 0o700)
126                 except:
127                     self.set_gui(
128                         "error", _("Error creating {0}").format(homedir), [], False
129                     )
130         if not os.access(homedir, os.W_OK):
131             self.set_gui("error", _("{0} is not writable").format(homedir), [], False)
132
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)
137
138         if tbb_version:
139             # tarball filename
140             if self.architecture == "x86_64":
141                 arch = "linux64"
142             else:
143                 arch = "linux32"
144
145             if hasattr(self, "settings") and self.settings["force_en-US"]:
146                 language = "en-US"
147             else:
148                 language = self.language
149             tarball_filename = (
150                 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
151             )
152
153             # tarball
154             self.paths["tarball_url"] = (
155                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
156             )
157             self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
158             self.paths["tarball_filename"] = tarball_filename
159
160             # sig
161             self.paths["sig_url"] = (
162                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
163             )
164             self.paths["sig_file"] = (
165                 tbb_cache + "/download/" + tarball_filename + ".asc"
166             )
167             self.paths["sig_filename"] = tarball_filename + ".asc"
168         else:
169             self.paths = {
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"
175                 ),
176                 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
177                 "signing_keys": {
178                     "tor_browser_developers": os.path.join(
179                         SHARE, "tor-browser-developers.asc"
180                     )
181                 },
182                 "mirrors_txt": [
183                     os.path.join(SHARE, "mirrors.txt"),
184                     tbb_config + "/mirrors.txt",
185                 ],
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",
192                 "tbb": {
193                     "changelog": tbb_local
194                     + "/tbb/"
195                     + self.architecture
196                     + "/tor-browser_"
197                     + self.language
198                     + "/Browser/TorBrowser/Docs/ChangeLog.txt",
199                     "dir": tbb_local + "/tbb/" + self.architecture,
200                     "dir_tbb": tbb_local
201                     + "/tbb/"
202                     + self.architecture
203                     + "/tor-browser_"
204                     + self.language,
205                     "start": tbb_local
206                     + "/tbb/"
207                     + self.architecture
208                     + "/tor-browser_"
209                     + self.language
210                     + "/start-tor-browser.desktop",
211                 },
212             }
213
214         # Add the expected fingerprint for imported keys:
215         self.fingerprints = {
216             "tor_browser_developers": "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
217         }
218
219     # create a directory
220     @staticmethod
221     def mkdir(path):
222         try:
223             if not os.path.exists(path):
224                 os.makedirs(path, 0o700)
225                 return True
226         except:
227             print(_("Cannot create directory {0}").format(path))
228             return False
229         if not os.access(path, os.W_OK):
230             print(_("{0} is not writable").format(path))
231             return False
232         return True
233
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"])
239         self.import_keys()
240
241     def refresh_keyring(self, fingerprint=None):
242         if fingerprint is not None:
243             print("Refreshing local keyring... Missing key: " + fingerprint)
244         else:
245             print("Refreshing local keyring...")
246
247         # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
248         p = subprocess.Popen(
249             [
250                 "/usr/bin/gpg2",
251                 "--status-fd",
252                 "2",
253                 "--homedir",
254                 self.paths["gnupg_homedir"],
255                 "--auto-key-locate",
256                 "nodefault,wkd",
257                 "--locate-keys",
258                 "torbrowser@torproject.org",
259             ],
260             stderr=subprocess.PIPE,
261         )
262         p.wait()
263
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)
274                 else:
275                     print("Keyring refreshed successfully...")
276
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
280             ``Common.paths``
281         :rtype: bool
282         :returns: ``True`` if the key is now within the keyring (or was
283             previously and hasn't changed). ``False`` otherwise.
284         """
285         with gpg.Context() as c:
286             c.set_engine_info(
287                 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
288             )
289
290             impkey = self.paths["signing_keys"][key]
291             try:
292                 c.op_import(gpg.Data(file=impkey))
293             except:
294                 return False
295             else:
296                 result = c.op_import_result()
297                 if result and self.fingerprints[key] in result.imports[0].fpr:
298                     return True
299                 else:
300                     return False
301
302     # import gpg keys
303     def import_keys(self):
304         """Import all GnuPG keys.
305         :rtype: bool
306         :returns: ``True`` if all keys were successfully imported; ``False``
307             otherwise.
308         """
309         keys = [
310             "tor_browser_developers",
311         ]
312         all_imports_succeeded = True
313
314         for key in keys:
315             imported = self.import_key_and_check_status(key)
316             if not imported:
317                 print(
318                     _(
319                         "Could not import key with fingerprint: %s."
320                         % self.fingerprints[key]
321                     )
322                 )
323                 all_imports_succeeded = False
324
325         if not all_imports_succeeded:
326             print(_("Not all keys were imported successfully!"))
327
328         return all_imports_succeeded
329
330     # load mirrors
331     def load_mirrors(self):
332         self.mirrors = []
333         for srcfile in self.paths["mirrors_txt"]:
334             if not os.path.exists(srcfile):
335                 continue
336             for mirror in open(srcfile, "r").readlines():
337                 if mirror.strip() not in self.mirrors:
338                     self.mirrors.append(mirror.strip())
339
340     # load settings
341     def load_settings(self):
342         default_settings = {
343             "tbl_version": self.tbl_version,
344             "installed": False,
345             "download_over_tor": False,
346             "tor_socks_address": "127.0.0.1:9050",
347             "mirror": self.default_mirror,
348             "force_en-US": False,
349         }
350
351         if os.path.isfile(self.paths["settings_file"]):
352             settings = json.load(open(self.paths["settings_file"]))
353             resave = False
354
355             # detect installed
356             settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
357
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]
362                     resave = True
363
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:]
367                 resave = True
368
369             # make sure the version is current
370             if settings["tbl_version"] != self.tbl_version:
371                 settings["tbl_version"] = self.tbl_version
372                 resave = True
373
374             self.settings = settings
375             if resave:
376                 self.save_settings()
377
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"]))
381             self.save_settings()
382             os.remove(self.paths["settings_file_pickle"])
383             self.load_settings()
384
385         else:
386             self.settings = default_settings
387             self.save_settings()
388
389     # save settings
390     def save_settings(self):
391         json.dump(self.settings, open(self.paths["settings_file"], "w"))
392         return True