Add endpoint for fetching/manipulating rule-sets
diff --git a/endpoints/rules.py b/endpoints/rules.py
new file mode 100644
index 0000000..0d642fb
--- /dev/null
+++ b/endpoints/rules.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+import ahapi
+import plugins.configuration
+import re
+
+""" rules get/set endpoint for Blocky/4"""
+
+
+def validate_filter(filter):
+ """Ensures a search filter is valid"""
+ for entry in filter.split("\n"):
+ if entry:
+ k, o, v = entry.split(" ", 2) # key, operator, value
+ if o.startswith("!"): # exclude as search param?
+ o = o[1:]
+ if o == "=":
+ return True
+ elif o == "~=":
+ return True
+ elif o == "==":
+ return True
+ else:
+ raise TypeError(f"Unknown operator {o} in search filter: {entry}")
+
+
+async def process(state: plugins.configuration.BlockyConfiguration, request, formdata: dict) -> dict:
+
+ # Fetching rules?
+ if request.method == "GET":
+ rules = [x for x in state.sqlite.fetch("rules", limit=0)]
+ return rules
+
+ # Removing a rule?
+ if request.method == "DELETE":
+ rule_id = formdata.get("rule", -1)
+ rule = state.sqlite.fetchone("rules", id=rule_id)
+ if rule:
+ state.sqlite.delete("rules", id=rule_id)
+ return {"success": True, "status": "deleted", "message": f"Rule #{rule_id} has been deleted."}
+ else:
+ return {"success": False, "status": "not found", "message": f"Rule #{rule_id} does not exist."}
+
+ # Adding a rule?
+ if request.method == "PUT":
+ description = formdata.get("description")
+ assert description, "Please provide a description for your new rule"
+ aggtype = formdata.get("aggtype")
+ assert aggtype in ["requests", "bytes"], "aggtype must be either requests or bytes"
+ limit = int(formdata.get("limit"))
+ assert limit > 0, "limit must be greater than zero"
+ duration = formdata.get("duration")
+ assert re.match(r"^\d+[dhms]", duration), "duration must be of format 0-99[d/h/m/s], for instance 24h or 45m"
+ filters = formdata.get("filter", "")
+ try:
+ validate_filter(filters)
+ except TypeError as e:
+ assert AssertionError(e)
+ entry = {
+ "description": description,
+ "aggtype": aggtype,
+ "limit": limit,
+ "duration": duration,
+ "filters": filters,
+ }
+ # Check for duplicates first
+ entry_inserted = state.sqlite.fetchone("rules", **entry)
+ if entry_inserted:
+ return {
+ "success": False,
+ "status": "duplicate",
+ "message": f"Rule #{entry_inserted['id']} already exists with these parameters",
+ }
+
+ # Insert and return the ID it got
+ state.sqlite.insert("rules", entry)
+ entry_inserted = state.sqlite.fetchone("rules", **entry)
+ return {"success": True, "status": "added", "message": f"Rule #{entry_inserted['id']} has been added"}
+
+ # Patching a rule?
+ if request.method == "PATCH":
+ rule_id = int(formdata.get("rule", -1))
+ description = formdata.get("description")
+ assert description, "Please provide a description for your new rule"
+ aggtype = formdata.get("aggtype")
+ assert aggtype in ["requests", "bytes"], "aggtype must be either requests or bytes"
+ limit = int(formdata.get("limit"))
+ assert limit > 0, "limit must be greater than zero"
+ duration = formdata.get("duration")
+ assert re.match(r"^\d+[dhms]", duration), "duration must be of format 0-99[d/h/m/s], for instance 24h or 45m"
+ filters = formdata.get("filter", "")
+ try:
+ validate_filter(filters)
+ except TypeError as e:
+ assert AssertionError(e)
+ entry = {
+ "description": description,
+ "aggtype": aggtype,
+ "limit": limit,
+ "duration": duration,
+ "filters": filters,
+ }
+ # Check that rule exists
+ existing_entry = state.sqlite.fetchone("rules", id=rule_id)
+ if not existing_entry:
+ return {"success": False, "status": "not found", "message": f"Rule #{rule_id} does not exist"}
+
+ # Upsert rule
+ state.sqlite.upsert("rules", entry, id=rule_id)
+ return {"success": True, "status": "modified", "message": f"Rule #{rule_id} has been modified"}
+
+
+def register(config: plugins.configuration.BlockyConfiguration):
+ return ahapi.endpoint(process)