diff --git a/unmanic/libs/session.py b/unmanic/libs/session.py index d558a30d..3f32ec55 100644 --- a/unmanic/libs/session.py +++ b/unmanic/libs/session.py @@ -43,11 +43,21 @@ from unmanic.libs.unmodels import Installation +class RemoteApiException(Exception): + """ + RemoteApiException + Custom exception for errors contacting the remote Unmanic API + """ + + def __init__(self, message, status_code): + super().__init__(f"Session Error - Remote API [CODE: {status_code}]: {message}") + + class Session(object, metaclass=SingletonType): """ Session - Manages the Unbmanic applications session for unlocking + Manages the Unmanic applications session for unlocking features and fetching data from the Unmanic site API. """ @@ -103,6 +113,11 @@ class Session(object, metaclass=SingletonType): """ user_access_token = None + """ + user_refresh_token - The refresh token for acquiring an updated access token + """ + user_refresh_token = None + """ session_cookies - A stored copy of the session cookies to persist between restarts """ @@ -112,7 +127,7 @@ def __init__(self, *args, **kwargs): unmanic_logging = unlogger.UnmanicLogger.__call__() self.logger = unmanic_logging.get_logger(__class__.__name__) self.timeout = 30 - self.dev_local_api = kwargs.get('dev_local_api', False) + self.dev_api = kwargs.get('dev_api', None) self.requests_session = requests.Session() self.logger.info('Initialising new session object') @@ -212,6 +227,8 @@ def __store_installation_data(self, force_save_access_token=False): db_installation.created = self.created if self.user_access_token or force_save_access_token: db_installation.user_access_token = self.user_access_token + if self.user_refresh_token or force_save_access_token: + db_installation.user_refresh_token = self.user_refresh_token if self.session_cookies or force_save_access_token: db_installation.session_cookies = self.session_cookies db_installation.save() @@ -229,6 +246,7 @@ def __reset_session_installation_data(self): self.email = '' self.created = time.time() self.user_access_token = None + self.user_refresh_token = None self.__store_installation_data(force_save_access_token=True) self.__clear_session_auth() @@ -277,9 +295,8 @@ def get_site_url(self): """ api_proto = "https" api_domain = "api.unmanic.app" - if self.dev_local_api: - api_proto = "http" - api_domain = "api.unmanic.localhost" + if self.dev_api: + return self.dev_api return "{0}://{1}".format(api_proto, api_domain) def set_full_api_url(self, api_prefix, api_version, api_path): @@ -307,20 +324,17 @@ def api_get(self, api_prefix, api_version, api_path): r = self.requests_session.get(u, timeout=self.timeout) if r.status_code > 403: # There is an issue with the remote API - self.logger.debug( - "Sorry! There seems to be an issue with the remote servers. Please try GET request again later. Status code %s", - r.status_code) + raise RemoteApiException(f"GET request failed for {u}", r.status_code) if r.status_code == 401: # Verify the token. Refresh as required + self.logger.debug("Auto exec token verification (api_get)") token_verified = self.verify_token() # If successful, then retry request if token_verified: r = self.requests_session.get(u, timeout=self.timeout) if r.status_code > 403: # There is an issue with the remote API - self.logger.debug( - "Sorry! There seems to be an issue with the remote servers on retry. Please try GET request again later. Status code %s", - r.status_code) + raise RemoteApiException(f"GET request still failed for {u}", r.status_code) else: self.logger.debug('Failed to verify auth (api_get)') return r.json(), r.status_code @@ -339,20 +353,17 @@ def api_post(self, api_prefix, api_version, api_path, data): r = self.requests_session.post(u, json=data, timeout=self.timeout) if r.status_code > 403: # There is an issue with the remote API - self.logger.debug( - "Sorry! There seems to be an issue with the remote servers. Please try POST request again later. Status code %s", - r.status_code) + raise RemoteApiException(f"POST request failed for {u}", r.status_code) if r.status_code == 401: # Verify the token. Refresh as required + self.logger.debug("Auto exec token verification (api_post)") token_verified = self.verify_token() # If successful, then retry request if token_verified: r = self.requests_session.post(u, json=data, timeout=self.timeout) if r.status_code > 403: # There is an issue with the remote API - self.logger.debug( - "Sorry! There seems to be an issue with the remote servers on retry. Please try POST request again later. Status code %s", - r.status_code) + raise RemoteApiException(f"POST request still failed for {u}", r.status_code) else: self.logger.debug('Failed to verify auth (api_post)') return r.json(), r.status_code @@ -369,32 +380,27 @@ def verify_token(self): return True elif r.status_code > 403: # Issue with server... Just carry on with current access token can't fix that here. - self.logger.warning( - "Sorry! There seems to be an issue with the token auth servers. Please try again later. Status code %s", - r.status_code) - # Return True here to prevent the app from lowering the level - return True + raise RemoteApiException(f"Token verification request failed for {u}", r.status_code) # Access token is not valid. Refresh it. self.logger.debug('Unable to verify authentication token. Refreshing...') - u = self.set_full_api_url('support-auth-api', 1, 'user_auth/refresh_token') - r = self.requests_session.get(u, timeout=self.timeout) + d = {"refreshToken": self.user_refresh_token} + u = self.set_full_api_url('support-auth-api', 1, 'user_auth/token') + r = self.requests_session.post(u, json=d, timeout=self.timeout) if r.status_code in [202]: # Token refreshed # Store the updated access token response = r.json() self.__update_session_auth(access_token=response.get('data', {}).get('accessToken')) + # Store the updated refresh token + self.user_refresh_token = response.get('data', {}).get('refreshToken') # Store the updated session cookies self.session_cookies = base64.b64encode(pickle.dumps(self.requests_session.cookies)).decode('utf-8') self.__store_installation_data() return True elif r.status_code > 403: # Issue was with server... Just carry on with current access token can't fix that here. - self.logger.warning( - "Sorry! There seems to be an issue with the auth servers. Please try again later. Status code %s", - r.status_code) - # Return True here to prevent the app from lowering the level - return True + raise RemoteApiException(f"Token refresh request failed for {u}", r.status_code) elif r.status_code in [403]: # Log this failure in the debug logs self.logger.info('Failed to refresh access token.') @@ -419,12 +425,13 @@ def auth_user_account(self, force_checkin=False): post_data = {"uuid": self.get_installation_uuid()} response, status_code = self.api_post('support-auth-api', 1, 'app_auth/retrieve_app_token', post_data) if status_code in [200, 201, 202] and response.get('success'): + # Store the updated access token self.__update_session_auth(access_token=response.get('data', {}).get('accessToken')) + # Store the updated refresh token + self.user_refresh_token = response.get('data', {}).get('refreshToken') token_verified = self.verify_token() elif status_code > 403: - self.logger.warning( - "Failed to check in with Unmanic support auth API. Remote server error. Please try again later on.") - return + raise RemoteApiException("Failed to fetch initial app token frp, app_auth/retrieve_app_token", status_code) else: self.logger.info('Failed to check in with Unmanic support auth API.') for message in response.get('messages', []): @@ -436,9 +443,7 @@ def auth_user_account(self, force_checkin=False): response, status_code = self.api_get('support-auth-api', 1, 'user_auth/user_info') if status_code > 403: # Failed to fetch data from server. Ignore this for now. Will try again later. - self.logger.warning( - "Failed to check in with Unmanic user info API. Remote server error. Please try again later on.") - return + raise RemoteApiException("Failed to fetch user info from user_auth/user_info", status_code) if status_code in [200, 201, 202] and response.get('success'): # Get user data from response data user_data = response.get('data', {}).get('user') @@ -500,7 +505,7 @@ def register_unmanic(self, force=False): } # Refresh user auth - self.auth_user_account(force_checkin=force) + result = self.auth_user_account(force_checkin=force) # Register Unmanic registration_response, status_code = self.api_post('unmanic-api', 1, 'installation_auth/register', post_data) @@ -512,15 +517,15 @@ def register_unmanic(self, force=False): self.__store_installation_data() return True elif status_code > 403: - self.logger.warning( - "Failed to check in with Unmanic installation register API. Remote server error. Please try again later on.") - return True + raise RemoteApiException("Failed to register installation to installation_auth/register", status_code) # Allow an extension for the session for 7 days without an internet connection if self.__created_older_than_x_days(days=7): # Reset the session - Unmanic should phone home once every 7 days self.__reset_session_installation_data() return False + except RemoteApiException as e: + self.logger.error("Exception while registering Unmanic: %s", e) except Exception as e: self.logger.debug("Exception while registering Unmanic: %s", e, exc_info=True) if self.__check_session_valid(): diff --git a/unmanic/libs/unmodels/installation.py b/unmanic/libs/unmodels/installation.py index af4641e7..44bdb6b6 100644 --- a/unmanic/libs/unmodels/installation.py +++ b/unmanic/libs/unmodels/installation.py @@ -52,4 +52,5 @@ class Installation(BaseModel): # Store session tokens user_access_token = TextField(null=True) + user_refresh_token = TextField(null=True) session_cookies = TextField(null=True) diff --git a/unmanic/service.py b/unmanic/service.py index e30b7030..7d2d2e03 100755 --- a/unmanic/service.py +++ b/unmanic/service.py @@ -86,7 +86,7 @@ def __init__(self): self.db_connection = None self.developer = None - self.dev_local_api = None + self.dev_api = None self.event = threading.Event() @@ -170,10 +170,9 @@ def start_scheduled_tasks_manager(self): }) return scheduled_tasks_manager - @staticmethod - def initial_register_unmanic(dev_local_api): + def initial_register_unmanic(self): from unmanic.libs import session - s = session.Session(dev_local_api=dev_local_api) + s = session.Session(dev_api=self.dev_api) s.register_unmanic(s.get_installation_uuid()) def start_threads(self, settings): @@ -194,7 +193,7 @@ def start_threads(self, settings): main_logger.info("Starting all threads") # Register installation - self.initial_register_unmanic(self.dev_local_api) + self.initial_register_unmanic() # Setup job queue task_queue = TaskQueue(data_queues) @@ -281,9 +280,8 @@ def main(): parser.add_argument('--dev', action='store_true', help='Enable developer mode') - parser.add_argument('--dev-local-api', - action='store_true', - help='Enable development against local unmanic support api') + parser.add_argument('--dev-api', nargs='?', + help='Enable development against another unmanic support api') parser.add_argument('--port', nargs='?', help='Specify the port to run the webserver on') # parser.add_argument('--unmanic_path', nargs='?', @@ -314,7 +312,7 @@ def main(): # Run the main Unmanic service service = Service() service.developer = args.dev - service.dev_local_api = args.dev_local_api + service.dev_api = args.dev_api service.run()