]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
oops, can't have a comment before the #!
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2 """
3 Tor Browser Launcher
4 https://github.com/micahflee/torbrowser-launcher/
5
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28 """
29
30 import gettext
31 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
32
33 from twisted.internet import gtk2reactor
34 gtk2reactor.install()
35 from twisted.internet import reactor
36
37 import pygtk
38 pygtk.require('2.0')
39 import gtk
40
41 import os, sys, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
42
43 from twisted.web.client import Agent, ResponseDone
44 from twisted.web.http_headers import Headers
45 from twisted.internet.protocol import Protocol
46 from twisted.internet.ssl import ClientContextFactory
47
48 from OpenSSL.SSL import Context, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT
49 from OpenSSL.crypto import load_certificate, FILETYPE_PEM
50
51 class VerifyTorProjectCert(ClientContextFactory):
52
53     def __init__(self, torproject_pem):
54         self.torproject_ca = load_certificate(FILETYPE_PEM, open(torproject_pem, 'r').read())
55
56     def getContext(self, host, port):
57         ctx = ClientContextFactory.getContext(self)
58         ctx.set_verify_depth(0)
59         ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
60         return ctx
61
62     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
63         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
64
65
66 class TorBrowserLauncher:
67     def __init__(self):
68         # initialize the app
69         self.set_gui(None, '', [])
70         self.discover_arch_lang()
71         self.build_paths()
72         self.mkdir(self.paths['dir']['download'])
73         self.mkdir(self.paths['dir']['tbb'])
74         self.init_gnupg()
75
76         # allow buttons to have icons
77         try:
78             settings = gtk.settings_get_default()
79             settings.props.gtk_button_images = True
80         except:
81             pass
82
83         self.launch_gui = True
84
85         # if we haven't already hit an error
86         if self.gui != 'error':
87             # load settings
88             if self.load_settings():
89                 self.build_paths(self.settings['latest_version'])
90
91                 # is tbb already running and we just need to open a new firefox?
92                 if self.settings['installed_version']:
93                     vidalia_pid = None
94                     firefox_pid = None
95                     for p in psutil.process_iter():
96                         try:
97                             exe = None
98
99                             # old versions of psutil don't have exe
100                             if hasattr(p, 'exe'):
101                                 exe = p.exe
102                             # need to rely on cmdline instead
103                             else:
104                                 if len(p.cmdline) > 0:
105                                     exe = p.cmdline[0]
106                             
107                             if exe == self.paths['file']['vidalia_bin'] or exe == './App/vidalia':
108                                 vidalia_pid = p.pid
109                             if exe == self.paths['file']['firefox_bin']:
110                                 firefox_pid = p.pid
111
112                         except:
113                             pass
114
115                     if vidalia_pid and not firefox_pid:
116                         print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
117                         subprocess.Popen([self.paths['file']['firefox_bin'], '-no-remote', '-profile', self.paths['file']['firefox_profile']])
118                         return
119                     elif vidalia_pid and firefox_pid:
120                         print _('Vidalia and Firefox are already open, bringing them to focus')
121
122                         # figure out the window ids of vidalia and firefox
123                         vidalia_win_id = None
124                         firefox_win_id = None
125                         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
126                         for line in p.stdout.readlines():
127                             line_split = line.split()
128                             win_id = line_split[0]
129                             win_pid = int(line_split[2])
130                             if win_pid == vidalia_pid:
131                                 vidalia_win_id = win_id
132                             if win_pid == firefox_pid:
133                                 firefox_win_id = win_id
134
135                         # bring firefox to front, then vidalia
136                         if firefox_win_id:
137                             subprocess.call(['wmctrl', '-i', '-a', firefox_win_id])
138                         if vidalia_win_id:
139                             subprocess.call(['wmctrl', '-i', '-a', vidalia_win_id])
140
141                         return
142
143                 # how long was it since the last update check?
144                 # 86400 seconds = 24 hours
145                 current_timestamp = int(time.time())
146                 if current_timestamp - self.settings['last_update_check_timestamp'] >= 86400:
147                     # check for update
148                     print 'Checking for update'
149                     self.set_gui('task', _("Checking for Tor Browser update."), 
150                         ['download_update_check', 
151                          'attempt_update'])
152
153                 else:
154                     # no need to check for update
155                     print _('Checked for update within 24 hours, skipping')
156                     self.start_launcher()
157
158             else:
159                 self.set_gui('error', _("Error loading settings. Delete ~/.torbrowser and try again."), [])
160
161         if self.launch_gui:
162             # set up the window
163             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
164             self.window.set_title(_("Tor Browser"))
165             self.window.set_icon_from_file(self.paths['file']['icon'])
166             self.window.set_position(gtk.WIN_POS_CENTER)
167             self.window.set_border_width(10)
168             self.window.connect("delete_event", self.delete_event)
169             self.window.connect("destroy", self.destroy)
170
171             # build the rest of the UI
172             self.build_ui()
173
174     # download or run TBB
175     def start_launcher(self):
176       # is TBB already installed?
177       if os.path.isfile(self.paths['file']['start']) and os.access(self.paths['file']['start'], os.X_OK):
178         if self.settings['installed_version'] == self.settings['latest_version']:
179           # current version of tbb is installed, launch it
180           self.run(False)
181           self.launch_gui = False
182         elif self.settings['installed_version'] < self.settings['latest_version']:
183           # there is a tbb upgrade available
184           self.set_gui('task', _("Your Tor Browser is out of date."), 
185             ['download_tarball', 
186              'download_tarball_sig', 
187              'verify', 
188              'extract', 
189              'run'])
190         else:
191           # for some reason the installed tbb is newer than the current version?
192           self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
193
194       # not installed
195       else:
196           # are the tarball and sig already downloaded?
197           if os.path.isfile(self.paths['file']['tarball']) and os.path.isfile(self.paths['file']['tarball_sig']):
198               # start the gui with verify
199               self.set_gui('task', _("Installing Tor Browser."), 
200                   ['verify', 
201                    'extract', 
202                    'run'])
203
204           # first run
205           else:
206               self.set_gui('task', _("Downloading and installing Tor Browser."), 
207                   ['download_tarball', 
208                    'download_tarball_sig', 
209                    'verify', 
210                    'extract', 
211                    'run'])
212    
213     # discover the architecture and language
214     def discover_arch_lang(self):
215         # figure out the architecture
216         (sysname, nodename, release, version, machine) = os.uname()
217         self.architecture = machine
218
219         # figure out the language
220         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
221         default_locale = locale.getdefaultlocale()[0]
222         if default_locale == None:
223             self.language = 'en-US'
224         else:
225             self.language = default_locale.replace('_', '-')
226             if self.language not in available_languages:
227                 self.language = self.language.split('-')[0]
228                 if self.language not in available_languages:
229                     for l in available_languages:
230                         if l[0:2] == self.language:
231                             self.language = l
232             # if language isn't available, default to english
233             if self.language not in available_languages:
234                 self.language = 'en-US'
235
236     # build all relevant paths
237     def build_paths(self, tbb_version = None):
238         homedir = os.getenv('HOME')
239         if not homedir:
240             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
241             if os.path.exists(homedir) == False:
242                 try:
243                     os.mkdir(homedir, 0700)
244                 except:
245                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
246         if not os.access(homedir, os.W_OK):
247             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
248
249         tbb_data = '%s/.torbrowser' % homedir
250
251         if tbb_version:
252             tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
253             self.paths['file']['tarball'] = tbb_data+'/download/'+tarball_filename
254             self.paths['file']['tarball_sig'] = tbb_data+'/download/'+tarball_filename+'.asc'
255             self.paths['url']['tarball'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename
256             self.paths['url']['tarball_sig'] = 'https://www.torproject.org/dist/torbrowser/linux/'+tarball_filename+'.asc'
257             self.paths['filename']['tarball'] = tarball_filename
258             self.paths['filename']['tarball_sig'] = tarball_filename+'.asc'
259
260         else:
261             self.paths = {
262                 'dir': {
263                     'data': tbb_data,
264                     'download': tbb_data+'/download',
265                     'tbb': tbb_data+'/tbb/'+self.architecture,
266                     'gnupg_homedir': tbb_data+'/gnupg_homedir'
267                 },
268                 'file': {
269                     'settings': tbb_data+'/settings',
270                     'version': tbb_data+'/version',
271                     'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
272                     'vidalia_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
273                     'firefox_bin': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
274                     'firefox_profile': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
275                     'update_check': tbb_data+'/download/RecommendedTBBVersions',
276                     'icon': '/usr/share/pixmaps/torbrowser80.xpm',
277                     'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
278                     'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
279                     'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc'
280                 },
281                 'url': {
282                     'update_check': 'https://check.torproject.org/RecommendedTBBVersions'
283                 },
284                 'filename': {}
285             }
286
287     # create a directory
288     def mkdir(self, path):
289         try:
290             if os.path.exists(path) == False:
291                 os.makedirs(path, 0700)
292                 return True
293         except:
294             self.set_gui('error', _("Cannot create directory {0}").format(path), [], False)
295             return False
296         if not os.access(path, os.W_OK):
297             self.set_gui('error', _("{0} is not writable").format(path), [], False)
298             return False
299         return True
300
301     # if gnupg_homedir isn't set up, set it up
302     def init_gnupg(self):
303         if not os.path.exists(self.paths['dir']['gnupg_homedir']):
304             print _('Creating GnuPG homedir'), self.paths['dir']['gnupg_homedir']
305             if self.mkdir(self.paths['dir']['gnupg_homedir']):
306                 # import keys
307                 print _('Importing keys')
308                 p1 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['erinn_key']])
309                 p1.wait()
310                 p2 = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--import', self.paths['file']['sebastian_key']])
311                 p2.wait()
312
313     # there are different GUIs that might appear, this sets which one we want
314     def set_gui(self, gui, message, tasks, autostart=True):
315         self.gui = gui
316         self.gui_message = message
317         self.gui_tasks = tasks
318         self.gui_task_i = 0
319         self.gui_autostart = autostart
320
321     # set all gtk variables to False
322     def clear_ui(self):
323         if hasattr(self, 'box'):
324             self.box.destroy()
325         self.box = False
326
327         self.label = False
328         self.progressbar = False
329         self.button_box = False
330         self.start_button = False
331         self.exit_button = False
332
333     # build the application's UI
334     def build_ui(self):
335         self.box = gtk.VBox(False, 20)
336         self.window.add(self.box)
337
338         if self.gui == 'error':
339             # labels
340             self.label = gtk.Label( self.gui_message ) 
341             self.label.set_line_wrap(True)
342             self.box.pack_start(self.label, True, True, 0)
343             self.label.show()
344
345             #self.label2 = gtk.Label("You can fix the problem by deleting:\n"+self.paths['dir']['data']+"\n\nHowever, you will lose all your bookmarks and other Tor Browser preferences.") 
346             #self.label2.set_line_wrap(True)
347             #self.box.pack_start(self.label2, True, True, 0)
348             #self.label2.show()
349
350             # exit button
351             exit_image = gtk.Image()
352             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
353             self.exit_button = gtk.Button("Exit")
354             self.exit_button.set_image(exit_image)
355             self.exit_button.connect("clicked", self.destroy, None)
356             self.box.add(self.exit_button)
357             self.exit_button.show()
358
359         elif self.gui == 'task':
360             # label
361             self.label = gtk.Label( self.gui_message ) 
362             self.label.set_line_wrap(True)
363             self.box.pack_start(self.label, True, True, 0)
364             self.label.show()
365             
366             # progress bar
367             self.progressbar = gtk.ProgressBar(adjustment=None)
368             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
369             self.progressbar.set_pulse_step(0.01)
370             self.box.pack_start(self.progressbar, True, True, 0)
371
372             # button box
373             self.button_box = gtk.HButtonBox()
374             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
375             self.box.pack_start(self.button_box, True, True, 0)
376             self.button_box.show()
377
378             # start button
379             start_image = gtk.Image()
380             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
381             self.start_button = gtk.Button("Start")
382             self.start_button.set_image(start_image)
383             self.start_button.connect("clicked", self.start, None)
384             self.button_box.add(self.start_button)
385             if not self.gui_autostart:
386               self.start_button.show()
387
388             # exit button
389             exit_image = gtk.Image()
390             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
391             self.exit_button = gtk.Button("Exit")
392             self.exit_button.set_image(exit_image)
393             self.exit_button.connect("clicked", self.destroy, None)
394             self.button_box.add(self.exit_button)
395             self.exit_button.show()
396
397         self.box.show()
398         self.window.show()
399
400         if self.gui_autostart:
401             self.start(None)
402
403     # start button clicked, begin tasks
404     def start(self, widget, data=None):
405         # disable the start button
406         if self.start_button:
407             self.start_button.set_sensitive(False)
408
409         # start running tasks
410         self.run_task()
411       
412     # run the next task in the task list
413     def run_task(self):
414         self.refresh_gtk()
415
416         if self.gui_task_i >= len(self.gui_tasks):
417             self.destroy(False)
418             return
419
420         task = self.gui_tasks[self.gui_task_i]
421         
422         # get ready for the next task
423         self.gui_task_i += 1
424
425         if task == 'download_update_check':
426             print _('Downloading'), self.paths['url']['update_check']
427             self.download('update check', self.paths['url']['update_check'], self.paths['file']['update_check'])
428         
429         if task == 'attempt_update':
430             print _('Checking to see if update it needed')
431             self.attempt_update()
432
433         elif task == 'download_tarball':
434             print _('Downloading'), self.paths['url']['tarball']
435             self.download('tarball', self.paths['url']['tarball'], self.paths['file']['tarball'])
436
437         elif task == 'download_tarball_sig':
438             print _('Downloading'), self.paths['url']['tarball_sig']
439             self.download('signature', self.paths['url']['tarball_sig'], self.paths['file']['tarball_sig'])
440
441         elif task == 'verify':
442             print _('Verifying signature')
443             self.verify()
444
445         elif task == 'extract':
446             print _('Extracting'), self.paths['filename']['tarball']
447             self.extract()
448
449         elif task == 'run':
450             print _('Running'), self.paths['file']['start']
451             self.run()
452         
453         elif task == 'start_over':
454             print _('Starting download over again')
455             self.start_over()
456
457     def response_received(self, response):
458         class FileDownloader(Protocol):
459             def __init__(self, file, total, progress, done_cb):
460                 self.file = file
461                 self.total = total
462                 self.so_far = 0
463                 self.progress = progress
464                 self.all_done = done_cb
465
466             def dataReceived(self, bytes):
467                 self.file.write(bytes)
468                 self.so_far += len(bytes)
469                 percent = float(self.so_far) / float(self.total)
470                 self.progress.set_fraction(percent)
471                 amount = float(self.so_far)
472                 units = "bytes"
473                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
474                     if amount > size:
475                         units = unit
476                         amount = amount / float(size)
477                         break
478
479                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
480
481             def connectionLost(self, reason):
482                 print _('Finished receiving body:'), reason.getErrorMessage()
483                 self.all_done(reason)
484
485         dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
486         response.deliverBody(dl)
487
488     def response_finished(self, msg):
489         if msg.check(ResponseDone):
490             self.file_download.close()
491             # next task!
492             self.run_task()
493
494         else:
495             print "FINISHED", msg
496             ## FIXME handle errors
497
498     def download_error(self, f):
499         print _("Download error"), f
500         self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
501         self.clear_ui()
502         self.build_ui()
503
504     def download(self, name, url, path):
505         # initialize the progress bar
506         self.progressbar.set_fraction(0) 
507         self.progressbar.set_text(_('Downloading {0}').format(name))
508         self.progressbar.show()
509         self.refresh_gtk()
510
511         agent = Agent(reactor, VerifyTorProjectCert(self.paths['file']['torproject_pem']))
512         d = agent.request('GET', url,
513                           Headers({'User-Agent': ['torbrowser-launcher']}),
514                           None)
515
516         self.file_download = open(path, 'w')
517         d.addCallback(self.response_received).addErrback(self.download_error)
518         
519         if not reactor.running:
520             reactor.run()
521
522     def attempt_update(self):
523         # load the update check file
524         try:
525             versions = json.load(open(self.paths['file']['update_check']))
526             latest_version = None
527
528             end = '-Linux'
529             for version in versions:
530                 if str(version).find(end) != -1:
531                     latest_version = str(version)
532
533             if latest_version:
534                 self.settings['latest_version'] = latest_version[:-len(end)]
535                 self.settings['last_update_check_timestamp'] = int(time.time())
536                 self.save_settings()
537                 self.build_paths(self.settings['latest_version'])
538                 self.start_launcher()
539
540             else:
541                 # failed to find the latest version
542                 self.set_gui('error', _("Error checking for updates."), [], False)
543         
544         except:
545             # not a valid JSON object
546             self.set_gui('error', _("Error checking for updates."), [], False)
547
548         # now start over
549         self.clear_ui()
550         self.build_ui()
551
552     def verify(self):
553         # initialize the progress bar
554         self.progressbar.set_fraction(0) 
555         self.progressbar.set_text(_('Verifying Signature'))
556         self.progressbar.show()
557
558         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['dir']['gnupg_homedir'], '--verify', self.paths['file']['tarball_sig']])
559         self.pulse_until_process_exits(p)
560         
561         if p.returncode == 0:
562             self.run_task()
563         else:
564             self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False)
565             self.clear_ui()
566             self.build_ui()
567
568             if not reactor.running:
569                 reactor.run()
570
571     def extract(self):
572         # initialize the progress bar
573         self.progressbar.set_fraction(0) 
574         self.progressbar.set_text(_('Installing'))
575         self.progressbar.show()
576         self.refresh_gtk()
577
578         # make sure this file is a tarfile
579         if tarfile.is_tarfile(self.paths['file']['tarball']):
580           tf = tarfile.open(self.paths['file']['tarball'])
581           tf.extractall(self.paths['dir']['tbb'])
582         else:
583             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
584             self.clear_ui()
585             self.build_ui()
586
587         # installation is finished, so save installed_version
588         self.settings['installed_version'] = self.settings['latest_version']
589         self.save_settings()
590
591         self.run_task()
592
593     def run(self, run_next_task = True):
594         subprocess.Popen([self.paths['file']['start']])
595         if run_next_task:
596             self.run_task()
597
598     # make the progress bar pulse until process p (a Popen object) finishes
599     def pulse_until_process_exits(self, p):
600         while p.poll() == None:
601             time.sleep(0.01)
602             self.progressbar.pulse()
603             self.refresh_gtk()
604
605     # start over and download TBB again
606     def start_over(self):
607         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
608         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
609         self.gui_task_i = 0
610         self.start(None)
611
612     # load settings
613     def load_settings(self):
614         if os.path.isfile(self.paths['file']['settings']):
615             self.settings = pickle.load(open(self.paths['file']['settings']))
616             # sanity checks
617             if not 'installed_version' in self.settings:
618                 return False
619             if not 'latest_version' in self.settings:
620                 return False
621             if not 'last_update_check_timestamp' in self.settings:
622                 return False
623         else:
624             self.settings = {
625                 'installed_version': False,
626                 'latest_version': '0',
627                 'last_update_check_timestamp': 0
628             }
629             self.save_settings()
630         return True
631
632     # save settings
633     def save_settings(self):
634         pickle.dump(self.settings, open(self.paths['file']['settings'], 'w'))
635         return True
636     
637     # refresh gtk
638     def refresh_gtk(self):
639         while gtk.events_pending():
640             gtk.main_iteration(False)
641
642     # exit
643     def delete_event(self, widget, event, data=None):
644         return False
645     def destroy(self, widget, data=None):
646         if hasattr(self, 'file_download'):
647             self.file_download.close()
648         if reactor.running:
649             reactor.stop()
650
651 if __name__ == "__main__":
652     tor_browser_launcher_version = '0.0.1'
653
654     print _('Tor Browser Launcher')
655     print _('By Micah Lee, licensed under GPLv3')
656     print _('version {0}').format(tor_browser_launcher_version)
657     print 'https://github.com/micahflee/torbrowser-launcher'
658
659     app = TorBrowserLauncher()
660