Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
93 changes: 93 additions & 0 deletions matrix/README.md
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +5 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the stale TODO entry from the table of contents.

Lines 5-15 link to #todo, but the document has no matching section.

As per coding guidelines, Documentation should be concise, accurate, and written in British English: Keep the structure clear with a sensible heading hierarchy.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 15-15: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/README.md` around lines 5 - 15, The Table of contents contains a stale
entry linking to "#todo" that does not exist; open the README.md section
containing the bulleted list (the lines under "Table of contents") and remove
the "- [TODO](`#todo`)" list item so the TOC matches the actual headings and keeps
the documentation concise and accurate.


# 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
...
```
Comment on lines +34 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the examples to the 1.6 image series.

Lines 42 and 60 still pin bunkerity/bunkerweb:1.5.10, but this PR is explicitly about 1.6 compatibility. The examples should not steer users onto the wrong runtime version.

As per coding guidelines, Prefer concrete instructions, accurate examples, and explicit prerequisites.

Also applies to: 52-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/README.md` around lines 34 - 50, Update the Docker examples in
matrix/README.md to reference the 1.6 image series: replace all occurrences of
the image tag "bunkerity/bunkerweb:1.5.10" (seen in the bunkerweb service
blocks) with the appropriate 1.6 tag (e.g., "bunkerity/bunkerweb:1.6" or the
exact 1.6.x release you intend to document) so examples match this PR's 1.6
compatibility; ensure both instances mentioned in the diff are updated.


## 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. |
Comment on lines +86 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Regenerate the settings table from plugin.json.

This table already drifts from matrix/plugin.json — for example, MATRIX_ACCESS_TOKEN is shown as a space here, but its default is an empty string in matrix/plugin.json. Please rerun .tests/misc/json2md.py instead of maintaining the table by hand.

