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