Commit ff923c4a authored by echel0n's avatar echel0n
Browse files

Added back support for custom SSL certificate and certificate key files

parent 156be279
This diff is collapsed.
......@@ -98,9 +98,6 @@ class Core(object):
self.gui_views_dir = os.path.join(sickrage.PROG_DIR, 'core', 'webserver', 'views')
self.gui_app_dir = os.path.join(sickrage.PROG_DIR, 'core', 'webserver', 'app')
self.https_cert_file = None
self.https_key_file = None
self.trakt_api_key = '5c65f55e11d48c35385d9e8670615763a605fad28374c8ae553a7b7a50651ddd'
self.trakt_api_secret = 'b53e32045ac122a445ef163e6d859403301ffe9b17fb8321d428531b69022a82'
self.trakt_app_id = '4562'
......@@ -342,10 +339,6 @@ class Core(object):
# set socket timeout
socket.setdefaulttimeout(self.config.general.socket_timeout)
# set ssl cert/key filenames
self.https_cert_file = os.path.abspath(os.path.join(self.data_dir, 'server.crt'))
self.https_key_file = os.path.abspath(os.path.join(self.data_dir, 'server.key'))
# setup logger settings
self.log.logSize = self.config.general.log_size
self.log.logNr = self.config.general.log_nr
......
......@@ -50,11 +50,6 @@ import errno
import rarfile
import requests
from bs4 import BeautifulSoup
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import sickrage
from sickrage.core.enums import TorrentMethod
......@@ -760,53 +755,6 @@ def sanitize_scene_name(name, anime=False):
return name
def create_https_certificates(ssl_cert, ssl_key):
"""This function takes a domain name as a parameter and then creates a certificate and key with the
domain name(replacing dots by underscores), finally signing the certificate using specified CA and
returns the path of key and cert files. If you are yet to generate a CA then check the top comments"""
# Generate our key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
)
name = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, 'SiCKRAGE')
])
# path_len=0 means this cert can only sign itself, not other certs.
basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
now = datetime.datetime.utcnow()
cert = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name)
.public_key(key.public_key())
.serial_number(1000)
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=10 * 365))
.add_extension(basic_contraints, False)
# .add_extension(san, False)
.sign(key, hashes.SHA256(), default_backend())
)
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
key_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(ssl_key, 'wb') as key_out:
key_out.write(key_pem)
with open(ssl_cert, 'wb') as cert_out:
cert_out.write(cert_pem)
return True
def anon_url(*url):
"""
Return a URL string consisting of the Anonymous redirect URL and an arbitrary number of values appended.
......
......@@ -26,16 +26,13 @@ import ssl
import tornado.autoreload
import tornado.locale
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import ExtensionNotFound
from mako.lookup import TemplateLookup
from tornado.httpserver import HTTPServer
from tornado.ioloop import PeriodicCallback, IOLoop
from tornado.ioloop import IOLoop
from tornado.web import Application, RedirectHandler, StaticFileHandler
import sickrage
from sickrage.core.helpers import create_https_certificates
from sickrage.core.webserver.helpers import create_https_certificates, is_certificate_valid, certificate_needs_renewal
from sickrage.core.webserver.handlers.account import AccountLinkHandler, AccountUnlinkHandler, AccountIsLinkedHandler
from sickrage.core.webserver.handlers.announcements import AnnouncementsHandler, MarkAnnouncementSeenHandler, AnnouncementCountHandler
from sickrage.core.webserver.handlers.api import ApiSwaggerDotJsonHandler, ApiPingHandler, ApiProfileHandler
......@@ -136,6 +133,18 @@ class WebServer(object):
self.app = None
self.server = None
@property
def cert_file(self):
if os.path.exists(sickrage.app.config.general.https_cert):
return sickrage.app.config.general.https_cert
return os.path.abspath(os.path.join(sickrage.app.data_dir, 'server.crt'))
@property
def cert_key_file(self):
if os.path.exists(sickrage.app.config.general.https_key):
return sickrage.app.config.general.https_key
return os.path.abspath(os.path.join(sickrage.app.data_dir, 'server.key'))
def start(self):
self.started = True
......@@ -167,7 +176,7 @@ class WebServer(object):
# tornado SSL setup
if sickrage.app.config.general.enable_https:
if not self.load_ssl_certificate():
sickrage.app.log.info("Unable to retrieve CERT/KEY files from SiCKRAGE API, disabling HTTPS")
sickrage.app.log.info("Unable to load HTTPS certificate and key files, disabling HTTPS")
sickrage.app.config.general.enable_https = False
# Load templates
......@@ -482,7 +491,7 @@ class WebServer(object):
ssl_ctx = None
if sickrage.app.config.general.enable_https:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(sickrage.app.https_cert_file, sickrage.app.https_key_file)
ssl_ctx.load_cert_chain(self.cert_file, self.cert_key_file)
# Web Server
self.server = HTTPServer(self.app, ssl_options=ssl_ctx, xheaders=sickrage.app.config.general.handle_reverse_proxy)
......@@ -494,31 +503,41 @@ class WebServer(object):
raise SystemExit
def load_ssl_certificate(self, certificate=None, private_key=None):
sr_cert_file = os.path.abspath(os.path.join(sickrage.app.data_dir, 'server.crt'))
sr_cert_key_file = os.path.abspath(os.path.join(sickrage.app.data_dir, 'server.key'))
# Custom user provided HTTPS certificate and certificate key files
if os.path.exists(sickrage.app.config.general.https_cert) and os.path.exists(sickrage.app.config.general.https_key):
if certificate_needs_renewal(sickrage.app.config.general.https_cert):
return False
return True
# SiCKRAGE HTTPS certificate and certificate key files
if certificate and private_key:
with open(sickrage.app.https_cert_file, 'w') as cert_out:
with open(sr_cert_file, 'w') as cert_out:
cert_out.write(certificate)
with open(sickrage.app.https_key_file, 'w') as key_out:
with open(sr_cert_key_file, 'w') as key_out:
key_out.write(private_key)
else:
if os.path.exists(sickrage.app.https_key_file) and os.path.exists(sickrage.app.https_cert_file):
if self.is_certificate_valid() and not self.certificate_needs_renewal():
if os.path.exists(sr_cert_file) and os.path.exists(sr_cert_key_file):
if is_certificate_valid(sr_cert_file) and not certificate_needs_renewal(sr_cert_file):
return True
resp = sickrage.app.api.server.get_server_certificate(sickrage.app.config.general.server_id)
if not resp or 'certificate' not in resp or 'private_key' not in resp:
if not create_https_certificates(sickrage.app.https_cert_file, sickrage.app.https_key_file):
if not create_https_certificates(sr_cert_file, sr_cert_key_file):
return False
if not os.path.exists(sickrage.app.https_cert_file) or not os.path.exists(sickrage.app.https_key_file):
if not os.path.exists(sr_cert_file) or not os.path.exists(sr_cert_key_file):
return False
return True
with open(sickrage.app.https_cert_file, 'w') as cert_out:
with open(sr_cert_file, 'w') as cert_out:
cert_out.write(resp['certificate'])
with open(sickrage.app.https_key_file, 'w') as key_out:
with open(sr_cert_key_file, 'w') as key_out:
key_out.write(resp['private_key'])
sickrage.app.log.info("Loaded SSL certificate successfully, restarting server in 1 minute")
......@@ -529,52 +548,6 @@ class WebServer(object):
return True
def certificate_needs_renewal(self):
if not os.path.exists(sickrage.app.https_cert_file):
return
with open(sickrage.app.https_cert_file, 'rb') as f:
cert_pem = f.read()
cert = x509.load_pem_x509_certificate(cert_pem, default_backend())
not_valid_after = cert.not_valid_after
return not_valid_after - datetime.datetime.utcnow() < (cert.not_valid_after - cert.not_valid_before) / 2
def is_certificate_valid(self):
if not os.path.exists(sickrage.app.https_cert_file):
return
with open(sickrage.app.https_cert_file, 'rb') as f:
cert_pem = f.read()
cert = x509.load_pem_x509_certificate(cert_pem, default_backend())
issuer = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0]
subject = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0]
if 'ZeroSSL' not in issuer.value:
return False
if subject.value != f'{sickrage.app.config.general.server_id}.external.sickrage.direct':
return False
try:
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
sans = ext.get_values_for_type(x509.DNSName)
domains = [
f'{sickrage.app.config.general.server_id}.external.sickrage.direct',
f'{sickrage.app.config.general.server_id}.internal.sickrage.direct'
]
for domain in sans:
if domain not in domains:
return False
except ExtensionNotFound:
return False
return True
def shutdown(self):
if self.started:
self.started = False
......
......@@ -18,6 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import os
from tornado.web import authenticated
......@@ -256,11 +257,11 @@ class SaveGeneralHandler(BaseHandler):
sickrage.app.config.general.enable_https = checkbox_to_value(enable_https)
# if not change_https_cert(https_cert):
# results += ["Unable to create directory " + os.path.normpath(https_cert) + ", https cert directory not changed."]
#
# if not change_https_key(https_key):
# results += ["Unable to create directory " + os.path.normpath(https_key) + ", https key directory not changed."]
if os.path.exists(https_cert):
sickrage.app.config.general.https_cert = https_cert
if os.path.exists(https_key):
sickrage.app.config.general.https_key = https_key
sickrage.app.config.general.handle_reverse_proxy = checkbox_to_value(handle_reverse_proxy)
......
import datetime
import os
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509 import ExtensionNotFound
from cryptography.x509.oid import NameOID
import sickrage
def create_https_certificates(ssl_cert, ssl_key):
"""This function takes a domain name as a parameter and then creates a certificate and key with the
domain name(replacing dots by underscores), finally signing the certificate using specified CA and
returns the path of key and cert files. If you are yet to generate a CA then check the top comments"""
# Generate our key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
)
name = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, 'SiCKRAGE')
])
# path_len=0 means this cert can only sign itself, not other certs.
basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
now = datetime.datetime.utcnow()
cert = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name)
.public_key(key.public_key())
.serial_number(1000)
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=10 * 365))
.add_extension(basic_contraints, False)
# .add_extension(san, False)
.sign(key, hashes.SHA256(), default_backend())
)
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
key_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(ssl_key, 'wb') as key_out:
key_out.write(key_pem)
with open(ssl_cert, 'wb') as cert_out:
cert_out.write(cert_pem)
return True
def is_certificate_valid(cert_file):
if not os.path.exists(cert_file):
return
with open(cert_file, 'rb') as f:
cert_pem = f.read()
cert = x509.load_pem_x509_certificate(cert_pem, default_backend())
issuer = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0]
subject = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0]
if 'ZeroSSL' not in issuer.value:
return False
if subject.value != f'{sickrage.app.config.general.server_id}.external.sickrage.direct':
return False
try:
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
sans = ext.get_values_for_type(x509.DNSName)
domains = [
f'{sickrage.app.config.general.server_id}.external.sickrage.direct',
f'{sickrage.app.config.general.server_id}.internal.sickrage.direct'
]
for domain in sans:
if domain not in domains:
return False
except ExtensionNotFound:
return False
return True
def certificate_needs_renewal(cert_file):
if not os.path.exists(cert_file):
return
with open(cert_file, 'rb') as f:
cert_pem = f.read()
cert = x509.load_pem_x509_certificate(cert_pem, default_backend())
not_valid_after = cert.not_valid_after
return not_valid_after - datetime.datetime.utcnow() < (cert.not_valid_after - cert.not_valid_before) / 2
\ No newline at end of file
......@@ -957,51 +957,51 @@ c<%inherit file="../layouts/config.mako"/>
</div>
</div>
## <div id="content_enable_https">
## <div class="form-row form-group">
##
## <div class="col-lg-3 col-md-4 col-sm-5">
## <label class="component-title">${_('HTTPS certificate')}</label>
## </div>
## <div class="col-lg-9 col-md-8 col-sm-7 component-desc">
## <div class="form-row">
## <div class="col-md-12">
## <input name="https_cert" id="https_cert"
## value="${sickrage.app.config.general.https_cert}"
## class="form-control"
## autocapitalize="off"/>
## </div>
## </div>
## <div class="form-row">
## <div class="col-md-12">
## <label for="https_cert">
## ${_('file name or path to HTTPS certificate')}
## </label>
## </div>
## </div>
## </div>
## </div>
##
## <div class="form-row form-group">
## <div class="col-lg-3 col-md-4 col-sm-5">
## <label class="component-title">${_('HTTPS key')}</label>
## </div>
## <div class="col-lg-9 col-md-8 col-sm-7 component-desc">
## <div class="form-row">
## <div class="col-md-12">
## <input name="https_key" id="https_key"
## value="${sickrage.app.config.general.https_key}"
## class="form-control" autocapitalize="off"/>
## </div>
## </div>
## <div class="form-row">
## <div class="col-md-12">
## <label for="https_key">${_('file name or path to HTTPS key')}</label>
## </div>
## </div>
## </div>
## </div>
## </div>
<div id="content_enable_https">
<div class="form-row form-group">
<div class="col-lg-3 col-md-4 col-sm-5">
<label class="component-title">${_('Custom HTTPS certificate')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 component-desc">
<div class="form-row">
<div class="col-md-12">
<input name="https_cert" id="https_cert"
value="${sickrage.app.config.general.https_cert}"
class="form-control"
autocapitalize="off"/>
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="https_cert">
${_('path to a custom HTTPS certificate file')}
</label>
</div>
</div>
</div>
</div>
<div class="form-row form-group">
<div class="col-lg-3 col-md-4 col-sm-5">
<label class="component-title">${_('Custom HTTPS certificate key')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 component-desc">
<div class="form-row">
<div class="col-md-12">
<input name="https_key" id="https_key"
value="${sickrage.app.config.general.https_key}"
class="form-control" autocapitalize="off"/>
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="https_key">${_('path to a custom HTTPS key file')}</label>
</div>
</div>
</div>
</div>
</div>
<div class="form-row form-group">
<div class="col-lg-3 col-md-4 col-sm-5">
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment