Add support for using own client details (messy code, but it works!)

This commit is contained in:
blitzmann
2018-05-13 22:33:58 -04:00
parent e29ab817af
commit b6a1c4b308
5 changed files with 147 additions and 64 deletions

View File

@@ -42,7 +42,6 @@ logging_setup = None
cipher = None
clientHash = None
ESI_AUTH_PROXY = "https://www.pyfa.io" # "http://localhost:5015"
ESI_CACHE = 'esi_cache'
LOGLEVEL_MAP = {

View File

@@ -123,6 +123,7 @@ class EveFittings(wx.Frame):
ESIExceptionHandler(self, ex)
except Exception as ex:
del waitDialog
raise ex
def importFitting(self, event):
selection = self.fitView.fitSelection
@@ -159,7 +160,7 @@ class EveFittings(wx.Frame):
class ESIExceptionHandler(object):
# todo: make this a generate excetpion handler for all calls
def __init__(self, parentWindow, ex):
if ex.response['error'].startswith('Token is not valid'):
if ex.response['error'].startswith('Token is not valid') or ex.response['error'] == 'invalid_token': # todo: this seems messy, figure out a better response
dlg = wx.MessageDialog(parentWindow,
"There was an error validating characters' SSO token. Please try "
"logging into the character again to reset the token.", "Invalid Token",

View File

@@ -69,6 +69,7 @@ from service.settings import SettingsProvider
from service.fit import Fit
from service.character import Character
from service.update import Update
from service.esiAccess import SsoMode
# import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict
@@ -241,12 +242,12 @@ class MainFrame(wx.Frame):
self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin)
def ShowSsoLogin(self, event):
if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL:
if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL and getattr(event, "sso_mode", SsoMode.AUTO) == SsoMode.AUTO:
dlg = SsoLogin(self)
if dlg.ShowModal() == wx.ID_OK:
sEsi = Esi.getInstance()
# todo: verify that this is a correct SSO Info block
sEsi.handleLogin(dlg.ssoInfoCtrl.Value.strip())
sEsi.handleLogin({'SSOInfo': [dlg.ssoInfoCtrl.Value.strip()]})
def ShowUpdateBox(self, release, version):
dlg = UpdateDialog(self, release, version)

View File

@@ -11,7 +11,7 @@ import webbrowser
import eos.db
from eos.enum import Enum
from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException
from service.esiAccess import APIException, SsoMode
import gui.globalEvents as GE
from service.server import StoppableHTTPServer, AuthHandler
from service.settings import EsiSettings
@@ -39,9 +39,10 @@ class Esi(EsiAccess):
return cls._instance
def __init__(self):
super().__init__()
self.settings = EsiSettings.getInstance()
super().__init__()
# these will be set when needed
self.httpd = None
self.state = None
@@ -105,18 +106,19 @@ class Esi(EsiAccess):
def login(self):
serverAddr = None
if self.settings.get('loginMode') == LoginMethod.SERVER:
serverAddr = self.startServer()
# always start the local server if user is using client details. Otherwise, start only if they choose to do so.
if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER:
serverAddr = self.startServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0) # random port, or if it's custom application, use a defined port
uri = self.getLoginURI(serverAddr)
webbrowser.open(uri)
wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(login_mode=self.settings.get('loginMode')))
wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(sso_mode=self.settings.get('ssoMode'), login_mode=self.settings.get('loginMode')))
def stopServer(self):
pyfalog.debug("Stopping Server")
self.httpd.stop()
self.httpd = None
def startServer(self): # todo: break this out into two functions: starting the server, and getting the URI
def startServer(self, port): # todo: break this out into two functions: starting the server, and getting the URI
pyfalog.debug("Starting server")
# we need this to ensure that the previous get_request finishes, and then the socket will close
@@ -124,7 +126,7 @@ class Esi(EsiAccess):
self.stopServer()
time.sleep(1)
self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler)
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.name = "SsoCallbackServer"
@@ -133,8 +135,15 @@ class Esi(EsiAccess):
return 'http://localhost:{}'.format(port)
def handleLogin(self, ssoInfo):
auth_response = json.loads(base64.b64decode(ssoInfo))
def handleLogin(self, message):
# we already have authenticated stuff for the auto mode
if (self.settings.get('ssoMode') == SsoMode.AUTO):
ssoInfo = message['SSOInfo'][0]
auth_response = json.loads(base64.b64decode(ssoInfo))
else:
# otherwise, we need to fetch the information
auth_response = self.auth(message['code'][0])
res = self._session.get(
self.oauth_verify,
@@ -171,5 +180,5 @@ class Esi(EsiAccess):
pyfalog.debug("Handling SSO login with: {0}", message)
self.handleLogin(message['SSOInfo'][0])
self.handleLogin(message)

View File

@@ -1,16 +1,27 @@
'''
A lot of the inspiration (and straight up code copying!) for this class comes from EsiPy <https://github.com/Kyria/EsiPy>
Much of the credit goes to the maintainer of that package, Kyria <tweetfleet slack: @althalus>. The reasoning for no
longer using EsiPy was due to it's reliance on pyswagger, which has caused a bit of a headache in how it operates on a
low level.
Eventually I'll rewrite this to be a bit cleaner and a bit more generic, but for now, it works!
'''
# noinspection PyPackageRequirements
from logbook import Logger
import uuid
import time
import config
import base64
import datetime
from eos.enum import Enum
from eos.saveddata.ssocharacter import SsoCharacter
from service.settings import EsiSettings
from requests import Session
from urllib.parse import urlencode
from urllib.parse import urlencode, quote
pyfalog = Logger(__name__)
@@ -23,12 +34,17 @@ pyfalog = Logger(__name__)
# os.mkdir(cache_path)
#
# todo: move these over to getters that automatically determine which endpoint we use.
sso_url = "https://www.pyfa.io" # "https://login.eveonline.com" for actual login
esi_url = "https://esi.tech.ccp.is"
oauth_authorize = '%s/oauth/authorize' % sso_url
oauth_token = '%s/oauth/token' % sso_url
scopes = [
'esi-skills.read_skills.v1',
'esi-fittings.read_fittings.v1',
'esi-fittings.write_fittings.v1'
]
class SsoMode(Enum):
AUTO = 0
CUSTOM = 1
class APIException(Exception):
@@ -57,27 +73,10 @@ class ESIEndpoints(Enum):
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
# class Servers(Enum):
# TQ = 0
# SISI = 1
class EsiAccess(object):
def __init__(self):
if sso_url is None or sso_url == "":
raise AttributeError("sso_url cannot be None or empty "
"without app parameter")
self.settings = EsiSettings.getInstance()
self.oauth_authorize = '%s/oauth/authorize' % sso_url
self.oauth_token = '%s/oauth/token' % sso_url
# use ESI url for verify, since it's better for caching
if esi_url is None or esi_url == "":
raise AttributeError("esi_url cannot be None or empty")
self.oauth_verify = '%s/verify/' % esi_url
# session request stuff
self._session = Session()
self._session.headers.update({
@@ -87,6 +86,28 @@ class EsiAccess(object):
)
})
@property
def sso_url(self):
if (self.settings.get("ssoMode") == SsoMode.CUSTOM):
return "https://login.eveonline.com"
return "https://www.pyfa.io"
@property
def esi_url(self):
return "https://esi.tech.ccp.is"
@property
def oauth_verify(self):
return '%s/verify/' % self.esi_url
@property
def oauth_authorize(self):
return '%s/oauth/authorize' % self.sso_url
@property
def oauth_token(self):
return '%s/oauth/token' % self.sso_url
def getSkills(self, char):
return self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID)
@@ -114,20 +135,30 @@ class EsiAccess(object):
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode'),
'client_hash': config.getClientSecret()
}
if (self.settings.get("ssoMode") == SsoMode.AUTO):
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode'),
'client_hash': config.getClientSecret()
}
if redirect is not None:
args['redirect'] = redirect
if redirect is not None:
args['redirect'] = redirect
return '%s?%s' % (
oauth_authorize,
urlencode(args)
)
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
)
else:
return '%s?response_type=%s&redirect_uri=%s&client_id=%s%s%s' % (
self.oauth_authorize,
'code',
quote('http://localhost:6461', safe=''),
self.settings.get('clientID'),
'&scope=%s' % '+'.join(scopes) if scopes else '',
'&state=%s' % self.state
)
def get_oauth_header(self, token):
""" Return the Bearer Authorization header required in oauth calls
@@ -146,14 +177,62 @@ class EsiAccess(object):
if refreshToken is None:
raise AttributeError('No refresh token is defined.')
return {
'data': {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
},
data = {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
}
if self.settings.get('ssoMode') == SsoMode.AUTO:
# data is all we really need, the rest is handled automatically by pyfa.io
return {
'data': data,
'url': self.oauth_token,
}
# otherwise, we need to make the token with the client keys
return self.__make_token_request_parameters(data)
def __get_token_auth_header(self):
""" Return the Basic Authorization header required to get the tokens
:return: a dict with the headers
"""
# encode/decode for py2/py3 compatibility
auth_b64 = "%s:%s" % (self.settings.get('clientID'), self.settings.get('clientSecret'))
auth_b64 = base64.b64encode(auth_b64.encode('latin-1'))
auth_b64 = auth_b64.decode('latin-1')
return {'Authorization': 'Basic %s' % auth_b64}
def __make_token_request_parameters(self, params):
request_params = {
'headers': self.__get_token_auth_header(),
'data': params,
'url': self.oauth_token,
}
return request_params
def get_access_token_request_params(self, code):
return self.__make_token_request_parameters(
{
'grant_type': 'authorization_code',
'code': code,
}
)
def auth(self, code):
request_data = self.get_access_token_request_params(code)
res = self._session.post(**request_data)
if res.status_code != 200:
raise Exception(
request_data['url'],
res.status_code,
res.json()
)
json_res = res.json()
return json_res
def refresh(self, ssoChar):
request_data = self.get_refresh_token_params(config.cipher.decrypt(ssoChar.refreshToken).decode())
res = self._session.post(**request_data)
@@ -191,21 +270,15 @@ class EsiAccess(object):
def get(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.get("{}{}".format(esi_url, endpoint)))
# check for warnings, also status > 400
return self._after_request(self._session.get("{}{}".format(self.esi_url, endpoint)))
def post(self, ssoChar, endpoint, json, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.post("{}{}".format(esi_url, endpoint), data=json))
# check for warnings, also status > 400
return self._after_request(self._session.post("{}{}".format(self.esi_url, endpoint), data=json))
def delete(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.delete("{}{}".format(esi_url, endpoint)))
# check for warnings, also status > 400
return self._after_request(self._session.delete("{}{}".format(self.esi_url, endpoint)))