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.
40 SHARE = os.getenv('TBL_SHARE', sys.prefix + '/share') + '/torbrowser-launcher'
42 gettext.install('torbrowser-launcher')
44 # We're looking for output which:
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})")
54 def __init__(self, tbl_version):
55 self.tbl_version = tbl_version
58 self.default_mirror = 'https://dist.torproject.org/'
59 self.discover_arch_lang()
61 for d in self.paths['dirs']:
62 self.mkdir(self.paths['dirs'][d])
65 self.mkdir(self.paths['download_dir'])
66 self.mkdir(self.paths['tbb']['dir'])
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'
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'
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:
87 # if language isn't available, default to english
88 if self.language not in available_languages:
89 self.language = 'en-US'
91 # get value of environment variable, if it is not set return the default value
93 def get_env(var_name, default_value):
94 value = os.getenv(var_name)
99 # build all relevant paths
100 def build_paths(self, tbb_version=None):
101 homedir = os.getenv('HOME')
103 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
104 if not os.path.exists(homedir):
106 os.mkdir(homedir, 0o700)
108 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
109 if not os.access(homedir, os.W_OK):
110 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
112 tbb_config = '{0}/torbrowser'.format(self.get_env('XDG_CONFIG_HOME', '{0}/.config'.format(homedir)))
113 tbb_cache = '{0}/torbrowser'.format(self.get_env('XDG_CACHE_HOME', '{0}/.cache'.format(homedir)))
114 tbb_local = '{0}/torbrowser'.format(self.get_env('XDG_DATA_HOME', '{0}/.local/share'.format(homedir)))
115 old_tbb_data = '{0}/.torbrowser'.format(homedir)
119 if self.architecture == 'x86_64':
124 if hasattr(self, 'settings') and self.settings['force_en-US']:
127 language = self.language
128 tarball_filename = 'tor-browser-' + arch + '-' + tbb_version + '_' + language + '.tar.xz'
131 self.paths['tarball_url'] = '{0}torbrowser/' + tbb_version + '/' + tarball_filename
132 self.paths['tarball_file'] = tbb_cache + '/download/' + tarball_filename
133 self.paths['tarball_filename'] = tarball_filename
136 self.paths['sig_url'] = '{0}torbrowser/' + tbb_version + '/' + tarball_filename + '.asc'
137 self.paths['sig_file'] = tbb_cache + '/download/' + tarball_filename + '.asc'
138 self.paths['sig_filename'] = tarball_filename + '.asc'
142 'config': tbb_config,
146 'old_data_dir': old_tbb_data,
147 'tbl_bin': sys.argv[0],
148 'icon_file': os.path.join(os.path.dirname(SHARE), 'pixmaps/torbrowser.png'),
149 'torproject_pem': os.path.join(SHARE, 'torproject.pem'),
151 'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
153 'mirrors_txt': [os.path.join(SHARE, 'mirrors.txt'),
154 tbb_config + '/mirrors.txt'],
155 'download_dir': tbb_cache + '/download',
156 'gnupg_homedir': tbb_local + '/gnupg_homedir',
157 'settings_file': tbb_config + '/settings.json',
158 'settings_file_pickle': tbb_config + '/settings',
159 'version_check_url': 'https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US',
160 'version_check_file': tbb_cache + '/download/release.xml',
162 'changelog': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
163 self.language + '/Browser/TorBrowser/Docs/ChangeLog.txt',
164 'dir': tbb_local + '/tbb/' + self.architecture,
165 'dir_tbb': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' + self.language,
166 'start': tbb_local + '/tbb/' + self.architecture + '/tor-browser_' +
167 self.language + '/start-tor-browser.desktop'
171 # Add the expected fingerprint for imported keys:
172 self.fingerprints = {
173 'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
180 if not os.path.exists(path):
181 os.makedirs(path, 0o700)
184 print(_("Cannot create directory {0}").format(path))
186 if not os.access(path, os.W_OK):
187 print(_("{0} is not writable").format(path))
191 # if gnupg_homedir isn't set up, set it up
192 def init_gnupg(self):
193 if not os.path.exists(self.paths['gnupg_homedir']):
194 print(_('Creating GnuPG homedir'), self.paths['gnupg_homedir'])
195 self.mkdir(self.paths['gnupg_homedir'])
198 def refresh_keyring(self, fingerprint=None):
199 if fingerprint is not None:
200 print('Refreshing local keyring... Missing key: ' + fingerprint)
202 print('Refreshing local keyring...')
204 p = subprocess.Popen(['/usr/bin/gpg2', '--status-fd', '2',
205 '--homedir', self.paths['gnupg_homedir'],
206 '--keyserver', 'hkps://keys.openpgp.org',
207 '--refresh-keys'], stderr=subprocess.PIPE)
210 for output in p.stderr.readlines():
211 match = gnupg_import_ok_pattern.match(output)
212 if match and match.group(2) == 'IMPORT_OK':
213 fingerprint = str(match.group(4))
214 if match.group(3) == '0':
215 print('Keyring refreshed successfully...')
216 print(' No key updates for key: ' + fingerprint)
217 elif match.group(3) == '4':
218 print('Keyring refreshed successfully...')
219 print(' New signatures for key: ' + fingerprint)
221 print('Keyring refreshed successfully...')
223 def import_key_and_check_status(self, key):
224 """Import a GnuPG key and check that the operation was successful.
225 :param str key: A string specifying the key's filepath from
228 :returns: ``True`` if the key is now within the keyring (or was
229 previously and hasn't changed). ``False`` otherwise.
231 with gpg.Context() as c:
232 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
234 impkey = self.paths['signing_keys'][key]
236 c.op_import(gpg.Data(file=impkey))
240 result = c.op_import_result()
241 if result and self.fingerprints[key] in result.imports[0].fpr:
247 def import_keys(self):
248 """Import all GnuPG keys.
250 :returns: ``True`` if all keys were successfully imported; ``False``
253 keys = ['tor_browser_developers', ]
254 all_imports_succeeded = True
257 imported = self.import_key_and_check_status(key)
259 print(_('Could not import key with fingerprint: %s.'
260 % self.fingerprints[key]))
261 all_imports_succeeded = False
263 if not all_imports_succeeded:
264 print(_('Not all keys were imported successfully!'))
266 return all_imports_succeeded
269 def load_mirrors(self):
271 for srcfile in self.paths['mirrors_txt']:
272 if not os.path.exists(srcfile):
274 for mirror in open(srcfile, 'r').readlines():
275 if mirror.strip() not in self.mirrors:
276 self.mirrors.append(mirror.strip())
279 def load_settings(self):
281 'tbl_version': self.tbl_version,
283 'download_over_tor': False,
284 'tor_socks_address': '127.0.0.1:9050',
285 'mirror': self.default_mirror,
286 'force_en-US': False,
289 if os.path.isfile(self.paths['settings_file']):
290 settings = json.load(open(self.paths['settings_file']))
294 settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
296 # make sure settings file is up-to-date
297 for setting in default_settings:
298 if setting not in settings:
299 settings[setting] = default_settings[setting]
302 # make sure tor_socks_address doesn't start with 'tcp:'
303 if settings['tor_socks_address'].startswith('tcp:'):
304 settings['tor_socks_address'] = settings['tor_socks_address'][4:]
307 # make sure the version is current
308 if settings['tbl_version'] != self.tbl_version:
309 settings['tbl_version'] = self.tbl_version
312 self.settings = settings
316 # if settings file is still using old pickle format, convert to json
317 elif os.path.isfile(self.paths['settings_file_pickle']):
318 self.settings = pickle.load(open(self.paths['settings_file_pickle']))
320 os.remove(self.paths['settings_file_pickle'])
324 self.settings = default_settings
328 def save_settings(self):
329 json.dump(self.settings, open(self.paths['settings_file'], 'w'))