From 3abea4ad3c014457a5ca79eace9b6e713c9d167c Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 23 Apr 2016 19:03:01 -0700 Subject: [PATCH] Enable guest login with Plex.tv account --- data/interfaces/default/base.html | 19 ++++-- plexpy/__init__.py | 18 ++++- plexpy/http_handler.py | 2 +- plexpy/plextv.py | 46 ++++++++++--- plexpy/users.py | 108 ++++++++++++++++++++++++++---- plexpy/webauth.py | 34 ++++++---- plexpy/webserve.py | 25 ++++--- 7 files changed, 195 insertions(+), 57 deletions(-) diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index bd3a5611..dd48b955 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -212,17 +212,22 @@ from plexpy.helpers import anon_url % endif diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 51e9cbb9..d6c446bd 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -447,7 +447,8 @@ def dbcheck(): 'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL, friendly_name TEXT, ' 'thumb TEXT, custom_avatar_url TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, ' 'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, ' - 'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0)' + 'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0, allow_guest INTEGER DEFAULT 1, ' + 'user_token TEXT, server_token TEXT)' ) # notify_log table :: This is a table which logs notifications sent @@ -742,6 +743,21 @@ def dbcheck(): 'ALTER TABLE users ADD COLUMN deleted_user INTEGER DEFAULT 0' ) + # Upgrade users table from earlier versions + try: + c_db.execute('SELECT allow_guest FROM users') + except sqlite3.OperationalError: + logger.debug(u"Altering database. Updating database table users.") + c_db.execute( + 'ALTER TABLE users ADD COLUMN allow_guest INTEGER DEFAULT 1' + ) + c_db.execute( + 'ALTER TABLE users ADD COLUMN user_token TEXT' + ) + c_db.execute( + 'ALTER TABLE users ADD COLUMN server_token TEXT' + ) + # Upgrade notify_log table from earlier versions try: c_db.execute('SELECT poster_url FROM notify_log') diff --git a/plexpy/http_handler.py b/plexpy/http_handler.py index 199ad647..2c6561e7 100644 --- a/plexpy/http_handler.py +++ b/plexpy/http_handler.py @@ -90,7 +90,7 @@ class HTTPHandler(object): logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri) return None - if request_status == 200: + if request_status in (200, 201): try: if output_format == 'dict': output = helpers.convert_xml_to_dict(request_content) diff --git a/plexpy/plextv.py b/plexpy/plextv.py index 9963509d..dbde8697 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -20,6 +20,7 @@ from plexpy import logger, helpers, http_handler, database, users import xmltodict import json from xml.dom import minidom +import requests import base64 import plexpy @@ -109,34 +110,38 @@ class PlexTV(object): Plex.tv authentication """ - def __init__(self, username=None, password=None): + def __init__(self, username=None, password=None, token=None): self.protocol = 'HTTPS' self.username = username self.password = password self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT + token = token if token else plexpy.CONFIG.PMS_TOKEN + self.request_handler = http_handler.HTTPHandler(host='plex.tv', port=443, - token=plexpy.CONFIG.PMS_TOKEN, + token=token, ssl_verify=self.ssl_verify) def get_plex_auth(self, output_format='raw'): uri = '/users/sign_in.xml' base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '') headers = {'Content-Type': 'application/xml; charset=utf-8', - 'Content-Length': '0', 'X-Plex-Device-Name': 'PlexPy', 'X-Plex-Product': 'PlexPy', - 'X-Plex-Version': 'v0.1 dev', + 'X-Plex-Version': plexpy.common.VERSION_NUMBER, + 'X-Plex-Platform': plexpy.common.PLATFORM, + 'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION, 'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID, - 'Authorization': 'Basic %s' % base64string + ":" + 'Authorization': 'Basic %s' % base64string } - + request = self.request_handler.make_request(uri=uri, proto=self.protocol, request_type='POST', headers=headers, - output_format=output_format) + output_format=output_format, + no_token=True) return request @@ -147,16 +152,35 @@ class PlexTV(object): try: xml_head = plextv_response.getElementsByTagName('user') if xml_head: - auth_token = xml_head[0].getAttribute('authenticationToken') + user = {'auth_token': xml_head[0].getAttribute('authenticationToken'), + 'user_id': xml_head[0].getAttribute('id') + } else: logger.warn(u"PlexPy PlexTV :: Could not get Plex authentication token.") except Exception as e: logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_token: %s." % e) - return [] + return None - return auth_token + return user else: - return [] + return None + + def get_server_token(self): + servers = self.get_plextv_server_list(output_format='xml') + server_token = '' + + try: + xml_head = servers.getElementsByTagName('Server') + except Exception as e: + logger.warn(u"PlexPy PlexTV :: Unable to parse XML for get_server_token: %s." % e) + return None + + for a in xml_head: + if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER: + server_token = helpers.get_xml_attr(a, 'accessToken') + break + + return server_token def get_plextv_user_data(self): plextv_response = self.get_plex_auth(output_format='dict') diff --git a/plexpy/users.py b/plexpy/users.py index 012188d0..1ce4d443 100644 --- a/plexpy/users.py +++ b/plexpy/users.py @@ -15,6 +15,63 @@ from plexpy import logger, datatables, common, database, helpers +def user_login(username=None, password=None): + from plexpy import plextv + + if not username and not password: + return None + + user_data = Users() + + # Try to login to Plex.tv to check if the user has a vaild account + plex_tv = plextv.PlexTV(username=username, password=password) + user = plex_tv.get_token() + if user: + user_token = user['auth_token'] + user_id = user['user_id'] + + # Retrieve user token from the database and check against the Plex.tv token. + # Also Make sure 'allow_guest' access is enabled for the user. + # The user tokens should match if it is the same PlexPy install. + tokens = user_data.get_tokens(user_id=user_id) + if tokens and tokens['allow_guest'] and user_token == tokens['user_token']: + # Successful login + return True + + # Otherwise it is a new user or token is no longer valid. + # Check if the user is in the database. + user_details = user_data.get_details(user_id=user_id) + if user_details['allow_guest'] and user_id == str(user_details['user_id']): + + # The user is in the database, so try to retrieve a new server token. + # If a server token is returned, then the user is a vaild friend + plex_tv = plextv.PlexTV(token=user_token) + server_token = plex_tv.get_server_token() + if server_token: + + # Register the new user / update the access tokens. + monitor_db = database.MonitorDatabase() + try: + logger.debug(u"PlexPy Users :: Regestering tokens for user '%s' in the database." % username) + monitor_db.action('UPDATE users SET user_token = ?, server_token = ? WHERE user_id = ?', + [user_token, server_token, user_id]) + # Successful login + return True + except Exception as e: + logger.warn(u"PlexPy Users :: Unable to register user '%s' in database: %s." % (username, e)) + return None + else: + logger.warn(u"PlexPy Users :: Unable to retrieve Plex.tv server token.") + return None + else: + logger.warn(u"PlexPy Users :: Unable to register user '%s'. User not in the database." % username) + return None + else: + logger.warn(u"PlexPy Users :: Unable to retrieve Plex.tv user token.") + return None + + return None + class Users(object): @@ -69,7 +126,8 @@ class Users(object): 'session_history_metadata.parent_media_index', 'session_history_media_info.transcode_decision', 'users.do_notify as do_notify', - 'users.keep_history as keep_history' + 'users.keep_history as keep_history', + 'users.allow_guest as allow_guest' ] try: query = data_tables.ssp_query(table_name='users', @@ -135,7 +193,8 @@ class Users(object): 'parent_media_index': item['parent_media_index'], 'transcode_decision': item['transcode_decision'], 'do_notify': helpers.checked(item['do_notify']), - 'keep_history': helpers.checked(item['keep_history']) + 'keep_history': helpers.checked(item['keep_history']), + 'allow_guest': helpers.checked(item['allow_guest']) } rows.append(row) @@ -241,7 +300,7 @@ class Users(object): return dict - def set_config(self, user_id=None, friendly_name='', custom_thumb='', do_notify=1, keep_history=1): + def set_config(self, user_id=None, friendly_name='', custom_thumb='', do_notify=1, keep_history=1, allow_guest=1): if str(user_id).isdigit(): monitor_db = database.MonitorDatabase() @@ -249,7 +308,9 @@ class Users(object): value_dict = {'friendly_name': friendly_name, 'custom_avatar_url': custom_thumb, 'do_notify': do_notify, - 'keep_history': keep_history} + 'keep_history': keep_history, + 'allow_guest': allow_guest + } try: monitor_db.upsert('users', value_dict, key_dict) except Exception as e: @@ -267,7 +328,8 @@ class Users(object): 'is_allow_sync': 0, 'is_restricted': 0, 'do_notify': 0, - 'keep_history': 1 + 'keep_history': 1, + 'allow_guest': 0 } if not user_id and not user: @@ -279,13 +341,13 @@ class Users(object): try: if str(user_id).isdigit(): query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ - 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \ + 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, allow_guest ' \ 'FROM users ' \ 'WHERE user_id = ? ' result = monitor_db.select(query, args=[user_id]) elif user: query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ - 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \ + 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history, allow_guest ' \ 'FROM users ' \ 'WHERE username = ? ' result = monitor_db.select(query, args=[user]) @@ -319,7 +381,8 @@ class Users(object): 'is_allow_sync': item['is_allow_sync'], 'is_restricted': item['is_restricted'], 'do_notify': item['do_notify'], - 'keep_history': item['keep_history'] + 'keep_history': item['keep_history'], + 'allow_guest': item['allow_guest'] } return user_details @@ -488,7 +551,7 @@ class Users(object): try: if str(user_id).isdigit(): - logger.info(u"PlexPy DataFactory :: Deleting all history for user id %s from database." % user_id) + logger.info(u"PlexPy Users :: Deleting all history for user id %s from database." % user_id) session_history_media_info_del = \ monitor_db.action('DELETE FROM ' 'session_history_media_info ' @@ -520,7 +583,7 @@ class Users(object): try: if str(user_id).isdigit(): self.delete_all_history(user_id) - logger.info(u"PlexPy DataFactory :: Deleting user with id %s from database." % user_id) + logger.info(u"PlexPy Users :: Deleting user with id %s from database." % user_id) monitor_db.action('UPDATE users SET deleted_user = 1 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET keep_history = 0 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET do_notify = 0 WHERE user_id = ?', [user_id]) @@ -536,14 +599,14 @@ class Users(object): try: if user_id and str(user_id).isdigit(): - logger.info(u"PlexPy DataFactory :: Re-adding user with id %s to database." % user_id) + logger.info(u"PlexPy Users :: Re-adding user with id %s to database." % user_id) monitor_db.action('UPDATE users SET deleted_user = 0 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET keep_history = 1 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET do_notify = 1 WHERE user_id = ?', [user_id]) return 'Re-added user with id %s.' % user_id elif username: - logger.info(u"PlexPy DataFactory :: Re-adding user with username %s to database." % username) + logger.info(u"PlexPy Users :: Re-adding user with username %s to database." % username) monitor_db.action('UPDATE users SET deleted_user = 0 WHERE username = ?', [username]) monitor_db.action('UPDATE users SET keep_history = 1 WHERE username = ?', [username]) monitor_db.action('UPDATE users SET do_notify = 1 WHERE username = ?', [username]) @@ -568,4 +631,23 @@ class Users(object): except: return None - return None \ No newline at end of file + return None + + def get_tokens(self, user_id=None): + if user_id: + try: + monitor_db = database.MonitorDatabase() + query = 'SELECT allow_guest, user_token, server_token FROM users WHERE user_id = ?' + result = monitor_db.select_single(query, args=[user_id]) + if result: + tokens = {'allow_guest': result['allow_guest'], + 'user_token': result['user_token'], + 'server_token': result['server_token'] + } + return tokens + else: + return None + except: + return None + + return None diff --git a/plexpy/webauth.py b/plexpy/webauth.py index 87d1068e..94427c35 100644 --- a/plexpy/webauth.py +++ b/plexpy/webauth.py @@ -25,6 +25,7 @@ from datetime import datetime, timedelta import plexpy from plexpy import logger +from plexpy.users import user_login SESSION_KEY = '_cp_username' @@ -32,13 +33,16 @@ SESSION_KEY = '_cp_username' def check_credentials(username, password): """Verifies credentials for username and password. Returns None on success or a string describing the error on failure""" + if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): - return None + return True, u'admin' elif username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD: - return None + return True, u'admin' + elif user_login(username, password): + return True, u'guest' else: - return u"Incorrect username or password." + return False, None # An example implementation which uses an ORM could be: # u = User.get(username) @@ -53,7 +57,9 @@ def check_auth(*args, **kwargs): conditions that the user must fulfill""" conditions = cherrypy.request.config.get('auth.require', None) if conditions is not None: - (username, expiry) = cherrypy.session.get(SESSION_KEY) if cherrypy.session.get(SESSION_KEY) else (None, None) + session = cherrypy.session.get(SESSION_KEY) + username, user_group, expiry = session if session else (None, None, None) + if (username and expiry) and expiry > datetime.now(): cherrypy.request.login = username for condition in conditions: @@ -143,28 +149,30 @@ class AuthController(object): if username is None or password is None: return self.get_loginform() - error_msg = check_credentials(username, password) + (vaild_login, user_group) = check_credentials(username, password) - if error_msg: - logger.debug(u"Invalid login attempt from '%s'." % username) - return self.get_loginform(username, error_msg) - else: + if vaild_login: cherrypy.session.regenerate() cherrypy.request.login = username expiry = datetime.now() + (timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)) - cherrypy.session[SESSION_KEY] = (username, expiry) + cherrypy.session[SESSION_KEY] = (username, user_group, expiry) self.on_login(username) raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) + + else: + logger.debug(u"Invalid login attempt from '%s'." % username) + return self.get_loginform(username, u"Incorrect username or password.") @cherrypy.expose def logout(self): if not plexpy.CONFIG.HTTP_PASSWORD: raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) - sess = cherrypy.session - (username, expiry) = sess.get(SESSION_KEY) if sess.get(SESSION_KEY) else (None, None) - sess[SESSION_KEY] = None + cp_sess = cherrypy.session + session = cp_sess.get(SESSION_KEY) + username, user_group, expiry = session if session else (None, None, None) + cp_sess[SESSION_KEY] = None if username: cherrypy.request.login = None diff --git a/plexpy/webserve.py b/plexpy/webserve.py index c27ab9cc..09973df2 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -16,7 +16,7 @@ from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, \ datafactory, graphs, users, libraries, database, web_socket from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates -from plexpy.webauth import AuthController, requireAuth, member_of, name_is +from plexpy.webauth import AuthController, requireAuth, member_of, name_is, SESSION_KEY from mako.lookup import TemplateLookup from mako import exceptions @@ -49,9 +49,13 @@ def serve_template(templatename, **kwargs): server_name = plexpy.CONFIG.PMS_NAME + session = cherrypy.session.get(SESSION_KEY) + user, user_group, expiry = session if session else (None, None, None) + try: template = _hplookup.get_template(templatename) - return template.render(server_name=server_name, http_root=plexpy.HTTP_ROOT, **kwargs) + return template.render(http_root=plexpy.HTTP_ROOT, server_name=server_name, + user=user, user_group=user_group, expiry=expiry, **kwargs) except: return exceptions.html_error_template().render() @@ -64,7 +68,7 @@ class WebInterface(object): self.interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/') @cherrypy.expose - @requireAuth(member_of("admin")) + @requireAuth() def index(self): if plexpy.CONFIG.FIRST_RUN_COMPLETE: raise cherrypy.HTTPRedirect("home") @@ -149,7 +153,7 @@ class WebInterface(object): ##### Home ##### @cherrypy.expose - @requireAuth(member_of("admin")) + @requireAuth() def home(self): config = { "home_sections": plexpy.CONFIG.HOME_SECTIONS, @@ -162,7 +166,6 @@ class WebInterface(object): return serve_template(templatename="index.html", title="Home", config=config) @cherrypy.expose - @requireAuth(member_of("admin")) @addtoapi() def get_date_formats(self): """ Get the date and time formats used by plexpy """ @@ -183,7 +186,6 @@ class WebInterface(object): return json.dumps(formats) @cherrypy.expose - @requireAuth(member_of("admin")) def get_current_activity(self, **kwargs): try: @@ -206,7 +208,6 @@ class WebInterface(object): return serve_template(templatename="current_activity.html", data=None) @cherrypy.expose - @requireAuth(member_of("admin")) def get_current_activity_header(self, **kwargs): try: @@ -222,7 +223,6 @@ class WebInterface(object): return serve_template(templatename="current_activity_header.html", data=None) @cherrypy.expose - @requireAuth(member_of("admin")) def home_stats(self, **kwargs): data_factory = datafactory.DataFactory() @@ -243,7 +243,6 @@ class WebInterface(object): return serve_template(templatename="home_stats.html", title="Stats", data=stats_data) @cherrypy.expose - @requireAuth(member_of("admin")) def library_stats(self, **kwargs): data_factory = datafactory.DataFactory() @@ -254,7 +253,6 @@ class WebInterface(object): return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data) @cherrypy.expose - @requireAuth(member_of("admin")) def get_recently_added(self, count='0', **kwargs): try: @@ -1847,7 +1845,7 @@ class WebInterface(object): return serve_template(templatename="info_children_list.html", data=None, title="Children List") @cherrypy.expose - @requireAuth(member_of("admin")) + @requireAuth() def pms_image_proxy(self, img='', width='0', height='0', fallback=None, **kwargs): try: pms_connect = pmsconnect.PmsConnect() @@ -2433,3 +2431,8 @@ class WebInterface(object): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_update_staus() return json.dumps(result) + + @cherrypy.expose + def test_guest_login(self, username=None, password=None): + result = users.user_login(username=username, password=password) + return result