Create self-signed HTTPS certificates

This commit is contained in:
JonnyWong16 2016-02-13 16:08:43 -08:00
parent 0bd40405b5
commit 9d780701f5
6 changed files with 112 additions and 40 deletions

View file

@ -338,16 +338,49 @@ scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
<p class="help-block">Enable HTTPS for web server for encrypted communication.</p> <p class="help-block">Enable HTTPS for web server for encrypted communication.</p>
</div> </div>
<div id="https_options"> <div id="https_options">
<div class="checkbox">
<label>
<input type="checkbox" class="http-settings" name="https_create_cert" id="https_create_cert" value="1" ${config['https_create_cert']} /> Create Self-signed Certificate
</label>
<p class="help-block">Check to have PlexPy create a self-signed SSL certificate. Uncheck if you want to use your own certificate.</p>
</div>
<div class="form-group">
<label for="https_domain">HTTPS Domains</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_domain" name="https_domain" value="${config['https_domain']}">
</div>
</div>
<p class="help-block">The domain names used to access PlexPy, separated by commas (,).</p>
</div>
<div class="form-group">
<label for="https_ip">HTTPS IPs</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_ip" name="https_ip" value="${config['https_ip']}">
</div>
</div>
<p class="help-block">The IP addresses used to access PlexPy, separated by commas (,).</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="https_cert">HTTPS Cert</label> <label for="https_cert">HTTPS Cert</label>
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}"> <div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
</div>
</div>
<p class="help-block">The location of the SSL certificate.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="https_key">HTTPS Key</label> <label for="https_key">HTTPS Key</label>
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}"> <div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
</div>
</div>
<p class="help-block">The location of the SSL key.</p>
</div> </div>
</div> </div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p> <p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div> </div>
<div role="tabpanel" class="tab-pane" id="tabs-4"> <div role="tabpanel" class="tab-pane" id="tabs-4">

View file

@ -1,26 +1,21 @@
# -*- coding: latin-1 -*- # -*- coding: latin-1 -*-
# #
# Copyright (C) Martin Sjögren and AB Strakt 2001, All rights reserved # Copyright (C) AB Strakt
# Copyright (C) Jean-Paul Calderone 2008, All rights reserved # Copyright (C) Jean-Paul Calderone
# This file is licenced under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 or later (aka LGPL v2.1) # See LICENSE for details.
# Please see LGPL2.1.txt for more information
""" """
Certificate generation module. Certificate generation module.
""" """
from OpenSSL import crypto from OpenSSL import crypto
import time
TYPE_RSA = crypto.TYPE_RSA TYPE_RSA = crypto.TYPE_RSA
TYPE_DSA = crypto.TYPE_DSA TYPE_DSA = crypto.TYPE_DSA
serial = int(time.time())
def createKeyPair(type, bits): def createKeyPair(type, bits):
""" """
Create a public/private key pair. Create a public/private key pair.
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
bits - Number of bits to use in the key bits - Number of bits to use in the key
Returns: The public/private key pair in a PKey object Returns: The public/private key pair in a PKey object
@ -29,12 +24,11 @@ def createKeyPair(type, bits):
pkey.generate_key(type, bits) pkey.generate_key(type, bits)
return pkey return pkey
def createCertRequest(pkey, digest="md5", **name): def createCertRequest(pkey, digest="sha256", **name):
""" """
Create a certificate request. Create a certificate request.
Arguments: pkey - The key to associate with the request Arguments: pkey - The key to associate with the request
digest - Digestion method to use for signing, default is md5 digest - Digestion method to use for signing, default is sha256
**name - The name of the subject of the request, possible **name - The name of the subject of the request, possible
arguments are: arguments are:
C - Country name C - Country name
@ -49,18 +43,17 @@ def createCertRequest(pkey, digest="md5", **name):
req = crypto.X509Req() req = crypto.X509Req()
subj = req.get_subject() subj = req.get_subject()
for (key,value) in name.items(): for key, value in name.items():
setattr(subj, key, value) setattr(subj, key, value)
req.set_pubkey(pkey) req.set_pubkey(pkey)
req.sign(pkey, digest) req.sign(pkey, digest)
return req return req
def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"): def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256"):
""" """
Generate a certificate given a certificate request. Generate a certificate given a certificate request.
Arguments: req - Certificate request to use
Arguments: req - Certificate reqeust to use
issuerCert - The certificate of the issuer issuerCert - The certificate of the issuer
issuerKey - The private key of the issuer issuerKey - The private key of the issuer
serial - Serial number for the certificate serial - Serial number for the certificate
@ -68,9 +61,11 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
starts being valid starts being valid
notAfter - Timestamp (relative to now) when the certificate notAfter - Timestamp (relative to now) when the certificate
stops being valid stops being valid
digest - Digest method to use for signing, default is md5 digest - Digest method to use for signing, default is sha256
Returns: The signed certificate in an X509 object Returns: The signed certificate in an X509 object
""" """
issuerCert, issuerKey = issuerCertKey
notBefore, notAfter = validityPeriod
cert = crypto.X509() cert = crypto.X509()
cert.set_serial_number(serial) cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(notBefore) cert.gmtime_adj_notBefore(notBefore)
@ -80,3 +75,32 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
cert.set_pubkey(req.get_pubkey()) cert.set_pubkey(req.get_pubkey())
cert.sign(issuerKey, digest) cert.sign(issuerKey, digest)
return cert return cert
def createSelfSignedCertificate((issuerName, issuerKey), serial, (notBefore, notAfter), altNames, digest="sha256"):
"""
Generate a certificate given a certificate request.
Arguments: issuerName - The name of the issuer
issuerKey - The private key of the issuer
serial - Serial number for the certificate
notBefore - Timestamp (relative to now) when the certificate
starts being valid
notAfter - Timestamp (relative to now) when the certificate
stops being valid
altNames - The alternative names
digest - Digest method to use for signing, default is sha256
Returns: The signed certificate in an X509 object
"""
cert = crypto.X509()
cert.set_version(2)
cert.set_serial_number(serial)
cert.get_subject().CN = issuerName
cert.gmtime_adj_notBefore(notBefore)
cert.gmtime_adj_notAfter(notAfter)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(issuerKey)
if altNames:
cert.add_extensions([crypto.X509Extension("subjectAltName", False, altNames)])
cert.sign(issuerKey, digest)
return cert

