From c7360c8cc36c1ef5c46ba806bdd426e78901a408 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 7 Feb 2018 00:44:37 -0500 Subject: [PATCH] Get logging into EVE working and SSO characters saving, along with client hashes being stored alongside characters. --- config.py | 3 + eos/db/saveddata/crest.py | 11 ++-- eos/db/saveddata/queries.py | 27 +++++---- eos/saveddata/ssocharacter.py | 6 +- gui/crestFittings.py | 8 ++- service/crest.py | 105 +++++++++++++++++++--------------- service/esi_security_proxy.py | 3 - service/server.py | 10 ++-- 8 files changed, 95 insertions(+), 78 deletions(-) diff --git a/config.py b/config.py index 1127b72ae..bac448989 100644 --- a/config.py +++ b/config.py @@ -35,6 +35,9 @@ logPath = None loggingLevel = None logging_setup = None +ESI_AUTH_PROXY = "http://localhost:5015" # "https://blitzmann.pythonanywhere.com" // need to get this set up, and actually put on it's own domain +ESI_CACHE = 'esi_cache' + LOGLEVEL_MAP = { "critical": CRITICAL, "error": ERROR, diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py index 035cdb60d..429da243d 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/crest.py @@ -17,7 +17,7 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, String, DateTime +from sqlalchemy import Table, Column, Integer, String, DateTime, UniqueConstraint from sqlalchemy.orm import mapper import datetime @@ -27,12 +27,15 @@ from eos.saveddata.ssocharacter import SsoCharacter sso_table = Table("ssoCharacter", saveddata_meta, Column("ID", Integer, primary_key=True), Column("client", String, nullable=False), - Column("characterID", Integer, nullable=False, unique=True), - Column("characterName", String, nullable=False, unique=True), + Column("characterID", Integer, nullable=False), + Column("characterName", String, nullable=False), Column("refreshToken", String, nullable=False), Column("accessToken", String, nullable=False), Column("accessTokenExpires", DateTime, nullable=False), Column("created", DateTime, nullable=True, default=datetime.datetime.now), - Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), + UniqueConstraint('client', 'characterID', name='uix_client_characterID'), + UniqueConstraint('client', 'characterName', name='uix_client_characterName') + ) mapper(SsoCharacter, sso_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index b3c1840e8..9d4eaf55f 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -467,29 +467,28 @@ def getProjectedFits(fitID): raise TypeError("Need integer as argument") -def getSsoCharacters(eager=None): +def getSsoCharacters(clientHash, eager=None): eager = processEager(eager) with sd_lock: - characters = saveddata_session.query(SsoCharacter).options(*eager).all() + characters = saveddata_session.query(SsoCharacter).filter(SsoCharacter.client == clientHash).options(*eager).all() return characters @cachedQuery(SsoCharacter, 1, "lookfor") -def getSsoCharacter(lookfor, eager=None): +def getSsoCharacter(lookfor, clientHash, eager=None): + filter = SsoCharacter.client == clientHash + if isinstance(lookfor, int): - if eager is None: - with sd_lock: - character = saveddata_session.query(SsoCharacter).get(lookfor) - else: - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.ID == lookfor).first() + filter = and_(filter, SsoCharacter.characterID == lookfor) elif isinstance(lookfor, str): - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.name == lookfor).first() + filter = and_(filter, SsoCharacter.characterName == lookfor) else: raise TypeError("Need integer or string as argument") + + eager = processEager(eager) + with sd_lock: + character = saveddata_session.query(SsoCharacter).options(*eager).filter(filter).first() + return character @@ -544,7 +543,7 @@ def commit(): try: saveddata_session.commit() saveddata_session.flush() - except Exception: + except Exception as ex: saveddata_session.rollback() exc_info = sys.exc_info() raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 8fca830cb..88fcc4a24 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -25,13 +25,13 @@ import time class SsoCharacter(object): - def __init__(self, id, charID, name, client, accessToken, refreshToken=None): - self.ID = id + def __init__(self, charID, name, client, accessToken=None, refreshToken=None): self.characterID = charID self.characterName = name self.client = client self.accessToken = accessToken self.refreshToken = refreshToken + self.esi_client = None @reconstructor def init(self): @@ -51,7 +51,7 @@ class SsoCharacter(object): def update_token(self, tokenResponse): """ helper function to update token data from SSO response """ self.accessToken = tokenResponse['access_token'] - self.accessTokenExpires = datetime.fromtimestamp( + self.accessTokenExpires = datetime.datetime.fromtimestamp( time.time() + tokenResponse['expires_in'], ) if 'refresh_token' in tokenResponse: diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 718b6654b..902d827e4 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -310,7 +310,9 @@ class CrestMgmt(wx.Dialog): self.lcCharacters = wx.ListCtrl(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT) self.lcCharacters.InsertColumn(0, heading='Character') - self.lcCharacters.InsertColumn(1, heading='Refresh Token') + self.lcCharacters.InsertColumn(1, heading='Character ID') + self.lcCharacters.InsertColumn(2, heading='Access Token') + self.lcCharacters.InsertColumn(3, heading='Refresh Token') self.popCharList() @@ -347,8 +349,8 @@ class CrestMgmt(wx.Dialog): self.lcCharacters.DeleteAllItems() for index, char in enumerate(chars): - self.lcCharacters.InsertItem(index, char.name) - self.lcCharacters.SetStringItem(index, 1, char.refresh_token) + self.lcCharacters.InsertItem(index, char.characterName) + self.lcCharacters.SetStringItem(index, 1, char.refreshToken) self.lcCharacters.SetItemData(index, char.ID) self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) diff --git a/service/crest.py b/service/crest.py index 51c9f1309..2642b1438 100644 --- a/service/crest.py +++ b/service/crest.py @@ -5,6 +5,9 @@ import threading import copy import uuid import time +import config +import base64 +import json import eos.db from eos.enum import Enum @@ -14,8 +17,20 @@ from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler from service.pycrest.eve import EVE +from .esi_security_proxy import EsiSecurityProxy +from esipy import EsiClient +from esipy.cache import FileCache +import os + pyfalog = Logger(__name__) +server = "https://blitzmann.pythonanywhere.com" +cache_path = os.path.join(config.savePath, config.ESI_CACHE) + +if not os.path.exists(cache_path): + os.mkdir(cache_path) + +file_cache = FileCache(cache_path) class Servers(Enum): TQ = 0 @@ -39,6 +54,14 @@ class Crest(object): _instance = None + @classmethod + def genEsiClient(cls, security=None): + return EsiClient( + security=EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) if security is None else security, + cache=file_cache, + headers={'User-Agent': 'pyfa esipy'} + ) + @classmethod def getInstance(cls): if cls._instance is None: @@ -113,32 +136,30 @@ class Crest(object): wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) def getCrestCharacters(self): - chars = eos.db.getSsoCharacters() + chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars - def getCrestCharacter(self, charID): + def getSsoCharacter(self, charID): """ Get character, and modify to include the eve connection """ - if charID in self.charCache: - return self.charCache.get(charID) - - char = eos.db.getSsoCharacter(charID) - self.charCache[charID] = char + char = eos.db.getSsoCharacter(charID, config.getClientSecret()) + if char.esi_client is None: + char.esi_client = Crest.genEsiClient() return char def getFittings(self, charID): - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.get('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID)) def postFitting(self, charID, json): # @todo: new fitting ID can be recovered from Location header, # ie: Location -> https://api-sisi.testeveonline.com/characters/1611853631/fittings/37486494/ - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.post('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID), data=json) def delFitting(self, charID, fittingID): - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.delete('%scharacters/%d/fittings/%d/' % (char.eve._authed_endpoint, char.ID, fittingID)) def logout(self): @@ -154,19 +175,26 @@ class Crest(object): def startServer(self): pyfalog.debug("Starting server") + + # we need this to ensure that the previous get_request finishes, and then the socket will close if self.httpd: self.stopServer() time.sleep(1) - # we need this to ensure that the previous get_request finishes, and then the socket will close - self.httpd = StoppableHTTPServer(('localhost', 6461), AuthHandler) + + self.state = str(uuid.uuid4()) + self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) + port = self.httpd.socket.getsockname()[1] + + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) + + uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port)) self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) - self.serverThread.name = "CRESTServer" + self.serverThread.name = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() - self.state = str(uuid.uuid4()) - return self.eve.auth_uri(scopes=self.scopes, state=self.state) + return uri def handleLogin(self, message): if not message: @@ -178,41 +206,26 @@ class Crest(object): pyfalog.debug("Handling CREST login with: {0}", message) - if 'access_token' in message: # implicit - eve = EVE(**self.eve_options) - eve.temptoken_authorize( - access_token=message['access_token'][0], - expires_in=int(message['expires_in'][0]) - ) - self.ssoTimer = threading.Timer(int(message['expires_in'][0]), self.logout) - self.ssoTimer.start() + auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) - eve() - info = eve.whoami() + # We need to preload the ESI Security object beforehand with the auth response so that we can use verify to + # get character information + # init the security object + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - pyfalog.debug("Got character info: {0}", info) + esisecurity.update_token(auth_response) - self.implicitCharacter = CrestChar(info['CharacterID'], info['CharacterName']) - self.implicitCharacter.eve = eve - # self.implicitCharacter.fetchImage() + # we get the character information + cdata = esisecurity.verify() + print(cdata) - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.IMPLICIT)) - elif 'code' in message: - eve = EVE(**self.eve_options) - eve.authorize(message['code'][0]) - eve() - info = eve.whoami() + currentCharacter = self.getSsoCharacter(cdata['CharacterID']) - pyfalog.debug("Got character info: {0}", info) + if currentCharacter is None: + currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) + currentCharacter.esi_client = Crest.genEsiClient(esisecurity) + currentCharacter.update_token(auth_response) # this also sets the esi security token - # check if we have character already. If so, simply replace refresh_token - char = self.getCrestCharacter(int(info['CharacterID'])) - if char: - char.refresh_token = eve.refresh_token - else: - char = CrestChar(info['CharacterID'], info['CharacterName'], eve.refresh_token) - char.eve = eve - self.charCache[int(info['CharacterID'])] = char - eos.db.save(char) + eos.db.save(currentCharacter) - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) + wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication diff --git a/service/esi_security_proxy.py b/service/esi_security_proxy.py index f6263d5e8..21d0f8510 100644 --- a/service/esi_security_proxy.py +++ b/service/esi_security_proxy.py @@ -26,11 +26,9 @@ class EsiSecurityProxy(object): def __init__( self, - redirect_uri, **kwargs): """ Init the ESI Security Object - :param redirect_uri: the uri to redirect the user after login into SSO :param sso_url: the default sso URL used when no "app" is provided :param esi_url: the default esi URL used for verify endpoint :param app: (optionnal) the pyswagger app object @@ -43,7 +41,6 @@ class EsiSecurityProxy(object): esi_url = kwargs.pop('esi_url', "https://esi.tech.ccp.is") self.security_name = kwargs.pop('security_name', 'evesso') - self.redirect_uri = redirect_uri # we provide app object, so we don't use sso_url if app is not None: diff --git a/service/server.py b/service/server.py index 76c5a964e..34261294a 100644 --- a/service/server.py +++ b/service/server.py @@ -82,14 +82,14 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): try: if step2: self.server.callback(parts) - pyfalog.info("Successfully logged into CREST.") + pyfalog.info("Successfully logged into EVE.") msg = "If you see this message then it means you should be logged into CREST. You may close this window and return to the application." else: # For implicit mode, we have to serve up the page which will take the hash and redirect useing a querystring pyfalog.info("Processing response from EVE Online.") msg = "Processing response from EVE Online" except Exception as ex: - pyfalog.error("Error in CREST AuthHandler") + pyfalog.error("Error logging into EVE") pyfalog.error(ex) msg = "

Error

\n

{}

".format(ex.message) finally: @@ -109,10 +109,10 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): class StoppableHTTPServer(http.server.HTTPServer): def server_bind(self): http.server.HTTPServer.server_bind(self) - self.settings = CRESTSettings.getInstance() + # self.settings = CRESTSettings.getInstance() # Allow listening for x seconds - sec = self.settings.get('timeout') + sec = 120 pyfalog.debug("Running server for {0} seconds", sec) self.socket.settimeout(1) @@ -131,7 +131,7 @@ class StoppableHTTPServer(http.server.HTTPServer): pass def stop(self): - pyfalog.warning("Setting CREST server to stop.") + pyfalog.warning("Setting pyfa server to stop.") self.run = False def handle_timeout(self):