]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
25bb984504904f73e2b798fa65fe39034ec50609
[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         p = subprocess.Popen(['/usr/bin/gpg2', '--status-fd', '2',
197                               '--homedir', self.paths['gnupg_homedir'],
198                               '--keyserver', 'hkps://keys.openpgp.org',
199                               '--refresh-keys'], stderr=subprocess.PIPE)
200         p.wait()
201
202         for output in p.stderr.readlines():
203             match = gnupg_import_ok_pattern.match(output)
204             if match and match.group(2) == 'IMPORT_OK':
205                 fingerprint = str(match.group(4))
206                 if match.group(3) == '0':
207                     print('Keyring refreshed successfully...')
208                     print('  No key updates for key: ' + fingerprint)
209                 elif match.group(3) == '4':
210                     print('Keyring refreshed successfully...')
211                     print('  New signatures for key: ' + fingerprint)
212                 else:
213                     print('Keyring refreshed successfully...')
214
215     def import_key_and_check_status(self, key):
216         """Import a GnuPG key and check that the operation was successful.
217         :param str key: A string specifying the key's filepath from
218             ``Common.paths``
219         :rtype: bool
220         :returns: ``True`` if the key is now within the keyring (or was
221             previously and hasn't changed). ``False`` otherwise.
222         """
223         with gpg.Context() as c:
224             c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
225
226             impkey = self.paths['signing_keys'][key]
227             try:
228                 c.op_import(gpg.Data(file=impkey))
229             except:
230                 return False
231             else:
232                 result = c.op_import_result()
233                 if result and self.fingerprints[key] in result.imports[0].fpr:
234                     return True
235                 else:
236                     return False
237
238     # import gpg keys
239     def import_keys(self):
240         """Import all GnuPG keys.
241         :rtype: bool
242         :returns: ``True`` if all keys were successfully imported; ``False``
243             otherwise.
244         """
245         keys = ['tor_browser_developers', ]
246         all_imports_succeeded = True
247
248         for key in keys:
249             imported = self.import_key_and_check_status(key)
250             if not imported:
251                 print(_('Could not import key with fingerprint: %s.'
252                         % self.fingerprints[key]))
253                 all_imports_succeeded = False
254
255         if not all_imports_succeeded:
256             print(_('Not all keys were imported successfully!'))
257
258         return all_imports_succeeded
259
260     # load mirrors
261     def load_mirrors(self):
262         self.mirrors = []
263         for srcfile in self.paths['mirrors_txt']:
264             if not os.path.exists(srcfile):
265                 continue
266             for mirror in open(srcfile, 'r').readlines():
267                 if mirror.strip() not in self.mirrors:
268                     self.mirrors.append(mirror.strip())
269
270     # load settings
271     def load_settings(self):
272         default_settings = {
273             'tbl_version': self.tbl_version,
274             'installed': False,
275             'download_over_tor': False,
276             'tor_socks_address': '127.0.0.1:9050',
277             'mirror': self.default_mirror,
278             'force_en-US': False,
279         }
280
281         if os.path.isfile(self.paths['settings_file']):
282             settings = json.load(open(self.paths['settings_file']))
283             resave = False
284
285             # detect installed
286             settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
287
288             # make sure settings file is up-to-date
289             for setting in default_settings:
290                 if setting not in settings:
291                     settings[setting] = default_settings[setting]
292                     resave = True
293
294             # make sure tor_socks_address doesn't start with 'tcp:'
295             if settings['tor_socks_address'].startswith('tcp:'):
296                 settings['tor_socks_address'] = settings['tor_socks_address'][4:]
297                 resave = True
298
299             # make sure the version is current
300             if settings['tbl_version'] != self.tbl_version:
301                 settings['tbl_version'] = self.tbl_version
302                 resave = True
303
304             self.settings = settings
305             if resave:
306                 self.save_settings()
307
308         # if settings file is still using old pickle format, convert to json
309         elif os.path.isfile(self.paths['settings_file_pickle']):
310             self.settings = pickle.load(open(self.paths['settings_file_pickle']))
311             self.save_settings()
312             os.remove(self.paths['settings_file_pickle'])
313             self.load_settings()
314
315         else:
316             self.settings = default_settings
317             self.save_settings()
318
319     # save settings
320     def save_settings(self):
321         json.dump(self.settings, open(self.paths['settings_file'], 'w'))
322         return True