View file

@ -131,8 +131,11 @@ _CONFIG_DEFINITIONS = {
'HOME_STATS_COUNT': (int, 'General', 5), 'HOME_STATS_COUNT': (int, 'General', 5),
'HOME_STATS_CARDS': (list, 'General', ['top_tv', 'popular_tv', 'top_movies', 'popular_movies', 'top_music', \ 'HOME_STATS_CARDS': (list, 'General', ['top_tv', 'popular_tv', 'top_movies', 'popular_movies', 'top_music', \
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']), 'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
'HTTPS_CREATE_CERT': (int, 'General', 1),
'HTTPS_CERT': (str, 'General', ''), 'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''),
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
'HTTPS_IP': (str, 'General', '127.0.0.1'),
'HTTP_HOST': (str, 'General', '0.0.0.0'), 'HTTP_HOST': (str, 'General', '0.0.0.0'),
'HTTP_PASSWORD': (str, 'General', ''), 'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181), 'HTTP_PORT': (int, 'General', 8181),

View file

@ -378,7 +378,7 @@ def split_string(mystring, splitvar=','):
def create_https_certificates(ssl_cert, ssl_key): def create_https_certificates(ssl_cert, ssl_key):
""" """
Create a pair of self-signed HTTPS certificares and store in them in Create a self-signed HTTPS certificate and store in it in
'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed. 'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard). This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
@ -387,24 +387,24 @@ def create_https_certificates(ssl_cert, ssl_key):
from plexpy import logger from plexpy import logger
from OpenSSL import crypto from OpenSSL import crypto
from certgen import createKeyPair, createCertRequest, createCertificate, \ from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
TYPE_RSA, serial
# Create the CA Certificate serial = int(time.time())
cakey = createKeyPair(TYPE_RSA, 2048) domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
careq = createCertRequest(cakey, CN="Certificate Authority") ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years altNames = ','.join(domains + ips)
# Create the self-signed PlexPy certificate
logger.debug(u"Generating self-signed SSL certificate.")
pkey = createKeyPair(TYPE_RSA, 2048) pkey = createKeyPair(TYPE_RSA, 2048)
req = createCertRequest(pkey, CN="PlexPy") cert = createSelfSignedCertificate(("PlexPy", pkey), serial, (0, 60 * 60 * 24 * 365 * 10), altNames) # ten years
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
# Save the key and certificate to disk # Save the key and certificate to disk
try: try:
with open(ssl_key, "w") as fp:
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(ssl_cert, "w") as fp: with open(ssl_cert, "w") as fp:
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
with open(ssl_key, "w") as fp:
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
except IOError as e: except IOError as e:
logger.error("Error creating SSL key and certificate: %s", e) logger.error("Error creating SSL key and certificate: %s", e)
return False return False

View file

@ -14,7 +14,7 @@
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users, libraries from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users, libraries
from plexpy.helpers import checked, addtoapi, get_ip from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from mako import exceptions from mako import exceptions
@ -1112,8 +1112,11 @@ class WebInterface(object):
"http_password": http_password, "http_password": http_password,
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER), "launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
"enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS), "enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS),
"https_create_cert": checked(plexpy.CONFIG.HTTPS_CREATE_CERT),
"https_cert": plexpy.CONFIG.HTTPS_CERT, "https_cert": plexpy.CONFIG.HTTPS_CERT,
"https_key": plexpy.CONFIG.HTTPS_KEY, "https_key": plexpy.CONFIG.HTTPS_KEY,
"https_domain": plexpy.CONFIG.HTTPS_DOMAIN,
"https_ip": plexpy.CONFIG.HTTPS_IP,
"anon_redirect": plexpy.CONFIG.ANON_REDIRECT, "anon_redirect": plexpy.CONFIG.ANON_REDIRECT,
"api_enabled": checked(plexpy.CONFIG.API_ENABLED), "api_enabled": checked(plexpy.CONFIG.API_ENABLED),
"api_key": plexpy.CONFIG.API_KEY, "api_key": plexpy.CONFIG.API_KEY,
@ -1208,7 +1211,7 @@ class WebInterface(object):
# Handle the variable config options. Note - keys with False values aren't getting passed # Handle the variable config options. Note - keys with False values aren't getting passed
checked_configs = [ checked_configs = [
"launch_browser", "enable_https", "api_enabled", "freeze_db", "check_github", "get_file_sizes", "launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
"grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl", "grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl",
"movie_notify_enable", "tv_notify_enable", "music_notify_enable", "monitoring_use_websocket", "movie_notify_enable", "tv_notify_enable", "music_notify_enable", "monitoring_use_websocket",
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", "tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
@ -1217,7 +1220,7 @@ class WebInterface(object):
"refresh_libraries_on_startup", "refresh_users_on_startup", "refresh_libraries_on_startup", "refresh_users_on_startup",
"ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable", "ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable",
"pms_is_remote", "home_stats_type", "group_history_tables", "notify_consecutive", "pms_is_remote", "home_stats_type", "group_history_tables", "notify_consecutive",
"notify_recently_added", "notify_recently_added_grandparent", "monitor_remote_access" "notify_recently_added", "notify_recently_added_grandparent", "monitor_remote_access", "get_file_sizes"
] ]
for checked_config in checked_configs: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@ -1236,6 +1239,7 @@ class WebInterface(object):
# Check if we should refresh our data # Check if we should refresh our data
server_changed = False server_changed = False
https_changed = False
refresh_libraries = False refresh_libraries = False
refresh_users = False refresh_users = False
reschedule = False reschedule = False
@ -1268,6 +1272,14 @@ class WebInterface(object):
(kwargs['pms_is_remote'] != plexpy.CONFIG.PMS_IS_REMOTE): (kwargs['pms_is_remote'] != plexpy.CONFIG.PMS_IS_REMOTE):
server_changed = True server_changed = True
# If we change the HTTPS setting, make sure we generate a new certificate.
if kwargs['https_create_cert']:
if 'https_domain' in kwargs and (kwargs['https_domain'] != plexpy.CONFIG.HTTPS_DOMAIN) or \
'https_ip' in kwargs and (kwargs['https_ip'] != plexpy.CONFIG.HTTPS_IP) or \
'https_cert' in kwargs and (kwargs['https_cert'] != plexpy.CONFIG.HTTPS_CERT) or \
'https_key' in kwargs and (kwargs['https_key'] != plexpy.CONFIG.HTTPS_KEY):
https_changed = True
# Remove config with 'hscard-' prefix and change home_stats_cards to list # Remove config with 'hscard-' prefix and change home_stats_cards to list
if 'home_stats_cards' in kwargs: if 'home_stats_cards' in kwargs:
for k in kwargs.keys(): for k in kwargs.keys():
@ -1299,14 +1311,15 @@ class WebInterface(object):
# Write the config # Write the config
plexpy.CONFIG.write() plexpy.CONFIG.write()
# Get new server URLs for SSL communications. # Get new server URLs for SSL communications and get new server friendly name
if server_changed: if server_changed:
plextv.get_real_pms_url() plextv.get_real_pms_url()
# Get new server friendly name.
if server_changed:
pmsconnect.get_server_friendly_name() pmsconnect.get_server_friendly_name()
# Generate a new HTTPS certificate
if https_changed:
create_https_certificates(plexpy.CONFIG.HTTPS_CERT, plexpy.CONFIG.HTTPS_KEY)
# Reconfigure scheduler if intervals changed # Reconfigure scheduler if intervals changed
if reschedule: if reschedule:
plexpy.initialize_scheduler() plexpy.initialize_scheduler()

View file

@ -32,8 +32,7 @@ def initialize(options):
https_key = options['https_key'] https_key = options['https_key']
if enable_https: if enable_https:
# If either the HTTPS certificate or key do not exist, try to make # If either the HTTPS certificate or key do not exist, try to make self-signed ones.
# self-signed ones.
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)): if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
if not create_https_certificates(https_cert, https_key): if not create_https_certificates(https_cert, https_key):
logger.warn("Unable to create certificate and key. Disabling HTTPS") logger.warn("Unable to create certificate and key. Disabling HTTPS")