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