diff --git a/src/common/config.py b/src/common/config.py index 75cc1a13d..7de2de3e3 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -384,6 +384,9 @@ class Config: 'last_archiving_time': [opt_str, '1970-01-01T00:00:00Z', _('Last time we syncronized with logs from server.')], 'enable_message_carbons': [ opt_bool, False, _('If enabled and if server supports this feature, Gajim will receive messages sent and received by other resources.')], 'ft_send_local_ips': [ opt_bool, True, _('If enabled, Gajim will send your local IPs so your contact can connect to your machine to transfer files.')], + 'oauth2_refresh_token': [ opt_str, '', _('Latest token for Oauth2 authentication.')], + 'oauth2_client_id': [ opt_str, '0000000044077801', _('client_id for Oauth2 authentication.')], + 'oauth2_redirect_url': [ opt_str, 'http%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for Oauth2 authentication.')], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], diff --git a/src/common/connection.py b/src/common/connection.py index 2067be7f0..2eaf4a2dc 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -40,6 +40,7 @@ import operator import time import locale import hmac +import json try: randomsource = random.SystemRandom() @@ -2492,6 +2493,32 @@ class Connection(CommonConnection, ConnectionHandlers): def get_password(self, callback, type_): self.pasword_callback = (callback, type_) + if type_ == 'X-MESSENGER-OAUTH2': + client_id = gajim.config.get_per('accounts', self.name, + 'oauth2_client_id') + refresh_token = gajim.config.get_per('accounts', self.name, + 'oauth2_refresh_token') + if refresh_token: + renew_URL = 'https://oauth.live.com/token?client_id=' \ + '%(client_id)s&redirect_uri=https%%3A%%2F%%2Foauth.live.' \ + 'com%%2Fdesktop&grant_type=refresh_token&refresh_token=' \ + '%(refresh_token)s' % locals() + result = helpers.download_image(self.name, {'src': renew_URL})[0] + if result: + dict_ = json.loads(result) + if 'access_token' in dict_: + self.set_password(dict_['access_token']) + return + script_url = gajim.config.get_per('accounts', self.name, + 'oauth2_redirect_url') + token_URL = 'https://oauth.live.com/authorize?client_id=' \ + '%(client_id)s&scope=wl.messenger%%20wl.offline_access&' \ + 'response_type=code&redirect_uri=%(script_url)s' % locals() + helpers.launch_browser_mailer('url', token_URL) + self.disconnect(on_purpose=True) + gajim.nec.push_incoming_event(Oauth2CredentialsRequiredEvent(None, + conn=self)) + return if self.password: self.set_password(self.password) return diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py index 99e0617dc..28068a812 100644 --- a/src/common/connection_handlers_events.py +++ b/src/common/connection_handlers_events.py @@ -1783,6 +1783,10 @@ class PasswordRequiredEvent(nec.NetworkIncomingEvent): name = 'password-required' base_network_events = [] +class Oauth2CredentialsRequiredEvent(nec.NetworkIncomingEvent): + name = 'oauth2-credentials-required' + base_network_events = [] + class FailedDecryptEvent(nec.NetworkIncomingEvent): name = 'failed-decrypt' base_network_events = [] diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index dd7d13097..1063ed9a7 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -264,6 +264,12 @@ class SASL(PlugIn): self._owner._caller.get_password(self.set_password, self.mechanism) self.startsasl = SASL_IN_PROCESS raise NodeProcessed + if 'X-MESSENGER-OAUTH2' in self.mecs: + self.mecs.remove('X-MESSENGER-OAUTH2') + self.mechanism = 'X-MESSENGER-OAUTH2' + self._owner._caller.get_password(self.set_password, self.mechanism) + self.startsasl = SASL_IN_PROCESS + raise NodeProcessed self.startsasl = SASL_FAILURE log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and ' 'PLAIN mecanisms.') @@ -497,6 +503,10 @@ class SASL(PlugIn): '\n', '') node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'}, payload=[sasl_data]) + elif self.mechanism == 'X-MESSENGER-OAUTH2': + node = Node('auth', attrs={'xmlns': NS_SASL, + 'mechanism': 'X-MESSENGER-OAUTH2'}) + node.addData(password) self._owner.send(str(node)) diff --git a/src/dialogs.py b/src/dialogs.py index 5e348eeff..647b6f96b 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -2165,6 +2165,7 @@ class DoubleInputDialog: def on_okbutton_clicked(self, widget): user_input1 = self.input_entry1.get_text().decode('utf-8') user_input2 = self.input_entry2.get_text().decode('utf-8') + self.cancel_handler = None self.dialog.destroy() if not self.ok_handler: return diff --git a/src/gajim.py b/src/gajim.py index 4042f031a..e02a1a8f4 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -63,7 +63,7 @@ if os.name == 'nt': os.environ['PATH'] = ';'.join(new_list) from common import demandimport -#demandimport.enable() +demandimport.enable() demandimport.ignore += ['gobject._gobject', 'libasyncns', 'i18n', 'logging.NullHandler', 'dbus.glib', 'dbus.service', 'command_system.implementation.standard', diff --git a/src/gui_interface.py b/src/gui_interface.py index 13ce60d7e..00852f2d0 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -701,6 +701,30 @@ class Interface: _('Password Required'), text, _('Save password'), ok_handler=on_ok, cancel_handler=on_cancel) + def handle_oauth2_credentials(self, obj): + account = obj.conn.name + def on_ok(refresh): + gajim.config.set_per('accounts', account, 'oauth2_refresh_token', + refresh) + st = gajim.config.get_per('accounts', account, 'last_status') + msg = helpers.from_one_line(gajim.config.get_per('accounts', + account, 'last_status_msg')) + gajim.interface.roster.send_status(account, st, msg) + del self.pass_dialog[account] + + def on_cancel(): + gajim.config.set_per('accounts', account, 'oauth2_refresh_token', + '') + self.roster.set_state(account, 'offline') + self.roster.update_status_combobox() + del self.pass_dialog[account] + + instruction = _('Please copy / paste the refresh token from the website' + ' that has just been opened.') + self.pass_dialog[account] = dialogs.InputTextDialog( + _('Oauth2 Credentials'), instruction, is_modal=False, + ok_handler=on_ok, cancel_handler=on_cancel) + def handle_event_roster_info(self, obj): #('ROSTER_INFO', account, (jid, name, sub, ask, groups)) account = obj.conn.name @@ -1412,6 +1436,7 @@ class Interface: 'metacontacts-received': [self.handle_event_metacontacts], 'muc-admin-received': [self.handle_event_gc_affiliation], 'muc-owner-received': [self.handle_event_gc_config], + 'oauth2-credentials-required': [self.handle_oauth2_credentials], 'our-show': [self.handle_event_status], 'password-required': [self.handle_event_password_required], 'plain-connection': [self.handle_event_plain_connection],