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