From 9d780701f556c659cca7ebaeb83201742e2fb4ab Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sat, 13 Feb 2016 16:08:43 -0800 Subject: [PATCH] Create self-signed HTTPS certificates --- data/interfaces/default/settings.html | 39 ++++++++++++++++-- lib/certgen.py | 58 +++++++++++++++++++-------- plexpy/config.py | 3 ++ plexpy/helpers.py | 22 +++++----- plexpy/webserve.py | 27 +++++++++---- plexpy/webstart.py | 3 +- 6 files changed, 112 insertions(+), 40 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index d302590c..72484d83 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -338,16 +338,49 @@ scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]

Enable HTTPS for web server for encrypted communication.

+
+ +

Check to have PlexPy create a self-signed SSL certificate. Uncheck if you want to use your own certificate.

+
+
+ +
+
+ +
+
+

The domain names used to access PlexPy, separated by commas (,).

+
+
+ +
+
+ +
+
+

The IP addresses used to access PlexPy, separated by commas (,).

+
- +
+
+ +
+
+

The location of the SSL certificate.

- +
+
+ +
+
+

The location of the SSL key.

-

diff --git a/lib/certgen.py b/lib/certgen.py index 1b941161..8b60412b 100644 --- a/lib/certgen.py +++ b/lib/certgen.py @@ -1,26 +1,21 @@ # -*- coding: latin-1 -*- # -# Copyright (C) Martin Sjögren and AB Strakt 2001, All rights reserved -# Copyright (C) Jean-Paul Calderone 2008, All rights reserved -# This file is licenced under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 or later (aka LGPL v2.1) -# Please see LGPL2.1.txt for more information +# Copyright (C) AB Strakt +# Copyright (C) Jean-Paul Calderone +# See LICENSE for details. + """ Certificate generation module. """ from OpenSSL import crypto -import time TYPE_RSA = crypto.TYPE_RSA TYPE_DSA = crypto.TYPE_DSA -serial = int(time.time()) - - def createKeyPair(type, bits): """ Create a public/private key pair. - Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA bits - Number of bits to use in the key Returns: The public/private key pair in a PKey object @@ -29,12 +24,11 @@ def createKeyPair(type, bits): pkey.generate_key(type, bits) return pkey -def createCertRequest(pkey, digest="md5", **name): +def createCertRequest(pkey, digest="sha256", **name): """ Create a certificate 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 arguments are: C - Country name @@ -49,18 +43,17 @@ def createCertRequest(pkey, digest="md5", **name): req = crypto.X509Req() subj = req.get_subject() - for (key,value) in name.items(): + for key, value in name.items(): setattr(subj, key, value) req.set_pubkey(pkey) req.sign(pkey, digest) 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. - - Arguments: req - Certificate reqeust to use + Arguments: req - Certificate request to use issuerCert - The certificate of the issuer issuerKey - The private key of the issuer serial - Serial number for the certificate @@ -68,9 +61,11 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter starts being valid notAfter - Timestamp (relative to now) when the certificate 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 """ + issuerCert, issuerKey = issuerCertKey + notBefore, notAfter = validityPeriod cert = crypto.X509() cert.set_serial_number(serial) cert.gmtime_adj_notBefore(notBefore) @@ -80,3 +75,32 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter cert.set_pubkey(req.get_pubkey()) cert.sign(issuerKey, digest) 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 diff --git a/plexpy/config.py b/plexpy/config.py index d5d3a3a5..d76d264a 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -131,8 +131,11 @@ _CONFIG_DEFINITIONS = { 'HOME_STATS_COUNT': (int, 'General', 5), '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']), + 'HTTPS_CREATE_CERT': (int, 'General', 1), 'HTTPS_CERT': (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_PASSWORD': (str, 'General', ''), 'HTTP_PORT': (int, 'General', 8181), diff --git a/plexpy/helpers.py b/plexpy/helpers.py index f77fa798..c360638c 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -378,7 +378,7 @@ def split_string(mystring, splitvar=','): 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. 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 OpenSSL import crypto - from certgen import createKeyPair, createCertRequest, createCertificate, \ - TYPE_RSA, serial + from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA - # Create the CA Certificate - cakey = createKeyPair(TYPE_RSA, 2048) - careq = createCertRequest(cakey, CN="Certificate Authority") - cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years + serial = int(time.time()) + domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d] + ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d] + altNames = ','.join(domains + ips) + # Create the self-signed PlexPy certificate + logger.debug(u"Generating self-signed SSL certificate.") pkey = createKeyPair(TYPE_RSA, 2048) - req = createCertRequest(pkey, CN="PlexPy") - cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years + cert = createSelfSignedCertificate(("PlexPy", pkey), serial, (0, 60 * 60 * 24 * 365 * 10), altNames) # ten years # Save the key and certificate to disk try: - with open(ssl_key, "w") as fp: - fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) with open(ssl_cert, "w") as fp: 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: logger.error("Error creating SSL key and certificate: %s", e) return False diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 0248ddd7..da788a15 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -14,7 +14,7 @@ # along with PlexPy. If not, see . 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 import exceptions @@ -1112,8 +1112,11 @@ class WebInterface(object): "http_password": http_password, "launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER), "enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS), + "https_create_cert": checked(plexpy.CONFIG.HTTPS_CREATE_CERT), "https_cert": plexpy.CONFIG.HTTPS_CERT, "https_key": plexpy.CONFIG.HTTPS_KEY, + "https_domain": plexpy.CONFIG.HTTPS_DOMAIN, + "https_ip": plexpy.CONFIG.HTTPS_IP, "anon_redirect": plexpy.CONFIG.ANON_REDIRECT, "api_enabled": checked(plexpy.CONFIG.API_ENABLED), "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 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", "movie_notify_enable", "tv_notify_enable", "music_notify_enable", "monitoring_use_websocket", "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", "ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable", "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: if checked_config not in kwargs: @@ -1236,6 +1239,7 @@ class WebInterface(object): # Check if we should refresh our data server_changed = False + https_changed = False refresh_libraries = False refresh_users = False reschedule = False @@ -1268,6 +1272,14 @@ class WebInterface(object): (kwargs['pms_is_remote'] != plexpy.CONFIG.PMS_IS_REMOTE): 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 if 'home_stats_cards' in kwargs: for k in kwargs.keys(): @@ -1299,14 +1311,15 @@ class WebInterface(object): # Write the config 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: plextv.get_real_pms_url() - - # Get new server friendly name. - if server_changed: 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 if reschedule: plexpy.initialize_scheduler() diff --git a/plexpy/webstart.py b/plexpy/webstart.py index 0c15282c..a2e0d655 100644 --- a/plexpy/webstart.py +++ b/plexpy/webstart.py @@ -32,8 +32,7 @@ def initialize(options): https_key = options['https_key'] if enable_https: - # If either the HTTPS certificate or key do not exist, try to make - # self-signed ones. + # If either the HTTPS certificate or key do not exist, try to make 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 create_https_certificates(https_cert, https_key): logger.warn("Unable to create certificate and key. Disabling HTTPS")