Merge branch 'fix_arbitrary_code_execution' into develop
This commit is contained in:
commit
a5bdf6c3ac
64
owrx/dsp.py
64
owrx/dsp.py
@ -4,16 +4,26 @@ from owrx.js8 import Js8Parser
|
|||||||
from owrx.aprs import AprsParser
|
from owrx.aprs import AprsParser
|
||||||
from owrx.pocsag import PocsagParser
|
from owrx.pocsag import PocsagParser
|
||||||
from owrx.source import SdrSource, SdrSourceEventClient
|
from owrx.source import SdrSource, SdrSourceEventClient
|
||||||
from owrx.property import PropertyStack, PropertyLayer
|
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
|
||||||
|
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
|
||||||
from owrx.modes import Modes
|
from owrx.modes import Modes
|
||||||
from csdr import csdr
|
from csdr import csdr
|
||||||
import threading
|
import threading
|
||||||
|
import re
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ModulationValidator(OrValidator):
|
||||||
|
"""
|
||||||
|
This validator only allows alphanumeric characters and numbers, but no spaces or special characters
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$")))
|
||||||
|
|
||||||
|
|
||||||
class DspManager(csdr.output, SdrSourceEventClient):
|
class DspManager(csdr.output, SdrSourceEventClient):
|
||||||
def __init__(self, handler, sdrSource):
|
def __init__(self, handler, sdrSource):
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
@ -27,22 +37,42 @@ class DspManager(csdr.output, SdrSourceEventClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.props = PropertyStack()
|
self.props = PropertyStack()
|
||||||
|
|
||||||
# local demodulator properties not forwarded to the sdr
|
# local demodulator properties not forwarded to the sdr
|
||||||
self.props.addLayer(
|
# ensure strict validation since these can be set from the client
|
||||||
0,
|
# and are used to build executable commands
|
||||||
PropertyLayer().filter(
|
validators = {
|
||||||
"output_rate",
|
"output_rate": "int",
|
||||||
"hd_output_rate",
|
"hd_output_rate": "int",
|
||||||
"squelch_level",
|
"squelch_level": "num",
|
||||||
"secondary_mod",
|
"secondary_mod": ModulationValidator(),
|
||||||
"low_cut",
|
"low_cut": "num",
|
||||||
"high_cut",
|
"high_cut": "num",
|
||||||
"offset_freq",
|
"offset_freq": "int",
|
||||||
"mod",
|
"mod": ModulationValidator(),
|
||||||
"secondary_offset_freq",
|
"secondary_offset_freq": "int",
|
||||||
"dmr_filter",
|
"dmr_filter": "int",
|
||||||
),
|
}
|
||||||
)
|
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
|
||||||
|
|
||||||
|
self.props.addLayer(0, self.localProps)
|
||||||
|
# ensure strict validation since these can be set from the client
|
||||||
|
# and are used to build executable commands
|
||||||
|
validators = {
|
||||||
|
"output_rate": "int",
|
||||||
|
"hd_output_rate": "int",
|
||||||
|
"squelch_level": "num",
|
||||||
|
"secondary_mod": ModulationValidator(),
|
||||||
|
"low_cut": "num",
|
||||||
|
"high_cut": "num",
|
||||||
|
"offset_freq": "int",
|
||||||
|
"mod": ModulationValidator(),
|
||||||
|
"secondary_offset_freq": "int",
|
||||||
|
"dmr_filter": "int",
|
||||||
|
}
|
||||||
|
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
|
||||||
|
|
||||||
|
self.props.addLayer(0, self.localProps)
|
||||||
# properties that we inherit from the sdr
|
# properties that we inherit from the sdr
|
||||||
self.props.addLayer(
|
self.props.addLayer(
|
||||||
1,
|
1,
|
||||||
@ -188,7 +218,7 @@ class DspManager(csdr.output, SdrSourceEventClient):
|
|||||||
self.setProperty(k, v)
|
self.setProperty(k, v)
|
||||||
|
|
||||||
def setProperty(self, prop, value):
|
def setProperty(self, prop, value):
|
||||||
self.props[prop] = value
|
self.localProps[prop] = value
|
||||||
|
|
||||||
def getClientClass(self):
|
def getClientClass(self):
|
||||||
return SdrSource.CLIENT_USER
|
return SdrSource.CLIENT_USER
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from owrx.property.validators import Validator
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -154,6 +155,50 @@ class PropertyFilter(PropertyManager):
|
|||||||
return [k for k in self.pm.keys() if k in self.props]
|
return [k for k in self.pm.keys() if k in self.props]
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyValidationError(Exception):
|
||||||
|
def __init__(self, key, value):
|
||||||
|
super().__init__('Invalid value for property "{key}": "{value}"'.format(key=key, value=str(value)))
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyValidator(PropertyManager):
|
||||||
|
def __init__(self, pm: PropertyManager, validators=None):
|
||||||
|
self.pm = pm
|
||||||
|
self.pm.wire(self._fireCallbacks)
|
||||||
|
if validators is None:
|
||||||
|
self.validators = {}
|
||||||
|
else:
|
||||||
|
self.validators = {k: Validator.of(v) for k, v in validators.items()}
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def validate(self, key, value):
|
||||||
|
if key not in self.validators:
|
||||||
|
return
|
||||||
|
if not self.validators[key].isValid(value):
|
||||||
|
raise PropertyValidationError(key, value)
|
||||||
|
|
||||||
|
def setValidator(self, key, validator):
|
||||||
|
self.validators[key] = Validator.of(validator)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self.pm.__getitem__(item)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.validate(key, value)
|
||||||
|
return self.pm.__setitem__(key, value)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return self.pm.__contains__(item)
|
||||||
|
|
||||||
|
def __dict__(self):
|
||||||
|
return self.pm.__dict__()
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
return self.pm.__delitem__(key)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self.pm.keys()
|
||||||
|
|
||||||
|
|
||||||
class PropertyStack(PropertyManager):
|
class PropertyStack(PropertyManager):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
98
owrx/property/validators.py
Normal file
98
owrx/property/validators.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from functools import reduce
|
||||||
|
from operator import or_
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Validator(ABC):
|
||||||
|
@staticmethod
|
||||||
|
def of(x):
|
||||||
|
if isinstance(x, Validator):
|
||||||
|
return x
|
||||||
|
if callable(x):
|
||||||
|
return LambdaValidator(x)
|
||||||
|
if x in validator_types:
|
||||||
|
return validator_types[x]()
|
||||||
|
raise ValidatorException("Cannot create validator")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def isValid(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LambdaValidator(Validator):
|
||||||
|
def __init__(self, c):
|
||||||
|
self.callable = c
|
||||||
|
|
||||||
|
def isValid(self, value):
|
||||||
|
return self.callable(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TypeValidator(Validator):
|
||||||
|
def __init__(self, type):
|
||||||
|
self.type = type
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def isValid(self, value):
|
||||||
|
return isinstance(value, self.type)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegerValidator(TypeValidator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(int)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatValidator(TypeValidator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(float)
|
||||||
|
|
||||||
|
|
||||||
|
class StringValidator(TypeValidator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(str)
|
||||||
|
|
||||||
|
|
||||||
|
class BoolValidator(TypeValidator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(bool)
|
||||||
|
|
||||||
|
|
||||||
|
class OrValidator(Validator):
|
||||||
|
def __init__(self, *validators):
|
||||||
|
self.validators = validators
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def isValid(self, value):
|
||||||
|
return reduce(
|
||||||
|
or_,
|
||||||
|
[v.isValid(value) for v in self.validators],
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NumberValidator(OrValidator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(IntegerValidator(), FloatValidator())
|
||||||
|
|
||||||
|
|
||||||
|
class RegexValidator(StringValidator):
|
||||||
|
def __init__(self, regex: re.Pattern):
|
||||||
|
self.regex = regex
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def isValid(self, value):
|
||||||
|
return super().isValid(value) and self.regex.match(value) is not None
|
||||||
|
|
||||||
|
|
||||||
|
validator_types = {
|
||||||
|
"string": StringValidator,
|
||||||
|
"str": StringValidator,
|
||||||
|
"integer": IntegerValidator,
|
||||||
|
"int": IntegerValidator,
|
||||||
|
"number": NumberValidator,
|
||||||
|
"num": NumberValidator,
|
||||||
|
}
|
@ -48,3 +48,11 @@ class PropertyFilterTest(TestCase):
|
|||||||
pf["testkey"] = "new value"
|
pf["testkey"] = "new value"
|
||||||
self.assertEqual(pm["testkey"], "new value")
|
self.assertEqual(pm["testkey"], "new value")
|
||||||
self.assertEqual(pf["testkey"], "new value")
|
self.assertEqual(pf["testkey"], "new value")
|
||||||
|
|
||||||
|
def testRejectsWrite(self):
|
||||||
|
pm = PropertyLayer()
|
||||||
|
pm["testkey"] = "old value"
|
||||||
|
pf = PropertyFilter(pm, "otherkey")
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
pf["testkey"] = "new value"
|
||||||
|
self.assertEqual(pm["testkey"], "old value")
|
||||||
|
37
test/property/test_property_validator.py
Normal file
37
test/property/test_property_validator.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property import PropertyLayer, PropertyValidator, PropertyValidationError
|
||||||
|
from owrx.property.validators import NumberValidator, StringValidator
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyValidatorTest(TestCase):
|
||||||
|
def testPassesUnvalidated(self):
|
||||||
|
pm = PropertyLayer()
|
||||||
|
pv = PropertyValidator(pm)
|
||||||
|
pv["testkey"] = "testvalue"
|
||||||
|
self.assertEqual(pv["testkey"], "testvalue")
|
||||||
|
self.assertEqual(pm["testkey"], "testvalue")
|
||||||
|
|
||||||
|
def testPassesValidValue(self):
|
||||||
|
pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()})
|
||||||
|
pv["testkey"] = 42
|
||||||
|
self.assertEqual(pv["testkey"], 42)
|
||||||
|
|
||||||
|
def testThrowsErrorOnInvalidValue(self):
|
||||||
|
pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()})
|
||||||
|
with self.assertRaises(PropertyValidationError):
|
||||||
|
pv["testkey"] = "text"
|
||||||
|
|
||||||
|
def testSetValidator(self):
|
||||||
|
pv = PropertyValidator(PropertyLayer())
|
||||||
|
pv.setValidator("testkey", NumberValidator())
|
||||||
|
with self.assertRaises(PropertyValidationError):
|
||||||
|
pv["testkey"] = "text"
|
||||||
|
|
||||||
|
def testUpdateValidator(self):
|
||||||
|
pv = PropertyValidator(PropertyLayer(), {"testkey": StringValidator()})
|
||||||
|
# this should pass
|
||||||
|
pv["testkey"] = "text"
|
||||||
|
pv.setValidator("testkey", NumberValidator())
|
||||||
|
# this should raise
|
||||||
|
with self.assertRaises(PropertyValidationError):
|
||||||
|
pv["testkey"] = "text"
|
0
test/property/validators/__init__.py
Normal file
0
test/property/validators/__init__.py
Normal file
17
test/property/validators/test_bool_validator.py
Normal file
17
test/property/validators/test_bool_validator.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import BoolValidator
|
||||||
|
|
||||||
|
|
||||||
|
class NumberValidatorTest(TestCase):
|
||||||
|
def testPassesNumbers(self):
|
||||||
|
validator = BoolValidator()
|
||||||
|
self.assertTrue(validator.isValid(True))
|
||||||
|
self.assertTrue(validator.isValid(False))
|
||||||
|
|
||||||
|
def testDoesntPassOthers(self):
|
||||||
|
validator = BoolValidator()
|
||||||
|
self.assertFalse(validator.isValid(123))
|
||||||
|
self.assertFalse(validator.isValid(-2))
|
||||||
|
self.assertFalse(validator.isValid(.5))
|
||||||
|
self.assertFalse(validator.isValid("text"))
|
||||||
|
self.assertFalse(validator.isValid(object()))
|
15
test/property/validators/test_float_validator.py
Normal file
15
test/property/validators/test_float_validator.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import FloatValidator
|
||||||
|
|
||||||
|
|
||||||
|
class FloatValidatorTest(TestCase):
|
||||||
|
def testPassesNumbers(self):
|
||||||
|
validator = FloatValidator()
|
||||||
|
self.assertTrue(validator.isValid(.5))
|
||||||
|
|
||||||
|
def testDoesntPassOthers(self):
|
||||||
|
validator = FloatValidator()
|
||||||
|
self.assertFalse(validator.isValid(123))
|
||||||
|
self.assertFalse(validator.isValid(-2))
|
||||||
|
self.assertFalse(validator.isValid("text"))
|
||||||
|
self.assertFalse(validator.isValid(object()))
|
15
test/property/validators/test_integer_validator.py
Normal file
15
test/property/validators/test_integer_validator.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import IntegerValidator
|
||||||
|
|
||||||
|
|
||||||
|
class IntegerValidatorTest(TestCase):
|
||||||
|
def testPassesIntegers(self):
|
||||||
|
validator = IntegerValidator()
|
||||||
|
self.assertTrue(validator.isValid(123))
|
||||||
|
self.assertTrue(validator.isValid(-2))
|
||||||
|
|
||||||
|
def testDoesntPassOthers(self):
|
||||||
|
validator = IntegerValidator()
|
||||||
|
self.assertFalse(validator.isValid(.5))
|
||||||
|
self.assertFalse(validator.isValid("text"))
|
||||||
|
self.assertFalse(validator.isValid(object()))
|
21
test/property/validators/test_lambda_validator.py
Normal file
21
test/property/validators/test_lambda_validator.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from owrx.property.validators import LambdaValidator
|
||||||
|
|
||||||
|
|
||||||
|
class LambdaValidatorTest(TestCase):
|
||||||
|
def testPassesValue(self):
|
||||||
|
mock = Mock()
|
||||||
|
validator = LambdaValidator(mock.method)
|
||||||
|
validator.isValid("test")
|
||||||
|
mock.method.assert_called_once_with("test")
|
||||||
|
|
||||||
|
def testReturnsTrue(self):
|
||||||
|
validator = LambdaValidator(lambda x: True)
|
||||||
|
self.assertTrue(validator.isValid("any value"))
|
||||||
|
self.assertTrue(validator.isValid(3.1415926))
|
||||||
|
|
||||||
|
def testReturnsFalse(self):
|
||||||
|
validator = LambdaValidator(lambda x: False)
|
||||||
|
self.assertFalse(validator.isValid("any value"))
|
||||||
|
self.assertFalse(validator.isValid(42))
|
18
test/property/validators/test_number_validator.py
Normal file
18
test/property/validators/test_number_validator.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import NumberValidator
|
||||||
|
|
||||||
|
|
||||||
|
class NumberValidatorTest(TestCase):
|
||||||
|
def testPassesNumbers(self):
|
||||||
|
validator = NumberValidator()
|
||||||
|
self.assertTrue(validator.isValid(123))
|
||||||
|
self.assertTrue(validator.isValid(-2))
|
||||||
|
self.assertTrue(validator.isValid(.5))
|
||||||
|
|
||||||
|
def testDoesntPassOthers(self):
|
||||||
|
validator = NumberValidator()
|
||||||
|
# bool is a subclass of int, so it passes this test.
|
||||||
|
# not sure if we need to be more specific or if this is alright.
|
||||||
|
# self.assertFalse(validator.isValid(True))
|
||||||
|
self.assertFalse(validator.isValid("text"))
|
||||||
|
self.assertFalse(validator.isValid(object()))
|
17
test/property/validators/test_or_validator.py
Normal file
17
test/property/validators/test_or_validator.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import OrValidator, IntegerValidator, StringValidator
|
||||||
|
|
||||||
|
|
||||||
|
class OrValidatorTest(TestCase):
|
||||||
|
def testPassesAnyValidators(self):
|
||||||
|
validator = OrValidator(IntegerValidator(), StringValidator())
|
||||||
|
self.assertTrue(validator.isValid(42))
|
||||||
|
self.assertTrue(validator.isValid("text"))
|
||||||
|
|
||||||
|
def testRejectsOtherTypes(self):
|
||||||
|
validator = OrValidator(IntegerValidator(), StringValidator())
|
||||||
|
self.assertFalse(validator.isValid(.5))
|
||||||
|
|
||||||
|
def testRejectsIfNoValidator(self):
|
||||||
|
validator = OrValidator()
|
||||||
|
self.assertFalse(validator.isValid("any value"))
|
17
test/property/validators/test_regex_validator.py
Normal file
17
test/property/validators/test_regex_validator.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import RegexValidator
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class RegexValidatorTest(TestCase):
|
||||||
|
def testMatchesRegex(self):
|
||||||
|
validator = RegexValidator(re.compile("abc"))
|
||||||
|
self.assertTrue(validator.isValid("abc"))
|
||||||
|
|
||||||
|
def testDoesntMatchRegex(self):
|
||||||
|
validator = RegexValidator(re.compile("abc"))
|
||||||
|
self.assertFalse(validator.isValid("xyz"))
|
||||||
|
|
||||||
|
def testFailsIfValueIsNoString(self):
|
||||||
|
validator = RegexValidator(re.compile("abc"))
|
||||||
|
self.assertFalse(validator.isValid(42))
|
14
test/property/validators/test_string_validator.py
Normal file
14
test/property/validators/test_string_validator.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import StringValidator
|
||||||
|
|
||||||
|
class StringValidatorTest(TestCase):
|
||||||
|
def testPassesStrings(self):
|
||||||
|
validator = StringValidator()
|
||||||
|
self.assertTrue(validator.isValid("text"))
|
||||||
|
|
||||||
|
def testDoesntPassOthers(self):
|
||||||
|
validator = StringValidator()
|
||||||
|
self.assertFalse(validator.isValid(123))
|
||||||
|
self.assertFalse(validator.isValid(-2))
|
||||||
|
self.assertFalse(validator.isValid(.5))
|
||||||
|
self.assertFalse(validator.isValid(object()))
|
20
test/property/validators/test_validator.py
Normal file
20
test/property/validators/test_validator.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from owrx.property.validators import Validator, NumberValidator, LambdaValidator, StringValidator
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorTest(TestCase):
|
||||||
|
|
||||||
|
def testReturnsValidator(self):
|
||||||
|
validator = NumberValidator()
|
||||||
|
self.assertIs(validator, Validator.of(validator))
|
||||||
|
|
||||||
|
def testTransformsLambda(self):
|
||||||
|
def my_callable(v):
|
||||||
|
return True
|
||||||
|
validator = Validator.of(my_callable)
|
||||||
|
self.assertIsInstance(validator, LambdaValidator)
|
||||||
|
self.assertTrue(validator.isValid("test"))
|
||||||
|
|
||||||
|
def testGetsValidatorByKey(self):
|
||||||
|
validator = Validator.of("str")
|
||||||
|
self.assertIsInstance(validator, StringValidator)
|
Loading…
Reference in New Issue
Block a user