From 594bd62378b525de2d1551f156ef3804ddfe394f Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sat, 3 Apr 2021 12:05:11 -0400 Subject: calcurse-caldav: improve config file error handling The previous implementation allowed sections and keys other than those used by the script which led to a variety of bug reports due to typos in the configuration. Disallow entries other than those explicitly used and make both section and key names case-sensitive (previously, only section names where case-sensitive). Check that Hostname and Path are set before using them. Addresses GitHub issues #327 and #350. Signed-off-by: Lukas Fleischer --- contrib/caldav/calcurse-caldav.py | 191 ++++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 91 deletions(-) diff --git a/contrib/caldav/calcurse-caldav.py b/contrib/caldav/calcurse-caldav.py index 3644028..2fbec37 100755 --- a/contrib/caldav/calcurse-caldav.py +++ b/contrib/caldav/calcurse-caldav.py @@ -22,6 +22,70 @@ except ModuleNotFoundError: pass +class Config: + _map = {} + + def __init__(self, fn): + self._map = { + 'Auth': { + 'Password': None, + 'Username': None, + }, + 'CustomHeaders': {}, + 'General': { + 'AuthMethod': 'basic', + 'Binary': 'calcurse', + 'Debug': False, + 'DryRun': True, + 'HTTPS': True, + 'Hostname': None, + 'InsecureSSL': False, + 'Path': None, + 'SyncFilter': 'cal,todo', + 'Verbose': False, + }, + 'OAuth2': { + 'ClientID': None, + 'ClientSecret': None, + 'RedirectURI': 'http://127.0.0.1', + 'Scope': None, + }, + } + + config = configparser.RawConfigParser() + config.optionxform = str + if verbose: + print('Loading configuration from ' + configfn + '...') + try: + config.read_file(open(fn)) + except FileNotFoundError as e: + die('Configuration file not found: {}'.format(fn)) + + for sec in config.sections(): + if sec not in self._map: + die('Unexpected config section: {}'.format(sec)) + + if not self._map[sec]: + # Import section with custom key-value pairs. + self._map[sec] = dict(config.items(sec)) + continue + + # Import section with predefined keys. + for key, val in config.items(sec): + if key not in self._map[sec]: + die('Unexpected config key in section {}: {}'.format(sec, key)) + if type(self._map[sec][key]) == bool: + self._map[sec][key] = config.getboolean(sec, key) + else: + self._map[sec][key] = val + + def section(self, section): + return self._map[section] + + def get(self, section, key): + return self._map[section][key] + + def msgfmt(msg, prefix=''): lines = [] for line in msg.splitlines(): @@ -592,101 +656,46 @@ debug_raw = args.debug_raw password = os.getenv('CALCURSE_CALDAV_PASSWORD') # Read configuration. -config = configparser.RawConfigParser() -if verbose: - print('Loading configuration from ' + configfn + '...') -try: - config.read_file(open(configfn)) -except FileNotFoundError as e: - die('Configuration file not found: {}'.format(configfn)) - -if config.has_option('General', 'InsecureSSL'): - insecure_ssl = config.getboolean('General', 'InsecureSSL') -else: - insecure_ssl = False - -# Read config for "HTTPS" option (default=True) -if config.has_option('General', 'HTTPS'): - https = config.getboolean('General', 'HTTPS') -else: - https = True - -if config.has_option('General', 'Binary'): - calcurse = [config.get('General', 'Binary')] -else: - calcurse = ['calcurse'] - +config = Config(configfn) + +authmethod = config.get('General', 'AuthMethod').lower() +calcurse = [config.get('General', 'Binary')] +debug = debug or config.get('General', 'Debug') +dry_run = config.get('General', 'DryRun') +hostname = config.get('General', 'Hostname') +https = config.get('General', 'HTTPS') +insecure_ssl = config.get('General', 'InsecureSSL') +path = config.get('General', 'Path') +sync_filter = config.get('General', 'SyncFilter') +verbose = verbose or config.get('General', 'Verbose') + +password = password or config.get('Auth', 'Password') +username = config.get('Auth', 'Username') + +client_id = config.get('OAuth2', 'ClientID') +client_secret = config.get('OAuth2', 'ClientSecret') +redirect_uri = config.get('OAuth2', 'RedirectURI') +scope = config.get('OAuth2', 'Scope') + +custom_headers = config.section('CustomHeaders') + +# Append data directory to calcurse command. if datadir: check_dir(datadir) calcurse += ['-D', datadir] -if config.has_option('General', 'DryRun'): - dry_run = config.getboolean('General', 'DryRun') -else: - dry_run = True - -if not verbose and config.has_option('General', 'Verbose'): - verbose = config.getboolean('General', 'Verbose') - -if not debug and config.has_option('General', 'Debug'): - debug = config.getboolean('General', 'Debug') - -if config.has_option('General', 'AuthMethod'): - authmethod = config.get('General', 'AuthMethod').lower() -else: - authmethod = 'basic' - -if config.has_option('General', 'SyncFilter'): - sync_filter = config.get('General', 'SyncFilter') - - invalid_filter_values = validate_sync_filter() - - if len(invalid_filter_values): - die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values)) -else: - sync_filter = 'cal,todo' - -if config.has_option('Auth', 'UserName'): - username = config.get('Auth', 'UserName') -else: - username = None - -if config.has_option('Auth', 'Password') and not password: - password = config.get('Auth', 'Password') - -if config.has_section('CustomHeaders'): - custom_headers = dict(config.items('CustomHeaders')) -else: - custom_headers = {} - -if config.has_option('OAuth2', 'ClientID'): - client_id = config.get('OAuth2', 'ClientID') -else: - client_id = None - -if config.has_option('OAuth2', 'ClientSecret'): - client_secret = config.get('OAuth2', 'ClientSecret') -else: - client_secret = None - -if config.has_option('OAuth2', 'Scope'): - scope = config.get('OAuth2', 'Scope') -else: - scope = None - -if config.has_option('OAuth2', 'RedirectURI'): - redirect_uri = config.get('OAuth2', 'RedirectURI') -else: - redirect_uri = 'http://127.0.0.1' - -# Change URl prefix according to HTTP/HTTPS -if https: - urlprefix = "https://" -else: - urlprefix = "http://" - -hostname = config.get('General', 'HostName') -path = '/' + config.get('General', 'Path').strip('/') + '/' +# Validate sync filter. +invalid_filter_values = validate_sync_filter() +if len(invalid_filter_values): + die('Invalid value(s) in SyncFilter option: ' + ', '.join(invalid_filter_values)) + +# Ensure host name and path are defined and initialize *_uri. +if not hostname: + die('Hostname missing in configuration.') +if not path: + die('Path missing in configuration.') +urlprefix = "https://" if https else "http://" +path = '/{}/'.format(path.strip('/')) hostname_uri = urlprefix + hostname absolute_uri = hostname_uri + path -- cgit v1.2.3-54-g00ecf