Merge pull request #2590 from huangzheng2016/master

Update the SSO Login for Serenity and Singularity server's player
This commit is contained in:
Anton Vorobyov
2024-02-26 23:35:31 +04:00
committed by GitHub
12 changed files with 239 additions and 125 deletions

View File

@@ -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
@@ -22,6 +22,7 @@ import gui.mainFrame
from requests import Session
pyfalog = Logger(__name__)
_t = wx.GetTranslation
class Esi(EsiAccess):
@@ -69,8 +70,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
@@ -101,15 +102,36 @@ 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 = json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip()))
self.handleLogin(message)
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:
from gui.esiFittings import ESIExceptionHandler
try:
if self.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(0))
else:
pass
# todo: throw error
else:
self.handleServerRequest(json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip())))
except GenericSsoError as ex:
pyfalog.error(ex)
with wx.MessageDialog(
self.mainFrame,
str(ex),
_t("SSO Error"),
wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
except APIException as ex:
pyfalog.error(ex)
ESIExceptionHandler(ex)
pass
def stopServer(self):
pyfalog.debug("Stopping Server")
@@ -127,24 +149,26 @@ 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'])
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())
currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret(), self.server_base.name)
Esi.update_token(currentCharacter, auth_response)
@@ -153,7 +177,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.")
@@ -169,4 +193,4 @@ class Esi(EsiAccess):
pyfalog.debug("Handling SSO login with: {0}", message)
self.handleLogin(message)
self.handleLogin(message['code'])

View File

@@ -1,6 +1,7 @@
# noinspection PyPackageRequirements
from collections import namedtuple
import requests
from logbook import Logger
import uuid
import time
@@ -30,13 +31,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,10 +57,11 @@ class APIException(Exception):
class EsiAccess:
server_meta = {}
def __init__(self):
self.settings = EsiSettings.getInstance()
self.server_base: ApiBase = supported_servers[self.settings.get("server")]
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 = {
@@ -78,23 +73,38 @@ class EsiAccess:
self._session.headers.update(self._basicHeaders)
self._session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
self.mem_cached_session = {}
# 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)
def init(self, server_base):
self.server_base: config.ApiServer = server_base
self.server_name = self.server_base.name
try:
meta_call = self.cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
except:
# The http data of expire_after in evepc.163.com is -1
meta_call = requests.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
meta_call = 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"])
try:
jwks_call = self.cached_session.get(self.server_meta["jwks_uri"])
except:
jwks_call = requests.get(self.server_meta["jwks_uri"])
jwks_call.raise_for_status()
self.jwks = jwks_call.json()
@@ -116,7 +126,7 @@ class EsiAccess:
@property
def client_id(self):
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):
@@ -142,16 +152,25 @@ class EsiAccess:
'state': self.state
}
args = {
'response_type': 'code',
'redirect_uri': config.SSO_CALLBACK,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
if(self.server_name=="Serenity"):
args = {
'response_type': 'code',
'redirect_uri': self.server_base.callback,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'state': 'hilltech',
'device_id': 'eims'
}
else:
args = {
'response_type': 'code',
'redirect_uri': self.server_base.callback,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
@@ -252,6 +271,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:
@@ -280,17 +304,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

View File

@@ -392,6 +392,9 @@ class EsiSettings:
def set(self, type, value):
self.settings[type] = value
def keys(self):
return config.supported_servers.keys()
class StatViewSettings:
_instance = None