]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
3a3a430a1beb81af6351eaff1543e99f767ccf4b
[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 class Common(object):
54     def __init__(self, tbl_version):
55         self.tbl_version = tbl_version
56
57         # initialize the app
58         self.default_mirror = 'https://dist.torproject.org/'
59         self.discover_arch_lang()
60         self.build_paths()
61         for d in self.paths['dirs']:
62             self.mkdir(self.paths['dirs'][d])
63         self.load_mirrors()
64         self.load_settings()
65         self.mkdir(self.paths['download_dir'])
66         self.mkdir(self.paths['tbb']['dir'])
67         self.init_gnupg()
68
69     # discover the architecture and language
70     def discover_arch_lang(self):
71         # figure out the architecture
72         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
73
74         # figure out the language
75         available_languages = ['ar', 'ca', 'da', 'de', 'en-US', 'es-ES', 'fa', 'fr', 'ga-IE', 'he', 'id', 'is', 'it', 'ja', 'ko', 'nb-NO', 'nl', 'pl', 'pt-BR', 'ru', 'sv-SE', 'tr', 'vi', 'zh-CN', 'zh-TW']
76         default_locale = locale.getlocale()[0]
77         if default_locale is None:
78             self.language = 'en-US'
79         else:
80             self.language = default_locale.replace('_', '-')
81             if self.language not in available_languages:
82                 self.language = self.language.split('-')[0]
83                 if self.language not in available_languages:
84                     for l in available_languages:
85                         if l[0:2] == self.language:
86                             self.language = l
87             # if language isn't available, default to english
88             if self.language not in available_languages:
89                 self.language = 'en-US'
90
91     # get value of environment variable, if it is not set return the default value
92     @staticmethod
93     def get_env(var_name, default_value):
94         value = os.getenv(var_name)
95         if not value:
96             value = default_value
97         return value
98
99     # build all relevant paths
100     def build_paths(self, tbb_version=None):
101         homedir = os.getenv('HOME')
102         if not homedir:
103             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
104             if not os.path.exists(homedir):
105                 try:
106                     os.mkdir(homedir, 0o700)
107                 except:
108                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
109         if not os.access(homedir, os.W_OK):
110             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
111
112         tbb_config = '{0}/torbrowser'.format(self.get_env('XDG_CONFIG_HOME', '{0}/.config'.format(homedir)))
113         tbb_cache = '{0}/torbrowser'.format(self.get_env('XDG_CACHE_HOME', '{0}/.cache'.format(homedir)))
114         tbb_local = '{0}/torbrowser'.format(self.get_env('XDG_DATA_HOME', '{0}/.local/share'.format(homedir)))
115         old_tbb_data = '{0}/.torbrowser'.format(homedir)
116
117         if tbb_version:
118             # tarball filename
119             if self.architecture == 'x86_64':
120                 arch = 'linux64'
121             else:
122                 arch = 'linux32'
123
124             if hasattr(self, 'settings') and self.settings['force_en-US']:
125                 language = 'en-US'
126             else:
127                 language = self.language
128             tarball_filename = 'tor-browser-' + arch + '-' + tbb_version + '_' + language + '.tar.xz'
129
130             # tarball
131             self.paths['tarball_url'] = '{0}torbrowser/' + tbb_version + '/' + tarball_filename
132             self.paths['tarball_file'] = tbb_cache + '/download/' + tarball_filename
133             self.paths['tarball_filename'] = tarball_filename
134
135             # sig
136             self.paths['sig_url'] = '{0}torbrowser/' + tbb_version + '/' + tarball_filename + '.asc'
137             self.paths['sig_file'] = tbb_cache + '/download/' + tarball_filename + '.asc'
138             self.paths['sig_filename'] = tarball_filename + '.asc'
139         else:
140             self.paths = {
141                 'dirs': {
142                     'config': tbb_config,
143                     'cache': tbb_cache,
144                     'local': tbb_local,
145                 },
146                 'old_data_dir': old_tbb_data,
147                 'tbl_bin': sys.argv[0],
148                 'icon_file': os.path.join(os.path.dirname(SHARE), 'pixmaps/torbrowser.png'),
149                 'torproject_pem': os.path.join(SHARE, 'torproject.pem'),
150                 'signing_keys': {
151                     'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
152                 },
153                 'mirrors_txt': [os.path.join(SHARE, 'mirrors.txt'),
154                                 tbb_config + '/mirrors.txt'],
155                 'download_dir': tbb_cache + '/download',
156                 'gnupg_homedir': tbb_local + '/gnupg_homedir',
157                 'settings_file': tbb_config + '/settings.json',
158                 'settings_file_pickle': tbb_config + '/settings',
159                 'version_check_url': 'https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US',
160                 'version_check_file': tbb_cache + '/download/release.xml',
161                 'tbb': {
162                     'changelog': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
163                                  self.language + '/Browser/TorBrowser/Docs/ChangeLog.txt',
164                     'dir': tbb_local + '/tbb/' + self.architecture,
165                     'dir_tbb': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' + self.language,
166                     'start': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
167                              self.language + '/start-tor-browser.desktop'
168                 },
169             }
170
171         # Add the expected fingerprint for imported keys:
172         self.fingerprints = {
173             'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
174         }
175
176     # create a directory
177     @staticmethod
178     def mkdir(path):
179         try:
180             if not os.path.exists(path):
181                 os.makedirs(path, 0o700)
182                 return True
183         except:
184             print(_("Cannot create directory {0}").format(path))
185             return False
186         if not os.access(path, os.W_OK):
187             print(_("{0} is not writable").format(path))
188             return False
189         return True
190
191     # if gnupg_homedir isn't set up, set it up
192     def init_gnupg(self):
193         if not os.path.exists(self.paths['gnupg_homedir']):
194             print(_('Creating GnuPG homedir'), self.paths['gnupg_homedir'])
195             self.mkdir(self.paths['gnupg_homedir'])
196         self.import_keys()
197
198     def refresh_keyring(self, fingerprint=None):
199         if fingerprint is not None:
200             print('Refreshing local keyring... Missing key: ' + fingerprint)
201         else:
202             print('Refreshing local keyring...')
203
204         p = subprocess.Popen(['/usr/bin/gpg2', '--status-fd', '2',
205                               '--homedir', self.paths['gnupg_homedir'],
206                               '--keyserver', 'hkps://keys.openpgp.org',
207                               '--refresh-keys'], stderr=subprocess.PIPE)
208         p.wait()
209
210         for output in p.stderr.readlines():
211             match = gnupg_import_ok_pattern.match(output)
212             if match and match.group(2) == 'IMPORT_OK':
213                 fingerprint = str(match.group(4))
214                 if match.group(3) == '0':
215                     print('Keyring refreshed successfully...')
216                     print('  No key updates for key: ' + fingerprint)
217                 elif match.group(3) == '4':
218                     print('Keyring refreshed successfully...')
219                     print('  New signatures for key: ' + fingerprint)
220                 else:
221                     print('Keyring refreshed successfully...')
222
223     def import_key_and_check_status(self, key):
224         """Import a GnuPG key and check that the operation was successful.
225         :param str key: A string specifying the key's filepath from
226             ``Common.paths``
227         :rtype: bool
228         :returns: ``True`` if the key is now within the keyring (or was
229             previously and hasn't changed). ``False`` otherwise.
230         """
231         with gpg.Context() as c:
232             c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
233
234             impkey = self.paths['signing_keys'][key]
235             try:
236                 c.op_import(gpg.Data(file=impkey))
237             except:
238                 return False
239             else:
240                 result = c.op_import_result()
241                 if result and self.fingerprints[key] in result.imports[0].fpr:
242                     return True
243                 else:
244                     return False
245
246     # import gpg keys
247     def import_keys(self):
248         """Import all GnuPG keys.
249         :rtype: bool
250         :returns: ``True`` if all keys were successfully imported; ``False``
251             otherwise.
252         """
253         keys = ['tor_browser_developers', ]
254         all_imports_succeeded = True
255
256         for key in keys:
257             imported = self.import_key_and_check_status(key)
258             if not imported:
259                 print(_('Could not import key with fingerprint: %s.'
260                         % self.fingerprints[key]))
261                 all_imports_succeeded = False
262
263         if not all_imports_succeeded:
264             print(_('Not all keys were imported successfully!'))
265
266         return all_imports_succeeded
267
268     # load mirrors
269     def load_mirrors(self):
270         self.mirrors = []
271         for srcfile in self.paths['mirrors_txt']:
272             if not os.path.exists(srcfile):
273                 continue
274             for mirror in open(srcfile, 'r').readlines():
275                 if mirror.strip() not in self.mirrors:
276                     self.mirrors.append(mirror.strip())
277
278     # load settings
279     def load_settings(self):
280         default_settings = {
281             'tbl_version': self.tbl_version,
282             'installed': False,
283             'download_over_tor': False,
284             'tor_socks_address': '127.0.0.1:9050',
285             'mirror': self.default_mirror,
286             'force_en-US': False,
287         }
288
289         if os.path.isfile(self.paths['settings_file']):
290             settings = json.load(open(self.paths['settings_file']))
291             resave = False
292
293             # detect installed
294             settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
295
296             # make sure settings file is up-to-date
297             for setting in default_settings:
298                 if setting not in settings:
299                     settings[setting] = default_settings[setting]
300                     resave = True
301
302             # make sure tor_socks_address doesn't start with 'tcp:'
303             if settings['tor_socks_address'].startswith('tcp:'):
304                 settings['tor_socks_address'] = settings['tor_socks_address'][4:]
305                 resave = True
306
307             # make sure the version is current
308             if settings['tbl_version'] != self.tbl_version:
309                 settings['tbl_version'] = self.tbl_version
310                 resave = True
311
312             self.settings = settings
313             if resave:
314                 self.save_settings()
315
316         # if settings file is still using old pickle format, convert to json
317         elif os.path.isfile(self.paths['settings_file_pickle']):
318             self.settings = pickle.load(open(self.paths['settings_file_pickle']))
319             self.save_settings()
320             os.remove(self.paths['settings_file_pickle'])
321             self.load_settings()
322
323         else:
324             self.settings = default_settings
325             self.save_settings()
326
327     # save settings
328     def save_settings(self):
329         json.dump(self.settings, open(self.paths['settings_file'], 'w'))
330         return True