Merge pull request #1080 from JackDandy/feature/ChangeMoreSecurity

Change improve security.
This commit is contained in:
JackDandy 2018-04-02 21:32:25 +01:00 committed by GitHub
commit dfdc5caec4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 246 additions and 100 deletions

View file

@ -1,5 +1,10 @@
### 0.16.0 (2018-xx-xx xx:xx:xx UTC) ### 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 * 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 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) * Update cachecontrol library 0.12.3 (db54c40) to 0.12.4 (bd94f7e)

View file

@ -57,7 +57,7 @@ try:
except ValueError: except ValueError:
print('Sorry, requires Python module Cheetah 2.1.0 or newer.') print('Sorry, requires Python module Cheetah 2.1.0 or newer.')
sys.exit(1) sys.exit(1)
except: except (StandardError, Exception):
print('The Python module Cheetah is required') print('The Python module Cheetah is required')
sys.exit(1) sys.exit(1)
@ -327,23 +327,25 @@ class SickGear(object):
print(u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE) print(u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE)
sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE)
stack_size = None
try: try:
stack_size = int(sickbeard.CFG['General']['stack_size']) stack_size = int(sickbeard.CFG['General']['stack_size'])
except: except (StandardError, Exception):
stack_size = None stack_size = None
if stack_size: if stack_size:
try: try:
threading.stack_size(stack_size) threading.stack_size(stack_size)
except (StandardError, Exception) as e: except (StandardError, Exception) as er:
print('Stack Size %s not set: %s' % (stack_size, e.message)) print('Stack Size %s not set: %s' % (stack_size, er.message))
# check all db versions # check all db versions
for d, min_v, max_v, base_v, mo in [ 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'), ('failed.db', sickbeard.failed_db.MIN_DB_VERSION, sickbeard.failed_db.MAX_DB_VERSION,
('cache.db', sickbeard.cache_db.MIN_DB_VERSION, sickbeard.cache_db.MAX_DB_VERSION, sickbeard.cache_db.TEST_BASE_VERSION, 'CacheDb'), sickbeard.failed_db.TEST_BASE_VERSION, 'FailedDb'),
('sickbeard.db', sickbeard.mainDB.MIN_DB_VERSION, sickbeard.mainDB.MAX_DB_VERSION, sickbeard.mainDB.TEST_BASE_VERSION, 'MainDb') ('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() 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] self.webhost = (('0.0.0.0', '::')[sickbeard.WEB_IPV6], '')[sickbeard.WEB_IPV64]
# web server options # web server options
self.web_options = { self.web_options = dict(
'port': int(self.start_port), host=self.webhost,
'host': self.webhost, port=int(self.start_port),
'data_root': os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME), web_root=sickbeard.WEB_ROOT,
'web_root': sickbeard.WEB_ROOT, data_root=os.path.join(sickbeard.PROG_DIR, 'gui', sickbeard.GUI_NAME),
'log_dir': self.log_dir, log_dir=self.log_dir,
'username': sickbeard.WEB_USERNAME, username=sickbeard.WEB_USERNAME,
'password': sickbeard.WEB_PASSWORD, password=sickbeard.WEB_PASSWORD,
'enable_https': sickbeard.ENABLE_HTTPS, handle_reverse_proxy=sickbeard.HANDLE_REVERSE_PROXY,
'handle_reverse_proxy': sickbeard.HANDLE_REVERSE_PROXY, enable_https=False,
'https_cert': os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_CERT), https_cert=None,
'https_key': os.path.join(sickbeard.PROG_DIR, sickbeard.HTTPS_KEY), 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 # start web server
try: try:
@ -596,7 +604,7 @@ class SickGear(object):
# shutdown web server # shutdown web server
if self.webserver: if self.webserver:
logger.log('Shutting down Tornado') logger.log('Shutting down Tornado')
self.webserver.shutDown() self.webserver.shut_down()
try: try:
self.webserver.join(10) self.webserver.join(10)
except (StandardError, Exception): except (StandardError, Exception):
@ -636,6 +644,7 @@ class SickGear(object):
def exit(code): def exit(code):
os._exit(code) os._exit(code)
if __name__ == '__main__': if __name__ == '__main__':
if sys.hexversion >= 0x020600F0: if sys.hexversion >= 0x020600F0:
freeze_support() freeze_support()

View file

@ -20,6 +20,7 @@
<div id="config-content"> <div id="config-content">
<form id="configForm" action="saveAnime" method="post"> <form id="configForm" action="saveAnime" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">

View file

@ -42,6 +42,8 @@
<div id="config-content"> <div id="config-content">
<form id="configForm" action="saveGeneral" method="post"> <form id="configForm" action="saveGeneral" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>
@ -589,6 +591,17 @@
</label> </label>
</div> </div>
<div class="field-pair">
<label for="allowed_hosts">
<span class="component-title">Allowed browser hostnames</span>
<span class="component-desc">
<input type="text" name="allowed_hosts" id="allowed-hosts" value="$sg_str('ALLOWED_HOSTS')" class="form-control input-sm input300">
<p>blank for insecure allow all</p>
<div class="clear-left"><p>whitelist names that browse the interface (e.g. $request_host, my_hostname)</p></div>
</span>
</label>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes"> <input type="submit" class="btn config_submitter" value="Save Changes">
</fieldset> </fieldset>

View file

@ -38,6 +38,8 @@
<div id="config"> <div id="config">
<div id="config-content"> <div id="config-content">
<form id="configForm" action="$sbRoot/config/notifications/save_notifications" method="post"> <form id="configForm" action="$sbRoot/config/notifications/save_notifications" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>
<li><a href="#tabs-1">Home Theater / NAS</a></li> <li><a href="#tabs-1">Home Theater / NAS</a></li>

View file

@ -30,6 +30,7 @@
<div id="config-content" class="linefix"> <div id="config-content" class="linefix">
<form id="configForm" action="savePostProcessing" method="post"> <form id="configForm" action="savePostProcessing" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>

View file

@ -67,6 +67,7 @@
<div id="config-content"> <div id="config-content">
<form id="configForm" action="saveProviders" method="post"> <form id="configForm" action="saveProviders" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>

View file

@ -30,6 +30,7 @@
<div id="config-content" class="linefix"> <div id="config-content" class="linefix">
<form id="configForm" action="saveSearch" method="post"> <form id="configForm" action="saveSearch" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>

View file

@ -49,6 +49,7 @@
<div id="config-content"> <div id="config-content">
<form id="configForm" action="saveSubtitles" method="post"> <form id="configForm" action="saveSubtitles" method="post">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>

View file

@ -54,6 +54,7 @@
<form action="editShow" method="post" id="editShow" style="width:894px"> <form action="editShow" method="post" id="editShow" style="width:894px">
<input type="hidden" name="show" id="show" value="$show.indexerid"> <input type="hidden" name="show" id="show" value="$show.indexerid">
<input type="hidden" name="indexer" id="indexer" value="$show.indexer"> <input type="hidden" name="indexer" id="indexer" value="$show.indexer">
$xsrf_form_html
<div id="config-components"> <div id="config-components">
<ul> <ul>

View file

@ -38,6 +38,7 @@ var config = { sortArticle: #echo ['!1','!0'][$sg_var('SORT_ARTICLE')]# }
<h3>Existing show folders</h3> <h3>Existing show folders</h3>
<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> <form id="addShowForm" method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8">
$xsrf_form_html
<span#if $kwargs.get('hash_dir', None)# class="hide"#end if#> <span#if $kwargs.get('hash_dir', None)# class="hide"#end if#>
<p>Tip: shows are added quicker when usable show nfo and xml metadata is found</p> <p>Tip: shows are added quicker when usable show nfo and xml metadata is found</p>

View file

@ -43,6 +43,7 @@
#end if #end if
<form id="addShowForm"#if $kwargs.get('action')# class="fullwidth"#end if# method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8"> <form id="addShowForm"#if $kwargs.get('action')# class="fullwidth"#end if# method="post" action="$sbRoot/home/addShows/addNewShow" accept-charset="utf-8">
$xsrf_form_html
<fieldset class="sectionwrap step-one"> <fieldset class="sectionwrap step-one">
<legend class="legendStep"><p>#if $use_provided_info#Using known show information#else#Find show at TV info source#end if#</p></legend> <legend class="legendStep"><p>#if $use_provided_info#Using known show information#else#Find show at TV info source#end if#</p></legend>

View file

@ -18,6 +18,7 @@
<form name="processForm" method="post" action="processEpisode"> <form name="processForm" method="post" action="processEpisode">
<input type="hidden" id="type" name="type" value="manual"> <input type="hidden" id="type" name="type" value="manual">
$xsrf_form_html
<div id="postProcess" class="stepDiv"> <div id="postProcess" class="stepDiv">

View file

@ -30,7 +30,8 @@
<br /> <br />
<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addRecommendedShow" accept-charset="utf-8"> <form id="addShowForm" method="post" action="$sbRoot/home/addShows/addRecommendedShow" accept-charset="utf-8">
$xsrf_form_html
<fieldset class="sectionwrap step-one"> <fieldset class="sectionwrap step-one">
<legend class="legendStep"><p>Select a recommended show</p></legend> <legend class="legendStep"><p>Select a recommended show</p></legend>
@ -69,4 +70,4 @@
</div> </div>
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl") #include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")

View file

@ -44,9 +44,9 @@
#end if #end if
</style> </style>
</head> </head>
<body><div class="login"><form action="" method="post"><div class="login-img center-block form-group"></div> <body><div class="login"><form action="" method="post">$xsrf_form_html<div class="login-img center-block form-group"></div>
<div class="login-error"><div class="#if 'authfailed'==$resp then 'showme' else 'hide' #"><i class="error16"></i><span class="red-text">Authentication failed, please retry</span></div></div> <div class="login-error"><div class="#if 'authfailed'==$resp then 'showme' else 'hide' #"><i class="error16"></i><span class="red-text">Authentication failed, please retry</span></div></div>
<div class="form-group input-group"><span class="input-group-addon"><i class="icons icons-user" style=""></i></span><input name="username" class="form-control" placeholder="Username" type="text" autofocus></div> <div class="form-group input-group"><span class="input-group-addon"><i class="icons icons-user" style=""></i></span><input name="username" class="form-control" placeholder="Username" type="text" autofocus></div>
<div class="form-group input-group"><span class="input-group-addon"><i class="icons icons-lock" style=""></i></span><input name="password" class="form-control" placeholder="Password" type="password"></div> <div class="form-group input-group"><span class="input-group-addon"><i class="icons icons-lock" style=""></i></span><input name="password" class="form-control" placeholder="Password" type="password"></div>
<div class="form-group"><label for="remember_me" class="login-remember"><input id="remember_me" name="remember_me" type="checkbox" value="1" checked="checked"><span>Remember me</span></label><input class="btn pull-right" name="submit" type="submit" value="Login"></div> <div class="form-group"><label for="remember_me" class="login-remember"><input id="remember_me" name="remember_me" type="checkbox" value="1" checked="checked"><span>Remember me</span></label><input class="btn pull-right" name="submit" type="submit" value="Login"></div>
</form></div></body></html> </form></div></body></html>

View file

@ -83,6 +83,7 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name))
<h1 class="title">$title</h1> <h1 class="title">$title</h1>
#end if #end if
<form name="bulkChangeForm" method="post" action="bulkChange"> <form name="bulkChangeForm" method="post" action="bulkChange">
$xsrf_form_html
<table id="bulkChangeTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> <table id="bulkChangeTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead> <thead>

