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