"
+ 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"] = "
"
data["body"] = data["body"] .. "\n\n"
for header, value in pairs(headers) do
- data["formatted_body"] = data["formatted_body"] .. "
" .. header .. "
" .. value .. "
"
- 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"]
+ .. "