As per coding guidelines, Each plugin README contains a settings table generated from plugin.json via .tests/misc/json2md.py; regenerate it whenever settings change rather than hand-editing the table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/README.md` around lines 86 - 93, The settings table in README.md has
drifted from matrix/plugin.json; rerun the JSON-to-Markdown generator
(.tests/misc/json2md.py) against matrix/plugin.json to regenerate the table (so
defaults like MATRIX_ACCESS_TOKEN show as an empty string), replace the manually
edited table in README.md with the generated output, and commit the regenerated
README instead of hand-editing the table.

210 changes: 210 additions & 0 deletions matrix/matrix.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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)
Comment on lines +48 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the logger typo in the country lookup fallback.

Line 50 calls elf.logger, so the first failed country lookup raises instead of falling back to "Country unknown".

Suggested fix
-		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)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
local country, err = get_country(self.ctx.bw.remote_addr)
if not country then
self.logger:log(ERR, "can't get Country of IP " .. remote_addr .. " : " .. err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 48 - 50, The country lookup fallback has a
typo and uses an undefined variable: replace "elf.logger" with "self.logger",
reference the IP with "self.ctx.bw.remote_addr" in the log message (instead of
"remote_addr"), and ensure you set a safe fallback value (e.g. assign country =
"Country unknown") after logging so callers of get_country handling the nil case
don't raise; look for get_country, self.ctx.bw.remote_addr, self.logger and the
local variable country in the surrounding code to apply these changes.

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"] = "<p>Denied " .. request_method .. " from <b>" .. remote_addr .. "</b> (" .. country .. " • \"<i>" .. asn_org .. "</i>\" • " .. asn .. ") to " .. request_host .. self.ctx.bw.uri .. "<br>"
data["formatted_body"] = data["formatted_body"] .. "Reason <b>" .. reason .. "</b> (" .. encode(reason_data or {}) .. ").</p>"
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"] .. "<table><tr><th>Header</th><th>Value</th></tr>"
data["body"] = data["body"] .. "\n\n"
for header, value in pairs(headers) do
data["formatted_body"] = data["formatted_body"] .. "<tr><td>" .. header .. "</td><td>" .. value .. "</td></tr>"
data["body"] = data["body"] .. header .. ": " .. value .. "\n"
Comment on lines +70 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Escape HTML and flatten duplicate headers before concatenation.

Lines 70–80 inject request fields directly into formatted_body without sanitisation. Request-derived fields (request_method, remote_addr, request_host, URI, header names and values) can contain HTML special characters. Repeated HTTP headers from ngx.req.get_headers() arrive as tables, causing concatenation with .. to fail. Escape all dynamic fields for HTML and handle header arrays: local value = type(v) == "table" and table.concat(v, ", ") or tostring(v) before concatenation.

As per coding guidelines: validate and sanitise all request-derived input.

🧰 Tools
🪛 Luacheck (1.2.0)

[warning] 76-76: shadowing definition of variable 'err' on line 62

(W421)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 70 - 85, Sanitise and flatten request-derived
inputs before concatenating into data["formatted_body"] and data["body"]: run
request_method, remote_addr, request_host, self.ctx.bw.uri, reason, reason_data
and every header name/value through an HTML-escape helper (e.g., escape_html)
for formatted_body and tostring for plain body, and when reading headers from
ngx_req.get_headers() handle table values by flattening with table.concat(v, ",
") (or tostring on non-tables) so duplicate headers do not cause concatenation
errors; apply this transformation where data["formatted_body"] and data["body"]
are built and when iterating headers returned by ngx_req.get_headers().

Comment on lines +75 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not forward raw request headers to Matrix.

With MATRIX_INCLUDE_HEADERS=yes, this sends Authorization, Cookie, CSRF tokens, and similar secrets into a third-party room. Switch to an allow-list or redact sensitive header names before serialising them.

As per coding guidelines, Never log request bodies, cookies, bearer tokens, webhook secrets, or API keys.

🧰 Tools
🪛 Luacheck (1.2.0)

[warning] 76-76: shadowing definition of variable 'err' on line 62

(W421)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 75 - 85, When MATRIX_INCLUDE_HEADERS is
enabled in matrix.lua, avoid forwarding raw headers in the loop that appends to
data["formatted_body"] and data["body"]; instead implement an allow-list (e.g.,
only include common safe headers like "User-Agent", "Accept", "Host") or
explicitly redact sensitive names (e.g., "Authorization", "Cookie",
"Set-Cookie", "X-CSRF-Token", "Proxy-Authorization", "Authentication") before
serialising, and in the for header,value in pairs(headers) block replace
sensitive values with a placeholder like "[REDACTED]" (or skip adding those
headers entirely) so no bearer tokens, cookies, CSRF tokens, or secrets are sent
to Matrix.

end
data["formatted_body"] = data["formatted_body"] .. "</table>"
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)
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)
Comment on lines +111 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /workspace && find . -name "matrix.lua" -type f

Repository: bunkerity/bunkerweb-plugins

Length of output: 131


🏁 Script executed:

head -n 200 matrix/matrix.lua | tail -n +100

Repository: bunkerity/bunkerweb-plugins

Length of output: 3351


🌐 Web query:

Matrix client API transaction ID idempotency specification

💡 Result:

The Matrix Client-Server API uses client-generated transaction IDs (txnId) in the URL path of HTTP PUT requests to ensure idempotency. This prevents duplicate processing when clients retry requests due to network issues or timeouts. Key aspects of the specification: - Purpose: Distinguishes new requests from retransmissions, making requests idempotent. - Client behavior: Generate a unique txnId for each new request (recommended: v4 UUIDs or timestamp + monotonically increasing integer). Change txnId after each request completes. - Server behavior: If the txnId and full HTTP path match a previous request, treat as retransmission: return the identical HTTP status code and response body as the original (e.g., 200 OK with the original event_id for room send endpoints). Ignore the duplicate request. - Scope: Per device and per HTTP endpoint. Different endpoints (e.g., /rooms/{roomId}/send/{eventType}/{txnId} vs /sendToDevice/{eventType}/{txnId}) are distinct even with same txnId. Scoped to access token/device: re-login with new device creates new scope; token refresh retains scope. - POST endpoints: Some allow POST without txnId; PUT with txnId strongly recommended where optional. - Changes: Clarified in ~v1.7 (MSC via #1449). Improved txnId recommendations in v1.12 (#1888). Example endpoint: PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId} This applies to state events, room messages, send-to-device, etc. No separate /transactions endpoint for client-server; that's server-server.

Citations:


Use a collision-resistant transaction ID for Matrix idempotency keys.

The Matrix Client-Server API treats transaction IDs (txnId) as idempotency keys: when the server receives a request with the same txnId and endpoint path, it returns the cached response (same status code and body) rather than reprocessing. Using os.time() at lines 114 and 178 only provides second-level precision—multiple requests sent within the same second will share an identical txn_id, causing the Matrix server to collapse them into a single message.

Generate a unique ID per send instead. Options include:

  • Concatenate os.time() with ngx.var.msec for millisecond precision (if available)
  • Use ngx.var.request_id if per-request uniqueness suffices
  • Implement a monotonic counter within the plugin state (with careful locking for concurrent worker access)

Lines 114 and 178 both require fixes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 111 - 115, The txn_id currently set via
tostring(os.time()) is not collision-resistant; replace it with a per-send
unique id and update both places where txn_id is generated/used (the variable
named txn_id in matrix.lua). For example, build txn_id by concatenating
os.time() with a higher-resolution or unique per-request value: use ngx.var.msec
if available (tostring(os.time()) .. "-" .. ngx.var.msec), or ngx.var.request_id
when present, or combine ngx.now()*1000 with a short random suffix to guarantee
uniqueness across rapid successive sends; ensure both locations that set txn_id
use the same new generation logic so Matrix idempotency keys are unique per
send.

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
}
})
Comment on lines +124 to +131
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Locate the file and check its existence
find . -name "matrix.lua" -type f

Repository: bunkerity/bunkerweb-plugins

Length of output: 90


🏁 Script executed:

# Read the relevant sections of the file
if [ -f "matrix/matrix.lua" ]; then
  echo "=== Lines 124-131 ===" && sed -n '124,131p' matrix/matrix.lua
  echo ""
  echo "=== Lines 190-197 ===" && sed -n '190,197p' matrix/matrix.lua
  echo ""
  echo "=== Full file context (first check size) ===" && wc -l matrix/matrix.lua
fi

Repository: bunkerity/bunkerweb-plugins

Length of output: 662


🏁 Script executed:

# Search for all httpc:request_uri calls to understand timeout usage
rg "httpc:request_uri" -A 5 matrix/

Repository: bunkerity/bunkerweb-plugins

Length of output: 723


🏁 Script executed:

# Check how other plugins (coraza, virustotal, clamav) handle HTTP timeouts
rg "set_timeout|timeout" -A 2 -B 2 --type lua | head -100

Repository: bunkerity/bunkerweb-plugins

Length of output: 1683


🏁 Script executed:

# Search for resty.http usage in other plugins to see timeout patterns
rg "httpc.*=" -A 10 --type lua | grep -A 10 "resty.http" | head -80

Repository: bunkerity/bunkerweb-plugins

Length of output: 53


🏁 Script executed:

# Look for timeout patterns in virustotal.lua and coraza.lua specifically
for file in virustotal.lua coraza.lua clamav.lua; do
  echo "=== $file ===" 
  if [ -f "*/$file" ]; then
    find . -name "$file" -exec cat {} \; | grep -A 5 -B 5 "timeout\|request_uri" | head -50
  fi
done

Repository: bunkerity/bunkerweb-plugins

Length of output: 131


🏁 Script executed:

# Search for httpc initialization and set_timeout calls
rg "set_timeout|connect_timeout|send_timeout|read_timeout" --type lua

Repository: bunkerity/bunkerweb-plugins

Length of output: 496


Add explicit HTTP timeouts to Matrix requests.

Both httpc:request_uri() calls (lines 124–131 and 190–197) lack timeout configuration. Since these requests run on the worker thread, an unresponsive homeserver can stall processing and exhaust worker capacity. Call httpc:set_timeout() before request_uri() to match the pattern used in coraza and crowdsec plugins — for example, httpc:set_timeout(1000) for a 1-second timeout, or httpc:set_timeouts() for separate connect/send/read timeouts if configurable durations are preferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 124 - 131, The Matrix HTTP calls use
httpc:request_uri() without timeouts; before each request_uri call (e.g., the
PUT call that sends post_data to url using access_token and the other
request_uri call later) call httpc:set_timeout(1000) or httpc:set_timeouts(...)
to enforce a 1s (or configured) timeout so the worker thread won't block; update
both places that call httpc:request_uri() to set the timeout on the httpc
instance immediately before invoking request_uri().

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
Comment on lines +106 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Return immediately after HTTP client/request failures.

In both send paths, if http_new() or request_uri() fails, the code still calls httpc:close() and dereferences res.status. That turns an upstream failure into a Lua exception instead of a clean error path. Guard the nil branches and return before touching httpc or res.

Also applies to: 185-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@matrix/matrix.lua` around lines 106 - 136, The send path currently continues
after http_new() or httpc:request_uri() failures, which causes httpc:close() and
res.status dereferences on nil; update the logic in the Matrix send function(s)
to immediately return an error when http_new() fails (http_new -> httpc nil) and
likewise after request_uri() fails (res nil), ensuring you do not call
httpc:close() or access res.status in those failure branches; specifically, in
the code using http_new(), httpc, httpc:request_uri, httpc:close and res.status
(also apply the same fix to the second send path around the block referenced at
185-203), log the error and return early before touching httpc or res.

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
63 changes: 63 additions & 0 deletions matrix/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"id": "matrix",
"name": "Matrix",
"description": "Send alerts to a Matrix room via the Matrix API.",
"version": "1.1",
"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"
}
}
}
Loading