You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
8.2 KiB
313 lines
8.2 KiB
#
|
|
# Copyright 2025 Splunk Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
"""
|
|
Converters for Splunk configuration.
|
|
"""
|
|
|
|
|
|
import base64
|
|
import json
|
|
|
|
__all__ = [
|
|
"Converter",
|
|
"Normaliser",
|
|
"ChainOf",
|
|
"UserDefined",
|
|
"Unifier",
|
|
"Boolean",
|
|
"Lower",
|
|
"Upper",
|
|
"Mapping",
|
|
"Base64",
|
|
"JSON",
|
|
]
|
|
|
|
|
|
class Converter:
|
|
"""
|
|
Converting data: encode for in-coming request
|
|
and decode for out-coming response.
|
|
"""
|
|
|
|
def encode(self, value, request):
|
|
"""
|
|
Encode data from client for request.
|
|
|
|
:param value: value to encode for request
|
|
:param request: whole request data
|
|
:return:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def decode(self, value, response):
|
|
"""
|
|
Decode data from storage for response.
|
|
|
|
:param value: value to decode for response
|
|
:param response: whole response data
|
|
:return:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class Normaliser(Converter):
|
|
"""
|
|
Normalizing data: same converting logic for encode & decode.
|
|
"""
|
|
|
|
def normalize(self, value, data):
|
|
"""
|
|
Normalize a given value.
|
|
|
|
:param value: value to normalize
|
|
:param data: whole payload
|
|
:returns: normalized value.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def encode(self, value, request):
|
|
return self.normalize(value, request)
|
|
|
|
def decode(self, value, response):
|
|
return self.normalize(value, response)
|
|
|
|
|
|
class ChainOf(Converter):
|
|
"""
|
|
A composite of converters that will covert data with specified
|
|
converters on by one, and returns result from the last converter.
|
|
"""
|
|
|
|
def __init__(self, *converters):
|
|
"""
|
|
|
|
:param converters: a list of converters
|
|
"""
|
|
super().__init__()
|
|
self._converters = converters
|
|
|
|
def encode(self, value, request):
|
|
for converter in self._converters:
|
|
value = converter.encode(value, request)
|
|
return value
|
|
|
|
def decode(self, value, response):
|
|
import copy
|
|
|
|
converters = copy.copy(self._converters)
|
|
converters.reverse()
|
|
for converter in converters:
|
|
value = converter.decode(value, response)
|
|
return value
|
|
|
|
|
|
class UserDefined(Converter):
|
|
"""
|
|
User-defined normaliser.
|
|
|
|
The user-defined normaliser function should be in form:
|
|
``def fun(value, *args, **kwargs): ...``
|
|
|
|
Usage::
|
|
>>> def my_encoder(value, request, args):
|
|
>>> if request == args:
|
|
>>> return value
|
|
>>> else:
|
|
>>> return value
|
|
>>> my_converter = UserDefined(my_encoder, 'test_val')
|
|
>>> my_converter.encode('value', {'key': 'value'}, 'value1')
|
|
|
|
"""
|
|
|
|
def __init__(self, encoder, decoder=None, *args, **kwargs):
|
|
"""
|
|
|
|
:param encoder: user-defined function for encoding
|
|
:param decoder: user-defined function for decoding.
|
|
If None, it is the same to encoder.
|
|
:param args:
|
|
:param kwargs:
|
|
"""
|
|
super().__init__()
|
|
self._encoder = encoder
|
|
self._decoder = decoder or self._encoder
|
|
self._args = args
|
|
self._kwargs = kwargs
|
|
|
|
def encode(self, value, request):
|
|
return self._encoder(value, request, *self._args, **self._kwargs)
|
|
|
|
def decode(self, value, response):
|
|
return self._decoder(value, response, *self._args, **self._kwargs)
|
|
|
|
|
|
class Lower(Normaliser):
|
|
"""
|
|
Normalize a string to all lower cases.
|
|
"""
|
|
|
|
def normalize(self, value, data):
|
|
return value.strip().lower()
|
|
|
|
|
|
class Upper(Normaliser):
|
|
"""
|
|
Normalize a string to all upper cases.
|
|
"""
|
|
|
|
def normalize(self, value, data):
|
|
return value.strip().upper()
|
|
|
|
|
|
class Unifier(Normaliser):
|
|
"""
|
|
Many-to-one map for normalizing request & response.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
value_map,
|
|
default=None,
|
|
case_sensitive=False,
|
|
):
|
|
"""
|
|
|
|
:param value_map:
|
|
{"<unified value>": "<original value list>"}
|
|
:param default: default value for input not in specific list
|
|
:param case_sensitive: if it is False,
|
|
it will return lower case
|
|
"""
|
|
super().__init__()
|
|
self._case_sensitive = case_sensitive
|
|
self._default = default
|
|
self._value_map = {}
|
|
for val_new, val_old_list in value_map.items():
|
|
for val_old in val_old_list:
|
|
val_old = val_old if case_sensitive else val_old.lower()
|
|
assert val_old not in self._value_map, (
|
|
'Normaliser "Unifier" only supports Many-to-one mapping: %s'
|
|
% val_old
|
|
)
|
|
self._value_map[val_old] = val_new
|
|
|
|
def normalize(self, value, data):
|
|
need_lower = not self._case_sensitive and isinstance(value, str)
|
|
val_old = value.lower() if need_lower else value
|
|
val_default = self._default or value
|
|
return self._value_map.get(val_old, val_default)
|
|
|
|
|
|
class Boolean(Unifier):
|
|
"""
|
|
Normalize a boolean field.
|
|
|
|
Normalize given value to boolean: 0 or 1
|
|
(for False and True respectively).
|
|
If the given value is not-a-string or unrecognizable,
|
|
it returns default value.
|
|
"""
|
|
|
|
VALUES_TRUE = {"true", "t", "1", "yes", "y"}
|
|
VALUES_FALSE = {"false", "f", "0", "no", "n"}
|
|
|
|
def __init__(self, default=True):
|
|
"""
|
|
|
|
:param default: default for unrecognizable input of boolean.
|
|
"""
|
|
super().__init__(
|
|
value_map={
|
|
"1": Boolean.VALUES_TRUE,
|
|
"0": Boolean.VALUES_FALSE,
|
|
},
|
|
default="1" if default else "0",
|
|
case_sensitive=False,
|
|
)
|
|
|
|
|
|
class Mapping(Converter):
|
|
"""
|
|
One-to-one map between interface value and storage value.
|
|
If value is not in specific mapping,
|
|
it will return the original value.
|
|
"""
|
|
|
|
def __init__(self, value_map, case_sensitive=False):
|
|
"""
|
|
|
|
:param value_map: {"<interface value>": "<storage value>"}
|
|
:param case_sensitive: if it is False,
|
|
it will return lower case
|
|
"""
|
|
super().__init__()
|
|
self._case_sensitive = case_sensitive
|
|
self._map_interface, self._map_storage = {}, {}
|
|
for interface, storage in value_map.items():
|
|
self._check_and_set(interface, storage)
|
|
|
|
def _check_and_set(self, interface, storage):
|
|
if not self._case_sensitive:
|
|
interface = interface.lower()
|
|
storage = storage.lower()
|
|
assert interface not in self._map_interface, (
|
|
'Converter "Mapping" only supports one-to-one mapping: "%s"' % interface
|
|
)
|
|
assert storage not in self._map_storage, (
|
|
'Converter "Mapping" only supports one-to-one mapping: "%s"' % storage
|
|
)
|
|
self._map_interface[interface] = storage
|
|
self._map_storage[storage] = interface
|
|
|
|
def encode(self, value, request):
|
|
if self._case_sensitive:
|
|
interface = value
|
|
else:
|
|
interface = value.lower()
|
|
return self._map_interface.get(interface, value)
|
|
|
|
def decode(self, value, response):
|
|
if self._case_sensitive:
|
|
storage = value
|
|
else:
|
|
storage = value.lower()
|
|
return self._map_storage.get(storage, value)
|
|
|
|
|
|
class Base64(Converter):
|
|
"""
|
|
Covert input data to base64 string.
|
|
"""
|
|
|
|
def encode(self, value, request):
|
|
return base64.b64encode(value)
|
|
|
|
def decode(self, value, response):
|
|
return base64.b64decode(value)
|
|
|
|
|
|
class JSON(Converter):
|
|
"""
|
|
Converter between object and JSON string.
|
|
"""
|
|
|
|
def encode(self, value, request):
|
|
return json.dumps(value)
|
|
|
|
def decode(self, value, response):
|
|
return json.loads(value)
|