21 Commits

Author SHA1 Message Date
477b457be9 update the version 2021-01-26 16:53:22 +01:00
58b35ec0f9 update changelogs for 0.20.3 2021-01-26 16:28:56 +01:00
ae0748952f remove unused import, too 2021-01-25 19:40:06 +01:00
f81cf3570a don't check the type since older python doesn't have re.Pattern 2021-01-25 19:36:55 +01:00
b2e8fc5ad5 release version 0.20.2 2021-01-24 23:52:20 +01:00
b997e83095 update changelog 2021-01-24 23:51:01 +01:00
366f7247f2 code style 2021-01-24 22:54:58 +01:00
7e60efeae2 validate all parameters sent to dsp, refs #215 2021-01-24 22:29:23 +01:00
15940d0a2e extend StringValidator instead 2021-01-24 22:28:48 +01:00
d126c3acef allow regexes only on strings 2021-01-24 22:28:00 +01:00
a880b1f6f9 add regex validator 2021-01-24 22:03:53 +01:00
49577953c6 fix events 2021-01-24 21:58:15 +01:00
4b03ced1f7 add more validators 2021-01-24 21:58:02 +01:00
66dc4e5772 get validator by string 2021-01-24 21:25:26 +01:00
ad0a5c27db introduce PropertyValidator (wrapper) 2021-01-24 21:19:45 +01:00
40e531c0da start implementing a validation layer, refs #215 2021-01-24 20:53:51 +01:00
8b52988dcd add a test that makes sure that writing to a filtered property fails 2021-01-24 20:15:02 +01:00
862a251295 allow only limited parameters to be set on the dsp 2021-01-24 20:10:37 +01:00
8710a2a1d3 update version and changelog 2020-11-30 18:30:33 +01:00
b3fbf89f57 remove OSM fallback since it's broken 2020-11-30 18:10:34 +01:00
ad5e610cec update changelog for 0.20.0 2020-10-11 23:13:14 +02:00
19 changed files with 405 additions and 29 deletions

View File

@ -1,4 +1,14 @@
**unreleased**
**0.20.3**
- Fix a compatibility issue with python versions <= 3.6
**0.20.2**
- Fix a security problem that allowed arbitrary commands to be executed on the receiver
([See github issue #215](https://github.com/jketterl/openwebrx/issues/215))
**0.20.1**
- Remove broken OSM map fallback
**0.20.0**
- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver
on receiverbook.de
- Fixed file descriptor leaks to prevent "too many open files" errors

20
debian/changelog vendored
View File

@ -1,3 +1,23 @@
openwebrx (0.20.3) buster focal; urgency=low
* Fix a compatibility issue with python versions <= 3.6
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 26 Jan 2021 15:28:00 +0000
openwebrx (0.20.2) buster focal; urgency=high
* Fix a security problem that allowed arbitrary commands to be executed on
the receiver (See github issue #215:
https://github.com/jketterl/openwebrx/issues/215)
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 24 Jan 2021 22:50:00 +0000
openwebrx (0.20.1) buster focal; urgency=low
* Remove broken OSM map fallback
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 30 Nov 2020 17:29:00 +0000
openwebrx (0.20.0) buster focal; urgency=low
* Added the ability to sign multiple keys in a single request, thus enabling

View File

@ -223,26 +223,14 @@
case "config":
var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
var mapTypeId = config.google_maps_api_key ? 'roadmap' : 'OSM';
map = new google.maps.Map($('.openwebrx-map')[0], {
center: {
lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon
},
zoom: 5,
mapTypeId: mapTypeId
});
map.mapTypes.set("OSM", new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
return "https://maps.wikimedia.org/osm-intl/" + zoom + "/" + coord.x + "/" + coord.y + ".png";
},
tileSize: new google.maps.Size(256, 256),
name: "OpenStreetMap",
maxZoom: 18
}));
$.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s

View File

@ -4,16 +4,27 @@ from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser
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 csdr import csdr
import threading
import re
import logging
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):
def __init__(self, handler, sdrSource):
self.handler = handler
@ -27,19 +38,25 @@ class DspManager(csdr.output, SdrSourceEventClient):
}
self.props = PropertyStack()
# local demodulator properties not forwarded to the sdr
self.props.addLayer(0, PropertyLayer().filter(
"output_rate",
"hd_output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
"mod",
"secondary_offset_freq",
"dmr_filter",
))
# 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
self.props.addLayer(1, self.sdrSource.getProps().filter(
"audio_compression",
@ -177,7 +194,7 @@ class DspManager(csdr.output, SdrSourceEventClient):
self.setProperty(k, v)
def setProperty(self, prop, value):
self.props[prop] = value
self.localProps[prop] = value
def getClientClass(self):
return SdrSource.CLIENT_USER

View File

@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from owrx.property.validators import Validator
import logging
logger = logging.getLogger(__name__)
@ -148,6 +149,50 @@ class PropertyFilter(PropertyManager):
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):
def __init__(self):
super().__init__()

View File

@ -0,0 +1,97 @@
from abc import ABC, abstractmethod
from functools import reduce
from operator import or_
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):
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,
}

View File

@ -1,5 +1,5 @@
from distutils.version import LooseVersion
_versionstring = "0.20.0"
_versionstring = "0.20.3"
looseversion = LooseVersion(_versionstring)
openwebrx_version = "v{0}".format(looseversion)

View File

@ -11,7 +11,7 @@ class PropertyFilterTest(TestCase):
pf = PropertyFilter(pm, "testkey")
self.assertEqual(pf["testkey"], "testvalue")
def testMissesPropert(self):
def testMissesProperty(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "other_key")
@ -49,3 +49,11 @@ class PropertyFilterTest(TestCase):
pf["testkey"] = "new value"
self.assertEqual(pm["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")

View 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"

View File

View 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()))

View 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()))

View 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()))

View 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))

View 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()))

View 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"))

View 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))

View 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()))

View 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)