From d25b5b4bbafbafe3931c643db1f700334e266d1e Mon Sep 17 00:00:00 2001 From: Stavros Sachtouris <saxtouri@admin.grnet.gr> Date: Thu, 23 Apr 2015 15:40:28 +0300 Subject: [PATCH] Logout remote connection after getting credentials When a user clicks on "Get credentials", a new window appears where users can authenticate themselves. It is a remote window implementing whatever mechanism the target cloud implements. When the authentication window opens, the "Get credentials" button disappears. After the authentication is complete or canceled, the following operations take place synchronously and the order they appear bellow: - if the authentication window is open, close it - remove all cookies related to the account and pithos UI URLs - Reactivate the "Get credentials" button. --- agkyra/__init__.py | 2 +- agkyra/cli.py | 31 ++++++--- agkyra/gui.py | 16 ++--- agkyra/gui/menu.html | 147 ++++++++++++++++++++++++--------------- agkyra/gui/protocol.js | 9 +-- agkyra/gui/settings.html | 97 +++++++++++++++++++------- agkyra/gui/settings.js | 30 ++++++++ agkyra/protocol.py | 50 ++++++------- 8 files changed, 248 insertions(+), 134 deletions(-) diff --git a/agkyra/__init__.py b/agkyra/__init__.py index a82995d..11d27f8 100644 --- a/agkyra/__init__.py +++ b/agkyra/__init__.py @@ -1 +1 @@ - __version__ = '0.1' +__version__ = '0.1' diff --git a/agkyra/cli.py b/agkyra/cli.py index 6e6e0c7..d8c0412 100644 --- a/agkyra/cli.py +++ b/agkyra/cli.py @@ -1,16 +1,20 @@ import cmd import sys import logging -import json -from titanic import setup, syncer -from titanic.pithos_client import PithosFileClient -from titanic.localfs_client import FilesystemFileClient -import config +from agkyra.syncer import setup, syncer +from agkyra.syncer.pithos_client import PithosFileClient +from agkyra.syncer.localfs_client import LocalfsFileClient +from agkyra import config # from config import AgkyraConfig -logging.basicConfig(filename='agkyra.log', level=logging.DEBUG) LOG = logging.getLogger(__name__) +LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR)) +LOG.setLevel(logging.CRITICAL) + +SYNCER_LOG = logging.getLogger('agkyra.syncer') +SYNCER_LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR)) +SYNCER_LOG.setLevel(logging.CRITICAL) setup.GLOBAL_SETTINGS_NAME = config.AGKYRA_DIR @@ -41,11 +45,12 @@ class AgkyraCLI(cmd.Cmd): # Init syncer master = PithosFileClient(self.settings) - slave = FilesystemFileClient(self.settings) + slave = LocalfsFileClient(self.settings) self.syncer = syncer.FileSyncer(self.settings, master, slave) def preloop(self): """This runs when the shell loads""" + print 'Loading Agkyra (sometimes this takes a while)' if not self.is_shell: self.is_shell = True self.prompt = '\xe2\x9a\x93 ' @@ -128,14 +133,20 @@ class AgkyraCLI(cmd.Cmd): def do_start(self, line): """Start syncing""" - self.syncer.run() + if not getattr(self, '_syncer_initialized', False): + self.syncer.probe_and_sync_all() + self._syncer_initialized = True + if self.syncer.paused: + self.syncer.start_decide() def do_pause(self, line): """Pause syncing""" + if not self.syncer.paused: + self.syncer.pause_decide() def do_status(self, line): """Get current status (running/paused, progress)""" - print 'I have no idea' + print 'paused' if self.syncer.paused else 'running' # def do_shell(self, line): # """Run system, shell commands""" @@ -181,4 +192,4 @@ class AgkyraCLI(cmd.Cmd): # AgkyraCLI().run_onecmd(sys.argv) # or run a shell with -AgkyraCLI().cmdloop() +# AgkyraCLI().cmdloop() diff --git a/agkyra/gui.py b/agkyra/gui.py index 5e0068b..25c6804 100644 --- a/agkyra/gui.py +++ b/agkyra/gui.py @@ -1,6 +1,6 @@ from wsgiref.simple_server import make_server # from ws4py.websocket import EchoWebSocket -from protocol import WebSocketProtocol +from agkyra.protocol import WebSocketProtocol from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.client import WebSocketBaseClient @@ -20,11 +20,10 @@ LOG = logging.getLogger(__name__) class GUI(WebSocketBaseClient): """Launch the GUI when the helper server is ready""" - def __init__(self, addr, gui_exec_path, gui_id): + def __init__(self, addr, gui_id): """Initialize the GUI Launcher""" super(GUI, self).__init__(addr) self.addr = addr - self.gui_exec_path = gui_exec_path self.gui_id = gui_id self.start = self.connect @@ -39,11 +38,10 @@ class GUI(WebSocketBaseClient): with NamedTemporaryFile(delete=False) as fp: json.dump(dict(gui_id=self.gui_id, address=self.addr), fp) # subprocess.call blocks the execution - LOG.debug('RUN: %s %s' % (self.gui_exec_path, fp.name)) + LOG.debug('RUN: %s' % (fp.name)) subprocess.call([ - '/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw', - # self.gui_exec_path, - abspath('gui/gui.nw'), + abspath('agkyra/nwjs/nw'), + abspath('agkyra/gui.nw'), fp.name, '--data-path', abspath('~/.agkyra')]) LOG.debug('GUI process closed, remove temp file') @@ -85,11 +83,11 @@ class HelperServer(object): t.join() -def run(gui_exec_path): +def run(): """Prepare helper and GUI and run them in the proper order""" server = HelperServer() addr = 'ws://localhost:%s' % server.port - gui = GUI(addr, gui_exec_path, server.gui_id) + gui = GUI(addr, server.gui_id) LOG.info('Start helper server') server.start() diff --git a/agkyra/gui/menu.html b/agkyra/gui/menu.html index 7ba6e4f..29c67d1 100644 --- a/agkyra/gui/menu.html +++ b/agkyra/gui/menu.html @@ -4,6 +4,7 @@ <body> <script src="protocol.js"></script> <script src="settings.js"></script> + <script src="static/js/jquery.js"></script> <script type="text/javascript"> // Setup GUI @@ -42,7 +43,7 @@ menu.append(new gui.MenuItem({type: 'separator'})); pause_item = new gui.MenuItem({ // pause menu item icon: 'images/play_pause.png', - label: '', + label: 'NOT READY', type: 'normal', click: function() { if (paused) {post_start(socket);} else {post_pause(socket);} @@ -51,84 +52,42 @@ pause_item = new gui.MenuItem({ pause_item.enabled = false; menu.append(pause_item); -// Update progress -window.setInterval(function() { - var status = globals['status']; - var new_progress = progress_item.label; - var new_pause = pause_item.label; - var menu_modified = false; - if (status['paused'] !== null) { - switch(pause_item.label) { - case pause_syncing: if (status['paused']) { - // Update to "Paused - start syncing" - paused = true; - new_pause = start_syncing; - menu_modified = true; - } // else continue syncing - new_progress = status['progress'] + '%' + ' synced'; - break; - case start_syncing: if (status['paused']) return; - // else update to "Syncing - pause syncing" - paused = false; - new_pause = pause_syncing; - new_progress = status['progress'] + '%' + ' synced'; - menu_modified = true; - break; - default: - if (status['paused']) {new_pause = start_syncing; paused=true;} - else {new_pause = pause_syncing; paused=false;} - new_progress = status['progress'] + '%' + ' synced'; - pause_item.enabled = true; - menu_modified = true; - } - } - if (new_pause != pause_item.label) { - pause_item.label = new_pause; - menu_modified = true; - } - if (new_progress != progress_item.label) { - progress_item.label = new_progress; - menu_modified = true; - } - if (menu_modified) { - if (paused) progress_item.label += ' - paused'; - tray.menu = menu; - } - get_status(socket); -}, 1500); - // Menu actions contents -menu.append(new gui.MenuItem({ +var local_folder_menu = new gui.MenuItem({ label: 'Open local folder', icon: 'images/folder.png', + enabled: false, click: function () { var dir = globals['settings']['directory']; console.log('Open ' + dir); gui.Shell.showItemInFolder(dir); } -})); +}) +menu.append(local_folder_menu); -menu.append(new gui.MenuItem({ +var pithos_page_menu = new gui.MenuItem({ label: 'Launch Pithos+ page', icon: 'images/pithos.png', + enabled: false, click: function () { - var pithos_ui = globals['settings']['pithos_ui']; - console.log('Visit ' + pithos_ui); - gui.Shell.openExternal(pithos_ui); + console.log('Visit ' + get_pithos_ui()); + gui.Shell.openExternal(get_pithos_ui()); } -})); +}); +menu.append(pithos_page_menu); // Settings and About menu.append(new gui.MenuItem({type: 'separator'})); -menu.append(new gui.MenuItem({ +var settings_menu = new gui.MenuItem({ label: 'Settings', icon: 'images/settings.png', + enabled: false, click: function () { export_settings(globals.settings); if (windows['settings']) windows['settings'].close(); windows['settings'] = gui.Window.open("settings.html", { toolbar: false, focus: true, - width: 841, height: 520, + width: 841, height: 520 }); windows['settings'].on('closed', function() { new_settings = import_settings(); @@ -137,7 +96,8 @@ menu.append(new gui.MenuItem({ get_settings(socket); }); }, -})); +}); +menu.append(settings_menu); menu.append(new gui.MenuItem({ label: 'About', @@ -159,6 +119,79 @@ menu.append(new gui.MenuItem({ click: function() {post_shutdown(socket);} })); + +// Update progress +var client_ready = false; +window.setInterval(function() { + var menu_modified = false; + if (!client_ready) { + if (!globals.authenticated) return; + client_ready = true; + } + + if (client_ready) { + if (!settings_menu.enabled) { + if (globals.settings.url) refresh_endpoints(globals.settings.url); + settings_menu.enabled = true; + tray.menu = menu; + } + if (globals.settings.url && !pithos_page_menu.enabled) { + if (get_pithos_ui() != null) { + pithos_page_menu.enabled = true; + tray.menu = menu; + } else { refresh_endpoints(globals.settings.url); } + } + if (!local_folder_menu.enabled) { + if (globals.settings.directory) { + local_folder_menu.enabled = true; + tray.menu = menu; + } + } + } + + var status = globals['status']; + var new_progress = progress_item.label; + var new_pause = pause_item.label; + if (status['paused'] !== null) { + switch(pause_item.label) { + case pause_syncing: if (status['paused']) { + // Update to "Paused - start syncing" + paused = true; + new_pause = start_syncing; + menu_modified = true; + } // else continue syncing + new_progress = status['progress'] + '%' + ' synced'; + break; + case start_syncing: if (status['paused']) return; + // else update to "Syncing - pause syncing" + paused = false; + new_pause = pause_syncing; + new_progress = status['progress'] + '%' + ' synced'; + menu_modified = true; + break; + default: + if (status['paused']) {new_pause = start_syncing; paused=true;} + else {new_pause = pause_syncing; paused=false;} + new_progress = status['progress'] + '%' + ' synced'; + pause_item.enabled = true; + menu_modified = true; + } + } + if (new_pause != pause_item.label) { + pause_item.label = new_pause; + menu_modified = true; + } + if (new_progress != progress_item.label) { + progress_item.label = new_progress; + menu_modified = true; + } + if (menu_modified) { + if (paused) progress_item.label += ' - paused'; + tray.menu = menu; + } + get_status(socket); +}, 1500); + tray.menu = menu; </script> diff --git a/agkyra/gui/protocol.js b/agkyra/gui/protocol.js index 44f79a2..58bce87 100644 --- a/agkyra/gui/protocol.js +++ b/agkyra/gui/protocol.js @@ -16,10 +16,10 @@ var globals = { 'url': null, 'container': null, 'directory': null, - 'pithos_ui': null, 'exclude': null }, - 'status': {"progress": null, "paused": null} + 'status': {"progress": null, "paused": null}, + 'authenticated': false, } // Protocol: requests ::: responses @@ -65,12 +65,13 @@ socket.onopen = function() { } socket.onmessage = function(e) { var r = JSON.parse(e.data) - //console.log('RECV: ' + r['action']) + console.log('RECV: ' + r['action']) switch(r['action']) { case 'post gui_id': if (r['ACCEPTED'] === 202) { get_settings(this); get_status(this); + globals.authenticated = true; } else { console.log('Helper: ' + JSON.stringify(r)); closeWindows(); @@ -85,7 +86,7 @@ socket.onmessage = function(e) { } break; case 'get settings': - console.log(r); + // console.log(r); globals['settings'] = r; break; case 'put settings': diff --git a/agkyra/gui/settings.html b/agkyra/gui/settings.html index adf0661..85596c6 100644 --- a/agkyra/gui/settings.html +++ b/agkyra/gui/settings.html @@ -9,6 +9,7 @@ <script type="text/javascript"> var fs = require('fs'); var exclude = null; + remove_tokens = null; $(document).ready(function() { var url = get_setting('url'); if (url) $('#cloud-url').val(url); @@ -18,16 +19,11 @@ if (container) $('#container').val(container); var directory = get_setting('directory'); if (directory) $('#directory').html(directory); - exclude = get_setting('exclude'); - if (exclude) $('#exclude').val( - fs.readFileSync(exclude, encoding='utf-8')); - var pithos_ui = get_setting('pithos_ui'); - if (pithos_ui) { - $('#get_creds').show(); - } else { - console.log('No pithos view, remove credential button'); - $('#get_creds').hide(); - } + var exclude = get_setting('exclude'); + if (exclude) try { + $('#exclude').val( + fs.readFileSync(exclude, encoding='utf-8')); + } catch (err) {console.log(err);} }); function update_exclude(new_content) { @@ -45,30 +41,79 @@ //set_setting('token', $(this).val()) } + function remove_cookies(win, url) { + var removed_at_least_one = false + win.cookies.getAll({url: url}, function(cookies) { + $.each(cookies, function(i, cookie) { + console.log('I have a cookie to remove ' + cookie.name + ', ' + cookie.domain); + win.cookies.remove({url: url, name: cookie.name} ); + removed_at_least_one = true; + }); + }); + return removed_at_least_one; + } + var gui = require('nw.gui'); - function extract_cookie(url) { + var cred_win = null; + var logout_win = null; + var can_close_cred = false; + var got_cookie = false; + var can_close_logout = false; + var show_creds = true; + function get_credentials() { var cookie_name = '_pithos2_a'; - var logout_url = 'https://accounts.okeanos.grnet.gr/ui/logout' - var w = gui.Window.open(logout_url, { - focus: false, width: 20, height: 20 - }); - w.close(); - var w = gui.Window.open(logout_url, { - focus: true, width: 520, height: 920 + var lurl = get_account_ui() + '/logout?next=' + get_pithos_ui() + + show_creds = false; + $('#get_creds').hide(); + + got_cookie = false; + cred_win = gui.Window.open(lurl, { + focus: true, width: 820, height: 580, toolbar: false }); - w.cookies.onChanged.addListener(function(info) { + + cred_win.cookies.onChanged.addListener(function(info) { if (info.cookie.name === cookie_name) { console.log('Succesfully logged in'); extract_credentials(info.cookie); - w.close(); + got_cookie = true; } }); - } + cred_win.on('loaded', function() { + if (got_cookie) can_close_cred = true; + }); - function get_credentials() { - var pithos_ui = get_setting('pithos_ui'); - extract_cookie(pithos_ui); + cred_win.on('closed', function() { + logout_win = gui.Window.open( + get_account_ui() + '/logout', + {focus: true, width:20, height: 20 }); + logout_win.hide(); + logout_win.on('loaded', function() { + while( + remove_cookies(logout_win, get_account_ui()) || + remove_cookies(logout_win, get_pithos_ui())) {} + can_close_logout = true; + }); + }); } + + window.setInterval(function() { + // Refresh get_creds visibility, until refresh_endpoints + // changes are in effect + if (get_pithos_ui() && show_creds) { + $('#get_creds').show(); + } else {$('#get_creds').hide(); } + // Garbage collector + if (can_close_logout) { + can_close_logout = false; + logout_win.close(); + show_creds = true; + } + else if (can_close_cred) { + can_close_cred = false; + cred_win.close(); + } + }, 500); </script> </head> <body> @@ -88,7 +133,7 @@ </div> <div class="small-9 columns"> <input type="text" id="cloud-url" placeholder="Authentication URL" - onchange="set_setting('url', $(this).val())"> + onchange="set_setting('url', $(this).val()); refresh_endpoints($(this).val());"> <small class="error" style="visibility: hidden">Invalid entry</small> </div> </div> @@ -107,7 +152,7 @@ <div class="clearfix"> <a id="get_creds" class="button right" style="display: none;" - onclick="get_credentials();">Get credentials</a> + onclick="get_credentials();">Login to get credentials</a> </div> </fieldset> diff --git a/agkyra/gui/settings.js b/agkyra/gui/settings.js index 0df6d76..a3d1e91 100644 --- a/agkyra/gui/settings.js +++ b/agkyra/gui/settings.js @@ -17,3 +17,33 @@ function get_setting(key) { function set_setting(key, val) { global.settings[key] = val; } + +function refresh_endpoints(identity_url) { + $.post(identity_url + '/tokens', function(data) { + var endpoints = data.access.serviceCatalog + global.pithos_ui = null; + global.account_ui = null; + $.each(endpoints, function(i, endpoint) { + switch(endpoint.type) { + case 'object-store': try { + global.pithos_ui = endpoint['endpoints'][0]['SNF:uiURL']; + } catch(err) { console.log('Failed to get pithos_ui ' + err); } + break; + case 'account': try { + global.account_ui = endpoint['endpoints'][0]['SNF:uiURL']; + } catch(err) { console.log('Failed to get account_ui ' + err); } + break; + } + }); + }); +} + +function get_pithos_ui() { + if (global.pithos_ui) {return global.pithos_ui;} + else {return null;} +} + +function get_account_ui() { + if (global.account_ui) {return global.account_ui;} + else {return null;} +} diff --git a/agkyra/protocol.py b/agkyra/protocol.py index 8ed1f20..1641bd3 100644 --- a/agkyra/protocol.py +++ b/agkyra/protocol.py @@ -2,10 +2,8 @@ from ws4py.websocket import WebSocket import json import logging from os.path import abspath -from titanic import syncer -from config import AgkyraConfig -from kamaki.clients.astakos import AstakosClient -from kamaki.clients.pithos import PithosClient +from agkyra.syncer import syncer +from agkyra.config import AgkyraConfig LOG = logging.getLogger(__name__) @@ -72,33 +70,30 @@ class WebSocketProtocol(WebSocket): cnf = AgkyraConfig() def _load_settings(self): + LOG.debug('Start loading settings') sync = self.cnf.get('global', 'default_sync') cloud = self.cnf.get_sync(sync, 'cloud') - url = self.cnf.get_cloud(cloud, 'url') - token = self.cnf.get_cloud(cloud, 'token') - - astakos = AstakosClient(url, token) - self.settings['url'], self.settings['token'] = url, token - try: - endpoints = astakos.get_endpoints()['access']['serviceCatalog'] - for endpoint in endpoints: - if endpoint['type'] == PithosClient.service_type: - pithos_ui = endpoint['endpoints'][0]['SNF:uiURL'] - self.settings['pithos_ui'] = pithos_ui - break - except Exception as e: - LOG.debug('Failed to retrieve pithos_ui: %s' % e) + self.settings['url'] = self.cnf.get_cloud(cloud, 'url') + except Exception: + self.settings['url'] = None + try: + self.settings['token'] = self.cnf.get_cloud(cloud, 'token') + except Exception: + self.settings['url'] = None for option in ('container', 'directory', 'exclude'): self.settings[option] = self.cnf.get_sync(sync, option) + LOG.debug('Finished loading settings') + def _dump_settings(self): + LOG.debug('Saving settings') sync = self.cnf.get('global', 'default_sync') cloud = self.cnf.get_sync(sync, 'cloud') - old_url = self.cnf.get_cloud(cloud, 'url') + old_url = self.cnf.get_cloud(cloud, 'url') or '' while old_url != self.settings['url']: cloud = '%s_%s' % (cloud, sync) try: @@ -113,6 +108,9 @@ class WebSocketProtocol(WebSocket): for option in ('directory', 'container', 'exclude'): self.cnf.set_sync(sync, option, self.settings[option]) + self.cnf.write() + LOG.debug('Settings saved') + # Syncer-related methods def get_status(self): from random import randint @@ -121,7 +119,6 @@ class WebSocketProtocol(WebSocket): return self.status def get_settings(self): - self._load_settings() return self.settings def set_settings(self, new_settings): @@ -137,7 +134,6 @@ class WebSocketProtocol(WebSocket): # WebSocket connection methods def opened(self): LOG.debug('Helper: connection established') - self._load_settings() def closed(self, *args): LOG.debug('Helper: connection closed') @@ -149,7 +145,6 @@ class WebSocketProtocol(WebSocket): # Protocol handling methods def _post(self, r): """Handle POST requests""" - LOG.debug('CALLED with %s' % r) if self.accepted: action = r['path'] if action == 'shutdown': @@ -161,6 +156,7 @@ class WebSocketProtocol(WebSocket): }[action]() self.send_json({'OK': 200, 'action': 'post %s' % action}) elif r['gui_id'] == self.gui_id: + self._load_settings() self.accepted = True self.send_json({'ACCEPTED': 202, 'action': 'post gui_id'}) else: @@ -170,16 +166,16 @@ class WebSocketProtocol(WebSocket): def _put(self, r): """Handle PUT requests""" - if not self.accepted: - action = r['path'] - self.send_json({'UNAUTHORIZED': 401, 'action': 'put %s' % action}) - self.terminate() - else: + if self.accepted: LOG.debug('put %s' % r) action = r.pop('path') self.set_settings(r) r.update({'CREATED': 201, 'action': 'put %s' % action}) self.send_json(r) + else: + action = r['path'] + self.send_json({'UNAUTHORIZED': 401, 'action': 'put %s' % action}) + self.terminate() def _get(self, r): """Handle GET requests""" -- GitLab