View file

@ -62,6 +62,8 @@
<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?v=$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?v=$sbPID"></script>
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post"> <form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
$xsrf_form_html
<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus"> <input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus">
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3> <h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>

View file

@ -22,6 +22,8 @@
<script type="text/javascript" src="$sbRoot/js/massEdit.js?v=$sbPID"></script> <script type="text/javascript" src="$sbRoot/js/massEdit.js?v=$sbPID"></script>
<form action="massEditSubmit" method="post"> <form action="massEditSubmit" method="post">
$xsrf_form_html
<input type="hidden" name="toEdit" value="$showList"> <input type="hidden" name="toEdit" value="$showList">
<div class="optionWrapper"> <div class="optionWrapper">

View file

@ -45,6 +45,8 @@
<input type="hidden" id="selectSubLang" name="selectSubLang" value="$whichSubs"> <input type="hidden" id="selectSubLang" name="selectSubLang" value="$whichSubs">
<form action="$sbRoot/manage/downloadSubtitleMissed" method="post"> <form action="$sbRoot/manage/downloadSubtitleMissed" method="post">
$xsrf_form_html
<h2>Episodes without $subsLanguage subtitles.</h2> <h2>Episodes without $subsLanguage subtitles.</h2>
<br /> <br />
Download missed subtitles for selected episodes <input class="btn btn-inline" type="submit" value="Go" /> Download missed subtitles for selected episodes <input class="btn btn-inline" type="submit" value="Go" />

