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..a087b2af 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -54,6 +54,19 @@
+
+ +
+
+
+ + +
+
+ + +
+
@@ -367,7 +380,7 @@
- +
@@ -498,6 +511,9 @@ Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year
+
+ +
@@ -945,6 +961,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 +1249,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..78803803 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 @@ -93,6 +97,7 @@ DESTINATION_DIR = None LOSSLESS_DESTINATION_DIR = None FOLDER_FORMAT = None FILE_FORMAT = None +FILE_UNDERSCORES = False PATH_TO_XML = None PREFERRED_QUALITY = None PREFERRED_BITRATE = None @@ -291,7 +296,7 @@ def initialize(): HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, \ CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \ LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, \ - RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, INCLUDE_EXTRAS, EXTRAS, AUTOWANT_UPCOMING, AUTOWANT_ALL, KEEP_TORRENT_FILES, \ + RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, FILE_UNDERSCORES, CLEANUP_FILES, INCLUDE_EXTRAS, EXTRAS, AUTOWANT_UPCOMING, AUTOWANT_ALL, KEEP_TORRENT_FILES, \ ADD_ALBUM_ART, ALBUM_ART_FORMAT, EMBED_ALBUM_ART, EMBED_LYRICS, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, SEARCH_INTERVAL, \ TORRENTBLACKHOLE_DIR, NUMBEROFSEEDERS, ISOHUNT, KAT, PIRATEBAY, MININOVA, WAFFLES, WAFFLES_UID, WAFFLES_PASSKEY, \ RUTRACKER, RUTRACKER_USER, RUTRACKER_PASSWORD, WHATCD, WHATCD_USERNAME, WHATCD_PASSWORD, DOWNLOAD_TORRENT_DIR, \ @@ -304,7 +309,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 +350,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', '') @@ -373,6 +381,7 @@ def initialize(): RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0)) FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'Artist/Album [Year]') FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'Track Artist - Album [Year] - Title') + FILE_UNDERSCORES = bool(check_setting_int(CFG, 'General', 'file_underscores', 0)) CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0)) ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0)) ALBUM_ART_FORMAT = check_setting_str(CFG, 'General', 'album_art_format', 'folder') @@ -679,9 +688,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 +712,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 @@ -726,6 +743,7 @@ def config_write(): new_config['General']['rename_files'] = int(RENAME_FILES) new_config['General']['folder_format'] = FOLDER_FORMAT new_config['General']['file_format'] = FILE_FORMAT + new_config['General']['file_underscores'] = int(FILE_UNDERSCORES) new_config['General']['cleanup_files'] = int(CLEANUP_FILES) new_config['General']['add_album_art'] = int(ADD_ALBUM_ART) new_config['General']['album_art_format'] = ALBUM_ART_FORMAT 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/music_encoder.py b/headphones/music_encoder.py index 3ae6216b..2c54974c 100644 --- a/headphones/music_encoder.py +++ b/headphones/music_encoder.py @@ -224,9 +224,9 @@ def command(encoder,musicSource,musicDest,albumPath): opts = [] if headphones.ADVANCEDENCODER =='': if headphones.ENCODEROUTPUTFORMAT=='ogg': - opts.extend(['-acodec libvorbis']) + opts.extend(['-acodec', 'libvorbis']) if headphones.ENCODEROUTPUTFORMAT=='m4a': - opts.extend(['-strict experimental']) + opts.extend(['-strict', 'experimental']) if headphones.ENCODERVBRCBR=='cbr': opts.extend(['-ar', str(headphones.SAMPLINGFREQUENCY), '-ab', str(headphones.BITRATE) + 'k']) elif headphones.ENCODERVBRCBR=='vbr': diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index da1f4624..132ec931 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -43,28 +43,31 @@ def checkFolder(): if album['FolderName']: - # We're now checking sab config options after sending to determine renaming - but we'll keep the - # iterations in just in case we can't read the config for some reason + if album['Kind'] == 'nzb': + # We're now checking sab config options after sending to determine renaming - but we'll keep the + # iterations in just in case we can't read the config for some reason - nzb_album_possibilities = [ album['FolderName'], - sab_replace_dots(album['FolderName']), - sab_replace_spaces(album['FolderName']), - sab_replace_spaces(sab_replace_dots(album['FolderName'])) + nzb_album_possibilities = [ album['FolderName'], + sab_replace_dots(album['FolderName']), + sab_replace_spaces(album['FolderName']), + sab_replace_spaces(sab_replace_dots(album['FolderName'])) ] - - torrent_album_path = os.path.join(headphones.DOWNLOAD_TORRENT_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace') - - for nzb_folder_name in nzb_album_possibilities: - nzb_album_path = os.path.join(headphones.DOWNLOAD_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace') + for nzb_folder_name in nzb_album_possibilities: + + nzb_album_path = os.path.join(headphones.DOWNLOAD_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace') + + if os.path.exists(nzb_album_path): + logger.debug('Found %s in NZB download folder. Verifying....' % album['FolderName']) + verify(album['AlbumID'], nzb_album_path, 'nzb') + + if album['Kind'] == 'torrent': - if os.path.exists(nzb_album_path): - logger.debug('Found %s in NZB download folder. Verifying....' % album['FolderName']) - verify(album['AlbumID'], nzb_album_path, album['Kind']) - - if os.path.exists(torrent_album_path): - logger.debug('Found %s in torrent download folder. Verifying....' % album['FolderName']) - verify(album['AlbumID'], torrent_album_path, album['Kind']) + torrent_album_path = os.path.join(headphones.DOWNLOAD_TORRENT_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace') + + if os.path.exists(torrent_album_path): + logger.debug('Found %s in torrent download folder. Verifying....' % album['FolderName']) + verify(album['AlbumID'], torrent_album_path, 'torrent') def verify(albumid, albumpath, Kind=None, forced=False): @@ -325,7 +328,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) # Check to see if we're preserving the torrent dir if headphones.KEEP_TORRENT_FILES and Kind=="torrent": - new_folder = os.path.join(albumpath, 'headphones-modified').encode(headphones.SYS_ENCODING, 'replace') + new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace')) logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding") try: shutil.copytree(albumpath, new_folder) @@ -464,6 +467,9 @@ def addAlbumArt(artwork, albumpath, release): album_art_name = album_art_name.replace('?','_').replace(':', '_').encode(headphones.SYS_ENCODING, 'replace') + if headphones.FILE_UNDERSCORES: + album_art_name = album_art_name.replace(' ', '_') + if album_art_name.startswith('.'): album_art_name = album_art_name.replace(0, '_') @@ -493,6 +499,10 @@ def moveFiles(albumpath, release, tracks): artist = release['ArtistName'].replace('/', '_') album = release['AlbumTitle'].replace('/', '_') + if headphones.FILE_UNDERSCORES: + artist = artist.replace(' ', '_') + album = album.replace(' ', '_') + releasetype = release['Type'].replace('/', '_') if release['ArtistName'].startswith('The '): @@ -832,6 +842,9 @@ def renameFiles(albumpath, downloaded_track_list, release): new_file_name = new_file_name.replace('?','_').replace(':', '_').encode(headphones.SYS_ENCODING, 'replace') + if headphones.FILE_UNDERSCORES: + new_file_name = new_file_name.replace(' ', '_') + if new_file_name.startswith('.'): new_file_name = new_file_name.replace(0, '_') diff --git a/headphones/searcher.py b/headphones/searcher.py index 8e9c2869..3472499a 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1073,9 +1073,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): provider)) # Pirate Bay - if headphones.PIRATEBAY and headphones.TORRENT_DOWNLOADER == 0: - logger.warn("Cannot search Pirate Bay with Blackhole option set") - if headphones.PIRATEBAY and headphones.TORRENT_DOWNLOADER != 0: + if headphones.PIRATEBAY: provider = "The Pirate Bay" providerurl = url_fix("http://thepiratebay.sx/search/" + term + "/0/99/") if headphones.PREFERRED_QUALITY == 3 or losslessOnly: @@ -1117,9 +1115,15 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): title = ''.join(item.find("a", {"class" : "detLink"})) seeds = int(''.join(item.find("td", {"align" : "right"}))) url = item.findAll("a")[3]['href'] + if headphones.TORRENT_DOWNLOADER == 0: + tor_hash = re.findall("urn:btih:(.*?)&", url) + if len(tor_hash) > 0: + url = "http://torrage.com/torrent/"+str(tor_hash[0]).upper()+".torrent" + else: + url = None formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(u'\xa0', ' ') size = helpers.piratesize(formatted_size) - if size < maxsize and minimumseeders < seeds: + if size < maxsize and minimumseeders < seeds and url != None: resultlist.append((title, size, url, provider)) logger.info('Found %s. Size: %s' % (title, formatted_size)) else: @@ -1193,6 +1197,9 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): rightformat = False except Exception, e: rightformat = False + for findterm in term.split(" "): + if not findterm in title: + rightformat = False if rightformat == True and size < maxsize and minimumseeders < seeds: resultlist.append((title, size, url, provider)) logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) @@ -1427,6 +1434,11 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False): file_or_url = bestqual[2] torrentid = transmission.addTorrent(file_or_url) + + if not torrentid: + logger.error("Error sending torrent to Transmission. Are you sure it's running?") + return + torrent_folder_name = transmission.getTorrentFolder(torrentid) logger.info('Torrent folder name: %s' % torrent_folder_name) @@ -1449,7 +1461,7 @@ def preprocesstorrent(resultlist, pre_sorted_list=False): for result in resultlist: # get outta here if rutracker or piratebay - if result[3] == 'rutracker.org' or result[3] == 'The Pirate Bay': + if result[3] == 'rutracker.org': return True, result try: diff --git a/headphones/transmission.py b/headphones/transmission.py index 033a63d2..9b31bd96 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -32,8 +32,10 @@ def addTorrent(link): arguments = {'filename': link, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR} response = torrentAction(method,arguments) - + if not response: + return False + if response['result'] == 'success': name = response['arguments']['torrent-added']['name'] logger.info(u"Torrent sent to Transmission successfully") diff --git a/headphones/webserve.py b/headphones/webserve.py index b77d8cf7..1807409f 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, @@ -647,6 +650,7 @@ class WebInterface(object): "lossless_dest_dir" : headphones.LOSSLESS_DESTINATION_DIR, "folder_format" : headphones.FOLDER_FORMAT, "file_format" : headphones.FILE_FORMAT, + "file_underscores" : checked(headphones.FILE_UNDERSCORES), "include_extras" : checked(headphones.INCLUDE_EXTRAS), "autowant_upcoming" : checked(headphones.AUTOWANT_UPCOMING), "autowant_all" : checked(headphones.AUTOWANT_ALL), @@ -720,19 +724,23 @@ class WebInterface(object): preferred_words=None, required_words=None, ignored_words=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, torrentblackhole_dir=None, download_torrent_dir=None, numberofseeders=None, use_piratebay=0, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None, whatcd=0, whatcd_username=None, whatcd_password=None, rutracker=0, rutracker_user=None, rutracker_password=None, rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, album_art_format=None, embed_album_art=0, embed_lyrics=0, - destination_dir=None, lossless_destination_dir=None, folder_format=None, file_format=None, include_extras=0, single=0, ep=0, compilation=0, soundtrack=0, live=0, + destination_dir=None, lossless_destination_dir=None, folder_format=None, file_format=None, file_underscores=0, include_extras=0, single=0, ep=0, compilation=0, soundtrack=0, live=0, remix=0, spokenword=0, audiobook=0, autowant_upcoming=False, autowant_all=False, keep_torrent_files=False, interface=None, log_dir=None, cache_dir=None, music_encoder=0, encoder=None, xldprofile=None, bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, 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 @@ -806,6 +814,7 @@ class WebInterface(object): headphones.LOSSLESS_DESTINATION_DIR = lossless_destination_dir headphones.FOLDER_FORMAT = folder_format headphones.FILE_FORMAT = file_format + headphones.FILE_UNDERSCORES = file_underscores headphones.INCLUDE_EXTRAS = include_extras headphones.AUTOWANT_UPCOMING = autowant_upcoming headphones.AUTOWANT_ALL = autowant_all 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