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?
diff --git a/Headphones.py b/Headphones.py
index facb2191..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)
@@ -152,12 +152,13 @@ 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,
})
- 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/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/__init__.py b/headphones/__init__.py
index 2e1416c8..648382a8 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', 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', '')
@@ -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..79ac9b85 100644
--- a/headphones/helpers.py
+++ b/headphones/helpers.py
@@ -345,3 +345,37 @@ 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'
+ """
+ from headphones import logger
+
+ 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
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
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
|