3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2017 Micah Lee <micah@micahflee.com>
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
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
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.
44 SHARE = os.getenv('TBL_SHARE', sys.prefix+'/share/torbrowser-launcher')
47 gettext.install('torbrowser-launcher')
49 # We're looking for output which:
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})")
59 def __init__(self, tbl_version):
60 self.tbl_version = tbl_version
63 self.default_mirror = 'https://dist.torproject.org/'
64 self.discover_arch_lang()
66 for d in self.paths['dirs']:
67 self.mkdir(self.paths['dirs'][d])
70 self.mkdir(self.paths['download_dir'])
71 self.mkdir(self.paths['tbb']['dir'])
74 # allow buttons to have icons
76 gtk_settings = gtk.settings_get_default()
77 gtk_settings.props.gtk_button_images = True
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'
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'
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:
99 # if language isn't available, default to english
100 if self.language not in available_languages:
101 self.language = 'en-US'
103 # build all relevant paths
104 def build_paths(self, tbb_version=None):
105 homedir = os.getenv('HOME')
107 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
108 if not os.path.exists(homedir):
110 os.mkdir(homedir, 0o700)
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)
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)
123 if self.architecture == 'x86_64':
128 if hasattr(self, 'settings') and self.settings['force_en-US']:
131 language = self.language
132 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+language+'.tar.xz'
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
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'
146 'config': tbb_config,
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'),
156 'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
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',
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',
174 # Add the expected fingerprint for imported keys:
175 self.fingerprints = {
176 'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
183 if not os.path.exists(path):
184 os.makedirs(path, 0o700)
187 print(_("Cannot create directory {0}").format(path))
189 if not os.access(path, os.W_OK):
190 print(_("{0} is not writable").format(path))
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'])
201 def refresh_keyring(self, fingerprint=None):
202 if fingerprint is not None:
203 print('Refreshing local keyring... Missing key: ' + fingerprint)
205 print('Refreshing local keyring...')
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)
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)
226 print('Keyring refreshed successfully...')
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
233 :returns: ``True`` if the key is now within the keyring (or was
234 previously and hasn't changed). ``False`` otherwise.
237 with gpg.Context() as c:
238 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
240 impkey = self.paths['signing_keys'][key]
242 c.op_import(gpg.Data(file=impkey))
246 result = c.op_import_result()
247 if result and self.fingerprints[key] in result.imports[0].fpr:
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)
260 for output in p.stderr.readlines():
261 match = gnupg_import_ok_pattern.match(output)
263 if match.group().find(self.fingerprints[key]) >= 0:
270 def import_keys(self):
271 """Import all GnuPG keys.
273 :returns: ``True`` if all keys were successfully imported; ``False``
276 keys = ['tor_browser_developers',]
277 all_imports_succeeded = True
280 imported = self.import_key_and_check_status(key)
282 print(_('Could not import key with fingerprint: %s.'
283 % self.fingerprints[key]))
284 all_imports_succeeded = False
286 if not all_imports_succeeded:
287 print(_('Not all keys were imported successfully!'))
289 self.refresh_keyring()
290 return all_imports_succeeded
293 def load_mirrors(self):
295 for srcfile in self.paths['mirrors_txt']:
296 if not os.path.exists(srcfile):
298 for mirror in open(srcfile, 'r').readlines():
299 if mirror.strip() not in self.mirrors:
300 self.mirrors.append(mirror.strip())
303 def load_settings(self):
305 'tbl_version': self.tbl_version,
307 'download_over_tor': False,
308 'tor_socks_address': '127.0.0.1:9050',
309 'mirror': self.default_mirror,
310 'force_en-US': False,
313 if os.path.isfile(self.paths['settings_file']):
314 settings = json.load(open(self.paths['settings_file']))
318 settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
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]
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:]
331 # make sure the version is current
332 if settings['tbl_version'] != self.tbl_version:
333 settings['tbl_version'] = self.tbl_version
336 self.settings = settings
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']))
344 os.remove(self.paths['settings_file_pickle'])
348 self.settings = default_settings
352 def save_settings(self):
353 json.dump(self.settings, open(self.paths['settings_file'], 'w'))