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