From 06b480280546782115d1d164ca1871e0bda50993 Mon Sep 17 00:00:00 2001 From: jonas0b1011001 <43352574+jonas0b1011001@users.noreply.github.com> Date: Sat, 21 Sep 2024 13:41:50 +0200 Subject: [PATCH 1/4] initial --- README.md | 1 + matrix/README.md | 93 ++++++++++++++++++ matrix/matrix.lua | 209 ++++++++++++++++++++++++++++++++++++++++ matrix/plugin.json | 63 ++++++++++++ matrix/ui/actions.py | 28 ++++++ matrix/ui/template.html | 91 +++++++++++++++++ matrix/utils.lua | 21 ++++ 7 files changed, 506 insertions(+) create mode 100644 matrix/README.md create mode 100644 matrix/matrix.lua create mode 100644 matrix/plugin.json create mode 100644 matrix/ui/actions.py create mode 100644 matrix/ui/template.html create mode 100644 matrix/utils.lua diff --git a/README.md b/README.md index bd2de83..6c8576d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Each plugin is located in a subdirectory of this repository. A README file locat - [CrowdSec](https://github.com/bunkerity/bunkerweb-plugins/tree/main/crowdsec) - [Coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) - [Discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) +- [Matrix](https://github.com/bunkerity/bunkerweb-plugins/tree/main/matrix) - [Slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) - [VirusTotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) - [WebHook](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) diff --git a/matrix/README.md b/matrix/README.md new file mode 100644 index 0000000..2661e1b --- /dev/null +++ b/matrix/README.md @@ -0,0 +1,93 @@ +# Matrix Notification Plugin + +This [BunkerWeb](https://www.bunkerweb.io/?utm_campaign=self&utm_source=github) plugin will automatically send attack notifications to a Matrix room of your choice. + +# Table of contents + +- [Matrix Notification Plugin](#matrix-notification-plugin) +- [Table of contents](#table-of-contents) +- [Prerequisites](#prerequisites) +- [Setup](#setup) + - [Docker](#docker) + - [Swarm](#swarm) + - [Kubernetes](#kubernetes) +- [Settings](#settings) +- [TODO](#todo) + +# Prerequisites + +Please read the [plugins section](https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=github) of the BunkerWeb documentation first. + +You will need: +- A Matrix server URL (e.g., `https://matrix.org`). +- A valid access token for the Matrix user you want to sent notifications from. +- A room ID where notifications will be sent to. The matrix user has to be Member of that room. + +Please refer to your homeserver's docs if you need help setting these up. + +# Setup + +See the [plugins section](https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=github) of the BunkerWeb documentation for the installation procedure depending on your integration. + +There is no additional service setup required beyond configuring the plugin itself. + +## Docker + +```yaml +version: '3' + +services: + + bunkerweb: + image: bunkerity/bunkerweb:1.5.10 + ... + environment: + - USE_MATRIX=yes + - MATRIX_BASE_URL=https://matrix.org + - MATRIX_ROOM_ID=!yourRoomID:matrix.org + - MATRIX_ACCESS_TOKEN=your-access-token + ... +``` + +## Swarm + +```yaml +version: '3' + +services: + + mybunker: + image: bunkerity/bunkerweb:1.5.10 + .. + environment: + - USE_MATRIX=yes + - MATRIX_BASE_URL=https://matrix.org + - MATRIX_ROOM_ID=!yourRoomID:matrix.org + - MATRIX_ACCESS_TOKEN=your-access-token + ... +``` + +## Kubernetes + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress + annotations: + bunkerweb.io/USE_MATRIX: "yes" + bunkerweb.io/MATRIX_BASE_URL: "https://matrix.org" + bunkerweb.io/MATRIX_ROOM_ID: "!yourRoomID:matrix.org" + bunkerweb.io/MATRIX_ACCESS_TOKEN: "your-access-token" +``` + +# Settings + +| Setting | Default | Context | Multiple | Description | +| -------------------- | ---------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------- | +| `USE_MATRIX` | `no` | multisite | no | Enable sending alerts to a Matrix room. | +| `MATRIX_BASE_URL` | `https://matrix.org` | global | no | Base URL of the Matrix server. | +| `MATRIX_ROOM_ID` | `!yourRoomID:matrix.org` | global | no | Room ID of the Matrix room to send notifications to. | +| `MATRIX_ACCESS_TOKEN` | ` ` | global | no | Access token to authenticate with the Matrix server. | +| `MATRIX_ANONYMIZE_IP` | `no` | global | no | Mask the IP address in notifications. | +| `MATRIX_INCLUDE_HEADERS` | `no` | global | no | Include request headers in notifications. | diff --git a/matrix/matrix.lua b/matrix/matrix.lua new file mode 100644 index 0000000..e60949b --- /dev/null +++ b/matrix/matrix.lua @@ -0,0 +1,209 @@ +local cjson = require("cjson") +local class = require("middleclass") +local http = require("resty.http") +local plugin = require("bunkerweb.plugin") +local utils = require("bunkerweb.utils") +local matrix_utils = require("matrix.utils") + +local matrix = class("matrix", plugin) + +local ngx = ngx +local ngx_req = ngx.req +local ERR = ngx.ERR +local INFO = ngx.INFO +local ngx_timer = ngx.timer +local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR +local HTTP_OK = ngx.HTTP_OK +local http_new = http.new +local has_variable = utils.has_variable +local get_variable = utils.get_variable +local get_reason = utils.get_reason +local get_country = utils.get_country +local get_asn = utils.get_asn +local get_asn_org = matrix_utils.get_asn_org +local tostring = tostring +local encode = cjson.encode + +function matrix:initialize(ctx) + -- Call parent initialize + plugin.initialize(self, "matrix", ctx) +end + +function matrix:log(bypass_use_matrix) + -- Check if matrix is enabled + if not bypass_use_matrix then + if self.variables["USE_MATRIX"] ~= "yes" then + return self:ret(true, "matrix plugin not enabled") + end + end + -- Check if request is denied + local reason, reason_data = get_reason(self.ctx) + if reason == nil then + return self:ret(true, "request not denied") + end + -- Compute data + local request_host = ngx.var.host or "unknown host" + local remote_addr = self.ctx.bw.remote_addr + local request_method = self.ctx.bw.request_method + local country, err = get_country(self.ctx.bw.remote_addr) + if not country then + elf.logger:log(ERR, "can't get Country of IP " .. remote_addr .. " : " .. err) + country = "Country unknown" + else + country = tostring(country) + end + local asn, err = get_asn(remote_addr) + if not asn then + self.logger:log(ERR, "can't get ASN of IP " .. remote_addr .. " : " .. err) + asn = "ASN unknown" + else + asn = "ASN " .. tostring(asn) + end + local asn_org, err = get_asn_org(remote_addr) + if not asn_org then + self.logger:log(ERR, "can't get Organization of IP " .. remote_addr .. " : " .. err) + asn_org = "AS Organization unknown" + else + asn_org = tostring(asn_org) + end + local data = {} + data["formatted_body"] = "

Denied " .. request_method .. " from " .. remote_addr .. " (" .. country .. " • \"" .. asn_org .. "\" • " .. asn .. ") to " .. request_host .. self.ctx.bw.uri .. "
" + data["formatted_body"] = data["formatted_body"] .. "Reason " .. reason .. " (" .. encode(reason_data or {}) .. ").

" + data["body"] = "Denied " .. request_method .. " from " .. remote_addr .. " (" .. country .. " • \"" .. asn_org .. "\" • " .. asn .. ") to " .. request_host .. self.ctx.bw.uri .. "\n" + data["body"] = data["body"] .. "Reason " .. reason .. " (" .. encode(reason_data or {}) .. ")." + -- Add headers if enabled + if self.variables["MATRIX_INCLUDE_HEADERS"] == "yes" then + local headers, err = ngx_req.get_headers() + if not headers then + data["formatted_body"] = data["formatted_body"] .. "error while getting headers: " .. err + data["body"] = data["body"] .. "\n error while getting headers: " .. err + else + data["formatted_body"] = data["formatted_body"] .. "" + data["body"] = data["body"] .. "\n\n" + for header, value in pairs(headers) do + data["formatted_body"] = data["formatted_body"] .. "" + data["body"] = data["body"] .. header .. ": " .. value .. "\n" + end + data["formatted_body"] = data["formatted_body"] .. "
HeaderValue
" .. header .. "" .. value .. "
" + end + end + -- Anonymize IP if enabled + if self.variables["MATRIX_ANONYMIZE_IP"] == "yes" then + remote_addr = string.gsub(remote_addr, "%d+%.%d+$", "xxx.xxx") + data = string.gsub(data, self.ctx.bw.remote_addr, remote_addr) + end + -- Send request + local hdr, err = ngx_timer.at(0, self.send, self, data) + if not hdr then + return self:ret(true, "can't create report timer: " .. err) + end + return self:ret(true, "scheduled timer") +end + +-- luacheck: ignore 212 +function matrix.send(premature, self, data) + local httpc, err = http_new() + if not httpc then + self.logger:log(ERR, "can't instantiate http object : " .. err) + end + -- Prapare data + local base_url = self.variables["MATRIX_BASE_URL"] + local access_token = self.variables["MATRIX_ACCESS_TOKEN"] + local room_id = self.variables["MATRIX_ROOM_ID"] + local txn_id = tostring(os.time()) + local url = string.format("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", base_url, room_id, txn_id) + local message_data = { + msgtype = "m.text", + body = data["body"], + format = "org.matrix.custom.html", + formatted_body = data["formatted_body"] + } + local post_data = cjson.encode(message_data) + -- Send request + local res, err_http = httpc:request_uri(url, { + method = "PUT", + body = post_data, + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer " .. access_token -- Access Token im Header + } + }) + httpc:close() + if not res then + self.logger:log(ERR, "error while sending request : " .. err_http) + end + if res.status < 200 or res.status > 299 then + self.logger:log(ERR, "request returned status " .. tostring(res.status)) + return + end + self.logger:log(INFO, "request sent to matrix") +end + +function matrix:log_default() + -- Check if matrix is activated + local check, err = has_variable("USE_MATRIX", "yes") + if check == nil then + return self:ret(false, "error while checking variable USE_MATRIX (" .. err .. ")") + end + if not check then + return self:ret(true, "matrix plugin not enabled") + end + -- Check if default server is disabled + check, err = get_variable("DISABLE_DEFAULT_SERVER", false) + if check == nil then + return self:ret(false, "error while getting variable DISABLE_DEFAULT_SERVER (" .. err .. ")") + end + if check ~= "yes" then + return self:ret(true, "default server not disabled") + end + -- Call log method + return self:log(true) +end + +function matrix:api() + if self.ctx.bw.uri == "/matrix/ping" and self.ctx.bw.request_method == "POST" then + -- Check matrix connection + local check, err = has_variable("USE_MATRIX", "yes") + if check == nil then + return self:ret(true, "error while checking variable USE_MATRIX (" .. err .. ")") + end + if not check then + return self:ret(true, "matrix plugin not enabled") + end + -- Prepare data + local base_url = self.variables["MATRIX_BASE_URL"] + local access_token = self.variables["MATRIX_ACCESS_TOKEN"] + local room_id = self.variables["MATRIX_ROOM_ID"] + local txn_id = tostring(os.time()) + local url = string.format("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", base_url, room_id, txn_id) + local message_data = { + msgtype = "m.text", + body = "Test message from bunkerweb." + } + -- Send request + local httpc + httpc, err = http_new() + if not httpc then + self.logger:log(ERR, "can't instantiate http object : " .. err) + end + local res, err_http = httpc:request_uri(url, { + method = "PUT", + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer " .. access_token + }, + body = encode(message_data), + }) + httpc:close() + if not res then + self.logger:log(ERR, "error while sending request : " .. err_http) + end + if res.status < 200 or res.status > 299 then + return self:ret(true, "request returned status " .. tostring(res.status), HTTP_INTERNAL_SERVER_ERROR) + end + return self:ret(true, "request sent to matrix", HTTP_OK) + end + return self:ret(false, "success") +end + +return matrix \ No newline at end of file diff --git a/matrix/plugin.json b/matrix/plugin.json new file mode 100644 index 0000000..0174dd0 --- /dev/null +++ b/matrix/plugin.json @@ -0,0 +1,63 @@ +{ + "id": "matrix", + "name": "Matrix", + "description": "Send alerts to a Matrix room via the Matrix API.", + "version": "1.0", + "stream": "yes", + "settings": { + "USE_MATRIX": { + "context": "multisite", + "default": "no", + "help": "Enable sending alerts to a Matrix room.", + "id": "use-matrix", + "label": "Use Matrix", + "regex": "^(yes|no)$", + "type": "check" + }, + "MATRIX_BASE_URL": { + "context": "global", + "default": "https://matrix.org", + "help": "Base URL of the Matrix server (e.g., https://matrix.org).", + "id": "matrix-base-url", + "label": "Matrix Base URL", + "regex": "^.*$", + "type": "text" + }, + "MATRIX_ROOM_ID": { + "context": "global", + "default": "!yourRoomID:matrix.org", + "help": "Room ID of the Matrix room to send notifications to.", + "id": "matrix-room-id", + "label": "Matrix Room ID", + "regex": "^.*$", + "type": "text" + }, + "MATRIX_ACCESS_TOKEN": { + "context": "global", + "default": "", + "help": "Access token to authenticate with the Matrix server.", + "id": "matrix-access-token", + "label": "Matrix Access Token", + "regex": "^.*$", + "type": "password" + }, + "MATRIX_ANONYMIZE_IP": { + "context": "global", + "default": "no", + "help": "Mask the IP address in notifications.", + "id": "matrix-anonymize-ip", + "label": "Anonymize IP", + "regex": "^(yes|no)$", + "type": "check" + }, + "MATRIX_INCLUDE_HEADERS": { + "context": "global", + "default": "no", + "help": "Include request headers in notifications.", + "id": "matrix-include-headers", + "label": "Include Headers", + "regex": "^(yes|no)$", + "type": "check" + } + } +} diff --git a/matrix/ui/actions.py b/matrix/ui/actions.py new file mode 100644 index 0000000..d1eeb38 --- /dev/null +++ b/matrix/ui/actions.py @@ -0,0 +1,28 @@ +from traceback import format_exc + + +def pre_render(**kwargs): + pass + + +def matrix(**kwargs): + ping = {"ping_status": "unknown"} + + args = kwargs.get("args", False) + if not args: + return {**ping} + + is_ping = args.get("ping", False) + if not is_ping: + return {**ping} + + # Check ping + try: + ping_data = kwargs["app"].config["INSTANCES"].get_ping("matrix") + ping = {"ping_status": ping_data["status"]} + except BaseException: + error = f"Error while trying to ping matrix : {format_exc()}" + print(error, flush=True) + ping = {"ping_status": "error", "error": error} + + return {**ping} diff --git a/matrix/ui/template.html b/matrix/ui/template.html new file mode 100644 index 0000000..8624e16 --- /dev/null +++ b/matrix/ui/template.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block content %} + +
+ {% if is_used %} + + + +
+
TEST
+

+ Use the next button to send a test message to the configured + Matrix room. +

+ +
+ +
+ +
+ +
+ + + +

Unknown

+
+
+ + + {% else %} +
+
+
Plugin deactivated
+ + + +
+ +
+

{{ plugin.get('description') }}

+
+

{{ read_doc_text|safe }}

+
+ + {% endif %} +
+ +{% endblock %} diff --git a/matrix/utils.lua b/matrix/utils.lua new file mode 100644 index 0000000..6ad8f02 --- /dev/null +++ b/matrix/utils.lua @@ -0,0 +1,21 @@ +local mmdb = require "bunkerweb.mmdb" + +local _utils = {} + +_utils.get_asn_org = function(ip) + -- Check if mmdp is loaded + if not mmdb.asn_db then + return false, "mmdb asn not loaded" + end + -- Perform lookup + local ok, result, err = pcall(mmdb.asn_db.lookup, mmdb.asn_db, ip) + if not ok then + return nil, result + end + if not result then + return nil, err + end + return result.autonomous_system_organization, "success" +end + +return _utils \ No newline at end of file From 64db9f9cd857822d21325d2724ba657b40ddf9d0 Mon Sep 17 00:00:00 2001 From: jonas0b1011001 <43352574+jonas0b1011001@users.noreply.github.com> Date: Sat, 21 Sep 2024 14:47:10 +0200 Subject: [PATCH 2/4] fix anonymize ip --- matrix/matrix.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix/matrix.lua b/matrix/matrix.lua index e60949b..7f043c8 100644 --- a/matrix/matrix.lua +++ b/matrix/matrix.lua @@ -90,7 +90,8 @@ function matrix:log(bypass_use_matrix) -- Anonymize IP if enabled if self.variables["MATRIX_ANONYMIZE_IP"] == "yes" then remote_addr = string.gsub(remote_addr, "%d+%.%d+$", "xxx.xxx") - data = string.gsub(data, self.ctx.bw.remote_addr, remote_addr) + data["formatted_body"] = string.gsub(data["formatted_body"], self.ctx.bw.remote_addr, remote_addr) + data["body"] = string.gsub(data["body"], self.ctx.bw.remote_addr, remote_addr) end -- Send request local hdr, err = ngx_timer.at(0, self.send, self, data) From b0a9540a90b52f09567ecf4861df43e075889fe9 Mon Sep 17 00:00:00 2001 From: jonas0b1011001 <43352574+jonas0b1011001@users.noreply.github.com> Date: Sat, 8 Mar 2025 20:37:55 +0100 Subject: [PATCH 3/4] 1.6 compatibility --- matrix/plugin.json | 2 +- matrix/ui/actions.py | 42 ++++++++++--------- matrix/ui/template.html | 91 ----------------------------------------- 3 files changed, 23 insertions(+), 112 deletions(-) delete mode 100644 matrix/ui/template.html diff --git a/matrix/plugin.json b/matrix/plugin.json index 0174dd0..683cc2d 100644 --- a/matrix/plugin.json +++ b/matrix/plugin.json @@ -2,7 +2,7 @@ "id": "matrix", "name": "Matrix", "description": "Send alerts to a Matrix room via the Matrix API.", - "version": "1.0", + "version": "1.1", "stream": "yes", "settings": { "USE_MATRIX": { diff --git a/matrix/ui/actions.py b/matrix/ui/actions.py index d1eeb38..8045d94 100644 --- a/matrix/ui/actions.py +++ b/matrix/ui/actions.py @@ -1,28 +1,30 @@ +from logging import getLogger from traceback import format_exc def pre_render(**kwargs): - pass - - -def matrix(**kwargs): - ping = {"ping_status": "unknown"} + logger = getLogger("UI") + ret = { + "ping_status": { + "title": "MATRIX STATUS", + "value": "error", + "col-size": "col-12 col-md-6", + "card-classes": "h-100", + }, + } + try: + ping_data = kwargs["bw_instances_utils"].get_ping("matrix") + ret["ping_status"]["value"] = ping_data["status"] + except BaseException as e: + logger.debug(format_exc()) + logger.error(f"Failed to get matrix ping: {e}") + ret["error"] = str(e) - args = kwargs.get("args", False) - if not args: - return {**ping} + if "error" in ret: + return ret - is_ping = args.get("ping", False) - if not is_ping: - return {**ping} + return ret - # Check ping - try: - ping_data = kwargs["app"].config["INSTANCES"].get_ping("matrix") - ping = {"ping_status": ping_data["status"]} - except BaseException: - error = f"Error while trying to ping matrix : {format_exc()}" - print(error, flush=True) - ping = {"ping_status": "error", "error": error} - return {**ping} +def matrix(**kwargs): + pass \ No newline at end of file diff --git a/matrix/ui/template.html b/matrix/ui/template.html deleted file mode 100644 index 8624e16..0000000 --- a/matrix/ui/template.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends "base.html" %} -{% block content %} - -
- {% if is_used %} - - - -
-
TEST
-

- Use the next button to send a test message to the configured - Matrix room. -

- -
- -
- -
- -
- - - -

Unknown

-
-
- - - {% else %} -
-
-
Plugin deactivated
- - - -
- -
-

{{ plugin.get('description') }}

-
-

{{ read_doc_text|safe }}

-
- - {% endif %} -
- -{% endblock %} From 33a18cbc4ac53b0dac2255645066875a2e4fc9f4 Mon Sep 17 00:00:00 2001 From: TheophileDiot Date: Thu, 25 Jun 2026 16:25:40 +0200 Subject: [PATCH 4/4] fix(matrix): harden plugin before merge --- matrix/README.md | 24 +++---- matrix/matrix.lua | 152 +++++++++++++++++++++++++++++++++---------- matrix/plugin.json | 2 +- matrix/ui/actions.py | 60 ++++++++--------- matrix/utils.lua | 42 ++++++------ 5 files changed, 180 insertions(+), 100 deletions(-) diff --git a/matrix/README.md b/matrix/README.md index 2661e1b..86aa719 100644 --- a/matrix/README.md +++ b/matrix/README.md @@ -12,15 +12,15 @@ This [BunkerWeb](https://www.bunkerweb.io/?utm_campaign=self&utm_source=github) - [Swarm](#swarm) - [Kubernetes](#kubernetes) - [Settings](#settings) -- [TODO](#todo) # Prerequisites Please read the [plugins section](https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=github) of the BunkerWeb documentation first. You will need: + - A Matrix server URL (e.g., `https://matrix.org`). -- A valid access token for the Matrix user you want to sent notifications from. +- A valid access token for the Matrix user you want to send notifications from. - A room ID where notifications will be sent to. The matrix user has to be Member of that room. Please refer to your homeserver's docs if you need help setting these up. @@ -39,7 +39,7 @@ version: '3' services: bunkerweb: - image: bunkerity/bunkerweb:1.5.10 + image: bunkerity/bunkerweb:1.5.9 ... environment: - USE_MATRIX=yes @@ -57,7 +57,7 @@ version: '3' services: mybunker: - image: bunkerity/bunkerweb:1.5.10 + image: bunkerity/bunkerweb:1.5.9 .. environment: - USE_MATRIX=yes @@ -83,11 +83,11 @@ metadata: # Settings -| Setting | Default | Context | Multiple | Description | -| -------------------- | ---------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------- | -| `USE_MATRIX` | `no` | multisite | no | Enable sending alerts to a Matrix room. | -| `MATRIX_BASE_URL` | `https://matrix.org` | global | no | Base URL of the Matrix server. | -| `MATRIX_ROOM_ID` | `!yourRoomID:matrix.org` | global | no | Room ID of the Matrix room to send notifications to. | -| `MATRIX_ACCESS_TOKEN` | ` ` | global | no | Access token to authenticate with the Matrix server. | -| `MATRIX_ANONYMIZE_IP` | `no` | global | no | Mask the IP address in notifications. | -| `MATRIX_INCLUDE_HEADERS` | `no` | global | no | Include request headers in notifications. | +| Setting | Default | Context | Multiple | Description | +| ------------------------ | ------------------------ | --------- | -------- | --------------------------------------------------------- | +| `USE_MATRIX` | `no` | multisite | no | Enable sending alerts to a Matrix room. | +| `MATRIX_BASE_URL` | `https://matrix.org` | global | no | Base URL of the Matrix server (e.g., https://matrix.org). | +| `MATRIX_ROOM_ID` | `!yourRoomID:matrix.org` | global | no | Room ID of the Matrix room to send notifications to. | +| `MATRIX_ACCESS_TOKEN` | | global | no | Access token to authenticate with the Matrix server. | +| `MATRIX_ANONYMIZE_IP` | `no` | global | no | Mask the IP address in notifications. | +| `MATRIX_INCLUDE_HEADERS` | `no` | global | no | Include request headers in notifications. | diff --git a/matrix/matrix.lua b/matrix/matrix.lua index 7f043c8..c1c8546 100644 --- a/matrix/matrix.lua +++ b/matrix/matrix.lua @@ -1,9 +1,9 @@ local cjson = require("cjson") local class = require("middleclass") local http = require("resty.http") +local matrix_utils = require("matrix.utils") local plugin = require("bunkerweb.plugin") local utils = require("bunkerweb.utils") -local matrix_utils = require("matrix.utils") local matrix = class("matrix", plugin) @@ -23,6 +23,45 @@ local get_asn = utils.get_asn local get_asn_org = matrix_utils.get_asn_org local tostring = tostring local encode = cjson.encode +local escape_uri = ngx.escape_uri + +-- Escape characters that are significant in the org.matrix.custom.html body so that +-- attacker-controlled values (URI, Host, header names/values) can't break the markup. +local function html_escape(str) + return (string.gsub(tostring(str), "[&<>]", { ["&"] = "&", ["<"] = "<", [">"] = ">" })) +end + +-- Escape Lua pattern magic characters so a literal string can be used as a gsub pattern. +local function escape_pattern(str) + return (string.gsub(str, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")) +end + +-- Mask an IP for notifications. Handles both IPv4 and IPv6. +local function anonymize_ip(ip) + if string.find(ip, ":", 1, true) then + -- IPv6: keep the first three hextets, mask the remainder + local prefix = string.match(ip, "^(%x*:%x*:%x*):") + return prefix and (prefix .. ":xxxx") or "xxxx::xxxx" + end + -- IPv4: mask the last two octets + return (string.gsub(ip, "%d+%.%d+$", "xxx.xxx")) +end + +-- Per-worker, monotonically increasing counter to guarantee transaction-ID uniqueness. +-- ngx.now() is cached per event-loop cycle, so time + pid alone can still collide. +local txn_counter = 0 + +-- Build the Matrix "send message" endpoint URL. +-- The room ID (e.g. "!abc:matrix.org") must be percent-encoded in the path, and the +-- transaction ID must be unique: os.time() has 1s resolution and Matrix silently drops +-- duplicate transaction IDs, so bursts within the same second would lose notifications. +local function message_url(self) + local base_url = string.gsub(self.variables["MATRIX_BASE_URL"], "/+$", "") + local room_id = self.variables["MATRIX_ROOM_ID"] + txn_counter = txn_counter + 1 + local txn_id = string.format("%d_%d_%d", math.floor(ngx.now() * 1000), ngx.worker.pid(), txn_counter) + return string.format("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", base_url, escape_uri(room_id), txn_id) +end function matrix:initialize(ctx) -- Call parent initialize @@ -45,21 +84,23 @@ function matrix:log(bypass_use_matrix) local request_host = ngx.var.host or "unknown host" local remote_addr = self.ctx.bw.remote_addr local request_method = self.ctx.bw.request_method - local country, err = get_country(self.ctx.bw.remote_addr) + local country, err = get_country(remote_addr) if not country then - elf.logger:log(ERR, "can't get Country of IP " .. remote_addr .. " : " .. err) + self.logger:log(ERR, "can't get Country of IP " .. remote_addr .. " : " .. err) country = "Country unknown" else country = tostring(country) end - local asn, err = get_asn(remote_addr) + local asn + asn, err = get_asn(remote_addr) if not asn then self.logger:log(ERR, "can't get ASN of IP " .. remote_addr .. " : " .. err) asn = "ASN unknown" else asn = "ASN " .. tostring(asn) end - local asn_org, err = get_asn_org(remote_addr) + local asn_org + asn_org, err = get_asn_org(remote_addr) if not asn_org then self.logger:log(ERR, "can't get Organization of IP " .. remote_addr .. " : " .. err) asn_org = "AS Organization unknown" @@ -67,34 +108,75 @@ function matrix:log(bypass_use_matrix) asn_org = tostring(asn_org) end local data = {} - data["formatted_body"] = "

Denied " .. request_method .. " from " .. remote_addr .. " (" .. country .. " • \"" .. asn_org .. "\" • " .. asn .. ") to " .. request_host .. self.ctx.bw.uri .. "
" - data["formatted_body"] = data["formatted_body"] .. "Reason " .. reason .. " (" .. encode(reason_data or {}) .. ").

" - data["body"] = "Denied " .. request_method .. " from " .. remote_addr .. " (" .. country .. " • \"" .. asn_org .. "\" • " .. asn .. ") to " .. request_host .. self.ctx.bw.uri .. "\n" + data["formatted_body"] = "

Denied " + .. html_escape(request_method) + .. " from " + .. remote_addr + .. " (" + .. country + .. ' • "' + .. html_escape(asn_org) + .. '" • ' + .. asn + .. ") to " + .. html_escape(request_host) + .. html_escape(self.ctx.bw.uri) + .. "
" + data["formatted_body"] = data["formatted_body"] + .. "Reason " + .. html_escape(reason) + .. " (" + .. html_escape(encode(reason_data or {})) + .. ").

" + data["body"] = "Denied " + .. request_method + .. " from " + .. remote_addr + .. " (" + .. country + .. ' • "' + .. asn_org + .. '" • ' + .. asn + .. ") to " + .. request_host + .. self.ctx.bw.uri + .. "\n" data["body"] = data["body"] .. "Reason " .. reason .. " (" .. encode(reason_data or {}) .. ")." -- Add headers if enabled if self.variables["MATRIX_INCLUDE_HEADERS"] == "yes" then - local headers, err = ngx_req.get_headers() + local headers + headers, err = ngx_req.get_headers() if not headers then data["formatted_body"] = data["formatted_body"] .. "error while getting headers: " .. err data["body"] = data["body"] .. "\n error while getting headers: " .. err else - data["formatted_body"] = data["formatted_body"] .. "" + data["formatted_body"] = data["formatted_body"] .. "
HeaderValue
" data["body"] = data["body"] .. "\n\n" for header, value in pairs(headers) do - data["formatted_body"] = data["formatted_body"] .. "" - data["body"] = data["body"] .. header .. ": " .. value .. "\n" + -- Repeated headers are returned as a table by ngx.req.get_headers() + local header_value = type(value) == "table" and table.concat(value, ", ") or value + data["formatted_body"] = data["formatted_body"] + .. "" + data["body"] = data["body"] .. header .. ": " .. header_value .. "\n" end data["formatted_body"] = data["formatted_body"] .. "
HeaderValue
" .. header .. "" .. value .. "
" + .. html_escape(header) + .. "" + .. html_escape(header_value) + .. "
" end end -- Anonymize IP if enabled if self.variables["MATRIX_ANONYMIZE_IP"] == "yes" then - remote_addr = string.gsub(remote_addr, "%d+%.%d+$", "xxx.xxx") - data["formatted_body"] = string.gsub(data["formatted_body"], self.ctx.bw.remote_addr, remote_addr) - data["body"] = string.gsub(data["body"], self.ctx.bw.remote_addr, remote_addr) + local masked = anonymize_ip(remote_addr) + local pattern = escape_pattern(remote_addr) + data["formatted_body"] = (string.gsub(data["formatted_body"], pattern, masked)) + data["body"] = (string.gsub(data["body"], pattern, masked)) end -- Send request - local hdr, err = ngx_timer.at(0, self.send, self, data) + local hdr + hdr, err = ngx_timer.at(0, self.send, self, data) if not hdr then return self:ret(true, "can't create report timer: " .. err) end @@ -106,32 +188,31 @@ function matrix.send(premature, self, data) local httpc, err = http_new() if not httpc then self.logger:log(ERR, "can't instantiate http object : " .. err) + return end - -- Prapare data - local base_url = self.variables["MATRIX_BASE_URL"] + -- Prepare data local access_token = self.variables["MATRIX_ACCESS_TOKEN"] - local room_id = self.variables["MATRIX_ROOM_ID"] - local txn_id = tostring(os.time()) - local url = string.format("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", base_url, room_id, txn_id) + local url = message_url(self) local message_data = { msgtype = "m.text", body = data["body"], format = "org.matrix.custom.html", - formatted_body = data["formatted_body"] + formatted_body = data["formatted_body"], } - local post_data = cjson.encode(message_data) + local post_data = encode(message_data) -- Send request local res, err_http = httpc:request_uri(url, { method = "PUT", body = post_data, headers = { ["Content-Type"] = "application/json", - ["Authorization"] = "Bearer " .. access_token -- Access Token im Header - } + ["Authorization"] = "Bearer " .. access_token, + }, }) httpc:close() if not res then self.logger:log(ERR, "error while sending request : " .. err_http) + return end if res.status < 200 or res.status > 299 then self.logger:log(ERR, "request returned status " .. tostring(res.status)) @@ -172,32 +253,31 @@ function matrix:api() return self:ret(true, "matrix plugin not enabled") end -- Prepare data - local base_url = self.variables["MATRIX_BASE_URL"] local access_token = self.variables["MATRIX_ACCESS_TOKEN"] - local room_id = self.variables["MATRIX_ROOM_ID"] - local txn_id = tostring(os.time()) - local url = string.format("%s/_matrix/client/r0/rooms/%s/send/m.room.message/%s", base_url, room_id, txn_id) + local url = message_url(self) local message_data = { - msgtype = "m.text", - body = "Test message from bunkerweb." - } + msgtype = "m.text", + body = "Test message from bunkerweb.", + } -- Send request local httpc httpc, err = http_new() if not httpc then self.logger:log(ERR, "can't instantiate http object : " .. err) + return self:ret(true, "can't instantiate http object", HTTP_INTERNAL_SERVER_ERROR) end local res, err_http = httpc:request_uri(url, { method = "PUT", headers = { - ["Content-Type"] = "application/json", - ["Authorization"] = "Bearer " .. access_token - }, + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer " .. access_token, + }, body = encode(message_data), }) httpc:close() if not res then self.logger:log(ERR, "error while sending request : " .. err_http) + return self:ret(true, "error while sending request", HTTP_INTERNAL_SERVER_ERROR) end if res.status < 200 or res.status > 299 then return self:ret(true, "request returned status " .. tostring(res.status), HTTP_INTERNAL_SERVER_ERROR) @@ -207,4 +287,4 @@ function matrix:api() return self:ret(false, "success") end -return matrix \ No newline at end of file +return matrix diff --git a/matrix/plugin.json b/matrix/plugin.json index 683cc2d..8d806b9 100644 --- a/matrix/plugin.json +++ b/matrix/plugin.json @@ -2,7 +2,7 @@ "id": "matrix", "name": "Matrix", "description": "Send alerts to a Matrix room via the Matrix API.", - "version": "1.1", + "version": "1.10", "stream": "yes", "settings": { "USE_MATRIX": { diff --git a/matrix/ui/actions.py b/matrix/ui/actions.py index 8045d94..d5fe30a 100644 --- a/matrix/ui/actions.py +++ b/matrix/ui/actions.py @@ -1,30 +1,30 @@ -from logging import getLogger -from traceback import format_exc - - -def pre_render(**kwargs): - logger = getLogger("UI") - ret = { - "ping_status": { - "title": "MATRIX STATUS", - "value": "error", - "col-size": "col-12 col-md-6", - "card-classes": "h-100", - }, - } - try: - ping_data = kwargs["bw_instances_utils"].get_ping("matrix") - ret["ping_status"]["value"] = ping_data["status"] - except BaseException as e: - logger.debug(format_exc()) - logger.error(f"Failed to get matrix ping: {e}") - ret["error"] = str(e) - - if "error" in ret: - return ret - - return ret - - -def matrix(**kwargs): - pass \ No newline at end of file +from logging import getLogger +from traceback import format_exc + + +def pre_render(**kwargs): + logger = getLogger("UI") + ret = { + "ping_status": { + "title": "MATRIX STATUS", + "value": "error", + "col-size": "col-12 col-md-6", + "card-classes": "h-100", + }, + } + try: + ping_data = kwargs["bw_instances_utils"].get_ping("matrix") + ret["ping_status"]["value"] = ping_data["status"] + except BaseException as e: + logger.debug(format_exc()) + logger.error(f"Failed to get matrix ping: {e}") + ret["error"] = str(e) + + if "error" in ret: + return ret + + return ret + + +def matrix(**kwargs): + pass diff --git a/matrix/utils.lua b/matrix/utils.lua index 6ad8f02..1f311bd 100644 --- a/matrix/utils.lua +++ b/matrix/utils.lua @@ -1,21 +1,21 @@ -local mmdb = require "bunkerweb.mmdb" - -local _utils = {} - -_utils.get_asn_org = function(ip) - -- Check if mmdp is loaded - if not mmdb.asn_db then - return false, "mmdb asn not loaded" - end - -- Perform lookup - local ok, result, err = pcall(mmdb.asn_db.lookup, mmdb.asn_db, ip) - if not ok then - return nil, result - end - if not result then - return nil, err - end - return result.autonomous_system_organization, "success" -end - -return _utils \ No newline at end of file +local mmdb = require("bunkerweb.mmdb") + +local _utils = {} + +_utils.get_asn_org = function(ip) + -- Check if mmdb is loaded + if not mmdb.asn_db then + return false, "mmdb asn not loaded" + end + -- Perform lookup + local ok, result, err = pcall(mmdb.asn_db.lookup, mmdb.asn_db, ip) + if not ok then + return nil, result + end + if not result then + return nil, err + end + return result.autonomous_system_organization, "success" +end + +return _utils