Added files.
This commit is contained in:
215
flask/floritiweb/lib/forms/handle_params/__init__.py
Normal file
215
flask/floritiweb/lib/forms/handle_params/__init__.py
Normal 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
|
||||
45
flask/floritiweb/lib/forms/handle_params/field.py
Normal file
45
flask/floritiweb/lib/forms/handle_params/field.py
Normal 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
|
||||
93
flask/floritiweb/lib/forms/handle_params/timewindowcheck.py
Normal file
93
flask/floritiweb/lib/forms/handle_params/timewindowcheck.py
Normal 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
|
||||
Reference in New Issue
Block a user