3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2014 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.
29 from __future__ import print_function
31 import os, sys, platform, subprocess, locale, pickle, json, psutil, re
37 SHARE = os.getenv('TBL_SHARE', sys.prefix+'/share/torbrowser-launcher')
40 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'locale'))
42 from twisted.internet import gtk2reactor
46 # We're looking for output which:
48 # 1. The first portion must be `[GNUPG:] IMPORT_OK`
49 # 2. The second must be an integer between [0, 15], inclusive
50 # 3. The third must be an uppercased hex-encoded 160-bit fingerprint
51 gnupg_import_ok_pattern = re.compile(
52 "(\[GNUPG\:\]) (IMPORT_OK) ([0-9]|[1]?[0-5]) ([A-F0-9]{40})")
57 def __init__(self, tbl_version):
58 self.tbl_version = tbl_version
61 self.default_mirror = 'https://www.torproject.org/dist/'
62 self.discover_arch_lang()
64 for d in self.paths['dirs']:
65 self.mkdir(self.paths['dirs'][d])
68 self.mkdir(self.paths['download_dir'])
69 self.mkdir(self.paths['tbb']['dir'])
72 # allow buttons to have icons
74 gtk_settings = gtk.settings_get_default()
75 gtk_settings.props.gtk_button_images = True
79 # discover the architecture and language
80 def discover_arch_lang(self):
81 # figure out the architecture
82 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
84 # figure out the language
85 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
86 default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
87 if default_locale is None:
88 self.language = 'en-US'
90 self.language = default_locale.replace('_', '-')
91 if self.language not in available_languages:
92 self.language = self.language.split('-')[0]
93 if self.language not in available_languages:
94 for l in available_languages:
95 if l[0:2] == self.language:
97 # if language isn't available, default to english
98 if self.language not in available_languages:
99 self.language = 'en-US'
101 # build all relevant paths
102 def build_paths(self, tbb_version=None):
103 homedir = os.getenv('HOME')
105 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
106 if not os.path.exists(homedir):
108 os.mkdir(homedir, 0700)
110 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
111 if not os.access(homedir, os.W_OK):
112 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
114 tbb_config = '{0}/.config/torbrowser'.format(homedir)
115 tbb_cache = '{0}/.cache/torbrowser'.format(homedir)
116 tbb_local = '{0}/.local/share/torbrowser'.format(homedir)
117 old_tbb_data = '{0}/.torbrowser'.format(homedir)
121 if self.architecture == 'x86_64':
126 if hasattr(self, 'settings') and self.settings['force_en-US']:
129 language = self.language
130 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+language+'.tar.xz'
133 self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
134 self.paths['tarball_file'] = tbb_cache+'/download/'+tarball_filename
135 self.paths['tarball_filename'] = tarball_filename
138 self.paths['sig_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename+'.asc'
139 self.paths['sig_file'] = tbb_cache+'/download/'+tarball_filename+'.asc'
140 self.paths['sig_filename'] = tarball_filename+'.asc'
144 'config': tbb_config,
148 'old_data_dir': old_tbb_data,
149 'tbl_bin': sys.argv[0],
150 'icon_file': os.path.join(os.path.dirname(SHARE), 'pixmaps/torbrowser.png'),
151 'torproject_pem': os.path.join(SHARE, 'torproject.pem'),
153 'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
155 'mirrors_txt': [os.path.join(SHARE, 'mirrors.txt'),
156 tbb_config+'/mirrors.txt'],
157 'modem_sound': os.path.join(SHARE, 'modem.ogg'),
158 'download_dir': tbb_cache+'/download',
159 'gnupg_homedir': tbb_local+'/gnupg_homedir',
160 'settings_file': tbb_config+'/settings.json',
161 'settings_file_pickle': tbb_config+'/settings',
162 'version_check_url': 'https://dist.torproject.org/torbrowser/update_2/release/Linux_x86_64-gcc3/x/en-US',
163 'version_check_file': tbb_cache+'/download/release.xml',
165 'dir': tbb_local+'/tbb/'+self.architecture,
166 'dir_tbb': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language,
167 'start': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser.desktop',
168 'versions': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Browser/TorBrowser/Docs/sources/versions',
172 # Add the expected fingerprint for imported keys:
173 self.fingerprints = {
174 'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
181 if not os.path.exists(path):
182 os.makedirs(path, 0700)
185 print(_("Cannot create directory {0}").format(path))
187 if not os.access(path, os.W_OK):
188 print(_("{0} is not writable").format(path))
192 # if gnupg_homedir isn't set up, set it up
193 def init_gnupg(self):
194 if not os.path.exists(self.paths['gnupg_homedir']):
195 print(_('Creating GnuPG homedir'), self.paths['gnupg_homedir'])
196 self.mkdir(self.paths['gnupg_homedir'])
199 def import_key_and_check_status(self, key):
200 """Import a GnuPG key and check that the operation was successful.
202 :param str key: A string specifying the key's filepath from
203 ``Common.paths``, as well as its fingerprint in
204 ``Common.fingerprints``.
206 :returns: ``True`` if the key is now within the keyring (or was
207 previously and hasn't changed). ``False`` otherwise.
211 p = subprocess.Popen(['/usr/bin/gpg', '--status-fd', '2',
212 '--homedir', self.paths['gnupg_homedir'],
213 '--import', self.paths['signing_keys'][key]],
214 stderr=subprocess.PIPE)
217 for output in p.stderr.readlines():
218 match = gnupg_import_ok_pattern.match(output)
220 # The output must match everything in the
221 # ``gnupg_import_ok_pattern``, as well as the expected fingerprint:
222 if match.group().find(self.fingerprints[key]) >= 0:
229 def import_keys(self):
230 """Import all GnuPG keys.
233 :returns: ``True`` if all keys were successfully imported; ``False``
236 keys = ['tor_browser_developers',]
237 all_imports_succeeded = True
240 imported = self.import_key_and_check_status(key)
242 print(_('Could not import key with fingerprint: %s.'
243 % self.fingerprints[key]))
244 all_imports_succeeded = False
246 if not all_imports_succeeded:
247 print(_('Not all keys were imported successfully!'))
249 return all_imports_succeeded
252 def load_mirrors(self):
254 for srcfile in self.paths['mirrors_txt']:
255 if not os.path.exists(srcfile):
257 for mirror in open(srcfile, 'r').readlines():
258 if mirror.strip() not in self.mirrors:
259 self.mirrors.append(mirror.strip())
262 def load_settings(self):
264 'tbl_version': self.tbl_version,
266 'download_over_tor': False,
267 'modem_sound': False,
268 'tor_socks_address': 'tcp:127.0.0.1:9050',
269 'mirror': self.default_mirror,
270 'force_en-US': False,
273 if os.path.isfile(self.paths['settings_file']):
274 settings = json.load(open(self.paths['settings_file']))
278 settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
280 # make sure settings file is up-to-date
281 for setting in default_settings:
282 if setting not in settings:
283 settings[setting] = default_settings[setting]
286 # make sure the version is current
287 if settings['tbl_version'] != self.tbl_version:
288 settings['tbl_version'] = self.tbl_version
291 self.settings = settings
295 # if settings file is still using old pickle format, convert to json
296 elif os.path.isfile(self.paths['settings_file_pickle']):
297 self.settings = pickle.load(open(self.paths['settings_file_pickle']))
299 os.remove(self.paths['settings_file_pickle'])
303 self.settings = default_settings
307 def save_settings(self):
308 json.dump(self.settings, open(self.paths['settings_file'], 'w'))