###################################################################################################################### # WebTools helper unit # # Runs a seperate webserver on a specified port # Author: dane22, a Plex Community member # ###################################################################################################################### from consts import DEBUGMODE, WT_AUTH, VERSION, NAME, V3MODULES, BASEURL, UILANGUAGE, UILANGUAGEDEBUG, WT_URL import sys # Add modules dir to search path modules = Core.storage.join_path( Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle', 'Contents', 'Code', 'modules') sys.path.append(modules) from tornado.web import * from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from tornado.escape import json_encode, xhtml_escape import threading import os import sys import time import apiv3 from plextvhelper import plexTV from wtV3 import wtV3 # Below used to find path of this file from inspect import getsourcefile from os.path import abspath # TODO # from importlib import import_module # SNIFF....Tornado is V1.0.0, meaning no WebSocket :-( # Path to http folder within the bundle def getActualHTTPPath(): try: HTTPPath = os.path.normpath( Core.storage.join_path(Core.bundle_path, 'http')) if not os.path.isdir(HTTPPath): Log.Critical('Could not find my http path in: ' + HTTPPath) return '' else: return HTTPPath except Exception, e: Log.Exception('Exception in getActualHTTPPath was %s' % (str(e))) # Path to bundle folder within the bundle def isCorrectPath(req): try: installedPlugInPath = os.path.normpath( abspath(getsourcefile(lambda: 0)).split(str(NAME) + '.bundle', 1)[0]) targetPath = os.path.normpath(Core.storage.join_path( Core.app_support_path, Core.config.bundles_dir_name)) if installedPlugInPath != targetPath: Log.Debug('************************************************') Log.Debug('Wrong installation path detected!!!!') Log.Debug('') Log.Debug('Currently installed in:') Log.Debug(installedPlugInPath) Log.Debug('Correct path is:') Log.Debug(Core.storage.join_path(Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle')) Log.Debug('************************************************') installedPlugInPath, skipStr = abspath( getsourcefile(lambda: 0)).split('/Contents', 1) msg = '

Wrong installation path detected

' msg = msg + '

It seems like you installed ' + \ NAME + ' into the wrong folder

' msg = msg + '

You installed ' + NAME + ' here:

' msg = msg + installedPlugInPath msg = msg + '

but the correct folder is:

