# noinspection PyPackageRequirements import wx from logbook import Logger import threading import copy import uuid import time import config import base64 import json import eos.db from eos.enum import Enum from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE 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, EsiApp from esipy.cache import FileCache import os import logging 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 SISI = 1 class CrestModes(Enum): IMPLICIT = 0 USER = 1 from utils.timer import Timer class Crest(object): clientIDs = { Servers.TQ : 'f9be379951c046339dc13a00e6be7704', Servers.SISI: 'af87365240d644f7950af563b8418bad' } # @todo: move this to settings clientCallback = 'http://localhost:6461' clientTest = True _instance = None @classmethod def initEsiApp(cls): with Timer() as t: cls.esiapp = EsiApp(cache=None) with Timer() as t: cls.esi_v1 = cls.esiapp.get_v1_swagger with Timer() as t: cls.esi_v4 = cls.esiapp.get_v4_swagger @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: cls._instance = Crest() return cls._instance @classmethod def restartService(cls): # This is here to reseed pycrest values when changing preferences # We first stop the server n case one is running, as creating a new # instance doesn't do this. if cls._instance.httpd: cls._instance.stopServer() cls._instance = Crest() cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode')) return cls._instance def __init__(self): """ A note on login/logout events: the character login events happen whenever a characters is logged into via the SSO, regardless of mod. However, the mode should be send as an argument. Similarily, the Logout even happens whenever the character is deleted for either mode. The mode is sent as an argument, as well as the umber of characters still in the cache (if USER mode) """ self.settings = CRESTSettings.getInstance() self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] # these will be set when needed self.httpd = None self.state = None self.ssoTimer = None self.eve_options = { 'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')), 'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None, 'redirect_uri': self.clientCallback, 'testing': self.isTestServer } # Base EVE connection that is copied to all characters self.eve = EVE(**self.eve_options) self.implicitCharacter = None # The database cache does not seem to be working for some reason. Use # this as a temporary measure self.charCache = {} # need these here to post events import gui.mainFrame # put this here to avoid loop self.mainFrame = gui.mainFrame.MainFrame.getInstance() @property def isTestServer(self): return self.settings.get('server') == Servers.SISI def delCrestCharacter(self, charID): char = eos.db.getSsoCharacter(charID) del self.charCache[char.ID] eos.db.remove(char) wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache))) def delAllCharacters(self): chars = eos.db.getSsoCharacters() for char in chars: eos.db.remove(char) self.charCache = {} wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) def getSsoCharacters(self): chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars def getSsoCharacter(self, charID): """ Get character, and modify to include the eve connection """ 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.getSsoCharacter(charID) op = esi_v1.op['get_characters_character_id_fittings']( character_id=self.currentCharacter.character_id ) resp = self.currentCharacter.esi_client.request(op) 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.getSsoCharacter(charID) return char.eve.post('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID), data=json) def delFitting(self, charID, fittingID): char = self.getSsoCharacter(charID) return char.eve.delete('%scharacters/%d/fittings/%d/' % (char.eve._authed_endpoint, char.ID, fittingID)) def logout(self): """Logout of implicit character""" pyfalog.debug("Character logout") self.implicitCharacter = None wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode'))) def stopServer(self): pyfalog.debug("Stopping Server") self.httpd.stop() self.httpd = None 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) 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 = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() return uri def handleLogin(self, message): if not message: raise Exception("Could not parse out querystring parameters.") if message['state'][0] != self.state: pyfalog.warn("OAUTH state mismatch") raise Exception("OAUTH State Mismatch.") pyfalog.debug("Handling CREST login with: {0}", message) auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) # 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) esisecurity.update_token(auth_response) # we get the character information cdata = esisecurity.verify() print(cdata) currentCharacter = self.getSsoCharacter(cdata['CharacterID']) 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 eos.db.save(currentCharacter) wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication