Added files.

This commit is contained in:
Florian Heß
2025-05-10 09:38:36 +02:00
parent a61c0ae1ff
commit d4cf1e092f
21 changed files with 1388 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
import re
from .field import Field, ValidationError
import sys
class HandleParams:
def __init__(
self, params={}, sequence=None, ignored_fields=None, defaults=None, altered_fields=None
):
self.sequence = list(sequence) if sequence else []
self.unrecognized = []
self.required_fields = set()
self.altered_fields = dict(altered_fields) if altered_fields else {}
self.defaults = dict(defaults) if defaults else {}
for key in self.defaults.keys():
if len(params.get(key,'') or '') > 0:
self.defaults[key] = params[key]
self.fields = {}
self.ignored_fields = set(ignored_fields or [])
def add_field(self, name, properties, **alter_cfg):
properties['name'] = name
variant = self.altered_fields.get(name)
if variant is not None:
if variant in alter_cfg:
cfg = alter_cfg[variant]
if not isinstance(cfg, dict):
raise RuntimeError(f'{name}: Not a dictionary for variant {variant}: {cfg}')
alter_cfg = dict(cfg)
else:
# A variant is requested that is not known / supported.
known_keys = ','.join(alter_cfg)
raise RuntimeError(f"Field-def {name}: No variant {variant} (keys: {known_keys})")
else:
alter_cfg = {}
for attr, acfg in alter_cfg.items() :
if attr.startswith('wrap_'):
attr = attr[5:]
wrap = True
else:
wrap = False
orig = properties.get(attr)
properties[attr] = lambda orig, *args, **kwargs: acfg(orig, *args, **kwargs) if wrap else acfg
if properties.pop('required', False) == True:
self.required_fields.add(name)
field = Field(**properties)
default = self.defaults.get(name)
if default is not None:
default = filter_value(field.apply_filters, name, default)
try:
if field.validate(default): field.value = default
except RuntimeError as e: # HandleParams.ValidationError raised spurious errors when
if len(default) > 0: # used flask is used with uwsgi
raise
if hasattr(field, 'digest_key') or field.multiple:
self.sequence.append(name)
self.fields[name] = field
def validate_params(self, params, set_invalid_value=True):
success = True
ignore = self.ignored_fields
validate_seq = [*self.sequence]
defer_validations = set(validate_seq)
tmp = {}
## raise RuntimeError(type(params))
for fname in self.required_fields:
if len(params.get(fname, '')) == 0:
params[fname] = None
fieldlist = open('/tmp/validated_fields.txt','w')
def paramsiter():
for fldname, fldvalue in params.items():
if isinstance(fldvalue, list):
for v in fldvalue:
yield fldname, v
else:
yield fldname, fldvalue
## raise RuntimeError(list(paramsiter()))
## raise RuntimeError(list(paramsiter()))
## raise RuntimeError(params.items())
## raise RuntimeError("defer_validations", list(defer_validations))
## if "foerderung" in defer_validations:
## raise RuntimeError("foerderung in defer_valitations.")
## else:
## raise RuntimeError("foerderung not in defer_validations.")
## for fldname, fldvalue in params.items():
## raise RuntimeError(params.items())
## if fldname == 'foerderung':
## raise RuntimeError("type", type(fldvalue), fldvalue)
## if fldname == 'foerderung' and len(fldvalue) > 1:
## raise RuntimeError(fldvalue)
for fldname, fldvalue in paramsiter():
## raise RuntimeError(fldname)
## if len(fldvalue) > 1:
## raise RuntimeError(fldvalue)
## if fldname == 'foerderung':
## raise RuntimeError(fldname + ' ' + fldvalue, type(fldvalue))
if fldvalue is not None and len(fldvalue) == 0:
continue
if ( fldname not in defer_validations
and fldname in self.fields
):
## raise RuntimeError(fldname)
## if fldname == 'foerderung':
## raise RuntimeError('test')
## raise RuntimeError(fldname + ' ' + fldvalue)
field = self.fields[fldname]
value = filter_value(field.apply_filters, fldname, fldvalue)
try:
this_success = field.validate(value)
## von debugging sessions
if fldname == 'dsgvochecked' and this_success is False:
## raise RuntimeError("this_success: " + this_success)
raise RuntimeError(value)
except HandleParams.ValidationError as e:
field.error_msg = str(e)
field.is_valid = False
print(fldname, file=fieldlist)
this_success = False
else:
field.is_valid = True
take_this_value = value is not None if set_invalid_value else this_success
if take_this_value:
field.value = value
if not(success and this_success):
success = False
elif not fldname in ignore:
#if hasattr(self.fields[fldname], 'digest_key'):
# raise RuntimeError(fldname)
#elif fldname.startswith("nachweise"):
# raise RuntimeError(fldname)
## raise RuntimeError("validate_seq: ", validate_seq)
for fname in validate_seq:
key_tmpl = {}
field = self.fields[fname]
if hasattr(field, 'digest_key'):
key_tmpl = field.digest_key(fldname)
if not key_tmpl: continue
value = filter_value(field.apply_filters, fname, fldvalue)
key_tmpl['value'] = value
value = key_tmpl
tmp.setdefault(fname, {})
slot = tmp[fname]
elif fldname == fname:
value = filter_value(field.apply_filters, fname, fldvalue)
slot = tmp
## raise RuntimeError("slot: ",slot)
else:
continue
if field.multiple:
slot.setdefault(fldname, [])
slot[fname].append(value)
## raise RuntimeError(fldname + ' ' + value)
else:
slot[fldname] = value
## raise RuntimeError(tmp)
fullfilled = False
for fldname, fldvalue in tmp.items():
field = self.fields[fldname]
try:
##raise RuntimeError("fldvalue2: ", fldvalue)
this_success = field.validate(fldvalue)
except HandleParams.ValidationError as e:
field.error_msg = str(e)
field.is_valid = False
this_success = False
else:
field.is_valid = True
take_this_value = fldvalue is not None if set_invalid_value else this_success
if take_this_value:
field.value = fldvalue
fullfilled = True
if not(success and this_success):
success = False
#if not fullfilled:
# self.unrecognized.append(fldname)
# continue
##raise RuntimeError(tmp)
return success
ADD_FILTERS = {}
def filter_value(filters_to_apply, name, value):
if isinstance(value, str):
value = re.sub(r'[\;\"\(\)\{\}\\\`\|\%\r\<\>\&\0]', '', value)
for fname in filters_to_apply:
if not fname in ADD_FILTERS:
raise RuntimeError(f"No filter {fname}")
if not isinstance(value, list):
value = [list]
for v in value:
try:
value = FILTERS[fname](v)
except HandleParams.ValidationError as e:
field.error_msg = str(e)
return
return value

