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()