# -*- coding: utf-8 -*- import json import socket import _strptime # Workaround for threads using datetime: _striptime is locked import datetime import helper.loghandler import emby.core.credentials class ConnectionManager(): def __init__(self, EmbyServer): self.LOG = helper.loghandler.LOG('EMBY.core.connection_manager') self.LOG.debug("ConnectionManager initializing...") self.LOG.info("Begin connectToServer") self.user = {} self.EmbyServer = EmbyServer self.credentials = emby.core.credentials.Credentials() def get_server_address(self, server, mode): modes = {0: server.get('LocalAddress'), 1: server.get('RemoteAddress'), 2: server.get('ManualAddress')} #Local...Remote...Manual return modes.get(mode) or server.get('ManualAddress', server.get('LocalAddress', server.get('RemoteAddress'))) def get_serveraddress(self): server = self.get_server_info() return self.get_server_address(server, server['LastConnectionMode']) def clear_data(self): self.LOG.info("connection manager clearing data") self.user = None credentials = self.credentials.get_credentials() credentials['ConnectAccessToken'] = None credentials['ConnectUserId'] = None credentials['Servers'] = list() self.credentials.get_credentials(credentials) self.EmbyServer.Data['auth.server'] = None self.EmbyServer.Data['auth.user_id'] = None self.EmbyServer.Data['auth.token'] = None self.EmbyServer.Data['auth.ssl'] = None def revoke_token(self): self.LOG.info("revoking token") # self.get_server_info()['AccessToken'] = None # self.credentials.get_credentials(self.credentials.get_credentials()) # self.EmbyServer.Data['auth.token'] = None def get_available_servers(self): self.LOG.debug("Begin getAvailableServers") # Clone the credentials credentials = self.credentials.get_credentials() connect_servers = self._get_connect_servers(credentials) found_servers = self._find_servers(self._server_discovery()) if not connect_servers and not found_servers and not credentials['Servers']: # back out right away, no point in continuing self.LOG.info("Found no servers") return list() servers = list(credentials['Servers']) self._merge_servers(servers, found_servers) self._merge_servers(servers, connect_servers) servers = self._filter_servers(servers, connect_servers) credentials['Servers'] = servers self.credentials.get_credentials(credentials) return servers def login_to_connect(self, username, password): if not username: return False #"username cannot be empty" if not password: return False #"password cannot be empty" result = self._request_url({ 'type': "POST", 'url': self.get_connect_url("user/authenticate"), 'data': { 'nameOrEmail': username, 'rawpw': password }, 'dataType': "json" }, True, False) if not result: #Failed to login return False credentials = self.credentials.get_credentials() credentials['ConnectAccessToken'] = result['AccessToken'] credentials['ConnectUserId'] = result['User']['Id'] credentials['ConnectUser'] = result['User']['Name'] self.credentials.get_credentials(credentials) # Signed in self._on_connect_user_signin(result['User']) return result def login(self, server, username, password, clear, options): if not username: self.LOG.error("username cannot be empty") return False request = { 'type': "POST", 'url': self.get_emby_url(server, "Users/AuthenticateByName"), 'params': { 'username': username, 'pw': password or "", } } if clear: request['params']['pw'] = password or "" result = self._request_url(request, False, True) if not result: return False self._on_authenticated(result, options) return result def connect_to_address(self, address, options): if not address: return False address = self._normalize_address(address) public_info = self._try_connect(address, None, options) self.LOG.info("connectToAddress %s succeeded" % address) server = { 'ManualAddress': address, 'LastConnectionMode': 2 #Manual } self._update_server_info(server, public_info) server = self.connect_to_server(server, options) if not server: return False return server def connect_to_server(self, server, options): self.LOG.debug("Begin connectToServer") tests = [] if server.get('LastConnectionMode') != 1 and server.get('AccessToken'): #Remote tests.append(server['LastConnectionMode']) if 2 not in tests: #Manual tests.append(2) if 0 not in tests: #Local tests.append(0) if 1 not in tests: #Remote tests.append(1) return self._test_next_connection_mode(tests, 0, server, options) def connect(self, options): self.LOG.info("Begin connect") return self._connect_to_servers(self.get_available_servers(), options) def get_server_info(self): servers = self.credentials.get_credentials()['Servers'] for server in servers: if server['Id'] == self.EmbyServer.server_id: return server def get_connect_url(self, handler): return "https://connect.emby.media/service/%s" % handler def get_emby_url(self, base, handler): return "%s/emby/%s" % (base, handler) def _request_url(self, request, headers, MSGs): request['timeout'] = request.get('timeout') or 10 if headers: self._get_headers(request) return self.EmbyServer.http.request(request, MSGs) def _get_headers(self, request): headers = request.setdefault('headers', {}) if request.get('dataType') == "json": headers['Accept'] = "application/json" headers['Accept-Charset'] = "UTF-8,*" headers['Accept-encoding'] = "gzip" request.pop('dataType') headers['X-Application'] = "%s/%s" % (self.EmbyServer.Data['app.name'], self.EmbyServer.Data['app.version']) headers['Content-type'] = request.get('contentType', 'application/x-www-form-urlencoded; charset=UTF-8') def _connect_to_servers(self, servers, options): self.LOG.info("Begin connectToServers, with %s servers" % len(servers)) result = {} if len(servers) == 1: result = self.connect_to_server(servers[0], options) self.LOG.debug("resolving connectToServers with result['State']: %s" % result) return result first_server = self._get_last_used_server() # See if we have any saved credentials and can auto sign in if first_server is not None and first_server['DateLastAccessed'] != "2001-01-01T00:00:00Z": result = self.connect_to_server(first_server, options) if not result: return False if result['State'] in (3, 0): #SignedIn or Unavailable return result # Return loaded credentials if exists credentials = self.credentials.get_credentials() self._ensure_connect_user(credentials) return { 'Servers': servers, 'State': 4 if (not len(servers) and not self.user) else (result.get('State') or 1), 'ConnectUser': self.user #ConnectSignIn...ServerSelection } def _try_connect(self, url, timeout, options, MSGs=False): url = self.get_emby_url(url, "system/info/public") self.LOG.info("tryConnect url: %s" % url) return self._request_url({ 'type': "GET", 'url': url, 'dataType': "json", 'timeout': timeout, 'verify': options.get('ssl'), 'retry': False }, True, MSGs) def _test_next_connection_mode(self, tests, index, server, options): if index >= len(tests): self.LOG.info("Tested all connection modes. Failing server connection.") return False mode = tests[index] address = self.get_server_address(server, mode) skip_test = False self.LOG.info("testing connection mode %s with server %s" % (str(mode), server.get('Name'))) if mode == 0: #Local if self._string_equals_ignore_case(address, server.get('ManualAddress')): self.LOG.info("skipping LocalAddress test because it is the same as ManualAddress") skip_test = True if skip_test or not address: self.LOG.info("skipping test at index: %s" % index) return self._test_next_connection_mode(tests, index + 1, server, options) result = self._try_connect(address, 10, options, False) if not result: return self._test_next_connection_mode(tests, index + 1, server, options) self.LOG.info("calling onSuccessfulConnection with connection mode %s with server %s" % (mode, server.get('Name'))) return self._on_successful_connection(server, result, mode, options) def _on_successful_connection(self, server, system_info, connection_mode, options): credentials = self.credentials.get_credentials() if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False: if self._ensure_connect_user(credentials) is not False: if server.get('ExchangeToken'): self._add_authentication_info_from_connect(server, connection_mode, credentials, options) return self._after_connect_validated(server, credentials, system_info, connection_mode, True, options) def _string_equals_ignore_case(self, str1, str2): return (str1 or "").lower() == (str2 or "").lower() def _get_connect_user(self, user_id, access_token): if not user_id: return False #"null userId" if not access_token: return False #"null accessToken" return self._request_url({ 'type': "GET", 'url': self.get_connect_url('user?id=%s' % user_id), 'dataType': "json", 'headers': { 'X-Connect-UserToken': access_token } }, True, False) def _server_discovery(self): MULTI_GROUP = ("", 7359) MESSAGE = b"who is EmbyServer?" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) # This controls the socket.timeout exception sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) self.LOG.debug("MultiGroup : %s" % str(MULTI_GROUP)) self.LOG.debug("Sending UDP Data: %s" % MESSAGE) servers = [] try: sock.sendto(MESSAGE, MULTI_GROUP) except Exception as error: self.LOG.error("ERROR: %s" % error) return servers while True: data = None try: data, _ = sock.recvfrom(1024) # buffer size servers.append(json.loads(data)) except socket.timeout: self.LOG.info("Found Servers: %s" % servers) return servers except Exception as Error: self.LOG.error("Error trying to find servers: %s" % Error) return servers def _get_connect_servers(self, credentials): self.LOG.debug("Begin getConnectServers") servers = list() if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): return servers url = self.get_connect_url("servers?userId=%s" % credentials['ConnectUserId']) request = { 'type': "GET", 'url': url, 'dataType': "json", 'headers': { 'X-Connect-UserToken': credentials['ConnectAccessToken'] } } result = self._request_url(request, True, False) if not result: return [] for server in result: servers.append({ 'ExchangeToken': server['AccessKey'], 'ConnectServerId': server['Id'], 'Id': server['SystemId'], 'Name': server['Name'], 'RemoteAddress': server['Url'], 'LocalAddress': server['LocalAddress'], 'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser", }) return servers def _get_last_used_server(self): servers = self.credentials.get_credentials()['Servers'] if not len(servers): return return servers[0] def _merge_servers(self, list1, list2): for i in range(0, len(list2), 1): self.credentials.add_update_server(list1, list2[i]) return list1 def _find_servers(self, found_servers): servers = [] for found_server in found_servers: server = self._convert_endpoint_address_to_manual_address(found_server) if not server and not found_server.get('Address'): self.LOG.warning("Server %s has no address." % found_server) continue info = { 'Id': found_server['Id'], 'LocalAddress': server or found_server['Address'], 'Name': found_server['Name'] } servers.append(info) return servers def _filter_servers(self, servers, connect_servers): filtered = list() for server in servers: if server.get('ExchangeToken') is None: # It's not a connect server, so assume it's still valid filtered.append(server) continue for connect_server in connect_servers: if server['Id'] == connect_server['Id']: filtered.append(server) break return filtered 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] address += ":%s" % int(port_string) return self._normalize_address(address) return None def _normalize_address(self, address): # Attempt to correct bad input address = address.strip() address = address.lower() if 'http' not in address: address = "http://%s" % address return address def _ensure_connect_user(self, credentials): if self.user and self.user['Id'] == credentials['ConnectUserId']: return if credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): self.user = None result = self._get_connect_user(credentials['ConnectUserId'], credentials['ConnectAccessToken']) self._on_connect_user_signin(result) def _on_connect_user_signin(self, user): self.user = user self.LOG.info("connectusersignedin %s" % user) def _save_user_info_into_credentials(self, server, user): info = { 'Id': user['Id'], 'IsSignedInOffline': True } self.credentials.add_update_user(server, info) def _add_authentication_info_from_connect(self, server, connection_mode, credentials, options): if not server.get('ExchangeToken'): self.LOG.error('server ExchangeToken cannot be null') return False if not credentials.get('ConnectUserId'): self.LOG.error('credentials ConnectUserId cannot be null') return False auth = "Emby " auth += "Client=%s, " % self.EmbyServer.Data['app.name'] auth += "Device=%s, " % self.EmbyServer.Data['app.device_name'] auth += "DeviceId=%s, " % self.EmbyServer.Data['app.device_id'] auth += "Version=%s " % self.EmbyServer.Data['app.version'] auth = self._request_url({ 'url': self.get_emby_url(self.get_server_address(server, connection_mode), "Connect/Exchange"), 'type': "GET", 'dataType': "json", 'verify': options.get('ssl'), 'params': { 'ConnectUserId': credentials['ConnectUserId'] }, 'headers': { 'X-Emby-Token': server['ExchangeToken'], 'Authorization': auth } }, True, False) if not auth: server['UserId'] = None server['AccessToken'] = None return False server['UserId'] = auth['LocalUserId'] server['AccessToken'] = auth['AccessToken'] return auth def _after_connect_validated(self, server, credentials, system_info, connection_mode, verify_authentication, options): if options.get('enableAutoLogin') == False: self.EmbyServer.Data['auth.user_id'] = server.pop('UserId', None) self.EmbyServer.Data['auth.token'] = server.pop('AccessToken', None) elif verify_authentication and server.get('AccessToken'): if self._validate_authentication(server, connection_mode, options) is not False: self.EmbyServer.Data['auth.user_id'] = server['UserId'] self.EmbyServer.Data['auth.token'] = server['AccessToken'] return self._after_connect_validated(server, credentials, system_info, connection_mode, False, options) if server.get('AccessToken'): return False self._update_server_info(server, system_info) server['LastConnectionMode'] = connection_mode if options.get('updateDateLastAccessed') is not False: server['DateLastAccessed'] = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') self.credentials.add_update_server(credentials['Servers'], server) self.credentials.get_credentials(credentials) # Update configs self.EmbyServer.Data['auth.server'] = self.get_server_address(server, connection_mode) self.EmbyServer.Data['auth.server-name'] = server['Name'] self.EmbyServer.server_id = server['Id'] self.EmbyServer.Data['auth.ssl'] = options.get('ssl', self.EmbyServer.Data['auth.ssl']) result = {'Servers': [server], 'ConnectUser': self.user} result['State'] = 3 if server.get('AccessToken') else 2 #SignedIn...ServerSignIn # Connected return result def _validate_authentication(self, server, connection_mode, options): system_info = self._request_url({ 'type': "GET", 'url': self.get_emby_url(self.get_server_address(server, connection_mode), "System/Info"), 'verify': options.get('ssl'), 'dataType': "json", 'headers': { 'X-Emby-Token': server['AccessToken'] } }, True, False) if system_info: self._update_server_info(server, system_info) return True return False 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('LocalAddress'): server['LocalAddress'] = system_info['LocalAddress'] if system_info.get('WanAddress'): server['RemoteAddress'] = system_info['WanAddress'] if 'MacAddress' in system_info: server['WakeOnLanInfos'] = [{'MacAddress': system_info['MacAddress']}] def _on_authenticated(self, result, options): credentials = self.credentials.get_credentials() self.EmbyServer.Data['auth.user_id'] = result['User']['Id'] self.EmbyServer.Data['auth.token'] = result['AccessToken'] for server in credentials['Servers']: if server['Id'] == result['ServerId']: found_server = server break else: return {} # No server found if options.get('updateDateLastAccessed') is not False: found_server['DateLastAccessed'] = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') found_server['UserId'] = result['User']['Id'] found_server['AccessToken'] = result['AccessToken'] self.credentials.add_update_server(credentials['Servers'], found_server) self._save_user_info_into_credentials(found_server, result['User']) self.credentials.get_credentials(credentials)