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