' msg = msg + Core.storage.join_path( Core.app_support_path, Core.config.bundles_dir_name, NAME + '.bundle') req.clear() req.set_status(404) req.finish(msg) else: Log.Info('Verified a correct install path as: ' + targetPath) except Exception, e: Log.Exception('Exception in isCorrectPath was %s' % (str(e))) #************** webTools functions ****************************** ''' Here we have the supported functions ''' class webTools(object): # Defaults used by the rest of the class def __init__(self): # Not used yet return ''' Return version number, and other info ''' def getVersion(self): try: scheme = Dict['wt_csstheme'] if scheme == None: scheme = '' retVal = {'version': VERSION, 'PasswordSet': Dict['pwdset'], 'PlexTVOnline': plexTV().auth2myPlex( ), 'wt_csstheme': scheme, 'UILanguageDebug': UILANGUAGEDEBUG, 'UILanguage': Dict['UILanguage'], 'WT_URL': WT_URL} Log.Info('Version requested, returning ' + str(retVal)) return retVal except Exception, e: Log.Exception('Exception in getVersion: %s' % (str(e))) #**************** Handler Classes for Rest ********************** class MyStaticFileHandler(StaticFileHandler): def set_extra_headers(self, path): # Disable cache self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') class BaseHandler(RequestHandler): def get_current_user(self): return self.get_secure_cookie(NAME) ''' handler to force TLS ''' class ForceTSLHandler(RequestHandler): def get(self): ''' This is sadly up hill, due to the old version of Tornado used :-( ''' # Grap the host requested host, port = self.request.headers['Host'].split(':') newUrl = 'https://' + host + ':' + \ Prefs['WEB_Port_https'] + BASEURL + '/login' self.redirect(newUrl, permanent=True) ''' If user didn't enter the full path ''' class idxHandler(BaseHandler): @authenticated def get(self): # If hitting root, but without an ending slash, redirect to that if self.request.uri == BASEURL: self.redirect('%s/' % BASEURL) Log.Info('Returning: ' + Core.storage.join_path(getActualHTTPPath(), 'index.html')) self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.render(Core.storage.join_path(getActualHTTPPath(), 'index.html')) ''' Logout handler ''' class LogoutHandler(BaseHandler): @authenticated def get(self): Log.Info('Clearing Auth Cookie') self.clear_cookie(NAME) self.redirect('%s/' % BASEURL) class LoginHandler(BaseHandler): def get(self): isCorrectPath(self) Log.Info('Returning login page: ' + Core.storage.join_path(getActualHTTPPath(), 'login.html')) self.render(Core.storage.join_path(getActualHTTPPath(), 'login.html'), next=self.get_argument("next", "/")) def post(self): global AUTHTOKEN # Check for an auth header, in case a frontend wanted to use that # Header has precedence compared to params auth_header = self.request.headers.get('Authorization', None) if auth_header is None or not auth_header.startswith('Basic '): Log.Info('No Basic Auth header, so looking for params') user = self.get_argument('user', '') if user == '': if plexTV().auth2myPlex(): Log.Info('Missing username') self.clear() self.set_status(412) self.finish('Missing username') pwd = self.get_argument('pwd', '') if pwd == '': Log.Info('Missing password') self.clear() self.set_status(412) self.finish('Missing password') else: Log.Info('Auth header found') auth_decoded = String.Base64Decode(auth_header[6:]) user, pwd = auth_decoded.split(':', 2) Log.Info('User is: ' + user) # Allow no password when in debug mode if DEBUGMODE: if not WT_AUTH: self.allow() Log.Info('All is good, we are authenticated') self.redirect('%s/' % BASEURL) else: # Let's start by checking if the server is online if plexTV().auth2myPlex(): token = '' try: # Authenticate login_token = plexTV().login(user, pwd) if login_token == None: Log.Error( 'Bad credentials detected, denying access') self.clear() self.set_status(401) self.finish('Authentication error') return self retVal = plexTV().isServerOwner(login_token) self.clear() if retVal == 0: # All is good self.allow() Log.Info('All is good, we are authenticated') self.redirect('%s/' % BASEURL) elif retVal == 1: # Server not found Log.Info('Server not found on plex.tv') self.set_status(404) elif retVal == 2: # Not the owner Log.Info('User is not the server owner') self.set_status(403) else: # Unknown error Log.Critical('Unknown error, when authenticating') self.set_status(403) except Ex.HTTPError, e: Log.Exception('Exception in Login: ' + str(e)) self.clear() self.set_status(e.code) self.finish(str(e)) return self except Exception, e: Log.Exception('Exception in Login: ' + str(e)) self.clear() self.set_status(e.code) self.finish(str(e)) return self else: # Let's start by checking if the server is online try: if plexTV().auth2myPlex(): token = '' try: # Authenticate login_token = plexTV().login(user, pwd) if login_token == None: Log.Error( 'Bad credentials detected, denying access') self.clear() self.set_status(401) self.finish('Authentication error') return self retVal = plexTV().isServerOwner(login_token) self.clear() if retVal == 0: # All is good self.allow() Log.Info('All is good, we are authenticated') self.redirect('%s/' % BASEURL) elif retVal == 1: # Server not found Log.Info('Server not found on plex.tv') self.set_status(404) elif retVal == 2: # Not the owner Log.Info('User is not the server owner') self.set_status(403) else: # Unknown error Log.Critical('Unknown error, when authenticating') self.set_status(403) except Ex.HTTPError, e: Log.Exception('Exception in Login: ' + str(e)) self.clear() self.set_status(e.code) self.finish(str(e)) return self except Exception, e: Log.Exception('Exception in Login: ' + str(e)) self.clear() self.set_status(e.code) self.finish(str(e)) return self except Exception, e: Log.Exception('Exception in Login: ' + str(e)) self.clear() self.set_status(e.code) self.finish(str(e)) return self else: Log.Info('Server is not online according to plex.tv') # Server is offline if Dict['password'] == '': Log.Info( 'First local login, so we need to set the local password') Dict['password'] = pwd Dict['pwdset'] = True Dict.Save self.allow() self.redirect('%s/' % BASEURL) elif Dict['password'] == pwd: self.allow() Log.Info('Local password accepted') self.redirect('%s/' % BASEURL) elif Dict['password'] != pwd: Log.Critical( 'Either local login failed, or PMS lost connection to plex.tv') self.clear() self.set_status(401) def allow(self): self.set_secure_cookie(NAME, Hash.MD5( Dict['SharedSecret'] + Dict['password']), expires_days=None) class versionHandler(RequestHandler): def get(self, **params): self.set_header('Content-Type', 'application/json; charset=utf-8') self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.write(webTools().getVersion()) ''' Handler to get images from the DATA API ''' class imageHandler(RequestHandler): def get(self, **params): # Get name of image trash, image = self.request.uri.rsplit('/', 1) if Data.Exists(image): # Get file extention trash, extension = os.path.splitext(image) # Set content-type in header contenttype = 'image/' + extension[1:] self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.set_header('Content-Type', contenttype) try: # Redirect to unknown icon, in case frontend dev forgets ;-) if image == '': image = 'NoIcon.png' self.write(Data.Load(image)) self.finish() except Exception, e: Log.Exception('Exception in imageHandler was: %s' % (str(e))) else: self.set_status(404) self.finish() class translateHandler(RequestHandler): def get(self, **params): # Name of javascript fileName = 'translations.js' if Data.Exists(fileName): # Set content-type in header contenttype = 'application/javascript' self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.set_header('Content-Type', contenttype) try: self.write(Data.Load(fileName)) self.finish() except Exception, e: Log.Exception( 'Exception in translateHandler was: %s' % (str(e))) else: Log.Critical('Could not find translations.js') self.set_status(404) self.write('Missing translation script') self.finish() ''' getTranslationHandler ''' class getTranslationHandler(RequestHandler): def post(self, **params): self.set_header('Content-Type', 'application/json; charset=utf-8') self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.write(wtV3().GETTRANSLATE(self, **params)) ''' handlers ''' handlers = [(r"%s/login" % BASEURL, LoginHandler), (r"%s/logout" % BASEURL, LogoutHandler), (r"%s/version" % BASEURL, versionHandler), # Grap images from Data framework (r"%s/uas/Resources.*$" % BASEURL, imageHandler), # Grap translation.js from datastore (r"%s/static/_shared/translations.js" % BASEURL, translateHandler), (r'%s/' % BASEURL, idxHandler), # Index (r'%s' % BASEURL, idxHandler), # Index (r'%s/index.html' % BASEURL, idxHandler), # Index (r'%s/api/v3.*$' % BASEURL, apiv3.apiv3), # API V3 (r'%s/getTranslate.*$' % BASEURL, getTranslationHandler), # getTranslation (r'%s/(.*)' % BASEURL, MyStaticFileHandler, {'path': getActualHTTPPath()}) # Static files ] if Prefs['Force_SSL']: httpHandlers = [(r"%s/login" % BASEURL, ForceTSLHandler), (r"%s/logout" % BASEURL, LogoutHandler), (r"%s/version" % BASEURL, ForceTSLHandler), (r'%s/' % BASEURL, ForceTSLHandler), (r'%s/index.html' % BASEURL, ForceTSLHandler), # Grap images from Data framework (r"%s/uas/Resources.*$" % BASEURL, imageHandler), (r'%s/getTranslate.*$' % BASEURL, getTranslationHandler), # getTranslation # Grap translation.js from datastore (r"%s/static/_shared/translations.js" % BASEURL, translateHandler), (r'%s/api/v3.*$' % BASEURL, apiv3.apiv3) ] else: httpHandlers = handlers httpsHandlers = handlers #********* Tornado itself ******************* ''' Start the actual instance of tornado ''' def start_tornado(): myCookie = Hash.MD5(Dict['SharedSecret'] + NAME) login_url = BASEURL + '/login' settings = {"cookie_secret": "__" + myCookie + "__", "login_url": login_url} try: application = Application(httpHandlers, **settings) applicationTLS = Application(httpsHandlers, **settings) http_server = HTTPServer(application) # Use our own certificate for TLS CRTFile = os.path.join(Core.bundle_path, 'Contents', 'Code', 'Certificate', Prefs['Cert_CRT']) Log.Info('Certificate crt file is %s' % CRTFile) KEYFile = os.path.join(Core.bundle_path, 'Contents', 'Code', 'Certificate', Prefs['Cert_KEY']) Log.Info('Certificate key file is %s' % KEYFile) http_serverTLS = HTTPServer(applicationTLS, ssl_options={ "certfile": os.path.join(Core.bundle_path, 'Contents', 'Code', 'Certificate', CRTFile), "keyfile": KEYFile}) # Set web server port to the setting in the channel prefs port = int(Prefs['WEB_Port_http']) ports = int(Prefs['WEB_Port_https']) http_server.listen(port) http_serverTLS.listen(ports) Log.Debug('Starting tornado on ports %s and %s' % (port, ports)) IOLoop.instance().start() except Exception, e: Log.Exception('Problem starting Tornado: ' + str(e)) ''' Stop the actual instance of tornado ''' def stopWeb(): IOLoop.instance().add_callback(IOLoop.instance().stop) Log.Debug('Asked Tornado to exit') ''' Main call ''' # def startWeb(secretKey, version): def startWeb(secretKey): global SECRETKEY # Set the secret key for use by other calls in the future maybe? SECRETKEY = secretKey stopWeb() time.sleep(3) Log.Debug('tornado is handling the following URI: %s' % (handlers)) t = threading.Thread(target=start_tornado) t.start()