# -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals ################################################################################################# import json import socket from datetime import datetime from operator import itemgetter import traceback from typing import Optional import urllib3 from ..helper import LazyLogger from .credentials import Credentials from .api import API ################################################################################################# LOG = LazyLogger(__name__) CONNECTION_STATE = { 'Unavailable': 0, 'ServerSelection': 1, 'ServerSignIn': 2, 'SignedIn': 3 } ################################################################################################# class ConnectionManager(object): server_id: Optional[str] = None def __init__(self, client): LOG.debug("ConnectionManager initializing...") self.client = client self.config = client.config self.credentials = Credentials() self.API = API(client) def revoke_token(self): LOG.info("revoking token") self['server']['AccessToken'] = None self.credentials.set_credentials(self.credentials.get()) self.config.data['auth.token'] = None def get_available_servers(self): LOG.info("Begin getAvailableServers") # Clone the credentials credentials = self.credentials.get() found_servers = self.process_found_servers(self._server_discovery()) if not found_servers and not credentials['Servers']: # back out right away, no point in continuing LOG.info("Found no servers") return list() servers = list(credentials['Servers']) # Merges servers we already knew with newly found ones for found_server in found_servers: try: self.credentials.add_update_server(servers, found_server) except KeyError: continue servers.sort(key=itemgetter('DateLastAccessed'), reverse=True) credentials['Servers'] = servers self.credentials.set(credentials) return servers def login(self, server_url, username, password=None): if not username: raise AttributeError("username cannot be empty") if not server_url: raise AttributeError("server url cannot be empty") data = self.API.login(server_url, username, password) # returns empty dict on failure if not data: LOG.info("Failed to login as `"+username+"`") return {} LOG.info("Successfully logged in as %s" % (username)) # TODO Change when moving to database storage of server details credentials = self.credentials.get() self.config.data['auth.user_id'] = data['User']['Id'] self.config.data['auth.token'] = data['AccessToken'] for server in credentials['Servers']: if server['Id'] == data['ServerId']: found_server = server break else: return {} # No server found found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') found_server['UserId'] = data['User']['Id'] found_server['AccessToken'] = data['AccessToken'] self.credentials.add_update_server(credentials['Servers'], found_server) info = { 'Id': data['User']['Id'], 'IsSignedInOffline': True } self.credentials.add_update_user(server, info) self.credentials.set_credentials(credentials) return data def connect_to_address(self, address, options={}): if not address: return False address = self._normalize_address(address) try: response_url = self.API.check_redirect(address) if address != response_url: address = response_url LOG.info("connectToAddress %s succeeded", address) server = { 'address': address, } server = self.connect_to_server(server, options) if server is False: LOG.error("connectToAddress %s failed", address) return {'State': CONNECTION_STATE['Unavailable']} return server except Exception as error: LOG.exception(error) LOG.error("connectToAddress %s failed", address) return {'State': CONNECTION_STATE['Unavailable']} def connect_to_server(self, server, options={}): LOG.info("begin connectToServer") try: result = self.API.get_public_info(server.get('address')) if not result: LOG.error("Failed to connect to server: %s" % server.get('address')) return {'State': CONNECTION_STATE['Unavailable']} LOG.info("calling onSuccessfulConnection with server %s", server.get('Name')) self._update_server_info(server, result) credentials = self.credentials.get() return self._after_connect_validated(server, credentials, result, True, options) except Exception as e: LOG.error(traceback.format_exc()) LOG.error("Failing server connection. ERROR msg: {}".format(e)) return {'State': CONNECTION_STATE['Unavailable']} def connect(self, options={}): LOG.info("Begin connect") servers = self.get_available_servers() LOG.info("connect has %s servers", len(servers)) if not (len(servers)): # No servers provided return { 'State': ['ServerSelection'] } result = self.connect_to_server(servers[0], options) LOG.debug("resolving connect with result: %s", result) return result def jellyfin_token(self): # Called once monitor.py#163 return self.get_server_info(self.server_id)['AccessToken'] def get_server_info(self, server_id): if server_id is None: LOG.info("server_id is empty") return {} servers = self.credentials.get()['Servers'] for server in servers: if server['Id'] == server_id: return server def get_server_address(self, server_id): return self.get_server_info(server_id or self.server_id).get('address') def get_public_users(self): return self.client.jellyfin.get_public_users() def _server_discovery(self): MULTI_GROUP = ("", 7359) MESSAGE = b"who is JellyfinServer?" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) # This controls the socket.timeout exception sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) LOG.debug("MultiGroup : %s", str(MULTI_GROUP)) LOG.debug("Sending UDP Data: %s", MESSAGE) servers = [] try: sock.sendto(MESSAGE, MULTI_GROUP) except Exception as error: LOG.exception(traceback.format_exc()) LOG.exception(error) return servers while True: data = None addr = None try: data, addr = sock.recvfrom(1024) # buffer size servers.append(json.loads(data)) except socket.timeout: LOG.info("Found Servers: %s", servers) return servers except json.JSONDecodeError: LOG.warning("Unable to decode %r from %r.", data, addr) except Exception as e: LOG.error(traceback.format_exc()) LOG.exception("Error trying to find servers: %s", e) return servers def process_found_servers(self, found_servers): servers = [] for found_server in found_servers: server = self._convert_endpoint_address_to_manual_address(found_server) info = { 'Id': found_server['Id'], 'address': server or found_server['Address'], 'Name': found_server['Name'] } servers.append(info) return servers # TODO: Make IPv6 compatible def _convert_endpoint_address_to_manual_address(self, info): if info.get('Address') and info.get('EndpointAddress'): address = info['EndpointAddress'].split(':')[0] # Determine the port, if any parts = info['Address'].split(':') if len(parts) > 1: port_string = parts[len(parts) - 1] try: address += ":%s" % int(port_string) return self._normalize_address(address) except ValueError: pass return None def _normalize_address(self, address): # TODO: Try HTTPS first, then HTTP if that fails. if '://' not in address: address = 'http://' + address # Attempt to correct bad input url = urllib3.util.parse_url(address.strip()) if url.scheme is None: url = url._replace(scheme='http') if url.scheme == 'http' and url.port == 80: url = url._replace(port=None) if url.scheme == 'https' and url.port == 443: url = url._replace(port=None) return url.url def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options): if options.get('enableAutoLogin') is False: self.config.data['auth.user_id'] = server.pop('UserId', None) self.config.data['auth.token'] = server.pop('AccessToken', None) elif verify_authentication and server.get('AccessToken'): system_info = self.API.validate_authentication_token(server) if 'Status_Code' not in system_info: self._update_server_info(server, system_info) self.config.data['auth.user_id'] = server['UserId'] self.config.data['auth.token'] = server['AccessToken'] system_info['Status_Code'] = 200 return self._after_connect_validated(server, credentials, system_info, False, options) server['UserId'] = None server['AccessToken'] = None system_info['State'] = CONNECTION_STATE['Unavailable'] return system_info self._update_server_info(server, system_info) server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') self.credentials.add_update_server(credentials['Servers'], server) self.credentials.set(credentials) self.server_id = server['Id'] # Update configs self.config.data['auth.server'] = server['address'] self.config.data['auth.server-name'] = server['Name'] self.config.data['auth.server=id'] = server['Id'] self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl']) # Connected return { 'Servers': [server], 'State': CONNECTION_STATE['SignedIn'] if server.get('AccessToken') else CONNECTION_STATE['ServerSignIn'], } def _update_server_info(self, server, system_info): if server is None or system_info is None: return server['Name'] = system_info['ServerName'] server['Id'] = system_info['Id'] if system_info.get('address'): server['address'] = system_info['address']