Allow passing settings via environment variables (Issue #100)

This commit is contained in:
Daniel Perna 2018-06-28 23:38:24 +02:00
parent ec470ebb4a
commit c24654418b
4 changed files with 60 additions and 33 deletions

View file

@ -36,6 +36,7 @@ There are no dependencies on Python modules that are not part of the standard li
### Configuration
Near the top of the py-file you will find some global variables you can change to customize the configurator a little bit. If you are unfamiliar with Python: when setting variables of the type _string_, you have to write that within quotation marks. The default settings are fine for just checking this out quickly. With more customized setups you will have to change some settings though.
To keep your setting across updates it is also possible to save settings in an external file. In that case copy [settings.conf](https://github.com/danielperna84/hass-configurator/blob/master/settings.conf) whereever you like and append the full path to the file to the command when starting the configurator. E.g. `sudo .configurator.py /home/homeassistant/.homeassistant/mysettings.conf`. This file is in JSON format. So make sure it has a valid syntax (you can set the editor to JSON to get syntax highlighting for the settings). The major difference to the settings in the py-file is, that `None` becomes `null`.
Another way of passing settings (except those defined as lists like e.g. `ALLOWED_NETWORKS`) is by using [environment variables](https://en.wikipedia.org/wiki/Environment_variable). All settings passed via environment variables will overwrite the settings you have set in the `settings.conf` file. This allows you to provide settings in you systemd service file or the way it is usually done with Docker. The names of the environment variables have to be named exactly like the regular ones, prepended with the prefix `HC_`. You can customize this prefix in the `settings.conf` by setting `ENV_PREFIX` to something you like. `ENV_PREFIX` can not be set via environment variable.
#### LISTENIP (string)
The IP address the service is listening on. By default it is binding to `0.0.0.0`, which is every IPv4 interface on the system. When using `::`, all available IPv6- and IPv4-addresses will be used.
@ -72,6 +73,8 @@ If set to `true`, directories will be displayed at the top.
If set to _somesecretkeynobodycanguess_, you can browse to `https://your.configurator:3218/somesecretkeynobodycanguess` from any IP, and it will be removed from the `BANNED_IPS` list (in case it has been banned before) and added to the `ALLOWED_NETWORKS` list. Once the request has been processed you will automatically be redirected to the configurator. Think of this as dynamically allowing access from untrusted IPs by providing a secret key (_open sesame!_). Keep in mind, that once the IP has been added, you will either have to restart the configurator or manually remove the IP through the _Network status_ to revoke access.
#### VERIFY_HOSTNAME (string)
HTTP requests include the hostname to which the request has been made. To improve security you can set this parameter to `yourdomain.example.com`. This will check if the hostname within the request matches the one you are expecting. If it does not match, a `403 Forbidden` response will be sent. As a result attackers that scan your IP address won't be able to connect unless they know the correct hostname. Be careful with this option though, because it prohibits you from accessing the configurator directly via IP.
#### ENV_PREFIX (string)
To modify the default prefix for settings passed as environment variables (`HC_`) change this setting to another value that meets your demands.
__Note regarding `ALLOWED_NETWORKS`, `BANNED_IPS` and `BANLIMIT`__:
The way this is implemented works in the following order:

View file

@ -1,4 +1,5 @@
Version 0.3.0 (2018-)
- Allow passing settings (except lists) via environment variables (Issue #100) @danielperna84
Version 0.2.9 (2018-06-22)
- Material Icons and HASS-help now open in new tab instead of modal (Issues #85 and #34) @danielperna84

View file

@ -66,6 +66,8 @@ SESAME = None
# Verify the hostname used in the request. Block access if it doesn't match
# this value
VERIFY_HOSTNAME = None
# Prefix for environment variables
ENV_PREFIX = "HC_"
### End of options
LOGLEVEL = logging.INFO
@ -3352,11 +3354,30 @@ def signal_handler(sig, frame):
def load_settings(settingsfile):
global LISTENIP, LISTENPORT, BASEPATH, SSL_CERTIFICATE, SSL_KEY, HASS_API, \
HASS_API_PASSWORD, CREDENTIALS, ALLOWED_NETWORKS, BANNED_IPS, BANLIMIT, \
DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME, ENFORCE_BASEPATH
DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME, ENFORCE_BASEPATH, \
ENV_PREFIX
settings = {}
if settingsfile:
try:
if os.path.isfile(settingsfile):
with open(settingsfile) as fptr:
settings = json.loads(fptr.read())
except Exception as err:
LOG.warning(err)
LOG.warning("Not loading settings from file")
ENV_PREFIX = settings.get('ENV_PREFIX', ENV_PREFIX)
for key, value in os.environ.items():
if key.startswith(ENV_PREFIX):
# Convert booleans
if value in ['true', 'false', 'True', 'False']:
value = True if value in ['true', 'True'] else False
# Convert None / null
elif value in ['none', 'None' 'null']:
value = None
# Convert plain numbers
elif value.isnumeric():
value = int(value)
settings[key[len(ENV_PREFIX):]] = value
LISTENIP = settings.get("LISTENIP", LISTENIP)
LISTENPORT = settings.get("LISTENPORT", LISTENPORT)
BASEPATH = settings.get("BASEPATH", BASEPATH)
@ -3374,15 +3395,14 @@ def load_settings(settingsfile):
DIRSFIRST = settings.get("DIRSFIRST", DIRSFIRST)
SESAME = settings.get("SESAME", SESAME)
VERIFY_HOSTNAME = settings.get("VERIFY_HOSTNAME", VERIFY_HOSTNAME)
except Exception as err:
LOG.warning(err)
LOG.warning("Not loading static settings")
return False
def is_safe_path(basedir, path, follow_symlinks=True):
if basedir is None:
return True
if follow_symlinks:
return os.path.realpath(path).startswith(basedir)
return os.path.abspath(path).startswith(basedir)
return os.path.realpath(path).startswith(basedir.encode('utf-8'))
return os.path.abspath(path).startswith(basedir.encode('utf-8'))
def get_dircontent(path, repo=None):
dircontent = []
@ -3520,7 +3540,7 @@ class RequestHandler(BaseHTTPRequestHandler):
try:
if filename:
filename = unquote(filename[0]).encode('utf-8')
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), filename):
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH, filename):
raise OSError('Access denied.')
if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)):
with open(os.path.join(BASEDIR.encode('utf-8'), filename)) as fptr:
@ -3538,7 +3558,7 @@ class RequestHandler(BaseHTTPRequestHandler):
try:
if filename:
filename = unquote(filename[0]).encode('utf-8')
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), filename):
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH, filename):
raise OSError('Access denied.')
LOG.info(filename)
if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)):
@ -3565,7 +3585,7 @@ class RequestHandler(BaseHTTPRequestHandler):
if dirpath:
dirpath = unquote(dirpath[0]).encode('utf-8')
if os.path.isdir(dirpath):
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), dirpath):
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH, dirpath):
raise OSError('Access denied.')
repo = None
activebranch = None
@ -4396,6 +4416,8 @@ def main(args):
global HTTPD, CREDENTIALS
if args:
load_settings(args[0])
else:
load_settings(None)
LOG.info("Starting server")
CustomServer = SimpleServer
if ':' in LISTENIP:

View file

@ -14,5 +14,6 @@
"IGNORE_PATTERN": [],
"DIRSFIRST": false,
"SESAME": null,
"VERIFY_HOSTNAME": null
"VERIFY_HOSTNAME": null,
"ENV_PREFIX": "HC_"
}