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 = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
76 default_locale = locale.getlocale(locale.LC_MESSAGES)[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 # build all relevant paths
92 def build_paths(self, tbb_version=None):
93 homedir = os.getenv('HOME')
95 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
96 if not os.path.exists(homedir):
98 os.mkdir(homedir, 0o700)
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)
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)
111 if self.architecture == 'x86_64':
116 if hasattr(self, 'settings') and self.settings['force_en-US']:
119 language = self.language
120 tarball_filename = 'tor-browser-' + arch + '-' + tbb_version + '_' + language + '.tar.xz'
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
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'
134 'config': tbb_config,
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'),
144 'tor_browser_developers': os.path.join(SHARE, 'tor-browser-developers.asc')
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',
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'
164 # Add the expected fingerprint for imported keys:
165 self.fingerprints = {
166 'tor_browser_developers': 'EF6E286DDA85EA2A4BA7DE684E2C6E8793298290'
173 if not os.path.exists(path):
174 os.makedirs(path, 0o700)
177 print(_("Cannot create directory {0}").format(path))
179 if not os.access(path, os.W_OK):
180 print(_("{0} is not writable").format(path))
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'])
191 def refresh_keyring(self, fingerprint=None):
192 if fingerprint is not None:
193 print('Refreshing local keyring... Missing key: ' + fingerprint)
195 print('Refreshing local keyring...')
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)
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)
216 print('Keyring refreshed successfully...')
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
223 :returns: ``True`` if the key is now within the keyring (or was
224 previously and hasn't changed). ``False`` otherwise.
226 with gpg.Context() as c:
227 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.paths['gnupg_homedir'])
229 impkey = self.paths['signing_keys'][key]
231 c.op_import(gpg.Data(file=impkey))
235 result = c.op_import_result()
236 if result and self.fingerprints[key] in result.imports[0].fpr:
242 def import_keys(self):
243 """Import all GnuPG keys.
245 :returns: ``True`` if all keys were successfully imported; ``False``
248 keys = ['tor_browser_developers', ]
249 all_imports_succeeded = True
252 imported = self.import_key_and_check_status(key)
254 print(_('Could not import key with fingerprint: %s.'
255 % self.fingerprints[key]))
256 all_imports_succeeded = False
258 if not all_imports_succeeded:
259 print(_('Not all keys were imported successfully!'))
261 return all_imports_succeeded
264 def load_mirrors(self):
266 for srcfile in self.paths['mirrors_txt']:
267 if not os.path.exists(srcfile):
269 for mirror in open(srcfile, 'r').readlines():
270 if mirror.strip() not in self.mirrors:
271 self.mirrors.append(mirror.strip())
274 def load_settings(self):
276 'tbl_version': self.tbl_version,
278 'download_over_tor': False,
279 'tor_socks_address': '127.0.0.1:9050',
280 'mirror': self.default_mirror,
281 'force_en-US': False,
284 if os.path.isfile(self.paths['settings_file']):
285 settings = json.load(open(self.paths['settings_file']))
289 settings['installed'] = os.path.isfile(self.paths['tbb']['start'])
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]
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:]
302 # make sure the version is current
303 if settings['tbl_version'] != self.tbl_version:
304 settings['tbl_version'] = self.tbl_version
307 self.settings = settings
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']))
315 os.remove(self.paths['settings_file_pickle'])
319 self.settings = default_settings
323 def save_settings(self):
324 json.dump(self.settings, open(self.paths['settings_file'], 'w'))