Change improve security.

Change improve security with DNS rebinding prevention, set "Allowed browser hostnames" at config/General/Web Interface.
Change improve security with cross-site request forgery (xsrf) protection on web forms.
Change improve security by sending header flag httponly with cookies
Change improve security by sending header flag secure with SSL cookies
Change improve test for creating self-signed SSL cert.
Change force restart when switching SSL on/off.
Change enable Tornado serve_traceback feature.
Change PEP8 tweaks.
This commit is contained in:
JackDandy 2018-03-29 17:23:33 +01:00
parent de32a1aa67
commit f3310e29f2
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)
* 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)

View file

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

View file

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

View file

@ -42,6 +42,8 @@
<div id="config-content">
<form id="configForm" action="saveGeneral" method="post">
$xsrf_form_html
<div id="config-components">
<ul>
@ -589,6 +591,17 @@
</label>
</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">
</fieldset>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,6 +38,7 @@ var config = { sortArticle: #echo ['!1','!0'][$sg_var('SORT_ARTICLE')]# }
<h3>Existing show folders</h3>
<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#>
<p>Tip: shows are added quicker when usable show nfo and xml metadata is found</p>

View file

@ -43,6 +43,7 @@
#end if
<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">
<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">
<input type="hidden" id="type" name="type" value="manual">
$xsrf_form_html
<div id="postProcess" class="stepDiv">

View file

@ -30,7 +30,8 @@
<br />
<form id="addShowForm" method="post" action="$sbRoot/home/addShows/addRecommendedShow" accept-charset="utf-8">
$xsrf_form_html
<fieldset class="sectionwrap step-one">
<legend class="legendStep"><p>Select a recommended show</p></legend>
@ -69,4 +70,4 @@
</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
</style>
</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="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"><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>
#end if
<form name="bulkChangeForm" method="post" action="bulkChange">
$xsrf_form_html
<table id="bulkChangeTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead>

View file

@ -62,6 +62,8 @@
<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?v=$sbPID"></script>
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
$xsrf_form_html
<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>

View file

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

View file

@ -45,6 +45,8 @@
<input type="hidden" id="selectSubLang" name="selectSubLang" value="$whichSubs">
<form action="$sbRoot/manage/downloadSubtitleMissed" method="post">
$xsrf_form_html
<h2>Episodes without $subsLanguage subtitles.</h2>
<br />
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) {
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');

View file

@ -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

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'
"""
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
'''
(?:(?<![:.\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):
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 tornado import gen
from tornado.web import RequestHandler, StaticFileHandler, authenticated
from tornado import escape
from lib import adba
from lib import subliminal
from lib.dateutil import tz
@ -87,8 +88,10 @@ except ImportError:
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')
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()

View file

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