View file

@ -266,8 +266,10 @@ $(document).ready(function () {
}); });
function config_success(response) { function config_success(response) {
if (response == 'reload') { if ('reload' == response) {
window.location.reload(true); window.location.reload(true);
} else if ('restart' == response) {
window.location.href = sbRoot + $('a.restart').attr('href')
} }
$('.config_submitter').each(function () { $('.config_submitter').each(function () {
$(this).removeAttr('disabled'); $(this).removeAttr('disabled');

View file

@ -136,6 +136,7 @@ WEB_IPV64 = None
HANDLE_REVERSE_PROXY = False HANDLE_REVERSE_PROXY = False
SEND_SECURITY_HEADERS = True SEND_SECURITY_HEADERS = True
ALLOWED_HOSTS = None
PROXY_SETTING = None PROXY_SETTING = None
PROXY_INDEXERS = True 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, \ 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, \ DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, TIMEZONE_DISPLAY, \
WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEY, WEB_PORT, WEB_LOG, \ 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 # Gen Config/Advanced
global BRANCH, CUR_COMMIT_BRANCH, GIT_REMOTE, CUR_COMMIT_HASH, GIT_PATH, CPU_PRESET, ANON_REDIRECT, \ 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 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)) 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)) 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', '') ROOT_DIRS = check_setting_str(CFG, 'General', 'root_dirs', '')
if not re.match(r'\d+\|[^|]+(?:\|[^|]+)*', 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']['https_key'] = HTTPS_KEY
new_config['General']['handle_reverse_proxy'] = int(HANDLE_REVERSE_PROXY) new_config['General']['handle_reverse_proxy'] = int(HANDLE_REVERSE_PROXY)
new_config['General']['send_security_headers'] = int(SEND_SECURITY_HEADERS) 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_nzbs'] = int(USE_NZBS)
new_config['General']['use_torrents'] = int(USE_TORRENTS) new_config['General']['use_torrents'] = int(USE_TORRENTS)
new_config['General']['nzb_method'] = NZB_METHOD new_config['General']['nzb_method'] = NZB_METHOD

View file

@ -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' Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key'
""" """
try: try:
from OpenSSL import crypto # @UnresolvedImport from OpenSSL import crypto
from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, \ from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial
serial # @UnresolvedImport except (StandardError, Exception):
except Exception as e:
logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING) logger.log(u"pyopenssl module missing, please install for https access", logger.WARNING)
return False return False
@ -651,16 +650,17 @@ def create_https_certificates(ssl_cert, ssl_key):
careq = createCertRequest(cakey, CN='Certificate Authority') careq = createCertRequest(cakey, CN='Certificate Authority')
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
cname = 'SickGear'
pkey = createKeyPair(TYPE_RSA, 4096) 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 cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
# Save the key and certificate to disk # Save the key and certificate to disk
try: try:
open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) with open(ssl_key, 'w') as file_hd:
open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) file_hd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
except: 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) logger.log(u"Error creating SSL key and certificate", logger.ERROR)
return False return False
@ -1350,6 +1350,64 @@ def maybe_plural(number=1):
return ('s', '')[1 == number] 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
'''
(?:(?<![:.\w])\[? # Anchor address
(?:[A-F0-9]{1,4}:){6} # 6 words
(?:[A-F0-9]{1,4}:[A-F0-9]{1,4} # 2 words
| (?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3} # or 4 bytes
(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])
)(?![:.\w])) # Anchor address
'''
,
# IPv6 address (compressed and compressed mixed)
# 8 hexadecimal words, or 6 hexadecimal words followed by 4 decimal bytes
# All with optional leading zeros. Consecutive zeros may be replaced with ::
'''
(?:(?<![:.\w])\[?(?: # Anchor address
(?: # Mixed
(?:[A-F0-9]{1,4}:){6} # Non-compressed
|(?=(?:[A-F0-9]{0,4}:){2,6} # Compressed with 2 to 6 colons
(?:[0-9]{1,3}\.){3}[0-9]{1,3} # and 4 bytes
(?![:.\w])) # and anchored
(([0-9A-F]{1,4}:){1,5}|:)((:[0-9A-F]{1,4}){1,5}:|:) # and at most 1 double colon
|::(?:[A-F0-9]{1,4}:){5} # Compressed with 7 colons and 5 numbers
)
(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3} # 255.255.255.
(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) # 255
| # Standard
(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4} # Standard
| # Compressed
(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4} # Compressed with at most 7 colons
(?![:.\w])) # and anchored
(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:) # and at most 1 double colon
|(?:[A-F0-9]{1,4}:){7}:|:(:[A-F0-9]{1,4}){7} # Compressed with 8 colons
)(?![:.\w])) # Anchor address
'''
]])
def build_dict(seq, key): def build_dict(seq, key):
return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq)) return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq))

