From a5c087465c500452d5a8b1cd29d02d6b7bba917d Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 24 Aug 2013 09:20:37 +0530 Subject: [PATCH 1/4] Added backend code for ssl support (init.py, webstart.py) --- Headphones.py | 3 +++ headphones/__init__.py | 19 +++++++++++++++++-- headphones/helpers.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Headphones.py b/Headphones.py index facb2191..1ced1fba 100755 --- a/Headphones.py +++ b/Headphones.py @@ -152,6 +152,9 @@ def main(): 'http_host': headphones.HTTP_HOST, 'http_root': headphones.HTTP_ROOT, 'http_proxy': headphones.HTTP_PROXY, + 'enable_https': headphones.ENABLE_HTTPS, + 'https_cert': headphones.HTTPS_CERT, + 'https_key': headphones.HTTPS_KEY, 'http_username': headphones.HTTP_USERNAME, 'http_password': headphones.HTTP_PASSWORD, }) diff --git a/headphones/__init__.py b/headphones/__init__.py index 2e1416c8..23485334 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -73,6 +73,10 @@ HTTP_ROOT = None HTTP_PROXY = False LAUNCH_BROWSER = False +ENABLE_HTTPS = False +HTTPS_CERT = None +HTTPS_KEY = None + API_ENABLED = False API_KEY = None @@ -304,7 +308,7 @@ def initialize(): PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, MIRRORLIST, \ MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ XBMC_NOTIFY, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ - PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK + PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY if __INITIALIZED__: return False @@ -345,6 +349,9 @@ def initialize(): HTTP_PASSWORD = check_setting_str(CFG, 'General', 'http_password', '') HTTP_ROOT = check_setting_str(CFG, 'General', 'http_root', '/') HTTP_PROXY = bool(check_setting_int(CFG, 'General', 'http_proxy', 0)) + ENABLE_HTTPS = bool(check_setting_int(CFG, 'General', 'enable_https', 0)) + HTTPS_CERT = check_setting_str(CFG, 'General', 'https_cert', 'server.crt') + HTTPS_KEY = check_setting_str(CFG, 'General', 'https_key', 'server.key') LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) API_ENABLED = bool(check_setting_int(CFG, 'General', 'api_enabled', 0)) API_KEY = check_setting_str(CFG, 'General', 'api_key', '') @@ -679,9 +686,14 @@ def launch_browser(host, port, root): if host == '0.0.0.0': host = 'localhost' + + if ENABLE_HTTPS: + protocol = 'https' + else: + protocol = 'http' try: - webbrowser.open('http://%s:%i%s' % (host, port, root)) + webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root)) except Exception, e: logger.error('Could not launch browser: %s' % e) @@ -698,6 +710,9 @@ def config_write(): new_config['General']['http_password'] = HTTP_PASSWORD new_config['General']['http_root'] = HTTP_ROOT new_config['General']['http_proxy'] = int(HTTP_PROXY) + new_config['General']['enable_https'] = int(ENABLE_HTTPS) + new_config['General']['https_cert'] = HTTPS_CERT + new_config['General']['https_key'] = HTTPS_KEY new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) new_config['General']['api_enabled'] = int(API_ENABLED) new_config['General']['api_key'] = API_KEY diff --git a/headphones/helpers.py b/headphones/helpers.py index 19c98b7e..97b7abbf 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -345,3 +345,35 @@ def split_string(mystring): for each_word in mystring.split(','): mylist.append(each_word.strip()) return mylist + +def create_https_certificates(ssl_cert, ssl_key): + """ + Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard): + 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: + logger.warn(u"pyopenssl module missing, please install for https access") + return False + + # Create the CA Certificate + cakey = createKeyPair(TYPE_RSA, 1024) + careq = createCertRequest(cakey, CN='Certificate Authority') + cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years + + cname = 'Headphones' + pkey = createKeyPair(TYPE_RSA, 1024) + req = createCertRequest(pkey, CN=cname) + 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: + logger.error(u"Error creating SSL key and certificate") + return False + + return True From 482cb68bd67c32990661bafb57e317bf5b13dc63 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 24 Aug 2013 09:51:58 +0530 Subject: [PATCH 2/4] Fixed up some bugs/bad imports, added lib/certgen --- Headphones.py | 4 +-- headphones/__init__.py | 4 +-- headphones/helpers.py | 2 ++ headphones/webstart.py | 33 +++++++++++++++-- lib/certgen.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 lib/certgen.py diff --git a/Headphones.py b/Headphones.py index 1ced1fba..19cc82dd 100755 --- a/Headphones.py +++ b/Headphones.py @@ -142,7 +142,7 @@ def main(): # Force the http port if neccessary if args.port: http_port = args.port - logger.info('Starting Headphones on forced port: %i' % http_port) + logger.info('Using forced port: %i' % http_port) else: http_port = int(headphones.HTTP_PORT) @@ -159,8 +159,6 @@ def main(): 'http_password': headphones.HTTP_PASSWORD, }) - logger.info('Starting Headphones on port: %i' % http_port) - if headphones.LAUNCH_BROWSER and not args.nolaunch: headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT) diff --git a/headphones/__init__.py b/headphones/__init__.py index 23485334..648382a8 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -350,8 +350,8 @@ def initialize(): HTTP_ROOT = check_setting_str(CFG, 'General', 'http_root', '/') HTTP_PROXY = bool(check_setting_int(CFG, 'General', 'http_proxy', 0)) ENABLE_HTTPS = bool(check_setting_int(CFG, 'General', 'enable_https', 0)) - HTTPS_CERT = check_setting_str(CFG, 'General', 'https_cert', 'server.crt') - HTTPS_KEY = check_setting_str(CFG, 'General', 'https_key', 'server.key') + HTTPS_CERT = check_setting_str(CFG, 'General', 'https_cert', os.path.join(DATA_DIR, 'server.crt')) + HTTPS_KEY = check_setting_str(CFG, 'General', 'https_key', os.path.join(DATA_DIR, 'server.key')) LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) API_ENABLED = bool(check_setting_int(CFG, 'General', 'api_enabled', 0)) API_KEY = check_setting_str(CFG, 'General', 'api_key', '') diff --git a/headphones/helpers.py b/headphones/helpers.py index 97b7abbf..79ac9b85 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -351,6 +351,8 @@ def create_https_certificates(ssl_cert, ssl_key): Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard): Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' """ + from headphones import logger + try: from OpenSSL import crypto #@UnresolvedImport from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial #@UnresolvedImport diff --git a/headphones/webstart.py b/headphones/webstart.py index b67c801f..a30d3694 100644 --- a/headphones/webstart.py +++ b/headphones/webstart.py @@ -20,12 +20,31 @@ import cherrypy import headphones +from headphones import logger from headphones.webserve import WebInterface +from headphones.helpers import create_https_certificates def initialize(options={}): + #HTTPS stuff stolen from sickbeard + enable_https = options['enable_https'] + https_cert = options['https_cert'] + https_key = options['https_key'] - cherrypy.config.update({ + if enable_https: + # If either the HTTPS certificate or key do not exist, make some 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(u"Unable to create cert/key files, disabling HTTPS") + headphones.ENABLE_HTTPS = False + enable_https = False + + if not (os.path.exists(https_cert) and os.path.exists(https_key)): + logger.warn(u"Disabled HTTPS because of missing CERT and KEY files") + headphones.ENABLE_HTTPS = False + enable_https = False + + options_dict = { 'log.screen': False, 'server.thread_pool': 10, 'server.socket_port': options['http_port'], @@ -34,7 +53,17 @@ def initialize(options={}): 'tools.encode.on' : True, 'tools.encode.encoding' : 'utf-8', 'tools.decode.on' : True, - }) + } + + if enable_https: + options_dict['server.ssl_certificate'] = https_cert + options_dict['server.ssl_private_key'] = https_key + protocol = "https" + else: + protocol = "http" + + logger.info(u"Starting Headphones on " + protocol + "://" + str(options['http_host']) + ":" + str(options['http_port']) + "/") + cherrypy.config.update(options_dict) conf = { '/': { diff --git a/lib/certgen.py b/lib/certgen.py new file mode 100644 index 00000000..1b941161 --- /dev/null +++ b/lib/certgen.py @@ -0,0 +1,82 @@ +# -*- 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 +""" +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 + """ + pkey = crypto.PKey() + pkey.generate_key(type, bits) + return pkey + +def createCertRequest(pkey, digest="md5", **name): + """ + Create a certificate request. + + Arguments: pkey - The key to associate with the request + digest - Digestion method to use for signing, default is md5 + **name - The name of the subject of the request, possible + arguments are: + C - Country name + ST - State or province name + L - Locality name + O - Organization name + OU - Organizational unit name + CN - Common name + emailAddress - E-mail address + Returns: The certificate request in an X509Req object + """ + req = crypto.X509Req() + subj = req.get_subject() + + 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"): + """ + Generate a certificate given a certificate request. + + Arguments: req - Certificate reqeust to use + issuerCert - The certificate 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 + digest - Digest method to use for signing, default is md5 + Returns: The signed certificate in an X509 object + """ + cert = crypto.X509() + cert.set_serial_number(serial) + cert.gmtime_adj_notBefore(notBefore) + cert.gmtime_adj_notAfter(notAfter) + cert.set_issuer(issuerCert.get_subject()) + cert.set_subject(req.get_subject()) + cert.set_pubkey(req.get_pubkey()) + cert.sign(issuerKey, digest) + return cert From ea1d86b6b1f63f3b866376d6e149b51e9def0df5 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 24 Aug 2013 10:13:51 +0530 Subject: [PATCH 3/4] Added https options to web interface --- data/interfaces/default/config.html | 33 +++++++++++++++++++++++++++++ headphones/webserve.py | 9 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 879503ac..9ec49519 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -54,6 +54,19 @@
+
+ +
+
+
+ + +
+
+ + +
+
@@ -945,6 +958,25 @@ $('#api_key').val(data); }); }); + if ($("#enable_https").is(":checked")) + { + $("#https_options").show(); + } + else + { + $("#https_options").hide(); + } + + $("#enable_https").click(function(){ + if ($("#enable_https").is(":checked")) + { + $("#https_options").slideDown(); + } + else + { + $("#https_options").slideUp(); + } + }); if ($("#music_encoder").is(":checked")) { $("#encoderoptions").show(); @@ -1214,6 +1246,7 @@ initConfigCheckbox("#userutracker"); initConfigCheckbox("#usewhatcd"); initConfigCheckbox("#useapi"); + initConfigCheckbox("#enable_https"); } $(document).ready(function() { initThisPage(); diff --git a/headphones/webserve.py b/headphones/webserve.py index b77d8cf7..b8cdb94e 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -565,6 +565,9 @@ class WebInterface(object): "http_port" : headphones.HTTP_PORT, "http_pass" : headphones.HTTP_PASSWORD, "launch_browser" : checked(headphones.LAUNCH_BROWSER), + "enable_https" : checked(headphones.ENABLE_HTTPS), + "https_cert" : headphones.HTTPS_CERT, + "https_key" : headphones.HTTPS_KEY, "api_enabled" : checked(headphones.API_ENABLED), "api_key" : headphones.API_KEY, "download_scan_interval" : headphones.DOWNLOAD_SCAN_INTERVAL, @@ -726,13 +729,17 @@ class WebInterface(object): delete_lossless_files=0, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None, xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, synoindex_enabled=False, pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, mirror=None, customhost=None, customport=None, - customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, **kwargs): + customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, + enable_https=0, https_cert=None, https_key=None, **kwargs): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port headphones.HTTP_USERNAME = http_username headphones.HTTP_PASSWORD = http_password headphones.LAUNCH_BROWSER = launch_browser + headphones.ENABLE_HTTPS = enable_https + headphones.HTTPS_CERT = https_cert + headphones.HTTPS_KEY = https_key headphones.API_ENABLED = api_enabled headphones.API_KEY = api_key headphones.DOWNLOAD_SCAN_INTERVAL = download_scan_interval From 2e55b0eb5b083d3b9026ee6b0b2f05b20e0f0b25 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 24 Aug 2013 10:15:20 +0530 Subject: [PATCH 4/4] Modified gitignore to exclude cert & key files for https --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index fccbce9a..5ef2beed 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,11 @@ logs/* cache/* +# HTTPS Cert/Key # +################## +*.crt +*.key + # OS generated files # ###################### .DS_Store?