View File

@@ -0,0 +1,45 @@
CALLBACKS = {
'validate': lambda self, value: True,
'render': lambda self, *args, **kwargs: '',
'digest_key': None,
'rendervalue': lambda self, *args, **kwargs: self.value,
'initializer': None,
}
class Field:
def wrap_callback(self, cb):
if cb is None:
return
def func(*args, **kwargs):
return cb(self, *args, **kwargs)
return func
def __init__(self, *, is_multiple=False, **callbacks):
self.apply_filters = []
for cb in CALLBACKS.keys():
func = self.wrap_callback(
callbacks.get(cb, CALLBACKS[cb])
)
if func is not None:
setattr(self, cb, func)
self.initializer = callbacks.get('initializer', lambda: '')
#self.digest_key = callbacks.get('digest_key', None)
self.error_msg = ''
self.multiple = is_multiple
self.options_map = {}
self.is_valid = None
@property
def value(self):
if not hasattr(self, '_value'):
self._value = self.initializer()
return self._value
@value.setter
def value(self, value):
self._value = value
class ValidationError(RuntimeError):
pass

View File

@@ -0,0 +1,93 @@
from datetime import datetime
from . import ValidationError
import hmac, os, sqlite3
DB_FILENAME = "/var/tmp/www/forms-sent-timestamps.db"
SECRET = b"Dieser Zeitstempel wurde von uns gepraegt. -- UB/fh 24.5.24"
TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
MIN_DURATION = 10 # Sekunden
REFUSE_NTH_TIMESTAMP_REUSE = 3 # Diese Werte werden nur bei der Erstellung einer
MAX_REQUESTS_PER_IP = 10 # neuen SQLite3-Datenbank berücksichtigt.
def digest_of(value):
return hmac.new(SECRET, value.encode("utf-8"), "sha256").hexdigest()
def signed_fresh_timestamp_check(remote_ip, error=ValidationError):
def render_timestamp(self, name):
value = datetime.now().strftime(TIME_FORMAT)
value = "{}/{}".format(value, digest_of(value))
return f'<input name="{name}" type="hidden" value="{value}">'
def rendervalue(self, name):
value = datetime.now().strftime(TIME_FORMAT)
return "{}/{}".format(value, digest_of(value))
def timestamp_checker(field, value, remote_ip=remote_ip):
if value is None: raise error("not passed")
value, digest = value.split("/")
if not hmac.compare_digest(digest, digest_of(value)):
raise error("invalid")
timediff = datetime.now() - datetime.strptime(value, TIME_FORMAT)
db = init_db()
try:
c = db.cursor()
c.execute("""
INSERT OR IGNORE into timestamps (timestamp, digest, remote_ip) values (?, ?, ?)
""", (value, digest, remote_ip));
c.execute("UPDATE timestamps SET used=used+1 WHERE timestamp=? AND remote_ip=?",
(value, remote_ip)
)
db.commit()
except sqlite3.IntegrityError:
raise error("overused")
else:
if c.rowcount == 0:
raise error("missing")
elif timediff.days > 0:
raise error("outdated")
elif timediff.seconds < MIN_DURATION:
raise error(f"fast_as_spambot")
finally:
db.close()
return True
return {
"validate": timestamp_checker,
"render": render_timestamp,
"rendervalue": rendervalue,
"required": True
}
def init_db(filename=DB_FILENAME):
os.makedirs(os.path.dirname(filename), exist_ok=True)
conn = sqlite3.connect(filename)
if (not os.path.isfile(filename) or os.stat(filename).st_size == 0):
cursor = conn.cursor()
cursor.executescript(f"""
CREATE TABLE timestamps (
timestamp PRIMARY KEY,
digest,
remote_ip,
reqtime CURRENT_TIMESTAMP,
used DEFAULT 0 CHECK(used < {REFUSE_NTH_TIMESTAMP_REUSE})
);
CREATE TRIGGER block_hyperactive_ip
BEFORE INSERT ON timestamps
WHEN EXISTS (
SELECT 1
FROM timestamps
WHERE remote_ip=NEW.remote_ip
GROUP by remote_ip
HAVING count(timestamp)={MAX_REQUESTS_PER_IP}
)
BEGIN
SELECT RAISE(FAIL, "no more than {MAX_REQUESTS_PER_IP} requests per IP");
-- Still, cron'd housekeeping is required
END;
""")
conn.commit()
return conn