From f3310e29f28dfbcfa69b7838a1ecad9cd4cb0059 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Thu, 29 Mar 2018 17:23:33 +0100 Subject: [PATCH] Change improve security. Change improve security with DNS rebinding prevention, set "Allowed browser hostnames" at config/General/Web Interface. Change improve security with cross-site request forgery (xsrf) protection on web forms. Change improve security by sending header flag httponly with cookies Change improve security by sending header flag secure with SSL cookies Change improve test for creating self-signed SSL cert. Change force restart when switching SSL on/off. Change enable Tornado serve_traceback feature. Change PEP8 tweaks. --- CHANGES.md | 5 + SickBeard.py | 53 ++++---- .../interfaces/default/config_anime.tmpl | 1 + .../interfaces/default/config_general.tmpl | 13 ++ .../default/config_notifications.tmpl | 2 + .../default/config_postProcessing.tmpl | 1 + .../interfaces/default/config_providers.tmpl | 1 + .../interfaces/default/config_search.tmpl | 1 + .../interfaces/default/config_subtitles.tmpl | 1 + gui/slick/interfaces/default/editShow.tmpl | 1 + .../default/home_addExistingShow.tmpl | 1 + .../interfaces/default/home_newShow.tmpl | 1 + .../interfaces/default/home_postprocess.tmpl | 1 + .../default/home_recommendedShows.tmpl | 5 +- gui/slick/interfaces/default/login.tmpl | 4 +- gui/slick/interfaces/default/manage.tmpl | 1 + .../default/manage_episodeStatuses.tmpl | 2 + .../interfaces/default/manage_massEdit.tmpl | 2 + .../default/manage_subtitleMissed.tmpl | 2 + gui/slick/js/config.js | 4 +- sickbeard/__init__.py | 6 +- sickbeard/helpers.py | 76 ++++++++++-- sickbeard/webserve.py | 117 ++++++++++-------- sickbeard/webserveInit.py | 45 +++++-- 24 files changed, 246 insertions(+), 100 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index addf7ff3..425ff28f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ ### 0.16.0 (2018-xx-xx xx:xx:xx UTC) +* Change improve security with cross-site request forgery (xsrf) protection on web forms +* Change improve security by sending header flags httponly and secure with cookies +* Change improve security with DNS rebinding prevention, set "Allowed browser hostnames" at config/General/Web Interface +* Change improve test for creating self-signed SSL cert +* Change force restart when switching SSL on/off * Change hachoir targa and mpeg_ts mime parser tags so they validate * Update backports/ssl_match_hostname 3.5.0.1 (r18) to 3.7.0.1 (r28) * Update cachecontrol library 0.12.3 (db54c40) to 0.12.4 (bd94f7e) diff --git a/SickBeard.py b/SickBeard.py index 86d69a52..da76eb73 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -57,7 +57,7 @@ try: except ValueError: print('Sorry, requires Python module Cheetah 2.1.0 or newer.') sys.exit(1) -except: +except (StandardError, Exception): print('The Python module Cheetah is required') sys.exit(1) @@ -327,23 +327,25 @@ class SickGear(object): print(u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE) sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) - stack_size = None try: stack_size = int(sickbeard.CFG['General']['stack_size']) - except: + except (StandardError, Exception): stack_size = None if stack_size: try: threading.stack_size(stack_size) - except (StandardError, Exception) as e: - print('Stack Size %s not set: %s' % (stack_size, e.message)) + except (StandardError, Exception) as er: + print('Stack Size %s not set: %s' % (stack_size, er.message)) # check all db versions for d, min_v, max_v, base_v, mo in [ - ('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION, sickbeard.failed_db.TEST_BASE_VERSION, 'FailedDb'), - ('cache.db', sickbeard.cache_db.MIN_DB_VERSION, sickbeard.cache_db.MAX_DB_VERSION, sickbeard.cache_db.TEST_BASE_VERSION, 'CacheDb'), - ('sickbeard.db', sickbeard.mainDB.MIN_DB_VERSION, sickbeard.mainDB.MAX_DB_VERSION, sickbeard.mainDB.TEST_BASE_VERSION, 'MainDb') + ('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION, + sickbeard.failed_db.TEST_BASE_VERSION, 'FailedDb'), + ('cache.db', sickbeard.cache_db.MIN_DB_VERSION, sickbeard.cache_db.MAX_DB_VERSION, + sickbeard.cache_db.TEST_BASE_VERSION, 'CacheDb'), + ('sickbeard.db', sickbeard.mainDB.MIN_DB_VERSION, sickbeard.mainDB.MAX_DB_VERSION, + sickbeard.mainDB.TEST_BASE_VERSION, 'MainDb') ]: cur_db_version = db.DBConnection(d).checkDBVersion() @@ -409,19 +411,25 @@ class SickGear(object): self.webhost = (('0.0.0.0', '::')[sickbeard.WEB_IPV6], '')[sickbeard.WEB_IPV64] # web server options - self.web_options = { - 'port': int(self.start_port), - 'host': self.webhost, - 'data_root': os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), - 'web_root': sickbeard.WEB_ROOT, - 'log_dir': self.log_dir, - 'username': sickbeard.WEB_USERNAME, - 'password': sickbeard.WEB_PASSWORD, - 'enable_https': sickbeard.ENABLE_HTTPS, - 'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY, - 'https_cert': os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_CERT), - 'https_key': os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_KEY), - } + self.web_options = dict( + host=self.webhost, + port=int(self.start_port), + web_root=sickbeard.WEB_ROOT, + data_root=os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), + log_dir=self.log_dir, + username=sickbeard.WEB_USERNAME, + password=sickbeard.WEB_PASSWORD, + handle_reverse_proxy=sickbeard.HANDLE_REVERSE_PROXY, + enable_https=False, + https_cert=None, + https_key=None, + ) + if sickbeard.ENABLE_HTTPS: + self.web_options.update(dict( + enable_https=sickbeard.ENABLE_HTTPS, + https_cert=os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_CERT), + https_key=os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_KEY) + )) # start web server try: @@ -596,7 +604,7 @@ class SickGear(object): # shutdown web server if self.webserver: logger.log('Shutting down Tornado') - self.webserver.shutDown() + self.webserver.shut_down() try: self.webserver.join(10) except (StandardError, Exception): @@ -636,6 +644,7 @@ class SickGear(object): def exit(code): os._exit(code) + if __name__ == '__main__': if sys.hexversion >= 0x020600F0: freeze_support() diff --git a/gui/slick/interfaces/default/config_anime.tmpl b/gui/slick/interfaces/default/config_anime.tmpl index 45702851..8d721d90 100644 --- a/gui/slick/interfaces/default/config_anime.tmpl +++ b/gui/slick/interfaces/default/config_anime.tmpl @@ -20,6 +20,7 @@
+ $xsrf_form_html
diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index 39c92d1c..f1e31daf 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -42,6 +42,8 @@
+ $xsrf_form_html +
    @@ -589,6 +591,17 @@
