]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Allow url_list
[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 import requests
40
41 SHARE = os.getenv("TBL_SHARE", sys.prefix + "/share") + "/torbrowser-launcher"
42
43 gettext.install("torbrowser-launcher")
44
45 # We're looking for output which:
46 #
47 #  1. The first portion must be `[GNUPG:] IMPORT_OK`
48 #  2. The second must be an integer between [0, 15], inclusive
49 #  3. The third must be an uppercased hex-encoded 160-bit fingerprint
50 gnupg_import_ok_pattern = re.compile(
51     b"(\[GNUPG\:\]) (IMPORT_OK) ([0-9]|[1]?[0-5]) ([A-F0-9]{40})"
52 )
53
54
55 class Common(object):
56     def __init__(self, tbl_version):
57         self.tbl_version = tbl_version
58
59         # initialize the app
60         self.default_mirror = "https://dist.torproject.org/"
61         self.discover_arch_lang()
62         self.build_paths()
63         for d in self.paths["dirs"]:
64             self.mkdir(self.paths["dirs"][d])
65         self.load_mirrors()
66         self.load_settings()
67         # some settings require a path rebuild, like force_en-US
68         self.build_paths()
69         self.mkdir(self.paths["download_dir"])
70         self.mkdir(self.paths["tbb"]["dir"])
71         self.init_gnupg()
72
73     # discover the architecture and language
74     def discover_arch_lang(self):
75         # figure out the architecture
76         self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
77
78         # figure out the language
79         available_languages = [
80             "ar",
81             "ca",
82             "cs",
83             "da",
84             "de",
85             "el",
86             "en-US",
87             "es-AR",
88             "es-ES",
89             "fa",
90             "fr",
91             "ga-IE",
92             "he",
93             "hu",
94             "id",
95             "is",
96             "it",
97             "ja",
98             "ka",
99             "ko",
100             "lt",
101             "mk",
102             "ms",
103             "my",
104             "nb-NO",
105             "nl",
106             "pl",
107             "pt-BR",
108             "ro",
109             "ru",
110             "sv-SE",
111             "th",
112             "tr",
113             "vi",
114             "zh-CN",
115             "zh-TW",
116         ]
117
118         # a list of manually configured language fallback overriding
119         language_overrides = {
120             "zh-HK": "zh-TW",
121         }
122
123         locale.setlocale(locale.LC_MESSAGES, "")
124         default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
125         if default_locale is None:
126             self.language = "en-US"
127         else:
128             self.language = default_locale.replace("_", "-")
129             if self.language in language_overrides:
130                 self.language = language_overrides[self.language]
131             if self.language not in available_languages:
132                 self.language = self.language.split("-")[0]
133                 if self.language not in available_languages:
134                     for l in available_languages:
135                         if l[0:2] == self.language:
136                             self.language = l
137             # if language isn't available, default to english
138             if self.language not in available_languages:
139                 self.language = "en-US"
140
141     # get value of environment variable, if it is not set return the default value
142     @staticmethod
143     def get_env(var_name, default_value):
144         value = os.getenv(var_name)
145         if not value:
146             value = default_value
147         return value
148
149     # build all relevant paths
150     def build_paths(self, tbb_version=None):
151         homedir = os.getenv("HOME")
152         if not homedir:
153             homedir = "/tmp/.torbrowser-" + os.getenv("USER")
154             if not os.path.exists(homedir):
155                 try:
156                     os.mkdir(homedir, 0o700)
157                 except:
158                     self.set_gui(
159                         "error", _("Error creating {0}").format(homedir), [], False
160                     )
161
162         tbb_config = "{0}/torbrowser".format(
163             self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
164         )
165         tbb_cache = "{0}/torbrowser".format(
166             self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
167         )
168         tbb_local = "{0}/torbrowser".format(
169             self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
170         )
171         old_tbb_data = "{0}/.torbrowser".format(homedir)
172
173         if hasattr(self, "settings") and self.settings["force_en-US"]:
174             language = "en-US"
175         else:
176             language = self.language
177
178         if tbb_version:
179             # tarball filename
180             if self.architecture == "x86_64":
181                 arch = "linux64"
182             else:
183                 arch = "linux32"
184
185             tarball_filename = (
186                 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
187             )
188
189             # tarball
190             self.paths["tarball_url"] = (
191                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
192             )
193             self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
194             self.paths["tarball_filename"] = tarball_filename
195
196             # sig
197             self.paths["sig_url"] = (
198                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
199             )
200             self.paths["sig_file"] = (
201                 tbb_cache + "/download/" + tarball_filename + ".asc"
202             )
203             self.paths["sig_filename"] = tarball_filename + ".asc"
204         else:
205             self.paths = {
206                 "dirs": {
207                     "config": tbb_config,
208                     "cache": tbb_cache,
209                     "local": tbb_local,
210                 },
211                 "old_data_dir": old_tbb_data,
212                 "tbl_bin": sys.argv[0],
213                 "icon_file": os.path.join(
214                     os.path.dirname(SHARE), "pixmaps/torbrowser.png"
215                 ),
216                 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
217                 "signing_keys": {
218                     "tor_browser_developers": os.path.join(
219                         SHARE, "tor-browser-developers.asc"
220                     ),
221                     "wkd_tmp": os.path.join(tbb_cache, "torbrowser.gpg")
222                 },
223                 "mirrors_txt": [
224                     os.path.join(SHARE, "mirrors.txt"),
225                     tbb_config + "/mirrors.txt",
226                 ],
227                 "download_dir": tbb_cache + "/download",
228                 "gnupg_homedir": tbb_local + "/gnupg_homedir",
229                 "settings_file": tbb_config + "/settings.json",
230                 "settings_file_pickle": tbb_config + "/settings",
231                 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
232                 "version_check_file": tbb_cache + "/download/release.xml",
233                 "tbb": {
234                     "changelog": tbb_local
235                     + "/tbb/"
236                     + self.architecture
237                     + "/tor-browser_"
238                     + language
239                     + "/Browser/TorBrowser/Docs/ChangeLog.txt",
240                     "dir": tbb_local + "/tbb/" + self.architecture,
241                     "dir_tbb": tbb_local
242                     + "/tbb/"
243                     + self.architecture
244                     + "/tor-browser_"
245                     + language,
246                     "start": tbb_local
247                     + "/tbb/"
248                     + self.architecture
249                     + "/tor-browser_"
250                     + language
251                     + "/start-tor-browser.desktop",
252                 },
253             }
254
255         # Add the expected fingerprint for imported keys:
256         tor_browser_developers_fingerprint = "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
257         self.fingerprints = {
258             "tor_browser_developers": tor_browser_developers_fingerprint,
259             "wkd_tmp": tor_browser_developers_fingerprint,
260         }
261
262     # create a directory
263     @staticmethod
264     def mkdir(path):
265         try:
266             if not os.path.exists(path):
267                 os.makedirs(path, 0o700)
268                 return True
269         except:
270             print(_("Cannot create directory {0}").format(path))
271             return False
272         if not os.access(path, os.W_OK):
273             print(_("{0} is not writable").format(path))
274             return False
275         return True
276
277     # if gnupg_homedir isn't set up, set it up
278     def init_gnupg(self):
279         if not os.path.exists(self.paths["gnupg_homedir"]):
280             print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
281             self.mkdir(self.paths["gnupg_homedir"])
282         self.import_keys()
283
284     def proxies(self):
285         # Use tor socks5 proxy, if enabled
286         if self.settings["download_over_tor"]:
287             socks5_address = "socks5h://{}".format(self.settings["tor_socks_address"])
288             return {"https": socks5_address, "http": socks5_address}
289         else:
290             return None
291
292     def refresh_keyring(self):
293         print("Downloading latest Tor Browser signing key...")
294
295         # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
296         # Sometimes GPG throws errors, so comment this out and download it directly
297         # p = subprocess.Popen(
298         #     [
299         #         "gpg",
300         #         "--status-fd",
301         #         "2",
302         #         "--homedir",
303         #         self.paths["gnupg_homedir"],
304         #         "--auto-key-locate",
305         #         "nodefault,wkd",
306         #         "--locate-keys",
307         #         "torbrowser@torproject.org",
308         #     ],
309         #     stderr=subprocess.PIPE,
310         # )
311         # p.wait()
312
313         # Download the key from WKD directly
314         r = requests.get(
315             "https://torproject.org/.well-known/openpgpkey/hu/kounek7zrdx745qydx6p59t9mqjpuhdf?l=torbrowser",
316             proxies=self.proxies(),
317         )
318         if r.status_code != 200:
319             print(f"Error fetching key, status code = {r.status_code}")
320         else:
321             with open(self.paths["signing_keys"]["wkd_tmp"], "wb") as f:
322                 f.write(r.content)
323
324             if self.import_key_and_check_status("wkd_tmp"):
325                 print("Key imported successfully")
326             else:
327                 print("Key failed to import")
328
329     def import_key_and_check_status(self, key):
330         """Import a GnuPG key and check that the operation was successful.
331         :param str key: A string specifying the key's filepath from
332             ``Common.paths``
333         :rtype: bool
334         :returns: ``True`` if the key is now within the keyring (or was
335             previously and hasn't changed). ``False`` otherwise.
336         """
337         with gpg.Context() as c:
338             c.set_engine_info(
339                 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
340             )
341
342             impkey = self.paths["signing_keys"][key]
343             try:
344                 c.op_import(gpg.Data(file=impkey))
345             except:
346                 return False
347             else:
348                 result = c.op_import_result()
349                 if result and self.fingerprints[key] in result.imports[0].fpr:
350                     return True
351                 else:
352                     return False
353
354     # import gpg keys
355     def import_keys(self):
356         """Import all GnuPG keys.
357         :rtype: bool
358         :returns: ``True`` if all keys were successfully imported; ``False``
359             otherwise.
360         """
361         keys = [
362             "tor_browser_developers",
363         ]
364         all_imports_succeeded = True
365
366         for key in keys:
367             imported = self.import_key_and_check_status(key)
368             if not imported:
369                 print(
370                     _(
371                         "Could not import key with fingerprint: %s."
372                         % self.fingerprints[key]
373                     )
374                 )
375                 all_imports_succeeded = False
376
377         if not all_imports_succeeded:
378             print(_("Not all keys were imported successfully!"))
379
380         return all_imports_succeeded
381
382     # load mirrors
383     def load_mirrors(self):
384         self.mirrors = []
385         for srcfile in self.paths["mirrors_txt"]:
386             if not os.path.exists(srcfile):
387                 continue
388             for mirror in open(srcfile, "r").readlines():
389                 if mirror.strip() not in self.mirrors:
390                     self.mirrors.append(mirror.strip())
391
392     # load settings
393     def load_settings(self):
394         default_settings = {
395             "tbl_version": self.tbl_version,
396             "installed": False,
397             "download_over_tor": False,
398             "tor_socks_address": "127.0.0.1:9050",
399             "mirror": self.default_mirror,
400             "force_en-US": False,
401         }
402
403         if os.path.isfile(self.paths["settings_file"]):
404             settings = json.load(open(self.paths["settings_file"]))
405             resave = False
406
407             # detect installed
408             settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
409
410             # make sure settings file is up-to-date
411             for setting in default_settings:
412                 if setting not in settings:
413                     settings[setting] = default_settings[setting]
414                     resave = True
415
416             # make sure tor_socks_address doesn't start with 'tcp:'
417             if settings["tor_socks_address"].startswith("tcp:"):
418                 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
419                 resave = True
420
421             # make sure the version is current
422             if settings["tbl_version"] != self.tbl_version:
423                 settings["tbl_version"] = self.tbl_version
424                 resave = True
425
426             self.settings = settings
427             if resave:
428                 self.save_settings()
429
430         # if settings file is still using old pickle format, convert to json
431         elif os.path.isfile(self.paths["settings_file_pickle"]):
432             self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
433             self.save_settings()
434             os.remove(self.paths["settings_file_pickle"])
435             self.load_settings()
436
437         else:
438             self.settings = default_settings
439             self.save_settings()
440
441     # save settings
442     def save_settings(self):
443         json.dump(self.settings, open(self.paths["settings_file"], "w"))
444         return True