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 <lfleischer@calcurse.org>
This commit is contained in:
Lukas Fleischer 2021-04-03 12:05:11 -04:00
parent 4777c60056
commit 594bd62378

View File

@ -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))
config = Config(configfn)
if config.has_option('General', 'InsecureSSL'):
insecure_ssl = config.getboolean('General', 'InsecureSSL')
else:
insecure_ssl = False
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')
# Read config for "HTTPS" option (default=True)
if config.has_option('General', 'HTTPS'):
https = config.getboolean('General', 'HTTPS')
else:
https = True
password = password or config.get('Auth', 'Password')
username = config.get('Auth', 'Username')
if config.has_option('General', 'Binary'):
calcurse = [config.get('General', 'Binary')]
else:
calcurse = ['calcurse']
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):
# 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))
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('/') + '/'
# 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