]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Update list of languages that Tor Browser is available in
[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                 'keyserver_ca': os.path.join(SHARE, 'sks-keyservers.netCA.pem'),
143                 'signing_keys': {
144                     'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
145                 },
146                 'mirrors_txt': [os.path.join(SHARE, 'mirrors.txt'),
147                                 tbb_config + '/mirrors.txt'],
148                 'download_dir': tbb_cache + '/download',
149                 'gnupg_homedir': tbb_local + '/gnupg_homedir',
150                 'settings_file': tbb_config + '/settings.json',
151                 'settings_file_pickle': tbb_config + '/settings',
152                 'version_check_url': 'https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US',
153                 'version_check_file': tbb_cache + '/download/release.xml',
154                 'tbb': {
155                     'changelog': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
156                                  self.language + '/Browser/TorBrowser/Docs/ChangeLog.txt',
157                     'dir': tbb_local + '/tbb/' + self.architecture,
158                     'dir_tbb': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' + self.language,
159                     'start': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
160                              self.language + '/start-tor-browser.desktop'
161                 },
162             }
163
164         # Add the expected fingerprint for imported keys:
165         self.fingerprints = {
166             'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
167         }
168
169     # create a directory
170     @staticmethod
171     def mkdir(path):
172         try:
173             if not os.path.exists(path):
174                 os.makedirs(path, 0o700)
175                 return True
176         except:
177             print(_("Cannot create directory {0}").format(path))
178             return False
179         if not os.access(path, os.W_OK):
180             print(_("{0} is not writable").format(path))
181             return False
182         return True
183
184     # if gnupg_homedir isn't set up, set it up
185     def init_gnupg(self):
186         if not os.path.exists(self.paths['gnupg_homedir']):
187             print(_('Creating GnuPG homedir'), self.paths['gnupg_homedir'])
188             self.mkdir(self.paths['gnupg_homedir'])
189         self.import_keys()
190
191     def refresh_keyring(self, fingerprint=None):
192         if fingerprint is not None:
193             print('Refreshing local keyring... Missing key: ' + fingerprint)
194         else:
195             print('Refreshing local keyring...')
196
197         p = subprocess.Popen(['/usr/bin/gpg2', '--status-fd', '2',
198                               '--homedir', self.paths['gnupg_homedir'],
199                               '--keyserver', 'hkps://hkps.pool.sks-keyservers.net',
200                               '--keyserver-options', 'ca-cert-file=' + self.paths['keyserver_ca']
201                               + ',include-revoked,no-honor-keyserver-url,no-honor-pka-record',
202                               '--refresh-keys'], stderr=subprocess.PIPE)
203         p.wait()
204
205         for output in p.stderr.readlines():
206             match = gnupg_import_ok_pattern.match(output)
207             if match and match.group(2) == 'IMPORT_OK':
208                 fingerprint = str(match.group(4))
209                 if match.group(3) == '0':
210                     print('Keyring refreshed successfully...')
211                     print('  No key updates for key: ' + fingerprint)
212                 elif match.group(3) == '4':
213                     print('Keyring refreshed successfully...')
214                     print('  New signatures for key: ' + fingerprint)
215                 else:
216                     print('Keyring refreshed successfully...')
217
218     def import_key_and_check_status(self, key):
219         """Import a GnuPG key and check that the operation was successful.
220         :param str key: A string specifying the key's filepath from
221             ``Common.paths``
222         :rtype: bool
223         :returns: ``True`` if the key is now within the keyring (or was
224             previously and hasn't changed). ``False`` otherwise.
225         """
226         with gpg.Context() as c:
227             c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
228
229             impkey = self.paths['signing_keys'][key]
230             try:
231                 c.op_import(gpg.Data(file=impkey))
232             except:
233                 return False
234             else:
235                 result = c.op_import_result()
236                 if result and self.fingerprints[key] in result.imports[0].fpr:
237                     return True
238                 else:
239                     return False
240
241     # import gpg keys
242     def import_keys(self):
243         """Import all GnuPG keys.
244         :rtype: bool
245         :returns: ``True`` if all keys were successfully imported; ``False``
246             otherwise.
247         """
248         keys = ['tor_browser_developers', ]
249         all_imports_succeeded = True
250
251         for key in keys:
252             imported = self.import_key_and_check_status(key)
253             if not imported:
254                 print(_('Could not import key with fingerprint: %s.'
255                         % self.fingerprints[key]))
256                 all_imports_succeeded = False
257
258         if not all_imports_succeeded:
259             print(_('Not all keys were imported successfully!'))
260
261         return all_imports_succeeded
262
263     # load mirrors
264     def load_mirrors(self):
265         self.mirrors = []
266         for srcfile in self.paths['mirrors_txt']:
267             if not os.path.exists(srcfile):
268                 continue
269             for mirror in open(srcfile, 'r').readlines():
270                 if mirror.strip() not in self.mirrors:
271                     self.mirrors.append(mirror.strip())
272
273     # load settings
274     def load_settings(self):
275         default_settings = {
276             'tbl_version': self.tbl_version,
277             'installed': False,
278             'download_over_tor': False,
279             'tor_socks_address': '127.0.0.1:9050',
280             'mirror': self.default_mirror,
281             'force_en-US': False,
282         }
283
284         if os.path.isfile(self.paths['settings_file']):
285             settings = json.load(open(self.paths['settings_file']))
286             resave = False
287
288             # detect installed
289             settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
290
291             # make sure settings file is up-to-date
292             for setting in default_settings:
293                 if setting not in settings:
294                     settings[setting] = default_settings[setting]
295                     resave = True
296
297             # make sure tor_socks_address doesn't start with 'tcp:'
298             if settings['tor_socks_address'].startswith('tcp:'):
299                 settings['tor_socks_address'] = settings['tor_socks_address'][4:]
300                 resave = True
301
302             # make sure the version is current
303             if settings['tbl_version'] != self.tbl_version:
304                 settings['tbl_version'] = self.tbl_version
305                 resave = True
306
307             self.settings = settings
308             if resave:
309                 self.save_settings()
310
311         # if settings file is still using old pickle format, convert to json
312         elif os.path.isfile(self.paths['settings_file_pickle']):
313             self.settings = pickle.load(open(self.paths['settings_file_pickle']))
314             self.save_settings()
315             os.remove(self.paths['settings_file_pickle'])
316             self.load_settings()
317
318         else:
319             self.settings = default_settings
320             self.save_settings()
321
322     # save settings
323     def save_settings(self):
324         json.dump(self.settings, open(self.paths['settings_file'], 'w'))
325         return True