+
+ +
+ diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 170104ae..cc21718a 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -38,6 +38,8 @@
+ $xsrf_form_html +
  • Home Theater / NAS
  • diff --git a/gui/slick/interfaces/default/config_postProcessing.tmpl b/gui/slick/interfaces/default/config_postProcessing.tmpl index b0ed1099..332dc0ab 100644 --- a/gui/slick/interfaces/default/config_postProcessing.tmpl +++ b/gui/slick/interfaces/default/config_postProcessing.tmpl @@ -30,6 +30,7 @@
    + $xsrf_form_html
      diff --git a/gui/slick/interfaces/default/config_providers.tmpl b/gui/slick/interfaces/default/config_providers.tmpl index de618229..9b0fab4a 100644 --- a/gui/slick/interfaces/default/config_providers.tmpl +++ b/gui/slick/interfaces/default/config_providers.tmpl @@ -67,6 +67,7 @@
      + $xsrf_form_html
        diff --git a/gui/slick/interfaces/default/config_search.tmpl b/gui/slick/interfaces/default/config_search.tmpl index de149574..c01c8c3a 100755 --- a/gui/slick/interfaces/default/config_search.tmpl +++ b/gui/slick/interfaces/default/config_search.tmpl @@ -30,6 +30,7 @@
        + $xsrf_form_html
          diff --git a/gui/slick/interfaces/default/config_subtitles.tmpl b/gui/slick/interfaces/default/config_subtitles.tmpl index 11f978e0..c174660d 100644 --- a/gui/slick/interfaces/default/config_subtitles.tmpl +++ b/gui/slick/interfaces/default/config_subtitles.tmpl @@ -49,6 +49,7 @@
          + $xsrf_form_html
            diff --git a/gui/slick/interfaces/default/editShow.tmpl b/gui/slick/interfaces/default/editShow.tmpl index 6f513ad5..045a9fe7 100644 --- a/gui/slick/interfaces/default/editShow.tmpl +++ b/gui/slick/interfaces/default/editShow.tmpl @@ -54,6 +54,7 @@ + $xsrf_form_html
              diff --git a/gui/slick/interfaces/default/home_addExistingShow.tmpl b/gui/slick/interfaces/default/home_addExistingShow.tmpl index 23f3a87b..4ae7929b 100644 --- a/gui/slick/interfaces/default/home_addExistingShow.tmpl +++ b/gui/slick/interfaces/default/home_addExistingShow.tmpl @@ -38,6 +38,7 @@ var config = { sortArticle: #echo ['!1','!0'][$sg_var('SORT_ARTICLE')]# }

              Existing show folders

              + $xsrf_form_html

              Tip: shows are added quicker when usable show nfo and xml metadata is found

              diff --git a/gui/slick/interfaces/default/home_newShow.tmpl b/gui/slick/interfaces/default/home_newShow.tmpl index d701d1f7..8287cdfa 100644 --- a/gui/slick/interfaces/default/home_newShow.tmpl +++ b/gui/slick/interfaces/default/home_newShow.tmpl @@ -43,6 +43,7 @@ #end if + $xsrf_form_html

              #if $use_provided_info#Using known show information#else#Find show at TV info source#end if#

              diff --git a/gui/slick/interfaces/default/home_postprocess.tmpl b/gui/slick/interfaces/default/home_postprocess.tmpl index 4b00ec32..a14fd93b 100644 --- a/gui/slick/interfaces/default/home_postprocess.tmpl +++ b/gui/slick/interfaces/default/home_postprocess.tmpl @@ -18,6 +18,7 @@ + $xsrf_form_html
              diff --git a/gui/slick/interfaces/default/home_recommendedShows.tmpl b/gui/slick/interfaces/default/home_recommendedShows.tmpl index f7883969..6310e942 100644 --- a/gui/slick/interfaces/default/home_recommendedShows.tmpl +++ b/gui/slick/interfaces/default/home_recommendedShows.tmpl @@ -30,7 +30,8 @@
              - + $xsrf_form_html +

              Select a recommended show

              @@ -69,4 +70,4 @@
              -#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl") \ No newline at end of file +#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl") diff --git a/gui/slick/interfaces/default/login.tmpl b/gui/slick/interfaces/default/login.tmpl index 2ec5cfb1..1dbe2847 100644 --- a/gui/slick/interfaces/default/login.tmpl +++ b/gui/slick/interfaces/default/login.tmpl @@ -44,9 +44,9 @@ #end if - diff --git a/gui/slick/interfaces/default/manage.tmpl b/gui/slick/interfaces/default/manage.tmpl index 86779e88..c1f80d72 100644 --- a/gui/slick/interfaces/default/manage.tmpl +++ b/gui/slick/interfaces/default/manage.tmpl @@ -83,6 +83,7 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name))

              $title

              #end if
              +$xsrf_form_html diff --git a/gui/slick/interfaces/default/manage_episodeStatuses.tmpl b/gui/slick/interfaces/default/manage_episodeStatuses.tmpl index b27ee778..c9d85e12 100644 --- a/gui/slick/interfaces/default/manage_episodeStatuses.tmpl +++ b/gui/slick/interfaces/default/manage_episodeStatuses.tmpl @@ -62,6 +62,8 @@ + $xsrf_form_html +

              $ep_count episode#echo ('s', '')[1 == $ep_count]# marked $statusStrings[$whichStatus].lower() in ${len($sorted_show_ids)} show#echo ('s', '')[1 == len($sorted_show_ids)]#

              diff --git a/gui/slick/interfaces/default/manage_massEdit.tmpl b/gui/slick/interfaces/default/manage_massEdit.tmpl index 95179898..5373b11a 100644 --- a/gui/slick/interfaces/default/manage_massEdit.tmpl +++ b/gui/slick/interfaces/default/manage_massEdit.tmpl @@ -22,6 +22,8 @@ + $xsrf_form_html +
              diff --git a/gui/slick/interfaces/default/manage_subtitleMissed.tmpl b/gui/slick/interfaces/default/manage_subtitleMissed.tmpl index 63d0948d..2d07442a 100644 --- a/gui/slick/interfaces/default/manage_subtitleMissed.tmpl +++ b/gui/slick/interfaces/default/manage_subtitleMissed.tmpl @@ -45,6 +45,8 @@ + $xsrf_form_html +

              Episodes without $subsLanguage subtitles.


              Download missed subtitles for selected episodes diff --git a/gui/slick/js/config.js b/gui/slick/js/config.js index 336f55ab..a0f3780a 100644 --- a/gui/slick/js/config.js +++ b/gui/slick/js/config.js @@ -266,8 +266,10 @@ $(document).ready(function () { }); function config_success(response) { - if (response == 'reload') { + if ('reload' == response) { window.location.reload(true); + } else if ('restart' == response) { + window.location.href = sbRoot + $('a.restart').attr('href') } $('.config_submitter').each(function () { $(this).removeAttr('disabled'); diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 8897e94e..179fe3d0 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -136,6 +136,7 @@ WEB_IPV64 = None HANDLE_REVERSE_PROXY = False SEND_SECURITY_HEADERS = True +ALLOWED_HOSTS = None PROXY_SETTING = None PROXY_INDEXERS = True @@ -608,7 +609,8 @@ def initialize(console_logging=True): HOME_SEARCH_FOCUS, USE_IMDB_INFO, IMDB_ACCOUNTS, DISPLAY_FREESPACE, SORT_ARTICLE, FUZZY_DATING, TRIM_ZERO, \ DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, TIMEZONE_DISPLAY, \ WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEY, WEB_PORT, WEB_LOG, \ - ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY, SEND_SECURITY_HEADERS + ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY, \ + SEND_SECURITY_HEADERS, ALLOWED_HOSTS # Gen Config/Advanced global BRANCH, CUR_COMMIT_BRANCH, GIT_REMOTE, CUR_COMMIT_HASH, GIT_PATH, CPU_PRESET, ANON_REDIRECT, \ ENCRYPTION_VERSION, PROXY_SETTING, PROXY_INDEXERS, FILE_LOGGING_PRESET @@ -814,6 +816,7 @@ def initialize(console_logging=True): HANDLE_REVERSE_PROXY = bool(check_setting_int(CFG, 'General', 'handle_reverse_proxy', 0)) SEND_SECURITY_HEADERS = bool(check_setting_int(CFG, 'General', 'send_security_headers', 1)) + ALLOWED_HOSTS = check_setting_str(CFG, 'General', 'allowed_hosts', '') ROOT_DIRS = check_setting_str(CFG, 'General', 'root_dirs', '') if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', ROOT_DIRS): @@ -1622,6 +1625,7 @@ def save_config(): new_config['General']['https_key'] = HTTPS_KEY new_config['General']['handle_reverse_proxy'] = int(HANDLE_REVERSE_PROXY) new_config['General']['send_security_headers'] = int(SEND_SECURITY_HEADERS) + new_config['General']['allowed_hosts'] = ALLOWED_HOSTS new_config['General']['use_nzbs'] = int(USE_NZBS) new_config['General']['use_torrents'] = int(USE_TORRENTS) new_config['General']['nzb_method'] = NZB_METHOD diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index ed9d795f..fc1647bb 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -639,10 +639,9 @@ def create_https_certificates(ssl_cert, ssl_key): Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' """ try: - from OpenSSL import crypto # @UnresolvedImport - from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, \ - serial # @UnresolvedImport - except Exception as e: + from OpenSSL import crypto + from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial + except (StandardError, Exception): logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) return False @@ -651,16 +650,17 @@ def create_https_certificates(ssl_cert, ssl_key): careq = createCertRequest(cakey, CN='Certificate Authority') cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years - cname = 'SickGear' pkey = createKeyPair(TYPE_RSA, 4096) - req = createCertRequest(pkey, CN=cname) + req = createCertRequest(pkey, CN='SickGear') cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years # Save the key and certificate to disk try: - open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - except: + with open(ssl_key, 'w') as file_hd: + file_hd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + with open(ssl_cert, 'w') as file_hd: + file_hd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + except (StandardError, Exception): logger.log(u"Error creating SSL key and certificate", logger.ERROR) return False @@ -1350,6 +1350,64 @@ def maybe_plural(number=1): return ('s', '')[1 == number] +def re_valid_hostname(with_allowed=True): + return re.compile(r'(?i)(%slocalhost|.*\.local|%s|%s)$' % ( + '%s|' % (with_allowed + and (sickbeard.ALLOWED_HOSTS and re.escape(sickbeard.ALLOWED_HOSTS).replace(',', '|') or '.*') + or ''), socket.gethostname() or 'localhost', valid_ipaddr_expr())) + + +def valid_ipaddr_expr(): + """ + Returns a regular expression that will validate an ip address + :return: Regular expression + :rtype: String + """ + return r'(%s)' % '|'.join([re.sub('\s+(#.[^\r\n]+)?', '', x) for x in [ + # IPv4 address (accurate) + # Matches 0.0.0.0 through 255.255.255.255 + ''' + (?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) + ''' + , + # IPv6 address (standard and mixed) + # 8 hexadecimal words, or 6 hexadecimal words followed by 4 decimal bytes All with optional leading zeros + ''' + (?:(?' % web_handler.xsrf_token self.sbHost = headers.get('X-Forwarded-Host') if None is self.sbHost: sbHost = headers.get('Host') or 'localhost' @@ -207,7 +210,7 @@ class LoginHandler(BaseHandler): if self.get_current_user(): self.redirect(self.get_argument('next', '/home/')) else: - t = PageTemplate(headers=self.request.headers, file='login.tmpl') + t = PageTemplate(web_handler=self, file='login.tmpl') t.resp = self.get_argument('resp', '') self.set_status(401) self.finish(t.respond()) @@ -217,9 +220,12 @@ class LoginHandler(BaseHandler): password = sickbeard.WEB_PASSWORD if (self.get_argument('username') == username) and (self.get_argument('password') == password): - remember_me = int(self.get_argument('remember_me', default=0) or 0) + params = dict(expires_days=(None, 30)[int(self.get_argument('remember_me', default=0) or 0) > 0], + httponly=True) + if sickbeard.ENABLE_HTTPS: + params.update(dict(secure=True)) self.set_secure_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT), - sickbeard.COOKIE_SECRET, expires_days=30 if remember_me > 0 else None) + sickbeard.COOKIE_SECRET, **params) self.redirect(self.get_argument('next', '/home/')) else: next_arg = '&next=' + self.get_argument('next', '/home/') @@ -405,7 +411,7 @@ class RepoHandler(BaseStaticFileHandler): return super(RepoHandler, self).get_content_type() def index(self, basepath, filelist): - t = PageTemplate(headers=self.request.headers, file='repo_index.tmpl') + t = PageTemplate(web_handler=self, file='repo_index.tmpl') t.basepath = basepath t.filelist = filelist return t.respond() @@ -469,11 +475,11 @@ class RepoHandler(BaseStaticFileHandler): return fh.read().strip() def render_kodi_repo_addon_xml(self): - t = PageTemplate(headers=self.request.headers, file='repo_kodi_addon.tmpl') + t = PageTemplate(web_handler=self, file='repo_kodi_addon.tmpl') return t.respond().strip() def render_kodi_repo_addons_xml(self): - t = PageTemplate(headers=self.request.headers, file='repo_kodi_addons.tmpl') + t = PageTemplate(web_handler=self, file='repo_kodi_addons.tmpl') t.watchedstate_updater_addon_xml = re.sub( '(?m)^([\s]*<)', r'\t\1', '\n'.join(self.get_watchedstate_updater_addon_xml().split('\n')[1:])) # skip xml header @@ -568,7 +574,7 @@ class WebHandler(BaseHandler): def page_not_found(self): self.set_status(404) - t = PageTemplate(headers=self.request.headers, file='404.tmpl') + t = PageTemplate(web_handler=self, file='404.tmpl') return t.respond() @authenticated @@ -580,10 +586,8 @@ class WebHandler(BaseHandler): except: self.finish(self.page_not_found()) else: - kwargss = self.request.arguments - for arg, value in kwargss.items(): - if len(value) == 1: - kwargss[arg] = value[0] + kwargss = {k: v if not (isinstance(v, list) and 1 == len(v)) else v[0] + for k, v in self.request.arguments.iteritems() if '_xsrf' != k} result = method(**kwargss) if result: self.finish(result) @@ -770,7 +774,7 @@ class MainHandler(WebHandler): # add localtime to the dict cache_obj = image_cache.ImageCache() - t = PageTemplate(headers=self.request.headers, file='episodeView.tmpl') + t = PageTemplate(web_handler=self, file='episodeView.tmpl') t.fanart = {} for index, item in enumerate(sql_results): sql_results[index]['localtime'] = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(item['airdate'], @@ -1072,7 +1076,7 @@ r.close() self.redirect('/history/') def _genericMessage(self, subject, message): - t = PageTemplate(headers=self.request.headers, file='genericMessage.tmpl') + t = PageTemplate(web_handler=self, file='genericMessage.tmpl') t.submenu = self.HomeMenu() t.subject = subject t.message = message @@ -1136,7 +1140,7 @@ class Home(MainHandler): self.redirect('/home/showlistView/') def showlistView(self): - t = PageTemplate(headers=self.request.headers, file='home.tmpl') + t = PageTemplate(web_handler=self, file='home.tmpl') t.showlists = [] index = 0 if sickbeard.SHOWLIST_TAGVIEW == 'custom': @@ -1559,7 +1563,7 @@ class Home(MainHandler): def viewchanges(self): - t = PageTemplate(headers=self.request.headers, file='viewchanges.tmpl') + t = PageTemplate(web_handler=self, file='viewchanges.tmpl') t.changelist = [{'type': 'rel', 'ver': '', 'date': 'Nothing to display at this time'}] url = 'https://raw.githubusercontent.com/wiki/SickGear/SickGear/sickgear/CHANGES.md' @@ -1603,7 +1607,7 @@ class Home(MainHandler): if str(pid) != str(sickbeard.PID): return self.redirect('/home/') - t = PageTemplate(headers=self.request.headers, file='restart.tmpl') + t = PageTemplate(web_handler=self, file='restart.tmpl') t.shutdown = True sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN) @@ -1615,7 +1619,7 @@ class Home(MainHandler): if str(pid) != str(sickbeard.PID): return self.redirect('/home/') - t = PageTemplate(headers=self.request.headers, file='restart.tmpl') + t = PageTemplate(web_handler=self, file='restart.tmpl') t.shutdown = False sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) @@ -1664,7 +1668,7 @@ class Home(MainHandler): if None is season: return json.dumps(response) - t = PageTemplate(headers=self.request.headers, file='inc_displayShow.tmpl') + t = PageTemplate(web_handler=self, file='inc_displayShow.tmpl') t.show = show_obj my_db = db.DBConnection() @@ -1697,7 +1701,7 @@ class Home(MainHandler): if showObj is None: return self._genericMessage('Error', 'Show not in show list') - t = PageTemplate(headers=self.request.headers, file='displayShow.tmpl') + t = PageTemplate(web_handler=self, file='displayShow.tmpl') t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] try: @@ -2162,7 +2166,7 @@ class Home(MainHandler): bestQualities = [] if not location and not anyQualities and not bestQualities and not flatten_folders: - t = PageTemplate(headers=self.request.headers, file='editShow.tmpl') + t = PageTemplate(web_handler=self, file='editShow.tmpl') t.submenu = self.HomeMenu() t.expand_ids = all([kwargs.get('tvsrc'), kwargs.get('srcid')]) @@ -2666,7 +2670,7 @@ class Home(MainHandler): # present season DESC episode DESC on screen ep_obj_rename_list.reverse() - t = PageTemplate(headers=self.request.headers, file='testRename.tmpl') + t = PageTemplate(web_handler=self, file='testRename.tmpl') t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] t.ep_obj_list = ep_obj_rename_list t.show = showObj @@ -2922,7 +2926,7 @@ class Home(MainHandler): class HomePostProcess(Home): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='home_postprocess.tmpl') + t = PageTemplate(web_handler=self, file='home_postprocess.tmpl') t.submenu = [x for x in self.HomeMenu() if 'postprocess' not in x['path']] return t.respond() @@ -2979,7 +2983,7 @@ class HomePostProcess(Home): class NewHomeAddShows(Home): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='home_addShows.tmpl') + t = PageTemplate(web_handler=self, file='home_addShows.tmpl') t.submenu = self.HomeMenu() return t.respond() @@ -3194,7 +3198,7 @@ class NewHomeAddShows(Home): return s def massAddTable(self, rootDir=None, **kwargs): - t = PageTemplate(headers=self.request.headers, file='home_massAddTable.tmpl') + t = PageTemplate(web_handler=self, file='home_massAddTable.tmpl') t.submenu = self.HomeMenu() t.kwargs = kwargs @@ -3316,7 +3320,7 @@ class NewHomeAddShows(Home): self.set_header('Pragma', 'no-cache') self.set_header('Expires', '0') - t = PageTemplate(headers=self.request.headers, file='home_newShow.tmpl') + t = PageTemplate(web_handler=self, file='home_newShow.tmpl') t.submenu = self.HomeMenu() t.enable_anime_options = True t.enable_default_wanted = True @@ -4003,7 +4007,7 @@ class NewHomeAddShows(Home): Display the new show page which collects a tvdb id, folder, and extra options and posts them to addNewShow """ - t = PageTemplate(headers=self.request.headers, file='home_browseShows.tmpl') + t = PageTemplate(web_handler=self, file='home_browseShows.tmpl') t.submenu = self.HomeMenu() t.browse_type = browse_type t.browse_title = browse_title @@ -4046,7 +4050,7 @@ class NewHomeAddShows(Home): """ Prints out the page to add existing shows from a root dir """ - t = PageTemplate(headers=self.request.headers, file='home_addExistingShow.tmpl') + t = PageTemplate(web_handler=self, file='home_addExistingShow.tmpl') t.submenu = self.HomeMenu() t.enable_anime_options = False t.kwargs = kwargs @@ -4279,7 +4283,7 @@ class Manage(MainHandler): return [x for x in menu if exclude not in x['title']] def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='manage.tmpl') + t = PageTemplate(web_handler=self, file='manage.tmpl') t.submenu = self.ManageMenu('Bulk') return t.respond() @@ -4325,7 +4329,7 @@ class Manage(MainHandler): else: status_list = [] - t = PageTemplate(headers=self.request.headers, file='manage_episodeStatuses.tmpl') + t = PageTemplate(web_handler=self, file='manage_episodeStatuses.tmpl') t.submenu = self.ManageMenu('Episode') t.whichStatus = whichStatus @@ -4451,7 +4455,7 @@ class Manage(MainHandler): def subtitleMissed(self, whichSubs=None): - t = PageTemplate(headers=self.request.headers, file='manage_subtitleMissed.tmpl') + t = PageTemplate(web_handler=self, file='manage_subtitleMissed.tmpl') t.submenu = self.ManageMenu('Subtitle') t.whichSubs = whichSubs @@ -4533,7 +4537,7 @@ class Manage(MainHandler): def backlogOverview(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='manage_backlogOverview.tmpl') + t = PageTemplate(web_handler=self, file='manage_backlogOverview.tmpl') t.submenu = self.ManageMenu('Backlog') showCounts = {} @@ -4576,7 +4580,7 @@ class Manage(MainHandler): def massEdit(self, toEdit=None): - t = PageTemplate(headers=self.request.headers, file='manage_massEdit.tmpl') + t = PageTemplate(web_handler=self, file='manage_massEdit.tmpl') t.submenu = self.ManageMenu() if not toEdit: @@ -4963,7 +4967,7 @@ class Manage(MainHandler): if toRemove: return self.redirect('/manage/failedDownloads/') - t = PageTemplate(headers=self.request.headers, file='manage_failedDownloads.tmpl') + t = PageTemplate(web_handler=self, file='manage_failedDownloads.tmpl') t.over_limit = limit and len(sql_results) > limit t.failedResults = t.over_limit and sql_results[0:-1] or sql_results t.limit = str(limit) @@ -4974,7 +4978,7 @@ class Manage(MainHandler): class ManageSearches(Manage): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='manage_manageSearches.tmpl') + t = PageTemplate(web_handler=self, file='manage_manageSearches.tmpl') # t.backlog_pi = sickbeard.backlogSearchScheduler.action.get_progress_indicator() t.backlog_paused = sickbeard.searchQueueScheduler.action.is_backlog_paused() t.backlog_running = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() @@ -5051,7 +5055,7 @@ class ManageSearches(Manage): class showProcesses(Manage): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='manage_showProcesses.tmpl') + t = PageTemplate(web_handler=self, file='manage_showProcesses.tmpl') t.queue_length = sickbeard.showQueueScheduler.action.queue_length() t.show_list = sickbeard.showList t.show_update_running = sickbeard.showQueueScheduler.action.isShowUpdateRunning() or sickbeard.showUpdateScheduler.action.amActive @@ -5117,7 +5121,7 @@ class History(MainHandler): def index(self, limit=100): - t = PageTemplate(headers=self.request.headers, file='history.tmpl') + t = PageTemplate(web_handler=self, file='history.tmpl') t.limit = limit my_db = db.DBConnection(row_type='dict') @@ -5570,7 +5574,7 @@ class Config(MainHandler): return [x for x in menu if exclude not in x['title']] def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config.tmpl') + t = PageTemplate(web_handler=self, file='config.tmpl') t.submenu = self.ConfigMenu() return t.respond() @@ -5579,11 +5583,12 @@ class Config(MainHandler): class ConfigGeneral(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_general.tmpl') + t = PageTemplate(web_handler=self, file='config_general.tmpl') t.submenu = self.ConfigMenu('General') t.show_tags = ', '.join(sickbeard.SHOW_TAGS) t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers if sickbeard.indexerApi(i).config['active']]) + t.request_host = escape.xhtml_escape(self.request.host_name) return t.respond() def saveRootDirs(self, rootDirString=None): @@ -5649,7 +5654,8 @@ class ConfigGeneral(Config): trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None, use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None, web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, - handle_reverse_proxy=None, send_security_headers=None, home_search_focus=None, display_freespace=None, sort_article=None, auto_update=None, notify_on_update=None, + handle_reverse_proxy=None, send_security_headers=None, allowed_hosts=None, + home_search_focus=None, display_freespace=None, sort_article=None, auto_update=None, notify_on_update=None, proxy_setting=None, proxy_indexers=None, anon_redirect=None, git_path=None, git_remote=None, calendar_unprotected=None, fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None, indexer_timeout=None, rootDir=None, theme_name=None, default_home=None, use_imdb_info=None, @@ -5717,6 +5723,7 @@ class ConfigGeneral(Config): sickbeard.TIMEZONE_DISPLAY = timezone_display # Web interface + restart = False reload_page = False if sickbeard.WEB_USERNAME != web_username: sickbeard.WEB_USERNAME = web_username @@ -5731,6 +5738,7 @@ class ConfigGeneral(Config): sickbeard.WEB_PORT = config.to_int(web_port) # sickbeard.WEB_LOG is set in config.change_log_dir() + restart |= sickbeard.ENABLE_HTTPS != config.checkbox_to_value(enable_https) sickbeard.ENABLE_HTTPS = config.checkbox_to_value(enable_https) if not config.change_https_cert(https_cert): results += [ @@ -5743,6 +5751,10 @@ class ConfigGeneral(Config): sickbeard.WEB_IPV64 = config.checkbox_to_value(web_ipv64) sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) sickbeard.SEND_SECURITY_HEADERS = config.checkbox_to_value(send_security_headers) + hosts = ','.join(filter(lambda name: not helpers.re_valid_hostname(with_allowed=False).match(name), + config.clean_hosts(allowed_hosts).split(','))) + if not hosts or self.request.host_name in hosts: + sickbeard.ALLOWED_HOSTS = hosts # Advanced sickbeard.GIT_REMOTE = git_remote @@ -5767,6 +5779,11 @@ class ConfigGeneral(Config): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) + if restart: + self.clear_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT)) + self.write('restart') + reload_page = False + if reload_page: self.clear_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT)) self.write('reload') @@ -5796,7 +5813,7 @@ class ConfigGeneral(Config): class ConfigSearch(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_search.tmpl') + t = PageTemplate(web_handler=self, file='config_search.tmpl') t.submenu = self.ConfigMenu('Search') t.using_rls_ignore_words = [(show.indexerid, show.name) for show in sickbeard.showList if show.rls_ignore_words and @@ -5908,7 +5925,7 @@ class ConfigSearch(Config): class ConfigPostProcessing(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_postProcessing.tmpl') + t = PageTemplate(web_handler=self, file='config_postProcessing.tmpl') t.submenu = self.ConfigMenu('Processing') return t.respond() @@ -6092,7 +6109,7 @@ class ConfigPostProcessing(Config): class ConfigProviders(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_providers.tmpl') + t = PageTemplate(web_handler=self, file='config_providers.tmpl') t.submenu = self.ConfigMenu('Providers') return t.respond() @@ -6475,7 +6492,7 @@ class ConfigProviders(Config): class ConfigNotifications(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_notifications.tmpl') + t = PageTemplate(web_handler=self, file='config_notifications.tmpl') t.submenu = self.ConfigMenu('Notifications') t.root_dirs = [] if sickbeard.ROOT_DIRS: @@ -6774,7 +6791,7 @@ class ConfigNotifications(Config): class ConfigSubtitles(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_subtitles.tmpl') + t = PageTemplate(web_handler=self, file='config_subtitles.tmpl') t.submenu = self.ConfigMenu('Subtitle') return t.respond() @@ -6820,7 +6837,7 @@ class ConfigSubtitles(Config): class ConfigAnime(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='config_anime.tmpl') + t = PageTemplate(web_handler=self, file='config_anime.tmpl') t.submenu = self.ConfigMenu('Anime') return t.respond() @@ -6878,7 +6895,7 @@ class ErrorLogs(MainHandler): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file='errorlogs.tmpl') + t = PageTemplate(web_handler=self, file='errorlogs.tmpl') t.submenu = self.ErrorLogsMenu return t.respond() @@ -6906,7 +6923,7 @@ class ErrorLogs(MainHandler): def viewlog(self, min_level=logger.MESSAGE, max_lines=500): - t = PageTemplate(headers=self.request.headers, file='viewlogs.tmpl') + t = PageTemplate(web_handler=self, file='viewlogs.tmpl') t.submenu = self.ErrorLogsMenu min_level = int(min_level) @@ -6989,7 +7006,7 @@ class WebFileBrowser(MainHandler): class ApiBuilder(MainHandler): def index(self): """ expose the api-builder template """ - t = PageTemplate(headers=self.request.headers, file='apiBuilder.tmpl') + t = PageTemplate(web_handler=self, file='apiBuilder.tmpl') def titler(x): return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] @@ -7029,7 +7046,7 @@ class Cache(MainHandler): if not sql_results: sql_results = [] - t = PageTemplate(headers=self.request.headers, file='cache.tmpl') + t = PageTemplate(web_handler=self, file='cache.tmpl') t.cacheResults = sql_results return t.respond() diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 8db58fd1..ecb8945a 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -6,14 +6,13 @@ import webserve import webapi from sickbeard import logger -from sickbeard.helpers import create_https_certificates +from sickbeard.helpers import create_https_certificates, re_valid_hostname from tornado.web import Application -from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop class WebServer(threading.Thread): - def __init__(self, options={}, **kwargs): + def __init__(self, options=None): threading.Thread.__init__(self) self.daemon = True self.alive = True @@ -21,7 +20,7 @@ class WebServer(threading.Thread): self.io_loop = None self.server = None - self.options = options + self.options = options or {} self.options.setdefault('port', 8081) self.options.setdefault('host', '0.0.0.0') self.options.setdefault('log_dir', None) @@ -40,40 +39,60 @@ class WebServer(threading.Thread): self.https_key = self.options['https_key'] if self.enable_https: + make_cert = False + update_cfg = False + for (attr, ext) in [('https_cert', '.crt'), ('https_key', '.key')]: + ssl_path = getattr(self, attr, None) + if ssl_path and not os.path.isfile(ssl_path): + if not ssl_path.endswith(ext): + setattr(self, attr, os.path.join(ssl_path, 'server%s' % ext)) + setattr(sickbeard, attr.upper(), 'server%s' % ext) + make_cert = True + # If either the HTTPS certificate or key do not exist, make some self-signed ones. - if not (self.https_cert and os.path.exists(self.https_cert))\ - or not (self.https_key and os.path.exists(self.https_key)): + if make_cert: if not create_https_certificates(self.https_cert, self.https_key): logger.log(u'Unable to create CERT/KEY files, disabling HTTPS') + update_cfg |= False is not sickbeard.ENABLE_HTTPS sickbeard.ENABLE_HTTPS = False self.enable_https = False + else: + update_cfg = True - if not (os.path.exists(self.https_cert) and os.path.exists(self.https_key)): + if not (os.path.isfile(self.https_cert) and os.path.isfile(self.https_key)): logger.log(u'Disabled HTTPS because of missing CERT and KEY files', logger.WARNING) + update_cfg |= False is not sickbeard.ENABLE_HTTPS sickbeard.ENABLE_HTTPS = False self.enable_https = False + if update_cfg: + sickbeard.save_config() + # Load the app self.app = Application([], debug=False, + serve_traceback=True, autoreload=False, - gzip=True, + compress_response=True, cookie_secret=sickbeard.COOKIE_SECRET, + xsrf_cookies=True, login_url='%s/login/' % self.options['web_root']) + re_host_pattern = re_valid_hostname() + # webui login/logout handlers - self.app.add_handlers('.*$', [ + self.app.add_handlers(re_host_pattern, [ (r'%s/login(/?)' % self.options['web_root'], webserve.LoginHandler), (r'%s/logout(/?)' % self.options['web_root'], webserve.LogoutHandler), ]) # Web calendar handler (Needed because option Unprotected calendar) - self.app.add_handlers('.*$', [ + self.app.add_handlers(re_host_pattern, [ (r'%s/calendar' % self.options['web_root'], webserve.CalendarHandler), ]) # Static File Handlers - self.app.add_handlers('.*$', [ + self.app.add_handlers(re_host_pattern, [ # favicon (r'%s/(favicon\.ico)' % self.options['web_root'], webserve.BaseStaticFileHandler, {'path': os.path.join(self.options['data_root'], 'images/ico/favicon.ico')}), @@ -100,7 +119,7 @@ class WebServer(threading.Thread): ]) # Main Handler - self.app.add_handlers('.*$', [ + self.app.add_handlers(re_host_pattern, [ (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webserve.ApiBuilder), (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), (r'%s/imagecache(/?.*)' % self.options['web_root'], webserve.CachedImages), @@ -153,7 +172,7 @@ class WebServer(threading.Thread): # Ignore errors like 'ValueError: I/O operation on closed kqueue fd'. These might be thrown during a reload. pass - def shutDown(self): + def shut_down(self): self.alive = False if None is not self.io_loop: self.io_loop.stop()