diff --git a/config.py b/config.py index 91622b5fb..fa28ef0e1 100644 --- a/config.py +++ b/config.py @@ -44,12 +44,12 @@ experimentalFeatures = None version = None language = None -ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback']) +ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback', 'supports_auto_login']) supported_servers = { - "Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback'), + "Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback', True), # No point having SISI: https://developers.eveonline.com/blog/article/removing-datasource-singularity # "Singularity": ApiServer("Singularity", "sisilogin.testeveonline.com", "esi.evetech.net", 'b9c3cc79448f449ab17f3aebd018842e', 'https://pyfa-org.github.io/Pyfa/callback'), - "Serenity": ApiServer("Serenity", "login.evepc.163.com", "esi.evepc.163.com", 'bc90aa496a404724a93f41b4f4e97761', 'https://esi.evepc.163.com/ui/oauth2-redirect.html') + "Serenity": ApiServer("Serenity", "login.evepc.163.com", "esi.evepc.163.com", 'bc90aa496a404724a93f41b4f4e97761', 'https://esi.evepc.163.com/ui/oauth2-redirect.html', False) } SSO_LOGOFF_SERENITY='https://login.evepc.163.com/account/logoff' diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 467776f92..34ad42384 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -489,8 +489,6 @@ class SsoCharacterMgmt(AuxiliaryFrame): sEsi.login() except (KeyboardInterrupt, SystemExit): raise - except Exception as ex: - ESIServerExceptionHandler(self, ex) def delChar(self, event): item = self.lcCharacters.GetFirstSelected() diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py index 9a3eac1b5..a1e3d2a16 100644 --- a/gui/ssoLogin.py +++ b/gui/ssoLogin.py @@ -2,41 +2,47 @@ import wx import gui.mainFrame import webbrowser import gui.globalEvents as GE +import config +import time + +from service.settings import EsiSettings _t = wx.GetTranslation class SsoLogin(wx.Dialog): - def __init__(self): - mainFrame = gui.mainFrame.MainFrame.getInstance() - + def __init__(self, server: config.ApiServer, start_local_server=True): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + from service.esi import Esi super().__init__( - mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE, + self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), style=wx.DEFAULT_DIALOG_STYLE, size=wx.Size(450, 240) if "wxGTK" in wx.PlatformInfo else wx.Size(400, 240)) bSizer1 = wx.BoxSizer(wx.VERTICAL) - text = wx.StaticText(self, wx.ID_ANY, _t("Copy and paste the block of text provided by pyfa.io")) - bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + if start_local_server: + text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On.")) + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + bSizer1.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.EXPAND, 15) + text = wx.StaticText(self, wx.ID_ANY, _t("If auto-login fails, copy and paste the token provided by pyfa.io")) + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + elif server.name == "Serenity": + text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the url when your authorization is completed")) + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + + else: + text = wx.StaticText(self, wx.ID_ANY, _t("Please copy and paste the token provided by pyfa.io")) + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE) self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) self.ssoInfoCtrl.Layout() + self.ssoInfoCtrl.Bind(wx.EVT_TEXT, self.OnTextEnter) bSizer1.Add(self.ssoInfoCtrl, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10) - from service.settings import EsiSettings - import config - import time self.Esisettings = EsiSettings.getInstance() - if (self.Esisettings.get("server") == "Serenity"): - bSizer4 = wx.BoxSizer(wx.VERTICAL) - text = wx.StaticText(self, wx.ID_ANY, _t("Please copy the url when your authorization is completed")) - bSizer4.Add(text, 0, wx.ALL | wx.EXPAND, 10) - bSizer1.Add(bSizer4, 0, wx.ALL | wx.EXPAND, 10) - webbrowser.open(config.SSO_LOGOFF_SERENITY) - time.sleep(1) bSizer3 = wx.BoxSizer(wx.VERTICAL) bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10) @@ -46,49 +52,31 @@ class SsoLogin(wx.Dialog): self.SetSizer(bSizer1) self.Center() - - from service.esi import Esi - self.sEsi = Esi.getInstance() - uri = self.sEsi.get_login_uri(None) - - webbrowser.open(uri) - - -class SsoLoginServer(wx.Dialog): - - def __init__(self, port): - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - super().__init__(self.mainFrame, id=wx.ID_ANY, title=_t("SSO Login"), size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE) - - from service.esi import Esi - - self.sEsi = Esi.getInstance() - serverAddr = self.sEsi.startServer(port) + serverAddr = self.sEsi.startServer(0) if start_local_server else None uri = self.sEsi.get_login_uri(serverAddr) - bSizer1 = wx.BoxSizer(wx.VERTICAL) + if server.name == "Serenity": + webbrowser.open(config.SSO_LOGOFF_SERENITY) + time.sleep(1) + self.okBtn = self.FindWindow(wx.ID_OK) + webbrowser.open(uri) + self.okBtn.Enable(False) + self.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.OnLogin) self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) - text = wx.StaticText(self, wx.ID_ANY, _t("Waiting for character login through EVE Single Sign-On.")) - bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) - - bSizer3 = wx.BoxSizer(wx.VERTICAL) - bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10) - - bSizer3.Add(self.CreateStdDialogButtonSizer(wx.CANCEL), 0, wx.EXPAND) - bSizer1.Add(bSizer3, 0, wx.BOTTOM | wx.RIGHT | wx.LEFT | wx.EXPAND, 10) - - self.SetSizer(bSizer1) - self.Fit() - self.Center() - - webbrowser.open(uri) + def OnTextEnter(self, event): + t = event.String.strip() + if t == "": + self.okBtn.Enable(False) + else: + self.okBtn.Enable(True) + event.Skip() def OnLogin(self, event): - self.EndModal(wx.ID_OK) + self.EndModal(wx.ID_CANCEL) event.Skip() def OnDestroy(self, event): diff --git a/service/esi.py b/service/esi.py index 279f65dc3..606dd9742 100644 --- a/service/esi.py +++ b/service/esi.py @@ -6,14 +6,14 @@ import time import base64 import json import config -import webbrowser +import re import eos.db from service.const import EsiLoginMethod, EsiSsoMode from eos.saveddata.ssocharacter import SsoCharacter from service.esiAccess import APIException, GenericSsoError import gui.globalEvents as GE -from gui.ssoLogin import SsoLogin, SsoLoginServer +from gui.ssoLogin import SsoLogin from service.server import StoppableHTTPServer, AuthHandler from service.settings import EsiSettings from service.esiAccess import EsiAccess @@ -101,24 +101,20 @@ class Esi(EsiAccess): self.fittings_deleted.add(fittingID) def login(self): - # always start the local server if user is using client details. Otherwise, start only if they choose to do so. - if self.settings.get('loginMode') == EsiLoginMethod.SERVER: - with gui.ssoLogin.SsoLoginServer(0) as dlg: - dlg.ShowModal() - else: - with gui.ssoLogin.SsoLogin() as dlg: - if dlg.ShowModal() == wx.ID_OK: - message = {} - if (self.default_server_name == "Serenity"): - import re - s=re.search(r'(?<=code=)[a-zA-Z0-9\-_]*',dlg.ssoInfoCtrl.Value.strip()) - if s: - message['code']=s.group() - else: - message['code']=None + start_server = self.settings.get('loginMode') == EsiLoginMethod.SERVER and self.server_base.supports_auto_login + with gui.ssoLogin.SsoLogin(self.server_base, start_server) as dlg: + if dlg.ShowModal() == wx.ID_OK: + if self.default_server_name == "Serenity": + s = re.search(r'(?<=code=)[a-zA-Z0-9\-_]*', dlg.ssoInfoCtrl.Value.strip()) + if s: + # skip state verification and go directly through the auth code processing + self.handleLogin(s.group) else: - message = json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip())) - self.handleLogin(message) + pass + # todo: throw error + else: + self.handleServerRequest(json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip()))) + def stopServer(self): pyfalog.debug("Stopping Server") @@ -136,21 +132,23 @@ class Esi(EsiAccess): self.httpd = StoppableHTTPServer(('localhost', port), AuthHandler) port = self.httpd.socket.getsockname()[1] - self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,)) + self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerRequest,)) self.serverThread.name = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() return 'http://localhost:{}'.format(port) - def handleLogin(self, message): - auth_response, data = self.auth(message['code']) + def handleLogin(self, code): + auth_response, data = self.auth(code) currentCharacter = self.getSsoCharacter(data['name'], self.server_base.name) sub_split = data["sub"].split(":") - if (len(sub_split) != 3): + + if len(sub_split) != 3: raise GenericSsoError("JWT sub does not contain the expected data. Contents: %s" % data["sub"]) + cid = sub_split[-1] if currentCharacter is None: currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret(), self.server_base.name) @@ -162,7 +160,7 @@ class Esi(EsiAccess): # get (endpoint, char, data?) - def handleServerLogin(self, message): + def handleServerRequest(self, message): if not message: raise GenericSsoError("Could not parse out querystring parameters.") @@ -178,4 +176,4 @@ class Esi(EsiAccess): pyfalog.debug("Handling SSO login with: {0}", message) - self.handleLogin(message) + self.handleLogin(message['code'])