summaryrefslogtreecommitdiffhomepage
path: root/libs/apprise/config
diff options
context:
space:
mode:
Diffstat (limited to 'libs/apprise/config')
-rw-r--r--libs/apprise/config/ConfigBase.py284
-rw-r--r--libs/apprise/config/ConfigBase.pyi3
-rw-r--r--libs/apprise/config/ConfigFile.py4
-rw-r--r--libs/apprise/config/ConfigHTTP.py4
4 files changed, 255 insertions, 40 deletions
diff --git a/libs/apprise/config/ConfigBase.py b/libs/apprise/config/ConfigBase.py
index 22efd8e29..f2b958ed8 100644
--- a/libs/apprise/config/ConfigBase.py
+++ b/libs/apprise/config/ConfigBase.py
@@ -34,13 +34,18 @@ from ..AppriseAsset import AppriseAsset
from ..URLBase import URLBase
from ..common import ConfigFormat
from ..common import CONFIG_FORMATS
-from ..common import ConfigIncludeMode
+from ..common import ContentIncludeMode
from ..utils import GET_SCHEMA_RE
from ..utils import parse_list
from ..utils import parse_bool
from ..utils import parse_urls
+from ..utils import cwe312_url
from . import SCHEMA_MAP
+# Test whether token is valid or not
+VALID_TOKEN = re.compile(
+ r'(?P<token>[a-z0-9][a-z0-9_]+)', re.I)
+
class ConfigBase(URLBase):
"""
@@ -65,7 +70,7 @@ class ConfigBase(URLBase):
# By default all configuration is not includable using the 'include'
# line found in configuration files.
- allow_cross_includes = ConfigIncludeMode.NEVER
+ allow_cross_includes = ContentIncludeMode.NEVER
# the config path manages the handling of relative include
config_path = os.getcwd()
@@ -205,8 +210,8 @@ class ConfigBase(URLBase):
# Configuration files were detected; recursively populate them
# If we have been configured to do so
for url in configs:
- if self.recursion > 0:
+ if self.recursion > 0:
# Attempt to acquire the schema at the very least to allow
# our configuration based urls.
schema = GET_SCHEMA_RE.match(url)
@@ -219,6 +224,7 @@ class ConfigBase(URLBase):
url = os.path.join(self.config_path, url)
url = '{}://{}'.format(schema, URLBase.quote(url))
+
else:
# Ensure our schema is always in lower case
schema = schema.group('schema').lower()
@@ -229,27 +235,31 @@ class ConfigBase(URLBase):
'Unsupported include schema {}.'.format(schema))
continue
+ # CWE-312 (Secure Logging) Handling
+ loggable_url = url if not asset.secure_logging \
+ else cwe312_url(url)
+
# Parse our url details of the server object as dictionary
# containing all of the information parsed from our URL
results = SCHEMA_MAP[schema].parse_url(url)
if not results:
# Failed to parse the server URL
self.logger.warning(
- 'Unparseable include URL {}'.format(url))
+ 'Unparseable include URL {}'.format(loggable_url))
continue
# Handle cross inclusion based on allow_cross_includes rules
if (SCHEMA_MAP[schema].allow_cross_includes ==
- ConfigIncludeMode.STRICT
+ ContentIncludeMode.STRICT
and schema not in self.schemas()
and not self.insecure_includes) or \
SCHEMA_MAP[schema].allow_cross_includes == \
- ConfigIncludeMode.NEVER:
+ ContentIncludeMode.NEVER:
# Prevent the loading if insecure base protocols
ConfigBase.logger.warning(
'Including {}:// based configuration is prohibited. '
- 'Ignoring URL {}'.format(schema, url))
+ 'Ignoring URL {}'.format(schema, loggable_url))
continue
# Prepare our Asset Object
@@ -275,7 +285,7 @@ class ConfigBase(URLBase):
except Exception as e:
# the arguments are invalid or can not be used.
self.logger.warning(
- 'Could not load include URL: {}'.format(url))
+ 'Could not load include URL: {}'.format(loggable_url))
self.logger.debug('Loading Exception: {}'.format(str(e)))
continue
@@ -288,16 +298,23 @@ class ConfigBase(URLBase):
del cfg_plugin
else:
+ # CWE-312 (Secure Logging) Handling
+ loggable_url = url if not asset.secure_logging \
+ else cwe312_url(url)
+
self.logger.debug(
- 'Recursion limit reached; ignoring Include URL: %s' % url)
+ 'Recursion limit reached; ignoring Include URL: %s',
+ loggable_url)
if self._cached_servers:
- self.logger.info('Loaded {} entries from {}'.format(
- len(self._cached_servers), self.url()))
+ self.logger.info(
+ 'Loaded {} entries from {}'.format(
+ len(self._cached_servers),
+ self.url(privacy=asset.secure_logging)))
else:
self.logger.warning(
'Failed to load Apprise configuration from {}'.format(
- self.url()))
+ self.url(privacy=asset.secure_logging)))
# Set the time our content was cached at
self._cached_time = time.time()
@@ -527,6 +544,9 @@ class ConfigBase(URLBase):
# the include keyword
configs = list()
+ # Prepare our Asset Object
+ asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
+
# Define what a valid line should look like
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
@@ -563,27 +583,37 @@ class ConfigBase(URLBase):
continue
if config:
- ConfigBase.logger.debug('Include URL: {}'.format(config))
+ # CWE-312 (Secure Logging) Handling
+ loggable_url = config if not asset.secure_logging \
+ else cwe312_url(config)
+
+ ConfigBase.logger.debug(
+ 'Include URL: {}'.format(loggable_url))
# Store our include line
configs.append(config.strip())
continue
+ # CWE-312 (Secure Logging) Handling
+ loggable_url = url if not asset.secure_logging \
+ else cwe312_url(url)
+
# Acquire our url tokens
- results = plugins.url_to_dict(url)
+ results = plugins.url_to_dict(
+ url, secure_logging=asset.secure_logging)
if results is None:
# Failed to parse the server URL
ConfigBase.logger.warning(
- 'Unparseable URL {} on line {}.'.format(url, line))
+ 'Unparseable URL {} on line {}.'.format(
+ loggable_url, line))
continue
# Build a list of tags to associate with the newly added
# notifications if any were set
results['tag'] = set(parse_list(result.group('tags')))
- # Prepare our Asset Object
- results['asset'] = \
- asset if isinstance(asset, AppriseAsset) else AppriseAsset()
+ # Set our Asset Object
+ results['asset'] = asset
try:
# Attempt to create an instance of our plugin using the
@@ -591,13 +621,14 @@ class ConfigBase(URLBase):
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
# Create log entry of loaded URL
- ConfigBase.logger.debug('Loaded URL: {}'.format(plugin.url()))
+ ConfigBase.logger.debug(
+ 'Loaded URL: %s', plugin.url(privacy=asset.secure_logging))
except Exception as e:
# the arguments are invalid or can not be used.
ConfigBase.logger.warning(
'Could not load URL {} on line {}.'.format(
- url, line))
+ loggable_url, line))
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue
@@ -633,7 +664,9 @@ class ConfigBase(URLBase):
# Load our data (safely)
result = yaml.load(content, Loader=yaml.SafeLoader)
- except (AttributeError, yaml.error.MarkedYAMLError) as e:
+ except (AttributeError,
+ yaml.parser.ParserError,
+ yaml.error.MarkedYAMLError) as e:
# Invalid content
ConfigBase.logger.error(
'Invalid Apprise YAML data specified.')
@@ -671,7 +704,9 @@ class ConfigBase(URLBase):
continue
if not (hasattr(asset, k) and
- isinstance(getattr(asset, k), six.string_types)):
+ isinstance(getattr(asset, k),
+ (bool, six.string_types))):
+
# We can't set a function or non-string set value
ConfigBase.logger.warning(
'Invalid asset key "{}".'.format(k))
@@ -681,15 +716,23 @@ class ConfigBase(URLBase):
# Convert to an empty string
v = ''
- if not isinstance(v, six.string_types):
+ if (isinstance(v, (bool, six.string_types))
+ and isinstance(getattr(asset, k), bool)):
+
+ # If the object in the Asset is a boolean, then
+ # we want to convert the specified string to
+ # match that.
+ setattr(asset, k, parse_bool(v))
+
+ elif isinstance(v, six.string_types):
+ # Set our asset object with the new value
+ setattr(asset, k, v.strip())
+
+ else:
# we must set strings with a string
ConfigBase.logger.warning(
'Invalid asset value to "{}".'.format(k))
continue
-
- # Set our asset object with the new value
- setattr(asset, k, v.strip())
-
#
# global tag root directive
#
@@ -740,6 +783,10 @@ class ConfigBase(URLBase):
# we can. Reset it to None on each iteration
results = list()
+ # CWE-312 (Secure Logging) Handling
+ loggable_url = url if not asset.secure_logging \
+ else cwe312_url(url)
+
if isinstance(url, six.string_types):
# We're just a simple URL string...
schema = GET_SCHEMA_RE.match(url)
@@ -748,16 +795,18 @@ class ConfigBase(URLBase):
# config file at least has something to take action
# with.
ConfigBase.logger.warning(
- 'Invalid URL {}, entry #{}'.format(url, no + 1))
+ 'Invalid URL {}, entry #{}'.format(
+ loggable_url, no + 1))
continue
# We found a valid schema worthy of tracking; store it's
# details:
- _results = plugins.url_to_dict(url)
+ _results = plugins.url_to_dict(
+ url, secure_logging=asset.secure_logging)
if _results is None:
ConfigBase.logger.warning(
'Unparseable URL {}, entry #{}'.format(
- url, no + 1))
+ loggable_url, no + 1))
continue
# add our results to our global set
@@ -791,19 +840,20 @@ class ConfigBase(URLBase):
.format(key, no + 1))
continue
- # Store our URL and Schema Regex
- _url = key
-
# Store our schema
schema = _schema.group('schema').lower()
+ # Store our URL and Schema Regex
+ _url = key
+
if _url is None:
# the loop above failed to match anything
ConfigBase.logger.warning(
- 'Unsupported schema in urls, entry #{}'.format(no + 1))
+ 'Unsupported URL, entry #{}'.format(no + 1))
continue
- _results = plugins.url_to_dict(_url)
+ _results = plugins.url_to_dict(
+ _url, secure_logging=asset.secure_logging)
if _results is None:
# Setup dictionary
_results = {
@@ -830,12 +880,33 @@ class ConfigBase(URLBase):
if 'schema' in entries:
del entries['schema']
+ # support our special tokens (if they're present)
+ if schema in plugins.SCHEMA_MAP:
+ entries = ConfigBase._special_token_handler(
+ schema, entries)
+
# Extend our dictionary with our new entries
r.update(entries)
# add our results to our global set
results.append(r)
+ elif isinstance(tokens, dict):
+ # support our special tokens (if they're present)
+ if schema in plugins.SCHEMA_MAP:
+ tokens = ConfigBase._special_token_handler(
+ schema, tokens)
+
+ # Copy ourselves a template of our parsed URL as a base to
+ # work with
+ r = _results.copy()
+
+ # add our result set
+ r.update(tokens)
+
+ # add our results to our global set
+ results.append(r)
+
else:
# add our results to our global set
results.append(_results)
@@ -867,6 +938,17 @@ class ConfigBase(URLBase):
# Just use the global settings
_results['tag'] = global_tags
+ for key in list(_results.keys()):
+ # Strip out any tokens we know that we can't accept and
+ # warn the user
+ match = VALID_TOKEN.match(key)
+ if not match:
+ ConfigBase.logger.warning(
+ 'Ignoring invalid token ({}) found in YAML '
+ 'configuration entry #{}, item #{}'
+ .format(key, no + 1, entry))
+ del _results[key]
+
ConfigBase.logger.trace(
'URL #{}: {} unpacked as:{}{}'
.format(no + 1, url, os.linesep, os.linesep.join(
@@ -883,7 +965,8 @@ class ConfigBase(URLBase):
# Create log entry of loaded URL
ConfigBase.logger.debug(
- 'Loaded URL: {}'.format(plugin.url()))
+ 'Loaded URL: {}'.format(
+ plugin.url(privacy=asset.secure_logging)))
except Exception as e:
# the arguments are invalid or can not be used.
@@ -913,6 +996,135 @@ class ConfigBase(URLBase):
# Pop the element off of the stack
return self._cached_servers.pop(index)
+ @staticmethod
+ def _special_token_handler(schema, tokens):
+ """
+ This function takes a list of tokens and updates them to no longer
+ include any special tokens such as +,-, and :
+
+ - schema must be a valid schema of a supported plugin type
+ - tokens must be a dictionary containing the yaml entries parsed.
+
+ The idea here is we can post process a set of tokens provided in
+ a YAML file where the user provided some of the special keywords.
+
+ We effectivley look up what these keywords map to their appropriate
+ value they're expected
+ """
+ # Create a copy of our dictionary
+ tokens = tokens.copy()
+
+ for kw, meta in plugins.SCHEMA_MAP[schema]\
+ .template_kwargs.items():
+
+ # Determine our prefix:
+ prefix = meta.get('prefix', '+')
+
+ # Detect any matches
+ matches = \
+ {k[1:]: str(v) for k, v in tokens.items()
+ if k.startswith(prefix)}
+
+ if not matches:
+ # we're done with this entry
+ continue
+
+ if not isinstance(tokens.get(kw), dict):
+ # Invalid; correct it
+ tokens[kw] = dict()
+
+ # strip out processed tokens
+ tokens = {k: v for k, v in tokens.items()
+ if not k.startswith(prefix)}
+
+ # Update our entries
+ tokens[kw].update(matches)
+
+ # Now map our tokens accordingly to the class templates defined by
+ # each service.
+ #
+ # This is specifically used for YAML file parsing. It allows a user to
+ # define an entry such as:
+ #
+ # urls:
+ # - mailto://user:pass@domain:
+ #
+ # Under the hood, the NotifyEmail() class does not parse the `to`
+ # argument. It's contents needs to be mapped to `targets`. This is
+ # defined in the class via the `template_args` and template_tokens`
+ # section.
+ #
+ # This function here allows these mappings to take place within the
+ # YAML file as independant arguments.
+ class_templates = \
+ plugins.details(plugins.SCHEMA_MAP[schema])
+
+ for key in list(tokens.keys()):
+
+ if key not in class_templates['args']:
+ # No need to handle non-arg entries
+ continue
+
+ # get our `map_to` and/or 'alias_of' value (if it exists)
+ map_to = class_templates['args'][key].get(
+ 'alias_of', class_templates['args'][key].get('map_to', ''))
+
+ if map_to == key:
+ # We're already good as we are now
+ continue
+
+ if map_to in class_templates['tokens']:
+ meta = class_templates['tokens'][map_to]
+
+ else:
+ meta = class_templates['args'].get(
+ map_to, class_templates['args'][key])
+
+ # Perform a translation/mapping if our code reaches here
+ value = tokens[key]
+ del tokens[key]
+
+ # Detect if we're dealign with a list or not
+ is_list = re.search(
+ r'^(list|choice):.*',
+ meta.get('type'),
+ re.IGNORECASE)
+
+ if map_to not in tokens:
+ tokens[map_to] = [] if is_list \
+ else meta.get('default')
+
+ elif is_list and not isinstance(tokens.get(map_to), list):
+ # Convert ourselves to a list if we aren't already
+ tokens[map_to] = [tokens[map_to]]
+
+ # Type Conversion
+ if re.search(
+ r'^(choice:)?string',
+ meta.get('type'),
+ re.IGNORECASE) \
+ and not isinstance(value, six.string_types):
+
+ # Ensure our format is as expected
+ value = str(value)
+
+ # Apply any further translations if required (absolute map)
+ # This is the case when an arg maps to a token which further
+ # maps to a different function arg on the class constructor
+ abs_map = meta.get('map_to', map_to)
+
+ # Set our token as how it was provided by the configuration
+ if isinstance(tokens.get(map_to), list):
+ tokens[abs_map].append(value)
+
+ else:
+ tokens[abs_map] = value
+
+ # Return our tokens
+ return tokens
+
def __getitem__(self, index):
"""
Returns the indexed server entry associated with the loaded
diff --git a/libs/apprise/config/ConfigBase.pyi b/libs/apprise/config/ConfigBase.pyi
new file mode 100644
index 000000000..abff1204d
--- /dev/null
+++ b/libs/apprise/config/ConfigBase.pyi
@@ -0,0 +1,3 @@
+from .. import URLBase
+
+class ConfigBase(URLBase): ... \ No newline at end of file
diff --git a/libs/apprise/config/ConfigFile.py b/libs/apprise/config/ConfigFile.py
index 9f8102253..6fd1ecb23 100644
--- a/libs/apprise/config/ConfigFile.py
+++ b/libs/apprise/config/ConfigFile.py
@@ -28,7 +28,7 @@ import io
import os
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
-from ..common import ConfigIncludeMode
+from ..common import ContentIncludeMode
from ..AppriseLocale import gettext_lazy as _
@@ -44,7 +44,7 @@ class ConfigFile(ConfigBase):
protocol = 'file'
# Configuration file inclusion can only be of the same type
- allow_cross_includes = ConfigIncludeMode.STRICT
+ allow_cross_includes = ContentIncludeMode.STRICT
def __init__(self, path, **kwargs):
"""
diff --git a/libs/apprise/config/ConfigHTTP.py b/libs/apprise/config/ConfigHTTP.py
index c4ad29425..88352733c 100644
--- a/libs/apprise/config/ConfigHTTP.py
+++ b/libs/apprise/config/ConfigHTTP.py
@@ -28,7 +28,7 @@ import six
import requests
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
-from ..common import ConfigIncludeMode
+from ..common import ContentIncludeMode
from ..URLBase import PrivacyMode
from ..AppriseLocale import gettext_lazy as _
@@ -66,7 +66,7 @@ class ConfigHTTP(ConfigBase):
max_error_buffer_size = 2048
# Configuration file inclusion can always include this type
- allow_cross_includes = ConfigIncludeMode.ALWAYS
+ allow_cross_includes = ContentIncludeMode.ALWAYS
def __init__(self, headers=None, **kwargs):
"""