From dc997f0dc4ace69daa0d7cdb44a9d86f82738c14 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 7 May 2022 12:35:16 -0400 Subject: [PATCH] Various updates to support server-aware calls per character --- config.py | 14 ++++-- eos/db/migrations/upgrade47.py | 19 ++++++++ eos/db/saveddata/character.py | 1 + eos/db/saveddata/queries.py | 5 +- eos/saveddata/ssocharacter.py | 6 ++- .../pyfaEsiPreferences.py | 6 ++- gui/characterEditor.py | 2 +- gui/esiFittings.py | 7 ++- service/esi.py | 10 ++-- service/esiAccess.py | 48 +++++++++---------- service/settings.py | 2 +- 11 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 eos/db/migrations/upgrade47.py diff --git a/config.py b/config.py index 1855b96fc..91622b5fb 100644 --- a/config.py +++ b/config.py @@ -9,6 +9,7 @@ import hashlib from eos.const import FittingSlot from cryptography.fernet import Fernet +from collections import namedtuple pyfalog = Logger(__name__) @@ -43,13 +44,16 @@ experimentalFeatures = None version = None language = None -API_CLIENT_ID = '095d8cd841ac40b581330919b49fe746' -API_CLIENT_ID_SERENITY = 'bc90aa496a404724a93f41b4f4e97761' +ApiServer = namedtuple('ApiBase', ['name', 'sso', 'esi', 'client_id', 'callback']) +supported_servers = { + "Tranquility": ApiServer("Tranquility", "login.eveonline.com", "esi.evetech.net", '095d8cd841ac40b581330919b49fe746', 'https://pyfa-org.github.io/Pyfa/callback'), + # 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') +} -ESI_CACHE = 'esi_cache' -SSO_CALLBACK = 'https://pyfa-org.github.io/Pyfa/callback' -SSO_CALLBACK_SERENITY='https://esi.evepc.163.com/ui/oauth2-redirect.html' SSO_LOGOFF_SERENITY='https://login.evepc.163.com/account/logoff' +ESI_CACHE = 'esi_cache' LOGLEVEL_MAP = { "critical": CRITICAL, diff --git a/eos/db/migrations/upgrade47.py b/eos/db/migrations/upgrade47.py new file mode 100644 index 000000000..7aa5d5ca5 --- /dev/null +++ b/eos/db/migrations/upgrade47.py @@ -0,0 +1,19 @@ +""" +Migration 28 + +- adds baseItemID and mutaplasmidID to modules table +""" +import sqlalchemy + + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT server FROM ssoCharacter LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE ssoCharacter ADD COLUMN server VARCHAR;") + saveddata_engine.execute("UPDATE ssoCharacter SET server = 'Tranquility';") + + + + # update all characters to TQ diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 4dd84cf41..0c878b066 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -44,6 +44,7 @@ sso_table = Table("ssoCharacter", saveddata_meta, Column("client", String, nullable=False), Column("characterID", Integer, nullable=False), Column("characterName", String, nullable=False), + Column("server", String, nullable=False), Column("refreshToken", String, nullable=False), Column("accessToken", String, nullable=False), Column("accessTokenExpires", DateTime, nullable=False), diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index f8b5f79bd..4d4ad8f5c 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -493,9 +493,12 @@ def getSsoCharacters(clientHash, eager=None): @cachedQuery(SsoCharacter, 1, "lookfor", "clientHash") -def getSsoCharacter(lookfor, clientHash, eager=None): +def getSsoCharacter(lookfor, clientHash, server=None, eager=None): filter = SsoCharacter.client == clientHash + if server is not None: + filter = and_(filter, SsoCharacter.server == server) + if isinstance(lookfor, int): filter = and_(filter, SsoCharacter.ID == lookfor) elif isinstance(lookfor, str): diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 49b742442..6cbc29219 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -25,10 +25,11 @@ import time class SsoCharacter: - def __init__(self, charID, name, client, accessToken=None, refreshToken=None): + def __init__(self, charID, name, client, server, accessToken=None, refreshToken=None): self.characterID = charID self.characterName = name self.client = client + self.server = server self.accessToken = accessToken self.refreshToken = refreshToken self.accessTokenExpires = None @@ -37,6 +38,9 @@ class SsoCharacter: def init(self): pass + @property + def characterDisplay(self): + return "{} [{}]".format(self.characterName, self.server) def is_token_expired(self): if self.accessTokenExpires is None: return True diff --git a/gui/builtinPreferenceViews/pyfaEsiPreferences.py b/gui/builtinPreferenceViews/pyfaEsiPreferences.py index f769cbe6e..359a871c1 100644 --- a/gui/builtinPreferenceViews/pyfaEsiPreferences.py +++ b/gui/builtinPreferenceViews/pyfaEsiPreferences.py @@ -1,9 +1,11 @@ # noinspection PyPackageRequirements import wx +import config import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.preferenceView import PreferenceView +from service.esi import Esi from service.settings import EsiSettings # noinspection PyPackageRequirements @@ -82,7 +84,9 @@ class PFEsiPref(PreferenceView): def OnServerChange(self, event): source = self.chESIserver.GetString(self.chESIserver.GetSelection()) - self.settings.set("server",source) + esiService = Esi.getInstance() + esiService.init(config.supported_servers[source]) + self.settings.set("server", source) def getImage(self): return BitmapLoader.getBitmap("eve", "gui") diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 9f268556d..4a698ae91 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -856,7 +856,7 @@ class APIView(wx.Panel): noneID = self.charChoice.Append(_t("None"), None) for char in ssoChars: - currId = self.charChoice.Append(char.characterName, char.ID) + currId = self.charChoice.Append(char.characterDisplay, char.ID) if sso is not None and char.ID == sso.ID: self.charChoice.SetSelection(currId) diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 0f9950880..467776f92 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -96,7 +96,7 @@ class EveFittings(AuxiliaryFrame): self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.characterName, char.ID) + self.charChoice.Append(char.characterDisplay, char.ID) if len(chars) > 0: self.charChoice.SetSelection(0) @@ -330,7 +330,7 @@ class ExportToEve(AuxiliaryFrame): self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.characterName, char.ID) + self.charChoice.Append(char.characterDisplay, char.ID) if len(chars) > 0: self.charChoice.SetSelection(0) @@ -414,6 +414,7 @@ class SsoCharacterMgmt(AuxiliaryFrame): self.lcCharacters.InsertColumn(0, heading=_t('Character')) self.lcCharacters.InsertColumn(1, heading=_t('Character ID')) + self.lcCharacters.InsertColumn(2, heading=_t('Server')) self.popCharList() @@ -476,9 +477,11 @@ class SsoCharacterMgmt(AuxiliaryFrame): self.lcCharacters.InsertItem(index, char.characterName) self.lcCharacters.SetItem(index, 1, str(char.characterID)) self.lcCharacters.SetItemData(index, char.ID) + self.lcCharacters.SetItem(index, 2, char.server or "") self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE) + self.lcCharacters.SetColumnWidth(2, wx.LIST_AUTOSIZE) def addChar(self, event): try: diff --git a/service/esi.py b/service/esi.py index bc0ef14a8..279f65dc3 100644 --- a/service/esi.py +++ b/service/esi.py @@ -69,8 +69,8 @@ class Esi(EsiAccess): chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars - def getSsoCharacter(self, id): - char = eos.db.getSsoCharacter(id, config.getClientSecret()) + def getSsoCharacter(self, id, server=None): + char = eos.db.getSsoCharacter(id, config.getClientSecret(), server) eos.db.commit() return char @@ -109,7 +109,7 @@ class Esi(EsiAccess): with gui.ssoLogin.SsoLogin() as dlg: if dlg.ShowModal() == wx.ID_OK: message = {} - if (self.server_name == "Serenity"): + if (self.default_server_name == "Serenity"): import re s=re.search(r'(?<=code=)[a-zA-Z0-9\-_]*',dlg.ssoInfoCtrl.Value.strip()) if s: @@ -146,14 +146,14 @@ class Esi(EsiAccess): def handleLogin(self, message): auth_response, data = self.auth(message['code']) - currentCharacter = self.getSsoCharacter(data['name']) + currentCharacter = self.getSsoCharacter(data['name'], self.server_base.name) sub_split = data["sub"].split(":") 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()) + currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret(), self.server_base.name) Esi.update_token(currentCharacter, auth_response) diff --git a/service/esiAccess.py b/service/esiAccess.py index 4441a5fe3..ffd22fa62 100644 --- a/service/esiAccess.py +++ b/service/esiAccess.py @@ -30,13 +30,6 @@ scopes = [ 'esi-fittings.write_fittings.v1' ] -ApiBase = namedtuple('ApiBase', ['sso', 'esi']) -supported_servers = { - "Tranquility": ApiBase("login.eveonline.com", "esi.evetech.net"), - "Singularity": ApiBase("sisilogin.testeveonline.com", "esi.evetech.net"), - "Serenity": ApiBase("login.evepc.163.com", "esi.evepc.163.com") -} - class GenericSsoError(Exception): """ Exception used for generic SSO errors that aren't directly related to an API call """ @@ -63,11 +56,11 @@ class APIException(Exception): class EsiAccess: + server_meta = {} def __init__(self): self.settings = EsiSettings.getInstance() - self.server_name=self.settings.get('server') - self.server_base: ApiBase = supported_servers[self.server_name] - + self.default_server_name = self.settings.get('server') + self.default_server_base = config.supported_servers[self.default_server_name] # session request stuff self._session = Session() self._basicHeaders = { @@ -81,21 +74,25 @@ class EsiAccess: # Set up cached session. This is only used for SSO meta data for now, but can be expanded to actually handle # various ESI caching (using ETag, for example) in the future - cached_session = CachedSession( + self.cached_session = CachedSession( os.path.join(config.savePath, config.ESI_CACHE), backend="sqlite", cache_control=True, # Use Cache-Control headers for expiration, if available expire_after=timedelta(days=1), # Otherwise expire responses after one day stale_if_error=True, # In case of request errors, use stale cache data if possible ) - cached_session.headers.update(self._basicHeaders) - cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat() + self.cached_session.headers.update(self._basicHeaders) + self.cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat() + self.init(self.default_server_base) - meta_call = cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso) + def init(self, server_base): + self.server_base: config.ApiServer = server_base + self.server_name = self.server_base.name + meta_call = self.cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso) meta_call.raise_for_status() self.server_meta = meta_call.json() - jwks_call = cached_session.get(self.server_meta["jwks_uri"]) + jwks_call = self.cached_session.get(self.server_meta["jwks_uri"]) jwks_call.raise_for_status() self.jwks = jwks_call.json() @@ -117,10 +114,7 @@ class EsiAccess: @property def client_id(self): - if (self.server_name == "Serenity"): - return self.settings.get('clientID') or config.API_CLIENT_ID_SERENITY - else: - return self.settings.get('clientID') or config.API_CLIENT_ID + return self.settings.get('clientID') or self.server_base.client_id @staticmethod def update_token(char, tokenResponse): @@ -145,10 +139,11 @@ class EsiAccess: 'redirect': redirect, 'state': self.state } + if(self.server_name=="Serenity"): args = { 'response_type': 'code', - 'redirect_uri': config.SSO_CALLBACK_SERENITY, + 'redirect_uri': self.server_base.callback, 'client_id': self.client_id, 'scope': ' '.join(scopes), 'state': 'hilltech', @@ -157,7 +152,7 @@ class EsiAccess: else: args = { 'response_type': 'code', - 'redirect_uri': config.SSO_CALLBACK, + 'redirect_uri': self.server_base.callback, 'client_id': self.client_id, 'scope': ' '.join(scopes), 'code_challenge': code_challenge, @@ -264,6 +259,11 @@ class EsiAccess: "https://login.eveonline.com: {}".format(str(e))) def _before_request(self, ssoChar): + if ssoChar: + self.init(config.supported_servers[ssoChar.server]) + else: + self.init(self.default_server_base) + self._session.headers.clear() self._session.headers.update(self._basicHeaders) if ssoChar is None: @@ -292,17 +292,17 @@ class EsiAccess: def get(self, ssoChar, endpoint, **kwargs): self._before_request(ssoChar) endpoint = endpoint.format(**kwargs) - return self._after_request(self._session.get("{}{}".format(self.esi_url, endpoint))) + return self._after_request(self._session.get("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower()))) def post(self, ssoChar, endpoint, json, **kwargs): self._before_request(ssoChar) endpoint = endpoint.format(**kwargs) - return self._after_request(self._session.post("{}{}".format(self.esi_url, endpoint), data=json)) + return self._after_request(self._session.post("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower()), data=json)) def delete(self, ssoChar, endpoint, **kwargs): self._before_request(ssoChar) endpoint = endpoint.format(**kwargs) - return self._after_request(self._session.delete("{}{}".format(self.esi_url, endpoint))) + return self._after_request(self._session.delete("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower()))) # todo: move these off to another class which extends this one. This class should only handle the low level # authentication and diff --git a/service/settings.py b/service/settings.py index 36be2b6c5..c1e91f9dd 100644 --- a/service/settings.py +++ b/service/settings.py @@ -389,7 +389,7 @@ class EsiSettings: self.settings[type] = value def keys(self): - return list({"Tranquility":"Tranquility","Singularity":"Singularity","Serenity":"Serenity"}) + return config.supported_servers.keys() class StatViewSettings: