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