Software /
code /
prosody-modules
Changeset
6263:10a1016d1c3a
Merge update
author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
---|---|
date | Sun, 01 Jun 2025 11:43:16 +0700 |
parents | 6262:a72388da5cd4 (current diff) 6261:8c9eb4b6d02f (diff) |
children | 6273:8ceedc336d0d |
files | |
diffstat | 37 files changed, 974 insertions(+), 908 deletions(-) [+] |
line wrap: on
line diff
--- a/misc/grafana/prosody-dashboard.json Sun Jun 01 11:41:42 2025 +0700 +++ b/misc/grafana/prosody-dashboard.json Sun Jun 01 11:43:16 2025 +0700 @@ -1,11 +1,9 @@ { - "description" : "", "editable" : true, "fiscalYearStartMonth" : 0, "graphTooltip" : 1, "id" : 1, "links" : [], - "liveNow" : false, "panels" : [ { "collapsed" : false, @@ -21,6 +19,7 @@ "type" : "row" }, { + "description" : "This panel requires [mod_measure_process](https://modules.prosody.im/mod_measure_process.html)", "fieldConfig" : { "defaults" : { "color" : { @@ -34,6 +33,7 @@ "axisLabel" : "", "axisPlacement" : "right", "barAlignment" : 0, + "barWidthFactor" : 0.6, "drawStyle" : "line", "fillOpacity" : 10, "gradientMode" : "scheme", @@ -68,8 +68,7 @@ "mode" : "absolute", "steps" : [ { - "color" : "green", - "value" : null + "color" : "green" }, { "color" : "red", @@ -96,26 +95,33 @@ "showLegend" : true }, "tooltip" : { + "hideZeros" : false, "mode" : "single", "sort" : "none" } }, - "pluginVersion" : "8.2.5", + "pluginVersion" : "11.6.0", "targets" : [ { + "disableTextWrap" : false, + "editorMode" : "builder", "exemplar" : true, - "expr" : "rate(process_cpu_seconds_total{job=\"prosody\"}[$__interval])", + "expr" : "rate(process_cpu_seconds_total{job=~\"$job\",instance=~\"$instance\"}[$__rate_interval])", + "fullMetaSearch" : false, + "includeNullMetadata" : true, "instant" : false, "interval" : "", "intervalFactor" : 2, "legendFormat" : "usage", - "refId" : "A" + "refId" : "A", + "useBackend" : false } ], "title" : "CPU", "type" : "timeseries" }, { + "description" : "This panel requires [mod_measure_lua](https://modules.prosody.im/mod_measure_lua.html), [mod_measure_malloc](https://modules.prosody.im/mod_measure_malloc.html), and [mod_measure_process](https://modules.prosody.im/mod_measure_process.html),", "fieldConfig" : { "defaults" : { "color" : { @@ -128,6 +134,7 @@ "axisLabel" : "", "axisPlacement" : "right", "barAlignment" : 0, + "barWidthFactor" : 0.6, "drawStyle" : "line", "fillOpacity" : 10, "gradientMode" : "none", @@ -162,8 +169,7 @@ "mode" : "percentage", "steps" : [ { - "color" : "green", - "value" : null + "color" : "green" } ] }, @@ -177,9 +183,9 @@ "options" : { "mode" : "exclude", "names" : [ + "Lua", "RSS", - "Used", - "Lua" + "Used" ], "prefix" : "All except:", "readOnly" : true @@ -213,60 +219,79 @@ "showLegend" : true }, "tooltip" : { + "hideZeros" : false, "mode" : "single", "sort" : "none" } }, - "pluginVersion" : "8.2.5", + "pluginVersion" : "11.6.0", "targets" : [ { + "disableTextWrap" : false, + "editorMode" : "code", "exemplar" : true, - "expr" : "max_over_time(process_virtual_memory_bytes{job=\"prosody\"}[$__interval])", + "expr" : "max_over_time(process_virtual_memory_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "fullMetaSearch" : false, "hide" : false, + "includeNullMetadata" : true, "interval" : "", "intervalFactor" : 1, "legendFormat" : "Virtual", - "refId" : "VIRT" + "range" : true, + "refId" : "VIRT", + "useBackend" : false }, { "exemplar" : false, - "expr" : "max_over_time(process_resident_memory_bytes{job=\"prosody\"}[$__interval])", + "expr" : "max_over_time(process_resident_memory_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "interval" : "", "legendFormat" : "RSS", "refId" : "RSS" }, { "exemplar" : false, - "expr" : "max_over_time(malloc_heap_allocated_bytes{job=\"prosody\"}[$__interval])", + "expr" : "max_over_time(malloc_heap_allocated_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "hide" : false, "interval" : "", "legendFormat" : "Allocated ({{mode}})", "refId" : "Malloc allocated" }, { + "editorMode" : "code", "exemplar" : false, - "expr" : "max_over_time(malloc_heap_used_bytes{job=\"prosody\"}[$__interval])", + "expr" : "max_over_time(malloc_heap_used_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "hide" : false, "interval" : "", "legendFormat" : "Used", + "range" : true, "refId" : "Malloc Used" }, { + "editorMode" : "code", "exemplar" : false, - "expr" : "max_over_time(lua_heap_bytes{job=\"prosody\"}[$__interval])", + "expr" : "max_over_time(lua_heap_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "hide" : false, "interval" : "", "legendFormat" : "Lua", + "range" : true, "refId" : "Lua" }, { "exemplar" : false, - "expr" : "sum(lua_heap_bytes{job=\"prosody\"}) / (sum(prosody_mod_c2s__connections{job=\"prosody\"})+sum(prosody_mod_s2s__connections_inbound{job=\"prosody\"})+sum(prosody_mod_s2s__connections_outbound{job=\"prosody\"}))", + "expr" : "sum(lua_heap_bytes{job=~\"$job\",instance=~\"$instance\"}) / (\nsum(prosody_mod_c2s__connections{job=~\"$job\",instance=~\"$instance\"}) +\nsum(prosody_mod_s2s__connections_inbound{job=~\"$job\",instance=~\"$instance\"}) +\nsum(prosody_mod_s2s__connections_outbound{job=~\"$job\",instance=~\"$instance\"})\n)", "hide" : false, "interval" : "", "legendFormat" : "Lua (per connection)", "refId" : "LuaPerConn" + }, + { + "editorMode" : "code", + "expr" : "max_over_time(malloc_heap_unused_bytes{job=~\"$job\",instance=~\"$instance\"}[$__interval])", + "hide" : false, + "instant" : false, + "legendFormat" : "Unused", + "range" : true, + "refId" : "A" } ], "title" : "Memory", @@ -298,6 +323,7 @@ "axisLabel" : "", "axisPlacement" : "right", "barAlignment" : 0, + "barWidthFactor" : 0.6, "drawStyle" : "line", "fillOpacity" : 100, "gradientMode" : "none", @@ -329,8 +355,7 @@ "mode" : "absolute", "steps" : [ { - "color" : "green", - "value" : null + "color" : "green" }, { "color" : "red", @@ -357,14 +382,16 @@ "showLegend" : true }, "tooltip" : { + "hideZeros" : false, "mode" : "single", "sort" : "none" } }, + "pluginVersion" : "11.6.0", "targets" : [ { "exemplar" : true, - "expr" : "prosody_mod_c2s__connections{type=\"c2s\"}", + "expr" : "prosody_mod_c2s__connections{job=~\"$job\",instance=~\"$instance\",type=\"c2s\"}", "interval" : "", "legendFormat" : "{{ip_family}} {{type}}", "refId" : "c2s" @@ -386,6 +413,7 @@ "axisLabel" : "", "axisPlacement" : "right", "barAlignment" : 0, + "barWidthFactor" : 0.6, "drawStyle" : "line", "fillOpacity" : 100, "gradientMode" : "none", @@ -417,8 +445,7 @@ "mode" : "absolute", "steps" : [ { - "color" : "green", - "value" : null + "color" : "green" }, { "color" : "red", @@ -445,21 +472,23 @@ "showLegend" : true }, "tooltip" : { + "hideZeros" : false, "mode" : "single", "sort" : "none" } }, + "pluginVersion" : "11.6.0", "targets" : [ { "exemplar" : true, - "expr" : "sum(prosody_mod_s2s__connections_inbound{type=\"s2sin\"}) by (ip_family)", + "expr" : "sum(prosody_mod_s2s__connections_inbound{job=~\"$job\",instance=~\"$instance\",type=\"s2sin\"}) by (ip_family)", "interval" : "", "legendFormat" : "{{ip_family}} s2sin", "refId" : "s2sin" }, { "exemplar" : true, - "expr" : "sum(prosody_mod_s2s__connections_outbound{type=\"s2sout\"}) by (ip_family)", + "expr" : "sum(prosody_mod_s2s__connections_outbound{job=~\"$job\",instance=~\"$instance\",type=\"s2sout\"}) by (ip_family)", "hide" : false, "interval" : "", "legendFormat" : "{{ip_family}} s2sout", @@ -517,7 +546,8 @@ "layout" : "auto" }, "tooltip" : { - "show" : true, + "mode" : "single", + "showColorScale" : false, "yHistogram" : false }, "yAxis" : { @@ -525,13 +555,13 @@ "reverse" : false } }, - "pluginVersion" : "10.2.2", + "pluginVersion" : "11.6.0", "targets" : [ { "disableTextWrap" : false, "editorMode" : "builder", "exemplar" : false, - "expr" : "changes(prosody_mod_c2s__encrypted_total[$__interval])", + "expr" : "changes(prosody_mod_c2s__encrypted_total{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "fullMetaSearch" : false, "includeNullMetadata" : true, "interval" : "10m", @@ -592,7 +622,8 @@ "layout" : "auto" }, "tooltip" : { - "show" : true, + "mode" : "single", + "showColorScale" : false, "yHistogram" : false }, "yAxis" : { @@ -600,13 +631,13 @@ "reverse" : false } }, - "pluginVersion" : "10.2.2", + "pluginVersion" : "11.6.0", "targets" : [ { "disableTextWrap" : false, "editorMode" : "builder", "exemplar" : false, - "expr" : "changes(prosody_mod_s2s__encrypted_total[$__interval])", + "expr" : "changes(prosody_mod_s2s__encrypted_total{job=~\"$job\",instance=~\"$instance\"}[$__interval])", "format" : "time_series", "fullMetaSearch" : false, "includeNullMetadata" : true, @@ -635,15 +666,6 @@ "type" : "row" }, { - "cards" : {}, - "color" : { - "cardColor" : "#b4ff00", - "colorScale" : "sqrt", - "colorScheme" : "interpolateRdYlGn", - "exponent" : 0.5, - "mode" : "spectrum" - }, - "dataFormat" : "tsbuckets", "description" : "How long a session has been hibernating when a client resumes it", "fieldConfig" : { "defaults" : { @@ -666,13 +688,7 @@ "x" : 0, "y" : 27 }, - "heatmap" : {}, - "hideZeroBuckets" : true, - "highlightCards" : true, "id" : 14, - "legend" : { - "show" : true - }, "options" : { "calculate" : false, "calculation" : {}, @@ -701,7 +717,8 @@ }, "showValue" : "never", "tooltip" : { - "show" : true, + "mode" : "single", + "showColorScale" : false, "yHistogram" : false }, "yAxis" : { @@ -711,18 +728,17 @@ "unit" : "clocks" } }, - "pluginVersion" : "10.2.2", - "reverseYBuckets" : false, + "pluginVersion" : "11.6.0", "targets" : [ { "disableTextWrap" : false, "editorMode" : "builder", "exemplar" : true, - "expr" : "sum by(le) (changes(prosody_mod_smacks__resumption_age_seconds_bucket{host=\"$virtualhost\"}[$__interval]))", + "expr" : "rate(prosody_mod_smacks__resumption_age_seconds_bucket{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", "format" : "heatmap", "fullMetaSearch" : false, - "includeNullMetadata" : true, - "interval" : "600", + "includeNullMetadata" : false, + "interval" : "", "legendFormat" : "{{le}}", "range" : true, "refId" : "A", @@ -730,20 +746,7 @@ } ], "title" : "Resumption Age", - "tooltip" : { - "show" : true, - "showHistogram" : false - }, - "type" : "heatmap", - "xAxis" : { - "show" : true - }, - "yAxis" : { - "format" : "clocks", - "logBase" : 1, - "show" : true - }, - "yBucketBound" : "upper" + "type" : "heatmap" }, { "fieldConfig" : { @@ -758,6 +761,7 @@ "axisLabel" : "", "axisPlacement" : "auto", "barAlignment" : 0, + "barWidthFactor" : 0.6, "drawStyle" : "line", "fillOpacity" : 0, "gradientMode" : "none", @@ -788,118 +792,6 @@ "mode" : "absolute", "steps" : [ { - "color" : "green", - "value" : null - }, - { - "color" : "red", - "value" : 80 - } - ] - }, - "unit" : "pps" - }, - "overrides" : [] - }, - "gridPos" : { - "h" : 8, - "w" : 12, - "x" : 12, - "y" : 27 - }, - "id" : 16, - "options" : { - "legend" : { - "calcs" : [], - "displayMode" : "list", - "placement" : "bottom", - "showLegend" : true - }, - "tooltip" : { - "mode" : "single", - "sort" : "none" - } - }, - "targets" : [ - { - "editorMode" : "code", - "exemplar" : true, - "expr" : "rate(prosody_mod_smacks__tx_queued_stanzas_total{host=\"$virtualhost\"}[$__interval])", - "interval" : "600", - "legendFormat" : "queued on {{host}}", - "range" : true, - "refId" : "A" - }, - { - "editorMode" : "builder", - "exemplar" : true, - "expr" : "rate(prosody_mod_smacks__tx_acked_stanzas_count{host=\"$virtualhost\"}[$__interval])", - "hide" : false, - "interval" : "600", - "legendFormat" : "acked on {{host}}", - "range" : true, - "refId" : "B" - } - ], - "title" : "Stanzas", - "type" : "timeseries" - }, - { - "collapsed" : false, - "gridPos" : { - "h" : 1, - "w" : 24, - "x" : 0, - "y" : 35 - }, - "id" : 28, - "panels" : [], - "title" : "Mobile optimizations", - "type" : "row" - }, - { - "fieldConfig" : { - "defaults" : { - "color" : { - "mode" : "palette-classic" - }, - "custom" : { - "axisBorderShow" : false, - "axisCenteredZero" : false, - "axisColorMode" : "text", - "axisLabel" : "", - "axisPlacement" : "auto", - "barAlignment" : 0, - "drawStyle" : "points", - "fillOpacity" : 0, - "gradientMode" : "none", - "hideFrom" : { - "legend" : false, - "tooltip" : false, - "viz" : false - }, - "insertNulls" : false, - "lineInterpolation" : "linear", - "lineWidth" : 1, - "pointSize" : 5, - "scaleDistribution" : { - "type" : "linear" - }, - "showPoints" : "auto", - "spanNulls" : false, - "stacking" : { - "group" : "A", - "mode" : "none" - }, - "thresholdsStyle" : { - "mode" : "off" - } - }, - "mappings" : [], - "thresholds" : { - "mode" : "absolute", - "steps" : [ - { "color" : "green" }, { @@ -908,7 +800,89 @@ } ] }, - "unit" : "s" + "unit" : "pps" + }, + "overrides" : [] + }, + "gridPos" : { + "h" : 8, + "w" : 12, + "x" : 12, + "y" : 27 + }, + "id" : 16, + "options" : { + "legend" : { + "calcs" : [], + "displayMode" : "list", + "placement" : "bottom", + "showLegend" : true + }, + "tooltip" : { + "hideZeros" : false, + "mode" : "single", + "sort" : "none" + } + }, + "pluginVersion" : "11.6.0", + "targets" : [ + { + "disableTextWrap" : false, + "editorMode" : "builder", + "exemplar" : true, + "expr" : "rate(prosody_mod_smacks__tx_queued_stanzas_total{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__interval])", + "fullMetaSearch" : false, + "includeNullMetadata" : true, + "interval" : "600", + "legendFormat" : "queued on {{host}}", + "range" : true, + "refId" : "A", + "useBackend" : false + }, + { + "disableTextWrap" : false, + "editorMode" : "builder", + "exemplar" : true, + "expr" : "rate(prosody_mod_smacks__tx_acked_stanzas_count{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__interval])", + "fullMetaSearch" : false, + "hide" : false, + "includeNullMetadata" : true, + "interval" : "600", + "legendFormat" : "acked on {{host}}", + "range" : true, + "refId" : "B", + "useBackend" : false + } + ], + "title" : "Stanzas", + "type" : "timeseries" + }, + { + "collapsed" : false, + "gridPos" : { + "h" : 1, + "w" : 24, + "x" : 0, + "y" : 35 + }, + "id" : 28, + "panels" : [], + "title" : "Mobile optimizations", + "type" : "row" + }, + { + "fieldConfig" : { + "defaults" : { + "custom" : { + "hideFrom" : { + "legend" : false, + "tooltip" : false, + "viz" : false + }, + "scaleDistribution" : { + "type" : "linear" + } + } }, "overrides" : [] }, @@ -920,32 +894,135 @@ }, "id" : 18, "options" : { + "calculate" : false, + "cellGap" : 1, + "color" : { + "exponent" : 0.5, + "fill" : "dark-orange", + "mode" : "scheme", + "reverse" : false, + "scale" : "exponential", + "scheme" : "RdYlGn", + "steps" : 64 + }, + "exemplars" : { + "color" : "rgba(255,0,255,0.7)" + }, + "filterValues" : { + "le" : 1e-09 + }, "legend" : { - "calcs" : [], - "displayMode" : "list", - "placement" : "bottom", - "showLegend" : true + "show" : true + }, + "rowsFrame" : { + "layout" : "auto" }, "tooltip" : { "mode" : "single", - "sort" : "none" + "showColorScale" : false, + "yHistogram" : false + }, + "yAxis" : { + "axisPlacement" : "left", + "reverse" : false, + "unit" : "s" } }, + "pluginVersion" : "11.6.0", "targets" : [ { + "disableTextWrap" : false, "editorMode" : "builder", "exemplar" : false, - "expr" : "histogram_quantile(0.95, sum by(le) (rate(prosody_mod_csi_simple__buffer_hold_seconds_bucket{host=\"$virtualhost\"}[$__rate_interval])))", - "format" : "time_series", + "expr" : "rate(prosody_mod_csi_simple__buffer_hold_seconds_bucket{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", + "format" : "heatmap", + "fullMetaSearch" : false, + "includeNullMetadata" : false, "instant" : false, "interval" : "", - "legendFormat" : "CSI hold seconds", + "legendFormat" : "__auto", "range" : true, - "refId" : "A" + "refId" : "A", + "useBackend" : false } ], "title" : "Hold time", - "type" : "timeseries" + "type" : "heatmap" + }, + { + "fieldConfig" : { + "defaults" : { + "custom" : { + "hideFrom" : { + "legend" : false, + "tooltip" : false, + "viz" : false + }, + "scaleDistribution" : { + "type" : "linear" + } + } + }, + "overrides" : [] + }, + "gridPos" : { + "h" : 8, + "w" : 12, + "x" : 12, + "y" : 36 + }, + "id" : 20, + "options" : { + "calculate" : false, + "cellGap" : 1, + "color" : { + "exponent" : 0.5, + "fill" : "dark-orange", + "mode" : "scheme", + "reverse" : false, + "scale" : "exponential", + "scheme" : "PRGn", + "steps" : 64 + }, + "exemplars" : { + "color" : "rgba(255,0,255,0.7)" + }, + "filterValues" : { + "le" : 1e-09 + }, + "legend" : { + "show" : true + }, + "rowsFrame" : { + "layout" : "auto" + }, + "tooltip" : { + "mode" : "single", + "showColorScale" : false, + "yHistogram" : false + }, + "yAxis" : { + "axisPlacement" : "left", + "reverse" : false + } + }, + "pluginVersion" : "11.6.0", + "targets" : [ + { + "disableTextWrap" : false, + "editorMode" : "builder", + "expr" : "rate(prosody_mod_csi_simple__flush_stanza_count_bucket{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", + "format" : "heatmap", + "fullMetaSearch" : false, + "includeNullMetadata" : false, + "legendFormat" : "Stanzas flushed", + "range" : true, + "refId" : "A", + "useBackend" : false + } + ], + "title" : "Flush sizes", + "type" : "heatmap" }, { "fieldConfig" : { @@ -960,7 +1037,103 @@ "axisLabel" : "", "axisPlacement" : "auto", "barAlignment" : 0, - "drawStyle" : "points", + "barWidthFactor" : 0.6, + "drawStyle" : "line", + "fillOpacity" : 50, + "gradientMode" : "none", + "hideFrom" : { + "legend" : false, + "tooltip" : false, + "viz" : false + }, + "insertNulls" : false, + "lineInterpolation" : "smooth", + "lineWidth" : 1, + "pointSize" : 5, + "scaleDistribution" : { + "type" : "linear" + }, + "showPoints" : "auto", + "spanNulls" : false, + "stacking" : { + "group" : "A", + "mode" : "percent" + }, + "thresholdsStyle" : { + "mode" : "off" + } + }, + "mappings" : [], + "thresholds" : { + "mode" : "absolute", + "steps" : [ + { + "color" : "green" + }, + { + "color" : "red", + "value" : 80 + } + ] + }, + "unit" : "none" + }, + "overrides" : [] + }, + "gridPos" : { + "h" : 8, + "w" : 12, + "x" : 0, + "y" : 44 + }, + "id" : 32, + "options" : { + "legend" : { + "calcs" : [], + "displayMode" : "list", + "placement" : "bottom", + "showLegend" : true + }, + "tooltip" : { + "hideZeros" : false, + "mode" : "single", + "sort" : "none" + } + }, + "pluginVersion" : "11.6.0", + "targets" : [ + { + "disableTextWrap" : false, + "editorMode" : "builder", + "expr" : "avg_over_time(prosody_mod_csi__state_sessions{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__interval])", + "format" : "time_series", + "fullMetaSearch" : false, + "includeNullMetadata" : true, + "legendFormat" : "{{csi_state}}", + "range" : true, + "refId" : "A", + "useBackend" : false + } + ], + "title" : "CSI States", + "type" : "timeseries" + }, + { + "description" : "This panel requires [mod_measure_conn_buffers](https://modules.prosody.im/mod_measure_conn_buffers.html)", + "fieldConfig" : { + "defaults" : { + "color" : { + "mode" : "palette-classic" + }, + "custom" : { + "axisBorderShow" : false, + "axisCenteredZero" : false, + "axisColorMode" : "text", + "axisLabel" : "", + "axisPlacement" : "auto", + "barAlignment" : 0, + "barWidthFactor" : 0.6, + "drawStyle" : "line", "fillOpacity" : 0, "gradientMode" : "none", "hideFrom" : { @@ -998,7 +1171,7 @@ } ] }, - "unit" : "none" + "unit" : "bytes" }, "overrides" : [] }, @@ -1006,9 +1179,9 @@ "h" : 8, "w" : 12, "x" : 12, - "y" : 36 + "y" : 44 }, - "id" : 20, + "id" : 33, "options" : { "legend" : { "calcs" : [], @@ -1017,21 +1190,26 @@ "showLegend" : true }, "tooltip" : { + "hideZeros" : false, "mode" : "single", "sort" : "none" } }, + "pluginVersion" : "11.6.0", "targets" : [ { + "disableTextWrap" : false, "editorMode" : "builder", - "expr" : "histogram_quantile(0.95, sum by(le) (rate(prosody_mod_csi_simple__flush_stanza_count_bucket{host=\"$virtualhost\"}[$__rate_interval])))", - "format" : "time_series", - "legendFormat" : "Stanzas flushed", + "expr" : "max_over_time(prosody_mod_measure_conn_buffers__total_pending_tx{job=~\"$job\",instance=~\"$instance\",host=\"\"}[$__interval])", + "fullMetaSearch" : false, + "includeNullMetadata" : true, + "legendFormat" : "{{instance}}", "range" : true, - "refId" : "A" + "refId" : "A", + "useBackend" : false } ], - "title" : "Flush sizes", + "title" : "Connection Buffers", "type" : "timeseries" }, { @@ -1040,7 +1218,7 @@ "h" : 1, "w" : 24, "x" : 0, - "y" : 44 + "y" : 52 }, "id" : 22, "panels" : [], @@ -1048,17 +1226,6 @@ "type" : "row" }, { - "cards" : {}, - "color" : { - "cardColor" : "#b4ff00", - "colorScale" : "sqrt", - "colorScheme" : "interpolateRdYlGn", - "exponent" : 0.5, - "max" : 5, - "min" : 0, - "mode" : "opacity" - }, - "dataFormat" : "tsbuckets", "fieldConfig" : { "defaults" : { "custom" : { @@ -1078,20 +1245,16 @@ "h" : 8, "w" : 18, "x" : 0, - "y" : 45 + "y" : 53 }, - "heatmap" : {}, - "hideZeroBuckets" : false, - "highlightCards" : true, "id" : 8, - "legend" : { - "show" : false - }, "options" : { "calculate" : false, "calculation" : {}, "cellGap" : 2, - "cellValues" : {}, + "cellValues" : { + "decimals" : 0 + }, "color" : { "exponent" : 0.5, "fill" : "#b4ff00", @@ -1117,7 +1280,8 @@ }, "showValue" : "never", "tooltip" : { - "show" : true, + "mode" : "single", + "showColorScale" : false, "yHistogram" : false }, "yAxis" : { @@ -1126,18 +1290,17 @@ "unit" : "bytes" } }, - "pluginVersion" : "10.2.0", - "reverseYBuckets" : false, + "pluginVersion" : "11.6.0", "targets" : [ { "disableTextWrap" : false, "editorMode" : "builder", "exemplar" : true, - "expr" : "sum by(le) (changes(prosody_mod_http_file_share__upload_bytes_bucket{host=\"$virtualhost\"}[$__interval]))", + "expr" : "sum by(le) (increase(prosody_mod_http_file_share__upload_bytes_bucket{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[1h]))", "format" : "heatmap", "fullMetaSearch" : false, "includeNullMetadata" : true, - "interval" : "3600s", + "interval" : "1h", "intervalFactor" : 1, "legendFormat" : "{{le}}", "range" : true, @@ -1146,20 +1309,7 @@ } ], "title" : "Upload sizes", - "tooltip" : { - "show" : true, - "showHistogram" : false - }, - "type" : "heatmap", - "xAxis" : { - "show" : true - }, - "yAxis" : { - "format" : "bytes", - "logBase" : 1, - "show" : true - }, - "yBucketBound" : "upper" + "type" : "heatmap" }, { "fieldConfig" : { @@ -1186,7 +1336,7 @@ "h" : 8, "w" : 6, "x" : 18, - "y" : 45 + "y" : 53 }, "id" : 10, "options" : { @@ -1202,14 +1352,15 @@ }, "showThresholdLabels" : false, "showThresholdMarkers" : false, + "sizing" : "auto", "text" : {} }, - "pluginVersion" : "10.2.0", + "pluginVersion" : "11.6.0", "targets" : [ { "editorMode" : "builder", "exemplar" : true, - "expr" : "prosody_mod_http_file_share__total_storage_bytes{host=\"$virtualhost\"}", + "expr" : "prosody_mod_http_file_share__total_storage_bytes{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}", "instant" : false, "interval" : "", "legendFormat" : "", @@ -1218,10 +1369,226 @@ ], "title" : "Total uploads", "type" : "gauge" + }, + { + "collapsed" : false, + "gridPos" : { + "h" : 1, + "w" : 24, + "x" : 0, + "y" : 61 + }, + "id" : 34, + "panels" : [], + "title" : "Spam", + "type" : "row" + }, + { + "description" : "This panel requires [mod_anti_spam](https://modules.prosody.im/mod_anti_spam.html)", + "fieldConfig" : { + "defaults" : { + "color" : { + "mode" : "palette-classic" + }, + "custom" : { + "axisBorderShow" : false, + "axisCenteredZero" : false, + "axisColorMode" : "text", + "axisLabel" : "", + "axisPlacement" : "auto", + "barAlignment" : 0, + "barWidthFactor" : 0.6, + "drawStyle" : "line", + "fillOpacity" : 0, + "gradientMode" : "none", + "hideFrom" : { + "legend" : false, + "tooltip" : false, + "viz" : false + }, + "insertNulls" : false, + "lineInterpolation" : "linear", + "lineWidth" : 1, + "pointSize" : 5, + "scaleDistribution" : { + "type" : "linear" + }, + "showPoints" : "auto", + "spanNulls" : false, + "stacking" : { + "group" : "A", + "mode" : "none" + }, + "thresholdsStyle" : { + "mode" : "off" + } + }, + "mappings" : [], + "thresholds" : { + "mode" : "absolute", + "steps" : [ + { + "color" : "green" + }, + { + "color" : "red", + "value" : 80 + } + ] + }, + "unit" : "pps" + }, + "overrides" : [] + }, + "gridPos" : { + "h" : 8, + "w" : 12, + "x" : 0, + "y" : 62 + }, + "id" : 36, + "options" : { + "legend" : { + "calcs" : [], + "displayMode" : "list", + "placement" : "bottom", + "showLegend" : true + }, + "tooltip" : { + "hideZeros" : false, + "mode" : "single", + "sort" : "none" + } + }, + "pluginVersion" : "11.6.0", + "targets" : [ + { + "disableTextWrap" : false, + "editorMode" : "builder", + "expr" : "rate(prosody_mod_anti_spam__anti_spam_blocked_stanzas_total{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", + "fullMetaSearch" : false, + "includeNullMetadata" : true, + "legendFormat" : "{{reason}}", + "range" : true, + "refId" : "A", + "useBackend" : false + } + ], + "title" : "mod_anti_spam", + "type" : "timeseries" + }, + { + "description" : "This panel requires [mod_spam_reporting](https://modules.prosody.im/mod_spam_reporting.html) and [mod_report_forward](https://modules.prosody.im/mod_report_forward.html)", + "fieldConfig" : { + "defaults" : { + "color" : { + "mode" : "palette-classic" + }, + "custom" : { + "axisBorderShow" : false, + "axisCenteredZero" : false, + "axisColorMode" : "text", + "axisLabel" : "", + "axisPlacement" : "auto", + "barAlignment" : 0, + "barWidthFactor" : 1, + "drawStyle" : "line", + "fillOpacity" : 25, + "gradientMode" : "none", + "hideFrom" : { + "legend" : false, + "tooltip" : false, + "viz" : false + }, + "insertNulls" : false, + "lineInterpolation" : "smooth", + "lineWidth" : 1, + "pointSize" : 5, + "scaleDistribution" : { + "type" : "linear" + }, + "showPoints" : "auto", + "spanNulls" : false, + "stacking" : { + "group" : "A", + "mode" : "none" + }, + "thresholdsStyle" : { + "mode" : "off" + } + }, + "mappings" : [], + "thresholds" : { + "mode" : "absolute", + "steps" : [ + { + "color" : "green" + }, + { + "color" : "red", + "value" : 80 + } + ] + }, + "unit" : "pps" + }, + "overrides" : [] + }, + "gridPos" : { + "h" : 8, + "w" : 12, + "x" : 12, + "y" : 62 + }, + "id" : 35, + "options" : { + "legend" : { + "calcs" : [], + "displayMode" : "list", + "placement" : "bottom", + "showLegend" : true + }, + "tooltip" : { + "hideZeros" : false, + "mode" : "single", + "sort" : "none" + } + }, + "pluginVersion" : "11.6.0", + "targets" : [ + { + "disableTextWrap" : false, + "editorMode" : "builder", + "expr" : "rate(prosody_mod_spam_reporting__received_reports_total{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", + "fullMetaSearch" : false, + "includeNullMetadata" : true, + "interval" : "", + "legendFormat" : "{{report_type}} reports", + "range" : true, + "refId" : "A", + "useBackend" : false + }, + { + "disableTextWrap" : false, + "editorMode" : "code", + "expr" : "-rate(prosody_mod_report_forward__forwarded_reports_total{job=~\"$job\",instance=~\"$instance\",host=~\"$virtualhost\"}[$__rate_interval])", + "fullMetaSearch" : false, + "hide" : false, + "includeNullMetadata" : true, + "instant" : false, + "legendFormat" : "forwarded", + "range" : true, + "refId" : "B", + "useBackend" : false + } + ], + "title" : "Reports", + "type" : "timeseries" } ], + "preload" : false, "refresh" : "5m", - "schemaVersion" : 38, + "schemaVersion" : 41, "tags" : [ "prosody", "xmpp" @@ -1229,21 +1596,57 @@ "templating" : { "list" : [ { - "definition" : "label_values(host)", - "hide" : 0, - "includeAll" : false, - "label" : "VirtualHost", - "multi" : false, - "name" : "virtualhost", + "current" : { + "text" : "prosody", + "value" : "prosody" + }, + "definition" : "label_values(prosody_info,job)", + "label" : "Job", + "name" : "job", "options" : [], "query" : { - "query" : "label_values(host)", + "qryType" : 1, + "query" : "label_values(prosody_info,job)", "refId" : "PrometheusVariableQueryEditor-VariableQuery" }, "refresh" : 1, "regex" : "", - "skipUrlSync" : false, - "sort" : 0, + "type" : "query" + }, + { + "current" : { + "text" : "All", + "value" : [ + "$__all" + ] + }, + "definition" : "label_values(prosody_info{job=~\"$job\"},instance)", + "includeAll" : true, + "multi" : true, + "name" : "instance", + "options" : [], + "query" : { + "qryType" : 1, + "query" : "label_values(prosody_info{job=~\"$job\"},instance)", + "refId" : "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh" : 1, + "regex" : "", + "type" : "query" + }, + { + "definition" : "label_values({job=~\"$job\", instance=~\"$instance\"},host)", + "includeAll" : false, + "label" : "VirtualHost", + "name" : "virtualhost", + "options" : [], + "query" : { + "qryType" : 1, + "query" : "label_values({job=~\"$job\", instance=~\"$instance\"},host)", + "refId" : "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh" : 1, + "regex" : "", "type" : "query" } ] @@ -1256,6 +1659,5 @@ "timezone" : "", "title" : "Prosody", "uid" : "y1Onovt7z", - "version" : 91, - "weekStart" : "" + "version" : 134 }
--- a/mod_admin_web/admin_web/mod_admin_web.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_admin_web/admin_web/mod_admin_web.lua Sun Jun 01 11:43:16 2025 +0700 @@ -123,14 +123,8 @@ module:depends("admin_adhoc"); module:depends("http"); - local serve; - if not pcall(function () - local http_files = require "net.http.files"; - serve = http_files.serve; - end) then - serve = module:depends"http_files".serve; - end - local serve_file = serve { + local http_files = require "net.http.files"; + local serve_file = http_files.serve { path = module:get_directory() .. "/www_files"; };
--- a/mod_client_management/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_client_management/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -43,8 +43,8 @@ ## Compatibility -Requires Prosody trunk (as of 2023-03-29). Not compatible with Prosody 0.12 -and earlier. +Requires Prosody 13.0. +Not compatible with Prosody 0.12 and earlier. ## Developers
--- a/mod_client_management/mod_client_management.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_client_management/mod_client_management.lua Sun Jun 01 11:43:16 2025 +0700 @@ -430,18 +430,18 @@ -- Command -module:on_ready(function () - local console_env = module:shared("/*/admin_shell/env"); - if not console_env.user then return; end -- admin_shell probably not loaded +module:add_item("shell-command", { + section = "user"; + name = "clients"; + desc = "List a user's clients"; + args = { + { name = "jid"; type = "string" } + }; + host_selector = "jid"; + handler = function(self, user_jid) + local username = jid.split(user_jid); - function console_env.user:clients(user_jid) - local username, host = jid.split(user_jid); - local mod = prosody.hosts[host] and prosody.hosts[host].modules.client_management; - if not mod then - return false, ("Host does not exist on this server, or does not have mod_client_management loaded"); - end - - local clients = mod.get_active_clients(username); + local clients = get_active_clients(username); if not clients or #clients == 0 then return true, "No clients associated with this account"; end @@ -516,18 +516,24 @@ print(string.rep("-", self.session.width)); return true, ("%d clients"):format(#clients); end +}); - function console_env.user:revoke_client(user_jid, selector) -- luacheck: ignore 212/self - local username, host = jid.split(user_jid); - local mod = prosody.hosts[host] and prosody.hosts[host].modules.client_management; - if not mod then - return false, ("Host does not exist on this server, or does not have mod_client_management loaded"); - end +module:add_item("shell-command", { + section = "user"; + name = "revoke_client"; + desc = "Revoke access from a user's client"; + args = { + { name = "jid"; type = "string" }; + { name = "selector"; type = "string" }; + }; + host_selector = "jid"; + handler = function(self, user_jid, selector) -- luacheck: ignore 212/self + local username = jid.split(user_jid); - local revoked, err = revocation_errors.coerce(mod.revoke_client_access(username, selector)); + local revoked, err = revocation_errors.coerce(revoke_client_access(username, selector)); if not revoked then return false, err.text or err; end return true, "Client access revoked"; end -end); +});
--- a/mod_cloud_notify_encrypted/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_cloud_notify_encrypted/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -15,15 +15,19 @@ Details ======= -Add to modules_enabled, there are no configuration options. +Add to `modules_enabled`, there are no configuration options. -Depends on +When used with Prosody 0.12.x, it has an extra dependency on [luaossl](http://25thandclement.com/~william/projects/luaossl.html) which is available in Debian as [`lua-luaossl`](https://tracker.debian.org/pkg/lua-luaossl) or via `luarocks install luaossl`. -Compatibility -============= +Prosody 13.0.x and trunk does not require this. + +# Compatibility -Not tested, but hopefully works on 0.11.x and later. + Prosody Version Status + ----------------- ----------------------------------- + 13.0.x Works + 0.12.x Works (with `luaossl`, see above)
--- a/mod_cloud_notify_encrypted/mod_cloud_notify_encrypted.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_cloud_notify_encrypted/mod_cloud_notify_encrypted.lua Sun Jun 01 11:43:16 2025 +0700 @@ -1,13 +1,23 @@ local array = require "util.array"; local base64 = require "util.encodings".base64; local valid_utf8 = require "util.encodings".utf8.valid; -local ciphers = require "openssl.cipher"; +local have_crypto, crypto = pcall(require, "util.crypto"); local jid = require "util.jid"; local json = require "util.json"; local random = require "util.random"; local set = require "util.set"; local st = require "util.stanza"; +if not have_crypto then + local ossl_ciphers = require "openssl.cipher"; + crypto = {}; + -- FIXME: luaossl does not expose the EVP_CTRL_GCM_GET_TAG API, so we append 16 NUL bytes + -- Siskin does not validate the tag anyway. + function crypto.aes_128_gcm_encrypt(key, iv, message) + return ciphers.new("AES-128-GCM"):encrypt(key, iv):final(message)..string.rep("\0", 16); + end +end + local xmlns_jmi = "urn:xmpp:jingle-message:0"; local xmlns_jingle_apps_rtp = "urn:xmpp:jingle:apps:rtp:1"; local xmlns_push = "urn:xmpp:push:0"; @@ -127,9 +137,7 @@ local key_binary = base64.decode(encryption.key_base64); local push_json = json.encode(push_payload); - -- FIXME: luaossl does not expose the EVP_CTRL_GCM_GET_TAG API, so we append 16 NUL bytes - -- Siskin does not validate the tag anyway. - local encrypted_payload = base64.encode(ciphers.new("AES-128-GCM"):encrypt(key_binary, iv):final(push_json)..string.rep("\0", 16)); + local encrypted_payload = base64.encode(crypto.aes_128_gcm_encrypt(key_binary, iv, push_json)); local encrypted_element = st.stanza("encrypted", { xmlns = xmlns_push_encrypt, iv = base64.encode(iv) }) :text(encrypted_payload); if push_payload.type == "call" then
--- a/mod_conversejs/mod_conversejs.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_conversejs/mod_conversejs.lua Sun Jun 01 11:43:16 2025 +0700 @@ -28,17 +28,9 @@ local serve_dist = nil; local resources = module:get_option_path("conversejs_resources"); if resources then - local serve; - if prosody.process_type == "prosody" then - -- Prosody >= trunk / 0.12 - local http_files = require "net.http.files"; - serve = http_files.serve; - else - -- Prosody <= 0.11 - serve = module:depends "http_files".serve; - end + local http_files = require "net.http.files"; local mime_map = module:shared("/*/http_files/mime").types or {css = "text/css"; js = "application/javascript"}; - serve_dist = serve({path = resources; mime_map = mime_map}); + serve_dist = http_files.serve({path = resources; mime_map = mime_map}); cdn_url = module:http_url(); end @@ -118,10 +110,10 @@ local add_tags = module:get_option_array("conversejs_tags", {}); -local service_name = module:get_option_string("name", "Prosody IM and Converse.js"); -local service_short_name = module:get_option_string("short_name", "Converse"); -local service_description = module:get_option_string("description", "Messaging Freedom") -local pwa_color = module:get_option_string("pwa_color", "#397491") +local service_name = module:get_option_string("conversejs_name", module:get_option_string("name", "Prosody IM and Converse.js")); +local service_short_name = module:get_option_string("conversejs_short_name", "Converse"); +local service_description = module:get_option_string("conversejs_description", "Messaging Freedom") +local pwa_color = module:get_option_string("conversejs_pwa_color", "#397491") module:provides("http", { title = "Converse.js";
--- a/mod_dnsupdate/mod_dnsupdate.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_dnsupdate/mod_dnsupdate.lua Sun Jun 01 11:43:16 2025 +0700 @@ -1,6 +1,7 @@ module:set_global(); local config = require "core.configmanager"; +local modulemanager = require "core.modulemanager"; local argparse = require "util.argparse"; local dns = require"net.adns".resolver(); local async = require "util.async"; @@ -8,8 +9,7 @@ local nameprep = require"util.encodings".stringprep.nameprep; local idna_to_ascii = require"util.encodings".idna.to_ascii; -local virtualhost_services = { "xmpp-client"; "xmpps-client"; "xmpp-server"; "xmpps-server" } -local component_services = { "xmpp-server"; "xmpps-server" } +local services = { "xmpp-client"; "xmpps-client"; "xmpp-server"; "xmpps-server" } local function validate_dnsname_option(options, option_name, default) local host = options[option_name]; @@ -56,15 +56,11 @@ module:log("error", "Host %q fails IDNA", vhost); return 1; end - local is_component = config.get(vhost, "component_module"); - if not is_component and not config.get(vhost, "defined") then + if not config.get(vhost, "component_module") and not config.get(vhost, "defined") then module:log("error", "Host %q is not defined in the config", vhost); return 1; end - local services = virtualhost_services; - if is_component then services = component_services; end - local domain = validate_dnsname_option(opts, "domain"); if not domain then module:log("error", "--domain is required"); @@ -86,7 +82,17 @@ ["xmpps-server"] = module:get_option_array("s2s_direct_tls_ports", {}); }; - if opts.multiplex then + local modules_enabled = modulemanager.get_modules_for_host(vhost); + if not modules_enabled:contains("c2s") then + configured_ports["xmpp-client"] = {}; + configured_ports["xmpps-client"] = {}; + end + if not modules_enabled:contains("s2s") then + configured_ports["xmpp-server"] = {}; + configured_ports["xmpps-server"] = {}; + end + + if modules_enabled:contains("net_multiplex") then for opt, ports in pairs(configured_ports) do ports:append(module:get_option_array(opt:sub(1, 5) == "xmpps" and "ssl_ports" or "ports", {})); end @@ -102,26 +108,31 @@ print("ttl " .. tostring(opts.ttl or 60 * 60)); for _, service in ipairs(services) do - local ports = set.new(configured_ports[service]); - local records = (async.wait_for(existing_srv[service])); - if opts.remove or opts.reset then + local config_ports = set.new(configured_ports[service]); + local dns_ports = set.new(); + + if (opts.reset or opts.remove) and not opts.each then print(("del _%s._tcp.%s IN SRV"):format(service, ihost)); else + local records = (async.wait_for(existing_srv[service])); for _, rr in ipairs(records) do - if ports:contains(rr.srv.port) and target == nameprep(rr.srv.target):gsub("%.$", "") then - ports:remove(rr.srv.port) - elseif not opts.each then - print(("del _%s._tcp.%s IN SRV"):format(service, ihost)); - break - else + if target == nameprep(rr.srv.target):gsub("%.$", "") then + dns_ports:add(rr.srv.port) + elseif opts.each then print(("del _%s._tcp.%s IN SRV %s"):format(service, ihost, rr)); end end end + if not opts.remove then - for port in ports do print(("add _%s._tcp.%s IN SRV 1 1 %d %s"):format(service, ihost, port, target)); end + if config_ports:empty() then + print(("add _%s._tcp.%s IN SRV 0 0 0 ."):format(service, ihost)); + else + for port in (config_ports - dns_ports) do + print(("add _%s._tcp.%s IN SRV 1 1 %d %s"):format(service, ihost, port, target)); + end + end end - if ports:empty() then print(("add _%s._tcp.%s IN SRV 0 0 0 ."):format(service, ihost)); end end print("show");
--- a/mod_http_libjs/mod_http_libjs.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_http_libjs/mod_http_libjs.lua Sun Jun 01 11:43:16 2025 +0700 @@ -1,16 +1,10 @@ +local http_files = require "net.http.files"; + local mime_map = module:shared("/*/http_files/mime").types or { css = "text/css", js = "application/javascript", }; -local serve; -if prosody.process_type == "prosody" then - local http_files = require "net.http.files"; - serve = http_files.serve; -else - serve = module:depends"http_files".serve; -end - local libjs_path = module:get_option_string("libjs_path", "/usr/share/javascript"); do -- sanity check @@ -25,6 +19,6 @@ module:provides("http", { default_path = "/share"; route = { - ["GET /*"] = serve({ path = libjs_path, mime_map = mime_map }); + ["GET /*"] = http_files.serve({ path = libjs_path, mime_map = mime_map }); } });
--- a/mod_http_muc_log/mod_http_muc_log.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_http_muc_log/mod_http_muc_log.lua Sun Jun 01 11:43:16 2025 +0700 @@ -581,16 +581,9 @@ local serve_static do - if prosody.process_type == "prosody" then - -- Prosody >= 0.12 - local http_files = require "net.http.files"; - serve = http_files.serve; - else - -- Prosody <= 0.11 - serve = module:depends "http_files".serve; - end + local http_files = require "net.http.files"; local mime_map = module:shared("/*/http_files/mime").types or { css = "text/css"; js = "application/javascript" }; - serve_static = serve({ path = resources; mime_map = mime_map }); + serve_static = http_files.serve({ path = resources; mime_map = mime_map }); end module:provides("http", {
--- a/mod_http_oauth2/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_http_oauth2/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -86,6 +86,15 @@ oauth2_security_policy = "default-src 'self'" -- this is the default ``` +For the Resource Owner Password Grant the `username` is expected to be the only +localpart by default. If the OAuth client includes the domainpart in the +`username` it submits (e.g. user@example.org instead of just user), set this to +`true`. Note that this requires all clients to follow this format. + +```lua +oauth2_expect_username_jid = false +``` + ### Token parameters The following options configure the lifetime of tokens issued by the module.
--- a/mod_http_oauth2/mod_http_oauth2.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_http_oauth2/mod_http_oauth2.lua Sun Jun 01 11:43:16 2025 +0700 @@ -134,6 +134,7 @@ local pkce_required = module:get_option_boolean("oauth2_require_code_challenge", true); local respect_prompt = module:get_option_boolean("oauth2_respect_oidc_prompt", false); +local expect_username_jid = module:get_option_boolean("oauth2_expect_username_jid", false); local verification_key; local sign_client, verify_client; @@ -397,21 +398,52 @@ return oauth_error("invalid_request"); end +local function make_client_secret(client_id) --> client_secret + return hashes.hmac_sha256(verification_key, client_id, true); +end + +local function verify_client_secret(client_id, client_secret) + return hashes.equals(make_client_secret(client_id), client_secret); +end + function grant_type_handlers.password(params) - local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); - local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); - local request_username, request_host, request_resource = jid.prepped_split(request_jid); + if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end + if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end + + local client = check_client(params.client_id); + if not client then + return oauth_error("invalid_client", "incorrect credentials"); + end - if not (request_username and request_host) or request_host ~= module.host then - return oauth_error("invalid_request", "invalid JID"); + if not verify_client_secret(params.client_id, params.client_secret) then + module:log("debug", "client_secret mismatch"); + return oauth_error("invalid_client", "incorrect credentials"); end - if not usermanager.test_password(request_username, request_host, request_password) then + + local request_username + + if expect_username_jid then + local request_jid = assert(params.username, oauth_error("invalid_request", "missing 'username' (JID)")); + local _request_username, request_host, request_resource = jid.prepped_split(request_jid); + + if not (_request_username and request_host) or request_host ~= module.host then + return oauth_error("invalid_request", "invalid JID"); + end + + request_username = _request_username + else + request_username = assert(params.username, oauth_error("invalid_request", "missing 'username'")); + end + + local request_password = assert(params.password, oauth_error("invalid_request", "missing 'password'")); + + if not usermanager.test_password(request_username, module.host, request_password) then return oauth_error("invalid_grant", "incorrect credentials"); end - local granted_jid = jid.join(request_username, request_host, request_resource); + local granted_jid = jid.join(request_username, module.host); local granted_scopes, granted_role = filter_scopes(request_username, params.scope); - return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, nil)); + return json.encode(new_access_token(granted_jid, granted_role, granted_scopes, client)); end function response_type_handlers.code(client, params, granted_jid, id_token) @@ -505,14 +537,6 @@ } end -local function make_client_secret(client_id) --> client_secret - return hashes.hmac_sha256(verification_key, client_id, true); -end - -local function verify_client_secret(client_id, client_secret) - return hashes.equals(make_client_secret(client_id), client_secret); -end - function grant_type_handlers.authorization_code(params) if not params.client_id then return oauth_error("invalid_request", "missing 'client_id'"); end if not params.client_secret then return oauth_error("invalid_request", "missing 'client_secret'"); end
--- a/mod_invite/mod_invite.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_invite/mod_invite.lua Sun Jun 01 11:43:16 2025 +0700 @@ -7,19 +7,12 @@ local rostermanager = require "core.rostermanager"; local tohtml = require "util.stanza".xml_escape local nodeprep = require "util.encodings".stringprep.nodeprep; +local http_files = require "net.http.files"; local tostring = tostring; local invite_storage = module:open_store(); local inviter_storage = module:open_store("inviter"); -local serve; -if prosody.process_type == "prosody" then - local http_files = require "net.http.files"; - serve = http_files.serve; -else - serve = module:depends"http_files".serve; -end - module:depends"adhoc"; module:depends"http"; @@ -132,7 +125,7 @@ module:provides("http", { route = { - ["GET /bootstrap.min.css"] = serve(module:get_directory() .. "/invite/bootstrap.min.css"); + ["GET /bootstrap.min.css"] = http_files.serve(module:get_directory() .. "/invite/bootstrap.min.css"); ["GET /*"] = generate_page; POST = handle_form; };
--- a/mod_invites_page/mod_invites_page.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_invites_page/mod_invites_page.lua Sun Jun 01 11:43:16 2025 +0700 @@ -34,12 +34,7 @@ -- Load HTTP-serving dependencies if prosody.shutdown then -- not if running under prosodyctl module:depends("http"); - - if prosody.process_type == "prosody" then - http_files = require "net.http.files"; - else - http_files = module:depends"http_files"; - end + http_files = require "net.http.files"; elseif prosody.process_type and module.get_option_period then module:depends("http"); http_files = require "net.http.files"; @@ -66,6 +61,8 @@ -- so the user will only receive a URI. The client should be able to handle this -- by automatically falling back to a client-specific landing page, per XEP-0401. if not invite.allow_registration then return; end + -- password reset invites are not currently supported + if invite.additional_data and invite.additional_data.allow_reset then return end invite.landing_page = render_url(invite_url_template, { host = module.host, invite = invite }); end
--- a/mod_jsxc/mod_jsxc.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_jsxc/mod_jsxc.lua Sun Jun 01 11:43:16 2025 +0700 @@ -19,17 +19,9 @@ local serve_dist = nil; local resources = module:get_option_path("jsxc_resources"); if resources then - local serve; - if prosody.process_type == "prosody" then - -- Prosody >= trunk / 0.12 - local http_files = require "net.http.files"; - serve = http_files.serve; - else - -- Prosody <= 0.11 - serve = module:depends "http_files".serve; - end + local http_files = require "net.http.files"; local mime_map = module:shared("/*/http_files/mime").types or { css = "text/css", js = "application/javascript" }; - serve_dist = serve({ path = resources, mime_map = mime_map }); + serve_dist = http_files.serve({ path = resources, mime_map = mime_map }); cdn_url = module:http_url(); end
--- a/mod_limits_exception/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_limits_exception/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -1,6 +1,8 @@ --- +labels: +- Stage-Obsolete summary: Allow specified JIDs to bypass rate limits -... +--- This module allows you to configure a list of JIDs that should be allowed to bypass rate limit restrictions.
--- a/mod_muc_anonymize_moderation_actions/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_muc_anonymize_moderation_actions/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -1,8 +1,10 @@ -<!-- -SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> -SPDX-License-Identifier: AGPL-3.0-only ---> -# mod_muc_anonymize_moderation_actions +--- +labels: +- 'Stage-Alpha' +summary: Anonymize moderator actions for participants +--- + +## Introduction This modules allows to anonymize affiliation and role changes in MUC rooms. @@ -11,13 +13,10 @@ This is particularly usefull to prevent some revenge when a moderator bans someone. -This module is under AGPL-3.0 license. - -It was tested on Prosody 0.12.x. ## Configuration -Just enable the module on your MUC VirtualHost. +Just enable the module on your MUC Component. The feature will be accessible throught the room configuration form. You can tweak the position of the settings in the MUC configuration form using `anonymize_moderation_actions_form_position`. @@ -26,7 +25,20 @@ By default, the field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom (default value is `78`). ``` lua -VirtualHost "muc.example.com" +Component "muc.example.com" "muc" modules_enabled = { "muc_anonymize_moderation_actions" } anonymize_moderation_actions_form_position = 96 ``` + +## Compatibility + + ------ ---------------------- + trunk Works as of 25-05-12 + 13 Works + 0.12 Works + ------ ---------------------- + +### License + +SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> +SPDX-License-Identifier: AGPL-3.0-only
--- a/mod_net_proxy/mod_net_proxy.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_net_proxy/mod_net_proxy.lua Sun Jun 01 11:43:16 2025 +0700 @@ -14,6 +14,7 @@ local net = require "util.net"; local set = require "util.set"; local portmanager = require "core.portmanager"; +local fmt = require "util.format".format; -- Backwards Compatibility local function net_ntop_bc(input) @@ -81,7 +82,7 @@ local proxy_data_mt = {}; proxy_data_mt.__index = proxy_data_mt; function proxy_data_mt:describe() - return string.format("proto=%s/%s src=%s:%d dst=%s:%d", + return fmt("proto=%s/%s src=%s:%d dst=%s:%d", self:addr_family_str(), self:transport_str(), self:src_addr(), self:src_port(), self:dst_addr(), self:dst_port()); end
--- a/mod_password_reset/mod_password_reset.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_password_reset/mod_password_reset.lua Sun Jun 01 11:43:16 2025 +0700 @@ -8,19 +8,12 @@ local dataforms_new = require "util.dataforms".new; local st = require "util.stanza"; local apply_template = require"util.interpolation".new("%b{}", st.xml_escape); +local http_files = require "net.http.files"; local reset_tokens = module:open_store(); local max_token_age = module:get_option_number("password_reset_validity", 86400); -local serve; -if prosody.process_type == "prosody" then - local http_files = require "net.http.files"; - serve = http_files.serve; -else - serve = module:depends"http_files".serve; -end - module:depends("adhoc"); module:depends("http"); local password_policy = module:depends("password_policy"); @@ -90,7 +83,7 @@ module:provides("http", { route = { - ["GET /bootstrap.min.css"] = serve(module:get_directory() .. "/password_reset/bootstrap.min.css"); + ["GET /bootstrap.min.css"] = http_files.serve(module:get_directory() .. "/password_reset/bootstrap.min.css"); ["GET /reset"] = generate_page; ["POST /reset"] = handle_form; };
--- a/mod_post_msg/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_post_msg/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -32,9 +32,9 @@ Authentication -------------- -Authentication is done by HTTP Basic. +Authentication is done by [HTTP Basic](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication). - Authentication: Basic BASE64( "username@virtualhost:password" ) + Authorization: Basic BASE64( "username@virtualhost:password" ) Payload formats ---------------
--- a/mod_push2/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_push2/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -16,6 +16,7 @@ ------------------------------------ ----------------- ------------------------------------------------------------------------------------------------------------------- `contact_uri` xmpp:server.tld Contact information for the server operator (usually as a `mailto:` URI is preferred) `push_max_hibernation_timeout` `259200` (72h) Number of seconds to extend the smacks timeout if no push was triggered yet (default: 72 hours) + `hibernate_past_first_push` `true` Keep hibernating using the `push_max_hibernation_timeout` even after first push Internal design notes =====================
--- a/mod_push2/mod_push2.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_push2/mod_push2.lua Sun Jun 01 11:43:16 2025 +0700 @@ -9,11 +9,14 @@ local crypto = require "util.crypto"; local jwt = require "util.jwt"; +pcall(function() module:depends("track_muc_joins") end) + local xmlns_push = "urn:xmpp:push2:0"; -- configuration local contact_uri = module:get_option_string("contact_uri", "xmpp:" .. module.host) local extended_hibernation_timeout = module:get_option_number("push_max_hibernation_timeout", 72*3600) -- use same timeout like ejabberd +local hibernate_past_first_push = module:get_option_boolean("hibernate_past_first_push", true) local host_sessions = prosody.hosts[module.host].sessions local push2_registrations = module:open_store("push2_registrations", "keyval") @@ -28,7 +31,22 @@ module:hook("account-disco-info", account_dico_info); local function parse_match(matchel) - local match = { match = matchel.attr.profile } + local match = { match = matchel.attr.profile, chats = {} } + + for chatel in matchel:childtags("filter") do + local chat = {} + if chatel:get_child("mention") then + chat.mention = true + end + if chatel:get_child("reply") then + chat.reply = true + end + match.chats[chatel.attr.jid] = chat + end + + match.grace = matchel:get_child_text("grace") + if match.grace then match.grace = tonumber(match.grace) end + local send = matchel:get_child("send", "urn:xmpp:push2:send:notify-only:0") if send then match.send = send.attr.xmlns @@ -156,7 +174,7 @@ end -- is this push a high priority one -local function is_important(stanza) +local function is_important(stanza, session) local is_voip_stanza, urgent_reason = is_voip(stanza) if is_voip_stanza then return true; end @@ -177,8 +195,17 @@ -- carbon copied outgoing messages are not important if carbon and stanza_direction == "out" then return false; end - -- groupchat subjects are not important here - if st_type == "groupchat" and stanza:get_child_text("subject") then + -- groupchat reflections are not important here + if st_type == "groupchat" and session and session.rooms_joined then + local muc = jid.bare(stanza.attr.from) + local from_nick = jid.resource(stanza.attr.from) + if from_nick == session.rooms_joined[muc] then + return false + end + end + + -- edits are not imporatnt + if stanza:get_child("replace", "urn:xmpp:message-correct:0") then return false end @@ -222,7 +249,7 @@ end if string.len(envelope_bytes) > max_data_size then local body = stanza:get_child_text("body") - if string.len(body) > 50 then + if body and string.len(body) > 50 then stanza_clone:maptags(function(el) if el.name == "body" then return nil @@ -251,8 +278,9 @@ end) envelope_bytes = tostring(envelope) end - if string.len(envelope_bytes) < max_data_size/2 then - envelope:text_tag("rpad", base64.encode(random.bytes(math.min(150, max_data_size/3 - string.len(envelope_bytes))))) + local padding_size = math.min(150, max_data_size/3 - string.len(envelope_bytes)) + if padding_size > 0 then + envelope:text_tag("rpad", base64.encode(random.bytes(padding_size))) envelope_bytes = tostring(envelope) end @@ -294,12 +322,12 @@ push_notification_payload:text_tag("jwt", signer(payload), { key = base64.encode(public_key) }) end -local function handle_notify_request(stanza, node, user_push_services, log_push_decline) +local function handle_notify_request(stanza, node, user_push_services, session, log_push_decline) local pushes = 0; if not #user_push_services then return pushes end local notify_push_services = {}; - if is_important(stanza) then + if is_important(stanza, session) then notify_push_services = user_push_services else for identifier, push_info in pairs(user_push_services) do @@ -329,7 +357,7 @@ if send_push then local push_notification_payload = st.stanza("notification", { xmlns = xmlns_push }) push_notification_payload:text_tag("client", push_info.client) - push_notification_payload:text_tag("priority", is_voip(stanza) and "high" or (is_important(stanza) and "normal" or "low")) + push_notification_payload:text_tag("priority", is_voip(stanza) and "high" or (is_important(stanza, session) and "normal" or "low")) if is_voip(stanza) then push_notification_payload:tag("voip"):up() end @@ -340,13 +368,47 @@ if match.match == "urn:xmpp:push2:match:all" then does_match = true elseif match.match == "urn:xmpp:push2:match:important" then - does_match = is_important(stanza) + does_match = is_important(stanza, session) elseif match.match == "urn:xmpp:push2:match:archived" then does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0") elseif match.match == "urn:xmpp:push2:match:archived-with-body" then does_match = stanza:get_child("stana-id", "urn:xmpp:sid:0") and has_body(stanza) end + local to_user, to_host = jid.split(stanza.attr.to) + to_user = to_user or session.username + to_host = to_host or module.host + + -- If another session has recent activity within configured grace period, don't send push + if does_match and match.grace and to_host == module.host and host_sessions[to_user] then + local now = os_time() + for _, session in pairs(host_sessions[to_user].sessions) do + if session.last_activity and session.push_registration_id ~= push_registration_id and (now - session.last_activity) < match.grace then + does_match = false + end + end + end + + local chat = match.chats and (match.chats[stanza.attr.from] or match.chats[jid.bare(stanza.attr.from)] or match.chats[jid.host(stanza.attr.from)]) + if does_match and chat then + does_match = false + + local nick = (session.rooms_joined and session.rooms_joined[jid.bare(stanza.attr.from)]) or to_user + + if not does_match and chat.mention then + local body = stanza:get_child_text("body") + if body and body:find(nick, 1, true) then + does_match = true + end + end + if not does_match and chat.reply then + local reply = stanza:get_child("reply", "urn:xmpp:reply:0") + if reply and (reply.attr.to == to_user.."@"..to_host or (jid.bare(reply.attr.to) == jid.bare(stanza.attr.from) and jid.resource(reply.attr.to) == nick)) then + does_match = true + end + end + end + if does_match and not sends_added[match.send] then sends_added[match.send] = true if match.send == "urn:xmpp:push2:send:notify-only" then @@ -384,7 +446,7 @@ module:hook("message/offline/handle", function(event) local node, user_push_services = get_push_settings(event.stanza, event.origin); module:log("debug", "Invoking handle_notify_request() for offline stanza"); - handle_notify_request(event.stanza, node, user_push_services, true); + handle_notify_request(event.stanza, node, user_push_services, event.origin, true); end, 1); -- publish on bare groupchat @@ -402,7 +464,7 @@ end end - handle_notify_request(event.stanza, node, notify_push_services, true); + handle_notify_request(event.stanza, node, notify_push_services, event.origin, true); end, 1); local function process_stanza_queue(queue, session, queue_type) @@ -415,14 +477,14 @@ local node, all_push_services = get_push_settings(stanza, session) local user_push_services = {[session.push_registration_id] = all_push_services[session.push_registration_id]} local stanza_type = "unimportant"; - if is_important(stanza) then stanza_type = "important"; end + if is_important(stanza, session) then stanza_type = "important"; end if not notified[stanza_type] then -- only notify if we didn't try to push for this stanza type already - if handle_notify_request(stanza, node, user_push_services, false) ~= 0 then + if handle_notify_request(stanza, node, user_push_services, session, false) ~= 0 then if session.hibernating and not session.first_hibernated_push then -- if the message was important -- then record the time of first push in the session for the smack module which will extend its hibernation -- timeout based on the value of session.first_hibernated_push - if is_important(stanza) then + if is_important(stanza, session) and not hibernate_past_first_push then session.first_hibernated_push = os_time(); -- check for prosody 0.12 mod_smacks if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then @@ -567,7 +629,7 @@ end end - handle_notify_request(stanza, to_user, notify_push_services, true); + handle_notify_request(stanza, to_user, notify_push_services, event.origin, true); end end @@ -577,6 +639,15 @@ module:hook("smacks-hibernation-stanza-queued", process_smacks_stanza); module:hook("archive-message-added", archive_message_added); +local function track_activity(event) + if has_body(event.stanza) or event.stanza:child_with_ns("http://jabber.org/protocol/chatstates") then + event.origin.last_activity = os_time() + end +end + +module:hook("pre-message/bare", track_activity) +module:hook("pre-message/full", track_activity) + module:log("info", "Module loaded"); function module.unload() module:log("info", "Unloading module");
--- a/mod_push2/push2.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_push2/push2.md Sun Jun 01 11:43:16 2025 +0700 @@ -15,6 +15,10 @@ <service>pusher@push.example.com</service> <client>https://push.example.com/adlfkjadafdasf</client> <match profile="urn:xmpp:push2:match:archived-with-body"> + <grace>144</grace> + <filter jid="somemuc@conference.example.com"> + <mention/> + </filter> <send xmlns="urn:xmpp:push2:send:notify-only:0"/> </match> </enable> @@ -26,6 +30,10 @@ The `<match/>` and `<send/>` elements define what profiles to use for matching stanzas and sending notifications. These are described later in this document. +The optional `<filter/>` child of `<match/>` allows extra filtering of pushes for only specific chats. No specified filters means muted, do not push. `<mention/>` means push on mentions, `<reply/>` means push on replies. + +The optional `<grace/>` child of `<match/>` allows specifying a "grace period" in seconds where activity on another session by the same user (such as sending a message) will temporarily pause sending push notifications. + ## Match and send profiles Different clients and push services have different requirements for push notifications, often due to the differing capabilities of target platforms.
--- a/mod_register_apps/mod_register_apps.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_register_apps/mod_register_apps.lua Sun Jun 01 11:43:16 2025 +0700 @@ -1,13 +1,6 @@ -- luacheck: ignore 631 module:depends("http"); -local http_files -if prosody.process_type then - -- Prosody >= 0.12 - http_files = require "net.http.files"; -else - -- Prosody <= 0.11 - http_files = module:depends "http_files"; -end +local http_files = require "net.http.files"; local app_config = module:get_option("site_apps", { { @@ -225,7 +218,7 @@ module:provides("http", { route = { - ["GET /assets/*"] = http_files and http_files.serve({ + ["GET /assets/*"] = http_files.serve({ path = module:get_directory().."/assets"; mime_map = mime_map; });
--- a/mod_rest/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_rest/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -63,11 +63,10 @@ and lastly, drop the "@host" from the username in your http queries, EG: ```lua -curl \ - https://chat.example.com:5281/rest/version/chat.example.com \ - -k \ +curl -sf \ + -H 'Accept: application/json' \ --user admin \ - -H 'Accept: application/json' + https://chat.example.com:5281/rest/version/chat.example.com ``` ## OAuth2
--- a/mod_rest/mod_rest.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_rest/mod_rest.lua Sun Jun 01 11:43:16 2025 +0700 @@ -664,6 +664,7 @@ -- strip some stuff, notably the optional traceback table that casues stack overflow in util.json local function simplify_error(e) + if not e then return end return { type = e.type; condition = e.condition;
--- a/mod_s2s_auth_dane/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_s2s_auth_dane/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -1,10 +1,16 @@ --- labels: -- Stage-Broken +- Stage-Obsolete - Type-S2SAuth summary: S2S authentication using DANE ... +::: {.alert .alert-warning} +This module no longer works with recent versions of Prosody. + +However, Prosody as of [13.0.0](//prosody.im/doc/release/13.0.0) ships with native support for [DANE](//prosody.im/doc/dnssec). +::: + Introduction ============
--- a/mod_s2s_auth_dane/mod_s2s_auth_dane.lua Sun Jun 01 11:41:42 2025 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,436 +0,0 @@ --- mod_s2s_auth_dane --- Copyright (C) 2013-2014 Kim Alvefur --- --- This file is MIT/X11 licensed. --- --- Implements DANE and Secure Delegation using DNS SRV as described in --- http://tools.ietf.org/html/draft-miller-xmpp-dnssec-prooftype --- --- Known issues: --- Could be done much cleaner if mod_s2s was using util.async --- --- TODO Things to test/handle: --- Negative or bogus answers --- No encryption offered --- Different hostname before and after STARTTLS - mod_s2s should complain --- Interaction with Dialback --- --- luacheck: ignore module - -module:set_global(); - -local have_async, async = pcall(require, "util.async"); -local noop = function () end -local unpack = table.unpack or _G.unpack; -local type = type; -local t_insert = table.insert; -local set = require"util.set"; -local dns_lookup = require"net.adns".lookup; -local hashes = require"util.hashes"; -local base64 = require"util.encodings".base64; -local idna_to_ascii = require "util.encodings".idna.to_ascii; -local idna_to_unicode = require"util.encodings".idna.to_unicode; -local nameprep = require"util.encodings".stringprep.nameprep; -local cert_verify_identity = require "util.x509".verify_identity; - -do - local net_dns = require"net.dns"; - if not net_dns.types or not net_dns.types[52] then - module:log("error", "No TLSA support available, DANE will not be supported"); - return - end -end - -local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n".. -"([0-9A-Za-z=+/\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-"; -local function pem2der(pem) - local typ, data = pem:match(pat); - if typ and data then - return base64.decode(data), typ; - end -end -local use_map = { ["DANE-EE"] = 3; ["DANE-TA"] = 2; ["PKIX-EE"] = 1; ["PKIX-CA"] = 0 } - -local implemented_uses = set.new { "DANE-EE", "PKIX-EE" }; -do - local cert_mt = debug.getregistry()["SSL:Certificate"]; - if cert_mt and cert_mt.__index.issued then - -- Need cert:issued() for these - implemented_uses:add("DANE-TA"); - implemented_uses:add("PKIX-CA"); - else - module:log("debug", "The cert:issued() method is unavailable, DANE-TA and PKIX-CA can't be enabled"); - end - if not cert_mt.__index.pubkey then - module:log("debug", "The cert:pubkey() method is unavailable, the SPKI usage can't be supported"); - end -end -local configured_uses = module:get_option_set("dane_uses", { "DANE-EE", "DANE-TA" }); -local enabled_uses = set.intersection(implemented_uses, configured_uses) / function(use) return use_map[use] end; -local unsupported = configured_uses - implemented_uses; -if not unsupported:empty() then - module:log("warn", "Unable to support DANE uses %s", tostring(unsupported)); -end - --- Find applicable TLSA records --- Takes a s2sin/out and a callback -local function dane_lookup(host_session, cb) - cb = cb or noop; - local log = host_session.log or module._log; - if host_session.dane ~= nil then return end -- Has already done a lookup - - if host_session.direction == "incoming" then - if not host_session.from_host then - log("debug", "Session doesn't have a 'from' host set"); - return; - end - -- We don't know what hostname or port to use for Incoming connections - -- so we do a SRV lookup and then request TLSA records for each SRV - -- Most servers will probably use the same certificate on outgoing - -- and incoming connections, so this should work well - local name = host_session.from_host and idna_to_ascii(host_session.from_host); - if not name then - log("warn", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host)); - return; - end - log("debug", "Querying SRV records from _xmpp-server._tcp.%s.", name); - host_session.dane = dns_lookup(function (answer, err) - host_session.dane = false; -- Mark that we already did the lookup - - if not answer then - log("debug", "Resolver error: %s", tostring(err)); - return cb(host_session); - end - - if answer.bogus then - log("warn", "Results are bogus!"); - -- Bad sign, probably not a good idea to do any fallback here - host_session.dane = answer; - elseif not answer.secure then - log("debug", "Results are not secure"); - return cb(host_session); - end - - local n = answer.n or #answer; - if n == 0 then - -- No SRV records, synthesize fallback host and port - -- this may behave oddly for connections in the other direction if - -- mod_s2s doesn't keep the answer around - answer[1] = { srv = { target = name, port = 5269 } }; - n = 1; - elseif n == 1 and answer[1].srv.target == '.' then - return cb(host_session); -- No service ... This shouldn't happen? - end - local srv_hosts = { answer = answer }; - host_session.srv_hosts = srv_hosts; - local dane; - for _, record in ipairs(answer) do - t_insert(srv_hosts, record.srv); - log("debug", "Querying TLSA record for %s:%d", record.srv.target, record.srv.port); - dns_lookup(function(dane_answer) - log("debug", "Got answer for %s:%d", record.srv.target, record.srv.port); - n = n - 1; - -- There are three kinds of answers - -- Insecure, Secure and Bogus - -- - -- We collect Secure answers for later use - -- - -- Insecure (legacy) answers are simply ignored - -- - -- If we get a Bogus (dnssec error) reply, keep the - -- status around. If there were only bogus replies, the - -- connection will be aborted. If there were at least - -- one non-Bogus reply, we proceed. If none of the - -- replies matched, we consider the connection insecure. - - if (dane_answer.bogus or dane_answer.secure) and not dane then - -- The first answer we care about - -- For services with only one SRV record, this will be the only one - log("debug", "First secure (or bogus) TLSA") - dane = dane_answer; - elseif dane_answer.bogus then - log("debug", "Got additional bogus TLSA") - dane.bogus = dane_answer.bogus; - elseif dane_answer.secure then - log("debug", "Got additional secure TLSA") - for _, dane_record in ipairs(dane_answer) do - t_insert(dane, dane_record); - end - end - if n == 0 then - if dane then - host_session.dane = dane; - if #dane > 0 and dane.bogus then - -- Got at least one non-bogus reply, - -- This should trigger a failure if one of them did not match - log("warn", "Ignoring bogus replies"); - dane.bogus = nil; - end - if #dane == 0 and dane.bogus == nil then - -- Got no usable data - host_session.dane = false; - end - end - return cb(host_session); - end - end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA"); - end - end, "_xmpp-server._tcp."..name..".", "SRV"); - return true; - elseif host_session.direction == "outgoing" then - -- Prosody has already done SRV lookups for outgoing session, so check if those are secure - local srv_hosts = host_session.srv_hosts; - if not ( srv_hosts and srv_hosts.answer and srv_hosts.answer.secure ) then - return; -- No secure SRV records, fall back to non-DANE mode - -- Empty response were not kept by older mod_s2s/s2sout - end - -- Do TLSA lookup for currently selected SRV record - local srv_choice = srv_hosts[host_session.srv_choice or 0] or { target = idna_to_ascii(host_session.to_host), port = 5269 }; - log("debug", "Querying TLSA record for %s:%d", srv_choice.target, srv_choice.port); - host_session.dane = dns_lookup(function(answer) - if answer and ((answer.secure and #answer > 0) or answer.bogus) then - srv_choice.dane = answer; - else - srv_choice.dane = false; - end - host_session.dane = srv_choice.dane; - return cb(host_session); - end, ("_%d._tcp.%s."):format(srv_choice.port, srv_choice.target), "TLSA"); - return true; - end -end - -local function pause(host_session) - host_session.log("debug", "Pausing connection until DANE lookup is completed"); - host_session.conn:pause() -end - -local function resume(host_session) - host_session.log("debug", "DANE lookup completed, resuming connection"); - host_session.conn:resume() -end - -if have_async then - function pause(host_session) - host_session.log("debug", "Pausing connection until DANE lookup is completed"); - local wait, done = async.waiter(); - host_session._done_waiting_for_dane = done; - wait(); - end - local function _resume(_, host_session) - if host_session._done_waiting_for_dane then - host_session.log("debug", "DANE lookup completed, resuming connection"); - host_session._done_waiting_for_dane(); - host_session._done_waiting_for_dane = nil; - end - end - function resume(host_session) - -- Something about the way luaunbound calls callbacks is messed up - if host_session._done_waiting_for_dane then - module:add_timer(0, _resume, host_session); - end - end -end - -local new_dane = module:get_option_boolean("use_dane", false); - -function module.add_host(module) - local function on_new_s2s(event) - local host_session = event.origin; - if host_session.type == "s2sout" or host_session.type == "s2sin" then - return; -- Already authenticated - end - if host_session.dane ~= nil then - return; -- Already done DANE lookup - end - dane_lookup(host_session, resume); - -- Let it run in parallel until we need to check the cert - end - - if not new_dane then - -- New outgoing connections - module:hook("stanza/http://etherx.jabber.org/streams:features", on_new_s2s, 501); - module:hook("s2sout-authenticate-legacy", on_new_s2s, 200); - end - - -- New incoming connections - module:hook("s2s-stream-features", on_new_s2s, 10); - - module:hook("s2s-authenticated", function(event) - local session = event.session; - if session.dane and type(session.dane) == "table" and next(session.dane) ~= nil and not session.secure then - -- TLSA record but no TLS, not ok. - -- TODO Optional? - -- Bogus replies should trigger this path - -- How does this interact with Dialback? - session:close({ - condition = "policy-violation", - text = "Encrypted server-to-server communication is required but was not " - ..((session.direction == "outgoing" and "offered") or "used") - }); - return false; - end - -- Cleanup - session.srv_hosts = nil; - end); -end - --- Compare one TLSA record against a certificate -local function one_dane_check(tlsa, cert, log) - local select, match, certdata = tlsa.select, tlsa.match; - - if select == 0 then - certdata = pem2der(cert:pem()); - elseif select == 1 and cert.pubkey then - certdata = pem2der(cert:pubkey()); - else - log("warn", "DANE selector %s is unsupported", tlsa:getSelector() or select); - return; - end - - if match == 1 then - certdata = hashes.sha256(certdata); - elseif match == 2 then - certdata = hashes.sha512(certdata); - elseif match ~= 0 then - log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match); - return; - end - - if #certdata ~= #tlsa.data then - log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data); - end - return certdata == tlsa.data; -end - -module:hook("s2s-check-certificate", function(event) - local session, cert, host = event.session, event.cert, event.host; - if not cert then return end - local log = session.log or module._log; - local dane = session.dane; - if type(dane) ~= "table" then - if dane == nil and dane_lookup(session, resume) then - pause(session); - dane = session.dane; - end - end - if type(dane) == "table" then - local match_found, supported_found; - for i = 1, #dane do - local tlsa = dane[i].tlsa; - log("debug", "TLSA #%d: %s", i, tostring(tlsa)) - local use = tlsa.use; - - if enabled_uses:contains(use) then - -- DANE-EE or PKIX-EE - if use == 3 or use == 1 then - -- Should we check if the cert subject matches? - local is_match = one_dane_check(tlsa, cert, log); - if is_match ~= nil then - supported_found = true; - end - if is_match and use == 1 and session.cert_chain_status ~= "valid" then - -- for usage 1, PKIX-EE, the chain has to be valid already - log("debug", "PKIX-EE TLSA matches untrusted certificate"); - is_match = false; - end - if is_match then - log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); - session.cert_identity_status = "valid"; - if use == 3 then -- DANE-EE, chain status equals DNSSEC chain status - session.cert_chain_status = "valid"; - end - match_found = true; - dane.matching = tlsa; - break; - end - -- DANE-TA or PKIX-CA - elseif use == 2 or use == 0 then - supported_found = true; - local chain = session.conn:socket():getpeerchain(); - for c = 1, #chain do - local cacert = chain[c]; - local is_match = one_dane_check(tlsa, cacert, log); - if is_match ~= nil then - supported_found = true; - end - if is_match and not cacert:issued(cert, unpack(chain)) then - is_match = false; - end - if is_match and use == 0 and session.cert_chain_status ~= "valid" then - -- for usage 0, PKIX-CA, identity and chain has to be valid already - is_match = false; - end - if is_match then - log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); - if use == 2 then -- DANE-TA - session.cert_identity_status = "valid"; - if cert_verify_identity(host, "xmpp-server", cert) then - session.cert_chain_status = "valid"; - -- else -- TODO Check against SRV target? - end - end - match_found = true; - dane.matching = tlsa; - break; - end - end - if match_found then break end - end - end - end - if supported_found and not match_found or dane.bogus then - -- No TLSA matched or response was bogus - local why = "No TLSA matched certificate"; - if dane.bogus then - why = "Bogus: "..tostring(dane.bogus); - end - log("warn", "DANE validation failed for %s: %s", host, why); - session.cert_identity_status = "invalid"; - session.cert_chain_status = "invalid"; - end - else - if session.cert_chain_status == "valid" and session.cert_identity_status ~= "valid" - and session.srv_hosts and session.srv_hosts.answer and session.srv_hosts.answer.secure then - local srv_hosts, srv_choice, srv_target = session.srv_hosts, session.srv_choice; - for i = srv_choice or 1, srv_choice or #srv_hosts do - srv_target = session.srv_hosts[i].target:gsub("%.?$",""); - log("debug", "Comparing certificate with Secure SRV target %s", srv_target); - srv_target = nameprep(idna_to_unicode(srv_target)); - if srv_target and cert_verify_identity(srv_target, "xmpp-server", cert) then - log("info", "Certificate for %s matches Secure SRV target %s", host, srv_target); - session.cert_identity_status = "valid"; - return; - end - end - end - end -end); - --- Telnet command -if module:get_option_set("modules_enabled", {}):contains("admin_telnet") then - module:depends("admin_telnet"); -- Make sure the env is there - local def_env = module:shared("admin_telnet/env"); - - local function annotate(session, line) - line = line or {}; - table.insert(line, "--"); - if session.dane == nil then - table.insert(line, "No DANE attempted, probably insecure SRV response"); - elseif session.dane == false then - table.insert(line, "DANE failed or response was insecure"); - elseif type(session.dane) ~= "table" then - table.insert(line, "Waiting for DANE records..."); - elseif session.dane.matching then - table.insert(line, "Matching DANE record:\n| " .. tostring(session.dane.matching)); - else - table.insert(line, "DANE records:\n| " .. tostring(session.dane)); - end - return table.concat(line, " "); - end - - function def_env.s2s:show_dane(...) - return self:show(..., annotate); - end -end -
--- a/mod_sasl2/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_sasl2/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -31,6 +31,7 @@ Prosody Version Status ----------------------- ---------------- - trunk as of 2024-11-24 Works + trunk as of 2025-05-25 Works + 13 Works 0.12 Does not work ----------------------- ----------------
--- a/mod_sasl2_bind2/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_sasl2_bind2/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -16,5 +16,6 @@ Prosody-Version Status --------------- ---------------------- - trunk Works as of 2024-12-21 + trunk Works as of 2025-05-25 + 13 Works 0.12 Does not work
--- a/mod_sasl2_fast/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_sasl2_fast/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -33,5 +33,6 @@ Prosody-Version Status --------------- ---------------------- - trunk Works as of 2024-12-21 + trunk Works as of 2025-05-25 + 13 Work 0.12 Does not work
--- a/mod_sasl2_sm/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_sasl2_sm/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -17,5 +17,6 @@ Prosody-Version Status --------------- ---------------------- - trunk Works as of 2024-12-21 + trunk Works as of 2025-05-25 + 13 Work 0.12 Does not work
--- a/mod_sasl_ssdp/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_sasl_ssdp/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -18,8 +18,8 @@ # Compatibility -For SASL2 (XEP-0388) clients, it is compatible with the mod_sasl2 community module. - -For clients using RFC 6120 SASL, it requires Prosody trunk 33e5edbd6a4a or -later. It is not compatible with Prosody 0.12 (it will load, but simply -won't do anything) for "legacy SASL". + Prosody-Version Status + --------------- ---------------------- + trunk Works as of 2025-05-25 + 13 Works + 0.12 Does not work
--- a/mod_twitter/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_twitter/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -1,6 +1,6 @@ --- labels: -- 'Stage-Alpha' +- 'Stage-Broken' summary: 'Simple example of working component and HTTP polling.' ...
--- a/mod_vcard_muc/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_vcard_muc/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -1,8 +1,9 @@ --- -summary: Support for MUC vCards and avatars labels: -- 'Stage-Stable' -... +- Stage-Deprecated +- Stage-Stable +summary: Support for MUC vCards and avatars +--- # Introduction @@ -10,9 +11,9 @@ # Usage -Add "vcard\_muc" to your modules\_enabled list: +Add "vcard_muc" to your modules_enabled list: -``` {.lua} +``` lua Component "conference.example.org" "muc" modules_enabled = { "vcard_muc", @@ -21,8 +22,7 @@ # Compatibility - ------------------------- ---------- - trunk^[as of 2024-10-22] Works - 0.12 Works - ------------------------- ---------- - + ------ ----------------------------------------- + 13 Room avatar feature included in Prosody + 0.12 Works + ------ -----------------------------------------
--- a/mod_warn_legacy_tls/README.md Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_warn_legacy_tls/README.md Sun Jun 01 11:43:16 2025 +0700 @@ -4,14 +4,13 @@ summary: Warn users of obsolete TLS Versions in clients --- - TLS 1.0 and TLS 1.1 are obsolete. This module warns clients if they are using those versions, to prepare for disabling them. (If you use the default prosody config, this module will be unnessesary in its default setting, since these protocols are not allowed anymore by any supported prosody version.) This module can be used to warn from TLS1.2 if you want to switch to modern security in the near future. # Configuration -``` {.lua} +``` lua modules_enabled = { -- other modules etc "warn_legacy_tls"; @@ -41,7 +40,8 @@ # Compatibility -Prosody-Version Status ---------------- --------------------- -trunk Works as of 24-12-16 -0.12 Works + Prosody-Version Status + ----------------- ---------------------- + trunk Works as of 25-05-25 + 13 Works + 0.12 Works
--- a/mod_welcome_page/mod_welcome_page.lua Sun Jun 01 11:41:42 2025 +0700 +++ b/mod_welcome_page/mod_welcome_page.lua Sun Jun 01 11:43:16 2025 +0700 @@ -3,6 +3,7 @@ local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, { urlescape = url_escape; }); +local http_files = require "net.http.files"; local template_path = module:get_option_string("welcome_page_template_path", module:get_directory().."/html"); local user_vars = module:get_option("welcome_page_variables", {}); @@ -61,15 +62,6 @@ return 303; end -local http_files -if prosody.process_type == "prosody" then - -- Prosody >= 0.12 - http_files = require "net.http.files"; -else - -- Prosody <= 0.11 - http_files = module:depends "http_files"; -end - module:provides("http", { default_path = "/"; route = {