View file

@ -63,6 +63,7 @@ from sickbeard.indexermapper import MapStatus, save_mapping, map_indexers_to_sho
from sickbeard.tv import show_not_found_retry_days, concurrent_show_not_found_days from sickbeard.tv import show_not_found_retry_days, concurrent_show_not_found_days
from tornado import gen from tornado import gen
from tornado.web import RequestHandler, StaticFileHandler, authenticated from tornado.web import RequestHandler, StaticFileHandler, authenticated
from tornado import escape
from lib import adba from lib import adba
from lib import subliminal from lib import subliminal
from lib.dateutil import tz from lib.dateutil import tz
@ -87,8 +88,10 @@ except ImportError:
class PageTemplate(Template): class PageTemplate(Template):
def __init__(self, headers, *args, **kwargs): def __init__(self, web_handler, *args, **kwargs):
headers = web_handler.request.headers
self.xsrf_form_html = '<input name="_xsrf" type="hidden" value="%s">' % web_handler.xsrf_token
self.sbHost = headers.get('X-Forwarded-Host') self.sbHost = headers.get('X-Forwarded-Host')
if None is self.sbHost: if None is self.sbHost:
sbHost = headers.get('Host') or 'localhost' sbHost = headers.get('Host') or 'localhost'
@ -207,7 +210,7 @@ class LoginHandler(BaseHandler):
if self.get_current_user(): if self.get_current_user():
self.redirect(self.get_argument('next', '/home/')) self.redirect(self.get_argument('next', '/home/'))
else: else:
t = PageTemplate(headers=self.request.headers, file='login.tmpl') t = PageTemplate(web_handler=self, file='login.tmpl')
t.resp = self.get_argument('resp', '') t.resp = self.get_argument('resp', '')
self.set_status(401) self.set_status(401)
self.finish(t.respond()) self.finish(t.respond())
@ -217,9 +220,12 @@ class LoginHandler(BaseHandler):
password = sickbeard.WEB_PASSWORD password = sickbeard.WEB_PASSWORD
if (self.get_argument('username') == username) and (self.get_argument('password') == 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), 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/')) self.redirect(self.get_argument('next', '/home/'))
else: else:
next_arg = '&next=' + self.get_argument('next', '/home/') next_arg = '&next=' + self.get_argument('next', '/home/')
@ -405,7 +411,7 @@ class RepoHandler(BaseStaticFileHandler):
return super(RepoHandler, self).get_content_type() return super(RepoHandler, self).get_content_type()
def index(self, basepath, filelist): 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.basepath = basepath
t.filelist = filelist t.filelist = filelist
return t.respond() return t.respond()
@ -469,11 +475,11 @@ class RepoHandler(BaseStaticFileHandler):
return fh.read().strip() return fh.read().strip()
def render_kodi_repo_addon_xml(self): 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() return t.respond().strip()
def render_kodi_repo_addons_xml(self): 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( t.watchedstate_updater_addon_xml = re.sub(
'(?m)^([\s]*<)', r'\t\1', '(?m)^([\s]*<)', r'\t\1',
'\n'.join(self.get_watchedstate_updater_addon_xml().split('\n')[1:])) # skip xml header '\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): def page_not_found(self):
self.set_status(404) self.set_status(404)
t = PageTemplate(headers=self.request.headers, file='404.tmpl') t = PageTemplate(web_handler=self, file='404.tmpl')
return t.respond() return t.respond()
@authenticated @authenticated
@ -580,10 +586,8 @@ class WebHandler(BaseHandler):
except: except:
self.finish(self.page_not_found()) self.finish(self.page_not_found())
else: else:
kwargss = self.request.arguments kwargss = {k: v if not (isinstance(v, list) and 1 == len(v)) else v[0]
for arg, value in kwargss.items(): for k, v in self.request.arguments.iteritems() if '_xsrf' != k}
if len(value) == 1:
kwargss[arg] = value[0]
result = method(**kwargss) result = method(**kwargss)
if result: if result:
self.finish(result) self.finish(result)
@ -770,7 +774,7 @@ class MainHandler(WebHandler):
# add localtime to the dict # add localtime to the dict
cache_obj = image_cache.ImageCache() cache_obj = image_cache.ImageCache()
t = PageTemplate(headers=self.request.headers, file='episodeView.tmpl') t = PageTemplate(web_handler=self, file='episodeView.tmpl')
t.fanart = {} t.fanart = {}
for index, item in enumerate(sql_results): for index, item in enumerate(sql_results):
sql_results[index]['localtime'] = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(item['airdate'], 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/') self.redirect('/history/')
def _genericMessage(self, subject, message): 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.submenu = self.HomeMenu()
t.subject = subject t.subject = subject
t.message = message t.message = message
@ -1136,7 +1140,7 @@ class Home(MainHandler):
self.redirect('/home/showlistView/') self.redirect('/home/showlistView/')
def showlistView(self): def showlistView(self):
t = PageTemplate(headers=self.request.headers, file='home.tmpl') t = PageTemplate(web_handler=self, file='home.tmpl')
t.showlists = [] t.showlists = []
index = 0 index = 0
if sickbeard.SHOWLIST_TAGVIEW == 'custom': if sickbeard.SHOWLIST_TAGVIEW == 'custom':
@ -1559,7 +1563,7 @@ class Home(MainHandler):
def viewchanges(self): 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'}] t.changelist = [{'type': 'rel', 'ver': '', 'date': 'Nothing to display at this time'}]
url = 'https://raw.githubusercontent.com/wiki/SickGear/SickGear/sickgear/CHANGES.md' url = 'https://raw.githubusercontent.com/wiki/SickGear/SickGear/sickgear/CHANGES.md'
@ -1603,7 +1607,7 @@ class Home(MainHandler):
if str(pid) != str(sickbeard.PID): if str(pid) != str(sickbeard.PID):
return self.redirect('/home/') return self.redirect('/home/')
t = PageTemplate(headers=self.request.headers, file='restart.tmpl') t = PageTemplate(web_handler=self, file='restart.tmpl')
t.shutdown = True t.shutdown = True
sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN) sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN)
@ -1615,7 +1619,7 @@ class Home(MainHandler):
if str(pid) != str(sickbeard.PID): if str(pid) != str(sickbeard.PID):
return self.redirect('/home/') return self.redirect('/home/')
t = PageTemplate(headers=self.request.headers, file='restart.tmpl') t = PageTemplate(web_handler=self, file='restart.tmpl')
t.shutdown = False t.shutdown = False
sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) sickbeard.events.put(sickbeard.events.SystemEvent.RESTART)
@ -1664,7 +1668,7 @@ class Home(MainHandler):
if None is season: if None is season:
return json.dumps(response) 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 t.show = show_obj
my_db = db.DBConnection() my_db = db.DBConnection()
@ -1697,7 +1701,7 @@ class Home(MainHandler):
if showObj is None: if showObj is None:
return self._genericMessage('Error', 'Show not in show list') 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}] t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}]
try: try:
@ -2162,7 +2166,7 @@ class Home(MainHandler):
bestQualities = [] bestQualities = []
if not location and not anyQualities and not bestQualities and not flatten_folders: 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.submenu = self.HomeMenu()
t.expand_ids = all([kwargs.get('tvsrc'), kwargs.get('srcid')]) t.expand_ids = all([kwargs.get('tvsrc'), kwargs.get('srcid')])
@ -2666,7 +2670,7 @@ class Home(MainHandler):
# present season DESC episode DESC on screen # present season DESC episode DESC on screen
ep_obj_rename_list.reverse() 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.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}]
t.ep_obj_list = ep_obj_rename_list t.ep_obj_list = ep_obj_rename_list
t.show = showObj t.show = showObj
@ -2922,7 +2926,7 @@ class Home(MainHandler):
class HomePostProcess(Home): class HomePostProcess(Home):
def index(self, *args, **kwargs): 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']] t.submenu = [x for x in self.HomeMenu() if 'postprocess' not in x['path']]
return t.respond() return t.respond()
@ -2979,7 +2983,7 @@ class HomePostProcess(Home):
class NewHomeAddShows(Home): class NewHomeAddShows(Home):
def index(self, *args, **kwargs): 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() t.submenu = self.HomeMenu()
return t.respond() return t.respond()
@ -3194,7 +3198,7 @@ class NewHomeAddShows(Home):
return s return s
def massAddTable(self, rootDir=None, **kwargs): 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.submenu = self.HomeMenu()
t.kwargs = kwargs t.kwargs = kwargs
@ -3316,7 +3320,7 @@ class NewHomeAddShows(Home):
self.set_header('Pragma', 'no-cache') self.set_header('Pragma', 'no-cache')
self.set_header('Expires', '0') 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.submenu = self.HomeMenu()
t.enable_anime_options = True t.enable_anime_options = True
t.enable_default_wanted = 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 Display the new show page which collects a tvdb id, folder, and extra options and
posts them to addNewShow 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.submenu = self.HomeMenu()
t.browse_type = browse_type t.browse_type = browse_type
t.browse_title = browse_title t.browse_title = browse_title
@ -4046,7 +4050,7 @@ class NewHomeAddShows(Home):
""" """
Prints out the page to add existing shows from a root dir 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.submenu = self.HomeMenu()
t.enable_anime_options = False t.enable_anime_options = False
t.kwargs = kwargs t.kwargs = kwargs
@ -4279,7 +4283,7 @@ class Manage(MainHandler):
return [x for x in menu if exclude not in x['title']] return [x for x in menu if exclude not in x['title']]
def index(self, *args, **kwargs): 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') t.submenu = self.ManageMenu('Bulk')
return t.respond() return t.respond()
@ -4325,7 +4329,7 @@ class Manage(MainHandler):
else: else:
status_list = [] 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.submenu = self.ManageMenu('Episode')
t.whichStatus = whichStatus t.whichStatus = whichStatus
@ -4451,7 +4455,7 @@ class Manage(MainHandler):
def subtitleMissed(self, whichSubs=None): 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.submenu = self.ManageMenu('Subtitle')
t.whichSubs = whichSubs t.whichSubs = whichSubs
@ -4533,7 +4537,7 @@ class Manage(MainHandler):
def backlogOverview(self, *args, **kwargs): 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') t.submenu = self.ManageMenu('Backlog')
showCounts = {} showCounts = {}
@ -4576,7 +4580,7 @@ class Manage(MainHandler):
def massEdit(self, toEdit=None): 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() t.submenu = self.ManageMenu()
if not toEdit: if not toEdit:
@ -4963,7 +4967,7 @@ class Manage(MainHandler):
if toRemove: if toRemove:
return self.redirect('/manage/failedDownloads/') 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.over_limit = limit and len(sql_results) > limit
t.failedResults = t.over_limit and sql_results[0:-1] or sql_results t.failedResults = t.over_limit and sql_results[0:-1] or sql_results
t.limit = str(limit) t.limit = str(limit)
@ -4974,7 +4978,7 @@ class Manage(MainHandler):
class ManageSearches(Manage): class ManageSearches(Manage):
def index(self, *args, **kwargs): 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_pi = sickbeard.backlogSearchScheduler.action.get_progress_indicator()
t.backlog_paused = sickbeard.searchQueueScheduler.action.is_backlog_paused() t.backlog_paused = sickbeard.searchQueueScheduler.action.is_backlog_paused()
t.backlog_running = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() t.backlog_running = sickbeard.searchQueueScheduler.action.is_backlog_in_progress()
@ -5051,7 +5055,7 @@ class ManageSearches(Manage):
class showProcesses(Manage): class showProcesses(Manage):
def index(self, *args, **kwargs): 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.queue_length = sickbeard.showQueueScheduler.action.queue_length()
t.show_list = sickbeard.showList t.show_list = sickbeard.showList
t.show_update_running = sickbeard.showQueueScheduler.action.isShowUpdateRunning() or sickbeard.showUpdateScheduler.action.amActive 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): 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 t.limit = limit
my_db = db.DBConnection(row_type='dict') 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']] return [x for x in menu if exclude not in x['title']]
def index(self, *args, **kwargs): 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() t.submenu = self.ConfigMenu()
return t.respond() return t.respond()
@ -5579,11 +5583,12 @@ class Config(MainHandler):
class ConfigGeneral(Config): class ConfigGeneral(Config):
def index(self, *args, **kwargs): 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.submenu = self.ConfigMenu('General')
t.show_tags = ', '.join(sickbeard.SHOW_TAGS) t.show_tags = ', '.join(sickbeard.SHOW_TAGS)
t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers
if sickbeard.indexerApi(i).config['active']]) if sickbeard.indexerApi(i).config['active']])
t.request_host = escape.xhtml_escape(self.request.host_name)
return t.respond() return t.respond()
def saveRootDirs(self, rootDirString=None): 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, 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, 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, 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, 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, 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, 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 sickbeard.TIMEZONE_DISPLAY = timezone_display
# Web interface # Web interface
restart = False
reload_page = False reload_page = False
if sickbeard.WEB_USERNAME != web_username: if sickbeard.WEB_USERNAME != web_username:
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_PORT = config.to_int(web_port)
# sickbeard.WEB_LOG is set in config.change_log_dir() # 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) sickbeard.ENABLE_HTTPS = config.checkbox_to_value(enable_https)
if not config.change_https_cert(https_cert): if not config.change_https_cert(https_cert):
results += [ results += [
@ -5743,6 +5751,10 @@ class ConfigGeneral(Config):
sickbeard.WEB_IPV64 = config.checkbox_to_value(web_ipv64) sickbeard.WEB_IPV64 = config.checkbox_to_value(web_ipv64)
sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy)
sickbeard.SEND_SECURITY_HEADERS = config.checkbox_to_value(send_security_headers) 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 # Advanced
sickbeard.GIT_REMOTE = git_remote sickbeard.GIT_REMOTE = git_remote
@ -5767,6 +5779,11 @@ class ConfigGeneral(Config):
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) 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: if reload_page:
self.clear_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT)) self.clear_cookie('sickgear-session-%s' % helpers.md5_for_text(sickbeard.WEB_PORT))
self.write('reload') self.write('reload')
@ -5796,7 +5813,7 @@ class ConfigGeneral(Config):
class ConfigSearch(Config): class ConfigSearch(Config):
def index(self, *args, **kwargs): 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.submenu = self.ConfigMenu('Search')
t.using_rls_ignore_words = [(show.indexerid, show.name) t.using_rls_ignore_words = [(show.indexerid, show.name)
for show in sickbeard.showList if show.rls_ignore_words and for show in sickbeard.showList if show.rls_ignore_words and
@ -5908,7 +5925,7 @@ class ConfigSearch(Config):
class ConfigPostProcessing(Config): class ConfigPostProcessing(Config):
def index(self, *args, **kwargs): 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') t.submenu = self.ConfigMenu('Processing')
return t.respond() return t.respond()
@ -6092,7 +6109,7 @@ class ConfigPostProcessing(Config):
class ConfigProviders(Config): class ConfigProviders(Config):
def index(self, *args, **kwargs): 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') t.submenu = self.ConfigMenu('Providers')
return t.respond() return t.respond()
@ -6475,7 +6492,7 @@ class ConfigProviders(Config):
class ConfigNotifications(Config): class ConfigNotifications(Config):
def index(self, *args, **kwargs): 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.submenu = self.ConfigMenu('Notifications')
t.root_dirs = [] t.root_dirs = []
if sickbeard.ROOT_DIRS: if sickbeard.ROOT_DIRS:
@ -6774,7 +6791,7 @@ class ConfigNotifications(Config):
class ConfigSubtitles(Config): class ConfigSubtitles(Config):
def index(self, *args, **kwargs): 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') t.submenu = self.ConfigMenu('Subtitle')
return t.respond() return t.respond()
@ -6820,7 +6837,7 @@ class ConfigSubtitles(Config):
class ConfigAnime(Config): class ConfigAnime(Config):
def index(self, *args, **kwargs): 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') t.submenu = self.ConfigMenu('Anime')
return t.respond() return t.respond()
@ -6878,7 +6895,7 @@ class ErrorLogs(MainHandler):
def index(self, *args, **kwargs): 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 t.submenu = self.ErrorLogsMenu
return t.respond() return t.respond()
@ -6906,7 +6923,7 @@ class ErrorLogs(MainHandler):
def viewlog(self, min_level=logger.MESSAGE, max_lines=500): 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 t.submenu = self.ErrorLogsMenu
min_level = int(min_level) min_level = int(min_level)
@ -6989,7 +7006,7 @@ class WebFileBrowser(MainHandler):
class ApiBuilder(MainHandler): class ApiBuilder(MainHandler):
def index(self): def index(self):
""" expose the api-builder template """ """ 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): def titler(x):
return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE]
@ -7029,7 +7046,7 @@ class Cache(MainHandler):
if not sql_results: if not sql_results:
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 t.cacheResults = sql_results
return t.respond() return t.respond()

View file

@ -6,14 +6,13 @@ import webserve
import webapi import webapi
from sickbeard import logger 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.web import Application
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
class WebServer(threading.Thread): class WebServer(threading.Thread):
def __init__(self, options={}, **kwargs): def __init__(self, options=None):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.daemon = True self.daemon = True
self.alive = True self.alive = True
@ -21,7 +20,7 @@ class WebServer(threading.Thread):
self.io_loop = None self.io_loop = None
self.server = None self.server = None
self.options = options self.options = options or {}
self.options.setdefault('port', 8081) self.options.setdefault('port', 8081)
self.options.setdefault('host', '0.0.0.0') self.options.setdefault('host', '0.0.0.0')
self.options.setdefault('log_dir', None) self.options.setdefault('log_dir', None)
@ -40,40 +39,60 @@ class WebServer(threading.Thread):
self.https_key = self.options['https_key'] self.https_key = self.options['https_key']
if self.enable_https: 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 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))\ if make_cert:
or not (self.https_key and os.path.exists(self.https_key)):
if not create_https_certificates(self.https_cert, self.https_key): if not create_https_certificates(self.https_cert, self.https_key):
logger.log(u'Unable to create CERT/KEY files, disabling HTTPS') logger.log(u'Unable to create CERT/KEY files, disabling HTTPS')
update_cfg |= False is not sickbeard.ENABLE_HTTPS
sickbeard.ENABLE_HTTPS = False sickbeard.ENABLE_HTTPS = False
self.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) 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 sickbeard.ENABLE_HTTPS = False
self.enable_https = False self.enable_https = False
if update_cfg:
sickbeard.save_config()
# Load the app # Load the app
self.app = Application([], self.app = Application([],
debug=False, debug=False,
serve_traceback=True,
autoreload=False, autoreload=False,
gzip=True, compress_response=True,
cookie_secret=sickbeard.COOKIE_SECRET, cookie_secret=sickbeard.COOKIE_SECRET,
xsrf_cookies=True,
login_url='%s/login/' % self.options['web_root']) login_url='%s/login/' % self.options['web_root'])
re_host_pattern = re_valid_hostname()
# webui login/logout handlers # 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/login(/?)' % self.options['web_root'], webserve.LoginHandler),
(r'%s/logout(/?)' % self.options['web_root'], webserve.LogoutHandler), (r'%s/logout(/?)' % self.options['web_root'], webserve.LogoutHandler),
]) ])
# Web calendar handler (Needed because option Unprotected calendar) # 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), (r'%s/calendar' % self.options['web_root'], webserve.CalendarHandler),
]) ])
# Static File Handlers # Static File Handlers
self.app.add_handlers('.*$', [ self.app.add_handlers(re_host_pattern, [
# favicon # favicon
(r'%s/(favicon\.ico)' % self.options['web_root'], webserve.BaseStaticFileHandler, (r'%s/(favicon\.ico)' % self.options['web_root'], webserve.BaseStaticFileHandler,
{'path': os.path.join(self.options['data_root'], 'images/ico/favicon.ico')}), {'path': os.path.join(self.options['data_root'], 'images/ico/favicon.ico')}),
@ -100,7 +119,7 @@ class WebServer(threading.Thread):
]) ])
# Main Handler # 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/builder(/?)(.*)' % self.options['web_root'], webserve.ApiBuilder),
(r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api),
(r'%s/imagecache(/?.*)' % self.options['web_root'], webserve.CachedImages), (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. # Ignore errors like 'ValueError: I/O operation on closed kqueue fd'. These might be thrown during a reload.
pass pass
def shutDown(self): def shut_down(self):
self.alive = False self.alive = False
if None is not self.io_loop: if None is not self.io_loop:
self.io_loop.stop() self.io_loop.stop()